Categorias
CakePHP Desenvolvimento Web

Dica Rápida: usando tipos “complexos” com Migrations no Phinx

Essa é uma dica bem curta e realmente rápida pra registrar algo que precisei pesquisar algumas vezes nos últimos anos e sempre me esqueço.

Cena: você define uma tabela no seu projeto e gostaria de usar uma coluna com o tipo tsvector (como citei em posts recentes) ou então uuid. Você quer usar as funções do banco de dados para gerar o valor default para a coluna. Como fazer isso usando Migrations baseada no Phinx sem recorrer a SQL cru?

Resposta: utilize a classe Phinx\Util\Literal.

$this->table('posts')
    ->addColumn('id', 'uuid', [
        'default' => \Phinx\Util\Literal::from('gen_random_uuid()'),
        'null' => false
    ])
    ->save();

Literal pode ser utilizado tanto para definir valor default baseado em funções quanto para conversão entre tipos para uma mesma coluna (fazer CAST do antigo formato para o novo).

Categorias
Desenvolvimento Web

Next.js + Vercel + WordPress

Uso e trabalho com PHP há muitos anos e lá atrás as coisas eram bem complicadas – CMS era um palavrão que ninguém entendia muito bem como funcionava, as opções eram escassas e invariavelmente a gente caia no colo do phpnuke (ou aspnuke, dependendo do seu azar), que era terrível de customizar e estender. Até que um dia surge o WordPress, essa coisa linda que gerencia quase metade das páginas da internet ainda hoje.

Como todo gerenciador de conteúdo dinâmico, exige certo processamento no servidor, e todo processamento toma tempo, por menor que seja.

Daí pulamos para 2021, com zilhões de páginas espalhadas por aí, uma competindo com a outra por alguns segundos de atenção e a gente com a vida corrida, picos e mais picos de ansiedade onde cada segundo de espera para abrir uma página gera frustração de desinteresse: a solução? otimizar a resposta da página para que carregue o mais rápido possível. Como fazer a página responder mais rápido? vamos fazer todo processamento antes do usuário acessar, e quando ele precisar, só enviamos o resultado que já está prontinho, e preferencialmente, em um servidor próximo.

Nessa direção comecei a estudar o Next.js, um framework baseado na biblioteca React (que por sua vez é baseado em JavaScript) que facilita a construção de páginas renderizadas estaticamente. Isso resolve a primeira parte do problema, retirando qualquer processamento no servidor no momento que alguém acessa a página.

A última parte, deixar as páginas o mais próximo possível do usuário, é feita pelo Vercel (que por sinal, é quem criou/mantém o Next.js). Ele abstrai a construção da página (com uma infraestrutura própria de CI/CD) e a sua distribuição.

E onde entra o WordPress? Bom, essa solução não oferece uma interface rica para edição de conteúdo como o WP (existem outras integrações para isso, mas o foco aqui é em quem já usa WP). A ideia é manter o WP para criar páginas/posts e usar o Next.js+Vercel para servir esse conteúdo de forma mais rápida.

Como fazer isso? Existem várias formas, mas uma das mais simples é instalar o plugin WP Graphql para expor seu conteúdo em uma API GraphQL estruturada, consumir essa API com o Next.js e aí ter sua versão estaticamente gerada.

E fica mais rápido? Compare aí: cauancabral.vercel.app

Para poucos acessos e uso pessoal, não tem nenhum custo envolvido.

Categorias
CakePHP Desenvolvimento Web PHP Projetos

Fulltext Search – Busca Textual com Postgres – Parte 3 (Final)

Encerramos aqui essa breve introdução sobre busca textual (fulltext search) com Postgres apresentando um plugin que pode te auxiliar (se você utiliza CakePHP + Postgres) na implementação da busca – caso não use, talvez sirva de inspiração para um fork.

A história desse plugin vem lá de 2015 quando precisei incluir em uma busca que desconsidera-se pequenos erros de grafia e permitia o uso de sinônimos. Tínhamos restrições de hardware para implementar a busca – era uma aplicação nova, com orçamento pequeno e dispunha de um servidor com apenas 2 GB de RAM. Usar Elasticsearch seria inviável.

Na época o CakePHP tinha recém chego a versão 3.0, com um ORM todo remodelado, muito mais flexível e extensível do que nas versões anteriores. Pensei: fácil, vou estender ele e adicionar suporte aos novos tipos de dados e índices (tsvector, GIN e GIST, como vimos anteriormente). Bom, na prática não era tão fácil, a extensibilidade ainda era pequena, havia muitas dependências acopladas. A solução foi criar um shell que era invocado a cada X minutos e regerava a tabela de buscas com os tipos e índices apropriados usando SQL puro. Funcionou e foi o suficiente porque o sistema era novo e não tinha milhões de registros – já prevíamos que escalando o banco de dados, precisaríamos de mais hardware para extrair a rotina de busca.

Mais de 6 anos se passaram, agora estamos com o CakePHP 4.2 e seu ORM muito mais flexível e desacoplado. E mais uma vez me foi dado o desafio de puxar uma busca que utilizava o Elasticsearch para dentro do banco de dados principal (Postgres). Assim nasceu o autopage/pg-search.

Considerando que você tenha uma instalação do CakePHP > 4.2.2, a instalação começa com composer:

$ composer require autopage/pg-search

Em seguida, carregue o plugin na sua aplicação:

$ bin/cake plugin load Autopage/PgSearch

Por último, precisamos configurar sua aplicação para utilizar o driver Postgres fornecido. Para isso, edite seu arquivo de configuração (ou variável de ambiente, se utilizar) config/app.php ou config/app_local.php:

// No ínicio do arquivo
use Autopage\PgSearch\Database\Driver\Postgres;
...

return [
...
// Dentro da configuração dos datasources
...
    'Datasources' => [
        'default' => [
            'driver' => Postgres::class,
...

Pronto, você já pode criar migrations (não depende disso), fixtures, Tables e querys com o CakePHP.

Uma migration poderia ser:

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;
use Phinx\Util\Literal;

class CriaTabelaBuscas extends AbstractMigration
{
    /**
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $tabela = $this->table('buscas');
        $tabela
            ->addColumn('original_id', 'integer', ['null' => false, 'default' => null])
            ->addColumn('nome', 'string', ['limit' => 255, 'null' => false, 'default' => null])
            ->addColumn('data', 'date', ['null' => false, 'default' => null])
            ->addColumn('conteudo', 'text', ['null' => false, 'default' => null])
            ->addColumn('conteudo_fts', Literal::from('tsvector'), ['null' => false, 'default' => null])
            ->addTimestamps('criado', 'modificado')
            ->create();
    }
}

Para salvar registros, basta popular a entidade. Repare que criei duas colunas conteudo, uma text e outra tsvector. A ideia é que a primeira contenha a forma original que exibiremos ao usuário, enquanto a tsvector é preparada para execução da busca. Na hora de popular a entidade, atribua o mesmo valor em conteudo e conteudo_fts, o driver cuidará da conversão necessária na hora de persistir.

Já uma consulta ao banco poderia ser feita com:

// Não esqueça de sanitizar antes de passar na query abaixo
$termo = $this->request->getQuery('q');
$resultados = $this->Buscas->find()
    ->where(["conteudo_fts @@ phraseto_tsquery('{$termo}')"])
    ->order(['data' => 'desc'])
    ->all();

Ainda tem um behavior que adiciona um finder especial para busca, você deve vincular ele a tabela associada com a tabela de buscas – nesse exemplo da migration, o behavior seria ligado na tabela OriginalsTable. Veja as configurações disponíveis na documentação do projeto.

É isso, com essa sequência de artigos espero ter mostrado que é possível oferecer uma busca boa o suficiente para a maioria das aplicações sem precisar estourar orçamento com hardware ou serviços externos. E claro, que essa implementação não precisa ser nenhum bicho de sete cabeças.

Se tiver alguma dúvida ou encontrar algum problema, pode usar tanto a caixa de comentários abaixo quanto o github.

Categorias
Desenvolvimento Web Tutoriais

Fulltext Search – Busca Textual com Postgres – Parte 2

Continuando de onde paramos na parte 1, vamos ver um pouco como configurar o mínimo para ter uma busca textual através do Postgres. Se você quer se aprofundar mais nas possibilidades, recomendo os seguintes recursos:

Antes de tudo: você tem uma instalação do Postgres funcionando né? Não? Veja se esse artigo ajuda, quando terminar retome daqui.

Precisamos entender que a busca textual parte de um processamento do texto, fazendo uma análise estrutural e semântica e convertendo nosso texto em um formato padronizado. Esse processamento é dependente do idioma e portanto, uma configuração feita para inglês é diferente de outra para português.

Felizmente o Postgres já vem com suporte básico a várias línguas, incluindo português, mas para uma busca mais flexível (pegando variações de plural/singular ou conjugações de verbo) precisamos incluir arquivos de dicionário.

Nesse gist eu disponibilizei os arquivos de dicionário para português do Brasil (pt-br). Eles foram extraídos do projeto Vero do LibreOffice e convertidos para utf-8. Sabendo da origem dos arquivos, vamos usá-los:

!/bin/bash

# Caminho das extensões do Postgresql, varia de ambiente para ambiente. As vezes, de versão para versão.
# Para descobrir programaticamente, precisa instalar o pacote `postgresql-dev` e então usar o `pg_config --sharedir`
# Lugares possíveis:
# Debian/Ubuntu: /usr/share/postgresql/VERSAO
# Alpine/docker: /usr/local/share/postgresql
PGSHARE=/usr/local/share/postgresql

mkdir -p $PGSHARE/tsearch_data

curl -sSL 'https://gist.github.com/CauanCabral/5ad952e0014c1cf21a87c3731397f078/raw/f525e8d21f6697c68303300713ddb28a4bccf384/pt_br.dict' -o $PGSHARE/tsearch_data/hunspell_pt_br.dict
curl -sSL'https://gist.github.com/CauanCabral/5ad952e0014c1cf21a87c3731397f078/raw/f525e8d21f6697c68303300713ddb28a4bccf384/pt_br.affix' -o $PGSHARE/tsearch_data/hunspell_pt_br.affix

Feito isso, você terá os arquivos de dicionário para pt-br onde o Postgres irá procurar.

O próximo passo é conectar ao banco de dados (pode ser via psql, via adminer ou pgAdmin, o que você preferir). Para evitar qualquer problema nesse primeiro teste, crie um banco de dados próprio para a gente e conecte a ele. Vou chamar aqui de busca_textual.

-- Habilitamos a extensão 'unaccent' para usar na normalização dos dados
CREATE EXTENSION IF NOT EXISTS unaccent SCHEMA public;

-- Criamos nosso dicionário, usando os arquivos que pegamos no gist
CREATE TEXT SEARCH DICTIONARY portuguese_hunspell (
    TEMPLATE = ispell,
    DictFile = hunspell_pt_br,
    AffFile = hunspell_pt_br,
    Stopwords = portuguese);

-- Criamos uma nova configuração tomando como ponto de partida 'portuguese'
CREATE TEXT SEARCH CONFIGURATION pt_br ( COPY = portuguese );

-- Alteramos nossa nova configuração, para usar o dicionário e unaccent
ALTER TEXT SEARCH CONFIGURATION pt_br
    ALTER MAPPING FOR
        asciiword, asciihword, hword_asciipart,
        word, hword, hword_part
WITH portuguese_hunspell, unaccent, portuguese_stem;

Com esses comandos nós criamos uma configuração chamada pt_br que instrui o Postgres a processar um texto fazendo:

  • Quebra do texto usando regras do português
  • Substituição da palavra pela sua forma não conjugada (por exemplo voto vira votar)
  • Remoção dos acentos da palavra

Fundamental destacar que a identificação de uma palavra (token é o termo mais apropriado) só acontece uma vez. Então se o token sendo processado existe no dicionário portuguese_hunspell, ele vai ser salvo e indexado da forma como esse dicionário trabalha ele (desconjugada a palavra). Se ela for acentuada, ela nem chegará a passar pelo filtro/dicionário de desacentuação.

Por exemplo: a palavra razões ao passar por esse dicionário é substituída por razão, por que ela existe no dicionário. Já a palavra inventada jasões passará despercebida nesse dicionário e cairá na regra de desacentuação e será substituída por jasoes.

Então a ordem dos dicionários/filtros na criação da configuração são determinantes para o resultado da sua busca. Se colocar a desacentuação antes do dicionário hunspell. nossos resultados anteriores seriam razoes e jasoes, respectivamente.

Tendo a configuração, agora podemos ver como ela funciona em frases.

Lembra do parágrafo mencionado na parte 1? Execute a query abaixo para ver como ele seria processado em nossa configuração:

SELECT * FROM ts_debug('pt_br', 'Algumas das principais razões da evasão escolar no Brasil atual são a pobreza, a dificuldade de acesso à escola, a necessidade de trabalho e, principalmente, o desinteresse pelos estudos. Segundo o Programa das Nações Unidas para o Desenvolvimento (PNUD), o país tem a terceira maior taxa de abandono escolar (24,3%) entre os 100 países com maior IDH (Índice de Desenvolvimento Humano), só atrás da Bósnia e Herzegovina (26,8%) e das ilhas de São Cristovão e Névis, no Caribe (26,5%). Na América Latina, só é superado pela Guatemala (35,2%) e pela Nicarágua (51,6%), não tendo sido divulgado o índice do Haiti.');

A saída deve ser tabular e conter algumas linhas como essas:

AliasDesc.TokenDicionáriosDicionárioLexema
wordWord, all lettersÍndice{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{índice}
blankSpace symbols{}NULLNULL
asciiwordWord, all ASCIIde{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{}
blankSpace symbols{}NULLNULL
asciiwordWord, all ASCIIDesenvolvimento{portuguese_hunspell,unaccent,portuguese_stem}portuguese_stem{desenvolv}
blankSpace symbols{}NULLNULL
asciiwordWord, all ASCIIHumano{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{humano,humanar}
blankSpace symbols),{}NULLNULL
wordWord, all letters{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{}
blankSpace symbols{}NULLNULL
wordWord, all lettersatrás{portuguese_hunspell,unaccent,portuguese_stem}unaccent{atras}
blankSpace symbols{}NULLNULL
asciiwordWord, all ASCIIda{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{}
blankSpace symbols{}NULLNULL
wordWord, all lettersBósnia{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{bósnio}
blankSpace symbols{}NULLNULL
asciiwordWord, all ASCIIe{portuguese_hunspell,unaccent,portuguese_stem}portuguese_hunspell{}
blankSpace symbols{}NULLNULL
asciiwordWord, all ASCIIHerzegovina{portuguese_hunspell,unaccent,portuguese_stem}portuguese_stem{herzegovin}
blankSpace symbols({}NULLNULL
uintUnsigned integer26{simple}simple{26}

Cada linha representa um token identificado no parágrafo, como ele foi identificado, por qual regra e qual o resultado final após a aplicação da regra.

Agora como salvamos no banco esse registro? Primeiro vamos criar uma tabela:

CREATE TABLE artigos (
   id SERIAL PRIMARY KEY,
   conteudo TEXT NOT NULL,
   conteudo_fts TSVECTOR NOT NULL
);

Em seguida, vamos inserir o registro (repare que conteúdo é duplicado, mas na segunda vez, usamos a função to_tsvector para converter ele):

INSERT INTO artigos (conteudo, conteudo_fts) VALUES (
    'Algumas das principais razões da evasão escolar no Brasil atual são a pobreza, a dificuldade de acesso à escola, a necessidade de trabalho e, principalmente, o desinteresse pelos estudos. Segundo o Programa das Nações Unidas para o Desenvolvimento (PNUD), o país tem a terceira maior taxa de abandono escolar (24,3%) entre os 100 países com maior IDH (Índice de Desenvolvimento Humano), só atrás da Bósnia e Herzegovina (26,8%) e das ilhas de São Cristovão e Névis, no Caribe (26,5%). Na América Latina, só é superado pela Guatemala (35,2%) e pela Nicarágua (51,6%), não tendo sido divulgado o índice do Haiti.',
    to_tsvector('pt_br', 'Algumas das principais razões da evasão escolar no Brasil atual são a pobreza, a dificuldade de acesso à escola, a necessidade de trabalho e, principalmente, o desinteresse pelos estudos. Segundo o Programa das Nações Unidas para o Desenvolvimento (PNUD), o país tem a terceira maior taxa de abandono escolar (24,3%) entre os 100 países com maior IDH (Índice de Desenvolvimento Humano), só atrás da Bósnia e Herzegovina (26,8%) e das ilhas de São Cristovão e Névis, no Caribe (26,5%). Na América Latina, só é superado pela Guatemala (35,2%) e pela Nicarágua (51,6%), não tendo sido divulgado o índice do Haiti.')
);

Agora sim podemos fazer uma consulta:

---- Bucansdo por 'nação unidas'
SELECT id, conteudo FROM artigos WHERE conteudo_fts @@ plainto_tsquery('pt_br', 'nação unidas');

---- A mesma busca da parte 1
SELECT id, conteudo FROM artigos WHERE conteudo_fts @@ plainto_tsquery('pt_br', 'evasão escolar do brasil');

Ambas vão encontrar nosso artigo.

Vale a pena estudar um pouco sobre as funções que o Postgres oferece na busca textual:

  • ts_debug(configuracao, texto): Ajuda a entender como sua configuração interpreta um texto.
  • ts_lexize(dicionario, palavra): Aplica a regra do dicionário na palavra/token.
  • to_tsvector(configuracao, texto): Converte seu texto para um vetor de busca usando sua configuração.
  • to_tsquery(configuracao, texto): Converte seu texto para uma query de busca de uma forma estrita. Palavras devem ser ligadas usando operadores explícitos (é uma junção? ou condicional?).
  • plainto_tsquery(configuracao, texto): Converte seu texto para uma query de busca usando uma linguagem natural, próxima ao do Google.

Da forma como está, você já consegue fazer uma busca textual básica, mas ela ainda é ineficiente porque não criamos nenhum índice. Colunas do tipo tsvector podem ser indexadas em índices dos tipos GIN e GIST – cada qual com sua vantagem. Em termos gerais, o GIN oferece uma tradeoff melhor entre tempo de escrita e otimização de busca. O GIST possui um custo consideravelmente maior para escrita mas oferece uma velocidade melhor de leitura.

Adicionando um índice em nossa tabela:

CREATE INDEX idx_artigos_conteudo_fts ON artigos USING gin(conteudo_fts);

E temos uma busca prática, sem depender de sistemas externos e que atendem vários cenários de pesquisa.

Ainda podemos trabalhar com ranqueamento do resultado, destaque do texto encontrado e dicionário de sinônimos que estão presentes em sistemas complexos e geralmente apontados como única solução para buscas de verdade.

Na próxima e última parte vou focar apenas no plugin Autopage\PgSearch, que implementa e abstrai uma parte desses recursos para aplicações CakePHP.

Categorias
Desenvolvimento Web Tutoriais

Fulltext Search – Busca Textual com Postgres – Parte 1

Hoje em dia qualquer aplicação precisa de uma interface de busca, mas ainda é muito comum achar sistemas que dependem de uma entrada do usuário quase perfeita para conseguir encontrar um resultado. Internamente, essas aplicações usam e abusam de operadores SQL como LIKE e ILIKE , por vezes contando com a ajuda do coringa %% para encontrar palavras ou expressões que contém a expressão pesquisada (uma substring em outras palavras).

Mas qual a diferença de usar um simples LIKE e uma busca textual? A diferença começa pelo fato de um índice de busca textual armazenar os dados depois de um pré-processamento. Esse pré-processamento costuma envolver uma normalização (tudo em caixa baixa, por exemplo), identificação de tokens (pedaços lógicos do texto) e descarte daquilo que não possui valor semântico (remove artigos e preposições que costumam aparecer muito sem ter valor real pra quem pesquisa, por exemplo), transformação das palavras encontradas em seus radicais básicos (processo chamado de stemming) e as vezes até incluindo ou substituindo essas palavras por sinônimos. Parece complicado né? E é, mas você não precisa ser um pesquisador em NLP (processamento de linguagem natural) pra ter uma busca textual simples.

Em termos práticos, o que você ganha com uma busca textual?

Considere uma tabela chamada artigos e nela uma coluna conteudo do tipo TEXT. É um cenário comum onde se usa o operador LIKE. Agora vamos colocar uma segunda coluna conteudo_fulltext, essa do tipo TSVECTOR, que é um tipo próprio para busca textual.

Dentre os artigos salvos nessa tabela, temos um com o seguinte parágrafo:

Algumas das principais razões da evasão escolar no Brasil atual são a pobreza, a dificuldade de acesso à escola, a necessidade de trabalho e, principalmente, o desinteresse pelos estudos. Segundo o Programa das Nações Unidas para o Desenvolvimento (PNUD), o país tem a terceira maior taxa de abandono escolar (24,3%) entre os 100 países com maior IDH (Índice de Desenvolvimento Humano), só atrás da Bósnia e Herzegovina (26,8%) e das ilhas de São Cristovão e Névis, no Caribe (26,5%). Na América Latina, só é superado pela Guatemala (35,2%) e pela Nicarágua (51,6%), não tendo sido divulgado o índice do Haiti.

Wikipedia

Imagine que alguém queira encontrar esse parágrafo na busca da nossa aplicação. Esse alguém pesquisa: “evasão escolar do brasil”.

Como isso seria tratado usando apenas um LIKE:

SELECT conteudo FROM artigos WHERE conteudo ILIKE '%evasão escolar do brasil%';

E nosso alguém ficaria frustrado, sem nenhum resultado. Porque ele incluiu a preposição do que não faz parte do trecho contido no parágrafo (nele temos um no).

Agora, se nosso conteúdo está indexado para busca textual, a mesma consulta viraria:

SELECT conteudo FROM artigos WHERE conteudo_fts @@ to_tsquery('evasão escolar do brasil');

E nesse caso, o alguém ficaria feliz com o resultado.

Talvez você esteja perguntando “por que ele não usa Elasticsearch/Sphinx?”. A pergunta é justa e a resposta tão quanto: pra não complicar antes de precisar complicar. Ambos os projetos são excelentes e oferecem recursos avançados/otimizados para busca textual. Mas se sua aplicação já utiliza Postgres e você não tem bilhões de registros para indexar, não existe razão para otimizar prematuramente.

Vale mencionar ainda que em 2019 o Ifood apresentou sua migração da busca do aplicativo para usar esse mesmo mecanismo.

Agora que a gente viu uma vantagem em usar busca textual, precisamos aprender como realmente usar.

Mas vou deixar isso para o próximo artigo porque está ficando muito longo.

Antes de finalizar, só quero deixar aqui que o objetivo é ter mais 2 partes nessa sequência de posts. No último vou explicar como utilizar (ou usar como inspiração) um plugin que desenvolvi para CakePHP que adiciona ao framework suporte a busca textual do Postgres: o Autopage\PgSearch.

Atualizado em 19/10/2021: aplicada a correção na query de exemplo conforme o apontamento feito pelo JC Bombardelli nos comentários. Muito obrigado.

Categorias
Desenvolvimento Web Programação Tutoriais

Git + GPG = Assinando suas contribuições

Recentemente o core do PHP passou por uma situação, no mínimo, inusitada: 2 commits foram feitos em nome de personalidades da comunidade. Isso é possível porque quando registramos uma alteração no git através de um commit, ele registra os dados que queremos que sejam registrados, incluindo o usuário e email que você disse ter (git config user.name "Cauan" e git config user.email "cauan@dominio.com"). Veja que você não precisa validar email nem nada.

Aplicações que hospedam projetos em git geralmente pedem pra gente validar o email usado nos commits, assim são capazes de inferir que aquele código enviado é de quem disse ter enviado. Mas é apenas uma associação “lógica”. O commit diz que foi feito por fulana, e a fulana é uma usuária válida aqui no Github/Gitlab, então vou exibir a foto e nome dessa fulana aqui na interface.

Se o git não garante que quem fez uma alteração é quem diz ser, como a gente pode garantir? Assinando nossas mudanças com uma chave criptográfica.

Felizmente o git permite que a gente especifique uma chave privada configurada no GPG e cuida de todo o processo de assinar nossas mudanças. Na outra ponta, nós compartilhamos a chave pública com o Github/Gitlab para que valide o commit assinado e então possam garantir que aquele código foi feito por quem diz ter feito.

O pessoal do PHP preparou um guia bem completo sobre como configurar o GPG e git para que o incidente não se repita. Vou colocar aqui o resumo do resumo para quem tiver alguma dificuldade com inglês, mas se tiver alguma dúvida, pode usar os comentários.

Atenção: todos os comando devem ser executados com seu usuário normal. Quando necessário, o sudo vai estar explicitado.

O símbolo $ não faz parte do comando, é só pra diferenciar o que é um comando e o que é uma saída de comando. Sempre que tiver o $, você pode copiar o que está na frente e executar no seu terminal.

Primeiro, instale o GPG:

SistemaComando(s)
macOS com Homebrewbrew install gpg
Ubuntu, Debian, Mint, Kalisudo apt install gnupg
CentOS, Fedora, RHELsudo yum install gnupg

Se você usa Zsh como shell, você precisa configurar um tty para que funcione corretamente os comandos interativos com o GPG:

$ mkdir "${HOME}"/.gnupg
$ chmod 700 "${HOME}"/.gnupg
$ >> "${HOME}"/.zshrc echo 'GPG_TTY="$(tty)" && export GPG_TTY || echo "Could not determine TTY: $?" >&2'
$ source "${HOME}"/.zshrc

Verificando que a instalação funcionou:

$ gpg --version | head -2
gpg (GnuPG) 2.2.21
libgcrypt 1.8.6
$ gpg-connect-agent /bye && echo 'GPG ok' || echo 'ERROR: GPG not running'
GPG ok
$ [ -r "${GPG_TTY}" ] && echo 'TTY ok' || echo 'ERROR: TTY not found'
TTY ok

Agora vamos gerar sua chave de assinatura GPG. É imprescindível escolher uma senha forte para proteger sua chave:

$ gpg --batch --generate-key <(echo '
Key-Type: RSA
Key-Length: 4096
Expire-Date: 0
Name-Real: Fulana de Tal
Name-Email: fulana@dominio.com.br
')

Precisamos descobrir a versão curta do ID da chave gerada:

$ gpg -K --keyid-format SHORT
sec   rsa4096/02783663 2020-08-26 [SCEA]
      79694216A0DECA5B53E94E96910A1F8402783663
uid         [ultimate] Fulana de Tal <fulana@dominio.com.br>

Repare no resultado do comando anterior a primeira linha. O que vem após rsa4096/ é o ID que nós queremos, nesse exemplo: 02783663.

Vamos deixar esse valor em uma variável de ambiente para facilitar o restante das configurações, basta executar:

$ export GPG_KEYID=02783663

Estamos terminando, precisamos dizer ao git para usar essa chave para assinar nossas alterações no repositório:

$ git config --global --replace user.signingkey "${GPG_KEYID}"
$ git config --global --replace commit.gpgsign true
$ git config --global --replace tag.gpgsign true

Por último, precisamos informar ao Github/Gitlab nossa chave, para que ele possa verificar o autor das mudanças:

$ gpg --armor --export "${GPG_KEYID}" | pbcopy

pbcopy é um comando do macOS para jogar informação na área de transferência (equivalente a um Ctrl+C). Substitua essa parte pelo comando equivalente no seu ambiente se for outro, ou simplesmente retire o trecho | pbcopy e a chave será exibe no terminal, daí basta selecionar e copiar.

Com a chave copiada, acesse suas configurações:

Feito isso, todos os seus novos commits deve ser assinados e se tudo ocorreu bem, aparecerão como verificados nos Git da vida:

Imagem com uma lista de commits contendo o selo "verified" do Github

Atualizado em 13/04/2021: eu segui esses passos do artigo original em um macOS sem o gpg previamente instalado e com o git relativamente atualizado. Caso seu ambiente já tenha gpg ou a versão do git seja muito antiga, verifique a seguinte resposta no StackOverflow. Agradecimento ao Elton Minetto pela dica nos comentários.

Atualizado em 01/05/2021: como alertou nos comentários o Adjamilton, depois da gente atualizar o conteúdo do arquivo .zshrc precisamos recarregar seus valores usando o comando source "${HOME}"/.zshrc para que as mudanças surtam efeito. O trecho já está atualizado.

Categorias
Desenvolvimento Web

Ambientes de desenvolvimento

Sempre que troco de computador me vejo pensando: será que hoje tem uma forma melhor (leia-se mais moderna) de configurar um ambiente de trabalho pra programador?

No final de 2018 recebi um notebook novo para ser utilizado no trabalho e foi nesse contexto que minha saga começou.

Desde que comecei a usar macOS como sistema principal, optei por deixar o sistema o mais leve/intocado possível, facilitando atualizações e não sobrecarregando a máquina quando não estou usando a trabalho. Pra isso, utilizei algumas formas de virtualização ao longo do tempo.

No começo, optei pelo VirtualBox, gerenciando o provisionamento e configuração com o Vagrant. Por um tempo foi bom, mas toda vez que ia iniciar um projeto novo, precisava configurar DNS, VirtualHost e várias configurações dentro da vm. E isso cansou.

Depois de quebrar a cabeça com o provisionamento algumas vezes, descobri o projeto Laravel Homestead, que resolvia a maioria dos meus problemas: nele é possível configurar novos projetos usando o arquivo de configuração (incluindo criar banco de dados, VirtualHost etc), sem precisar nem logar dentro da vm. Outra grande vantagem do Homestead é fornecer uma vm com diversas versões do PHP pré-configuradas, e usáveis simultaneamente – você pode ter diversos projetos, cada um com a versão do PHP.

Mas um fator ainda me incomodava: o sistema de arquivos do VirtualBox. É extremamente lento, tarefas que depende de disco intensivo (banco de dados e testes unitários, por exemplo) são bastante penalizados. A solução mais “simples” seria abrir a carteira e gastar alguns centenas de dólares em uma solução mais robusta de virtualização – como não tenho esses dólares disponível, nem cogitei a hipótese.

Foi aí que surgiu a oportunidade de me aprofundar nos estudos sobre Docker. Coincidiu com o lançamento do Docker for Mac, que eliminou a necessidade do VirtualBox para rodar os containers. Agora eu tinha a possibilidade de virtualizar o ambiente, com ferramentas nativas do macOS, e containers específicos para cada recurso / serviço necessário – evitando desperdício de RAM/CPU.

Enquanto eu experimentava a construção da imagem perfeita, descobri o Laradock, uma espécie de Homestead voltado para Docker. Com ele, você pode definir diversas dependências pré-configuradas usando um arquivo de ambiente (.env), criar suas imagens de acordo com suas necessidades e executar apenas aquilo que precisa.

Estou usando essa solução a 2 meses e tem me atendido muito bem.

Categorias
Desenvolvimento Web JavaEE Programação

Entrando no mundo “enterprise”

Em Maio/2016 fiz uma mudança radical em minha carreira profissional – optei por sair de uma empresa que trabalha com PHP, CakePHP e outras tecnologias que faziam parte da minha vida desde pelo menos 2008 para embarcar em um projeto novo, com pessoas novas e tecnologias completamente novas para mim.

Passei desde então a trabalhar com Java EE, Oracle Database, Bitbucket/Jira/Bamboo (sempre trabalhei com Github e Gitlab), Angular 2 + Typescript. Foi uma mudança e tanto de paradigmas. Tudo em nome do tão falado “enterprise” (não, eu acho que PHP/CakePHP pode ser tão enterprise quanto JavaEE, mas nem todo o mercado pensa assim).

Aprender a trabalhar com servidor de aplicação, pipelines de compilação – compilação a cada pequena alteração de código, a maior reclamação de um programador acostumado com linguagens interpretadas quando mudam, concorrência em sistemas que não são stateless… é um mundo todo novo, mas interessante.

Quero aproveitar todos esses aprendizados e experiências para voltar a escrever aqui, veremos o que vem a seguir.

Categorias
Desenvolvimento Web Ócio

De “webmaster” a “fullstack”

Num passado remoto, tínhamos o (D)HTML, Frontpage, Macromedia Flash, Fireworks, banco de dados MySQL junto ao ASP ou PHP. Tudo era novidade. Nem tudo funcionava. Duas categorias de trabalho surgiam: o webmaster e o webdesign. Cada um no seu quadrado, trabalhando com ferramentas bem delimitadas.

Os anos foram passando, o mundo evoluindo e novas novas novas novas tecnologias surgindo – HTML, JS, CSS amadureceram, bancos de dados especializados se popularizaram, como Redis, MongoDB, Cassandra; linguagens se multiplicaram.

Ser especialista em PHP não é mais suficiente; é preciso ter domínio de Bash, Python, Ruby, Java, Javascript, Scala, Go e por aí vai.

Querer desenvolver sem saber configurar uma máquina do zero com todos os seus requisitos é impensável – e olha que não são poucos os requisitos: um servidor http (nginx ou apache), o módulo para processamento da linguagem desejada, seu(s) banco(s) de dados (mysql, postgresql, mongodb, redis), ativar cache, serviço de fila de processos, monitoramento de logs, compilador para linguagens intermediárias (typescript, scss, sass).

Acho que só os detalhes para configuração da máquina já renderia um curso superior. Enfim, o mundo da TI está cada vez mais heterogêneo, graças a necessidade de interoperabilidade e em especial, ao movimento opensource – não há como lutar contra.

Nos idos dos anos 2004 iniciei minha carreira como webmaster – embora já me interessasse pelas áreas correlatas.

Hoje, após  13 anos, caminho cada vez mais para o perfil fullstack: gerenciando configurações de máquinas de desenvolvimento e produção, com ferramentas como o Ansible e SaltStack; utilização de linguagens que vão do Javascript ao C++, passando por PHP, Java(EE), Python; bancos de dados diversos (uso e gerenciamento); definição, configuração e acompanhamento de ferramentas para QA (builds e testes automatizados) – como Gitlab + CI, Jenkins, Trevis-CI, Bamboo e SonarQube.

Quando paro para pensar na quantidade de projetos que já participei e nas tecnologias envolvidas, tenho uma imensa satisfação, acompanhada de descrença. No dia-a-dia não vemos o quanto nos adaptamos, e quão rápido é o processo, por isso é legal parar vez ou outra para pensar no que já construímos.

Não consigo imaginar qual será o nome da minha função no futuro, mas espero ter uma lista ainda mais interessante de estudos e trabalhos realizados: que venham novas linguagens, patterns e cervejas.

Categorias
CakePHP Desenvolvimento Web Programação

CakePHP 3.0 – O Fim do Locale

 

A nova versão do meu framework favorito está em estágio avançado e trás uma infinidade de coisas legais.

Dentre as novidades, quero deixar uma dica rápida para um problema comum a qualquer um que não use data/decimais em formato dos EUA: até hoje, para o Cake 1.3 e 2.x eu utilizo o plugin Locale, que já falei a respeito aqui antes.

Com o CakePHP 3.0, o plugin é completamente desnecessário: o novo ORM é capaz de interpretar os dados enviados em formato local para o formato nativo da máquina, de forma transparente.

Você só precisa registrar que os tipos do ORM deve utilizar localização…

Diga ao seu ambiente qual seu locale (pode fazer isso no bootstrap.php):

ini_set('intl.default_locale', 'pt_BR');

Inclua as linhas abaixo no início do seu AppController:

use Cake\Database\Type;
// Habilita o parseamento de datas localizadas
Type::build('date')
 ->useLocaleParser()
 ->setLocaleFormat('dd/MM/yyyy');
Type::build('datetime')
 ->useLocaleParser()
 ->setLocaleFormat('dd/MM/yyyy HH:mm:ss');
Type::build('timestamp')
 ->useLocaleParser()
 ->setLocaleFormat('dd/MM/yyyy HH:mm:ss');

// Habilita o parseamento de decimal localizaddos
Type::build('decimal')
 ->useLocaleParser();
Type::build('float')
 ->useLocaleParser();

Pronto, os dados do seu formulário serão interpretados e convertidos antes de serem salvos.

Para alterar a validação de data, você deve usar algo assim:

$validator->add('birthday', 'valid', [
 'rule' => ['date', 'dmy'], // esse é o importante, onde você avisa que a data estará localizada
 'message' => __('Informe uma data válida')
 ]);

E em substituição ao LocaleHelper, você pode formatar suas datas com o método format disponível (já que o registro agora é um objeto), e no caso de float/decimal, você usa a lib Number.

use Cake\I18n\Number;
$data = $user->birthday->format('d/m/Y');
$salary = Number::format($user->salary);

E é isso, sem nenhum plugin, sua aplicação estará falando português (ou qualquer outra linguagem/localização que você deseje utilizar).