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
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
CakePHP

Dica Rápida: Migrando Aplicação CakePHP 3.x para 4.x

Embora tenha havido um grande esforço em manter o máximo de compatibilidade entre as duas recentes versões do framework, ao migrar uma aplicação de complexidade razoável encontrei várias dificuldades não tão bem documentadas.

Pra me poupar passar por esses problemas no futuro, e talvez te poupar de dor de cabeça também, aqui vai um resumo do que observar:

  • Rotas e links: revisite todas as suas rotas e criação de links/redirects. Nomes de prefixos, plugins e controladores devem ser escritos como são, sem alteração para caixa baixa (lowercase) e underscore. O que antes seria my_products agora deve ser MyProducts, como no nome da classe MyProductsController. No caso dos links, se antes ao usar o UrlHelper não havia problema de uma rota não ter sido mapeada, agora você terá uma exceção.
  • Plugins: não defina ou carregue suas rotas no bootstrap.php do plugin, isso fará com que a aplicação tenha qualquer outra rota ignorada.
  • Mocks: com a nova versão, veio também o uso do phpunit 8.5, que por sua vez, introduz algumas quebras de compatibilidade. Isso afeta em especial a criação de mocks em modelos ligados a behaviors (se você tentar usar o mock em um método do behavior). No pull-request que abri para o futuro CakePHP 4.1, é resolvida o problema de warnings gerados, porém, em alguns testes meus, é obrigatório incluir algum método concreto da Table/Model que está sendo mockada junto com o método de behavior na lista de métodos no mock – caso contrário, o warning volta a aparecer (esse problema acontecia por uma falha no pull-request que criei, mas fiz um segundo com uma implementação melhor e não há mais necessidade disso).
  • Tests: se em algum teste você faz um $this->loadPlugin('MyPlugin'); no seu setUp(), se esse plugin possuir rotas, as rotas da sua aplicação não serão carregadas. Houve uma mudança na verificação de inicialização das rotas e necessidade de carregamento – antes, havia uma variável de controle que só era definida após carregar as rotas da aplicação, agora, só é checado se já existe alguma rota declarada, e se houver, supõe-se que todo procedimento já foi realizado com sucesso.

Destes problemas, o mais trabalhoso de corrigir é o com as rotas/links – os outros três a dificuldade foi realmente entender o que estava acontecendo.

Categorias
CakePHP PHP

CakePHP 3 e Elasticsearch

Tanto o CakePHP quanto o Elasticsearch fazem parte da minha vida a alguns anos. No começo foi um pouco traumático – era preciso fazer chamadas via REST sem nenhuma abstração, utilizando curl ou streams.

Hoje contamos com diversas camadas intermediárias para facilitar a integração entre ambos, como o cliente em baixo nível oficial e o cliente em alto nível (Elastica).

Há ainda um projeto de datasource oficial que, embora nunca tenha chego em um release final, já incorpora algumas funcionalidades importantes da integração para uso do Elasticsearch como um backend para os repositórios no CakePHP.

Este projeto se chama cakephp/elastic-search e nas últimas semanas tive a oportunidade de ajudar em uma grande refatoração para torna-lo compatível com o Elasticsearch > 6 – a versão anterior do projeto suportava versões até a 2.5.

Dentre as mudanças mais importantes estão:

  • Troca da entidade Type pela Index, seguindo a diretriz do ES de remover suporte e múltiplos tipos em um mesmo índice;
  • Cada Index passa a especificar seu nome e tipo, em um mapeamento 1×1, como é obrigatório no ES desde a versão 6.0;
  • Atualização da dependência Elastica, para versão corrente, permitindo uso de todos os recursos recentes tanto do ES quanto do client.

Caso tenham alguma dúvida sobre uso e não queiram utilizar o github (por conta da língua ou outro motivo), utilizem os comentários que tentarei responder o mais rápido possível.

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).

Categorias
CakePHP Desenvolvimento Web PHP

CakePHP: Plugin Locale

Vamos falar um pouco sobre outro plugin para CakePHP que surgiu no coração da Radig: o Locale.

Meu amigo José Agripino já apresentou o plugin no próprio blog da Radig, mas como reescrevi quase que totalmente o plugin nos últimos dias, acredito ser a hora de falar dele novamente.

Nada melhor para ver a utilidade de algo como imaginar uma situação de uso real, então vamos lá…

Cenário 1: você desenvolve um sistema para brasileiros, e quer permitir a entrada de informações em formato local, isto é, datas com dia/mês/ano e números com vírgula separando decimais. O problema é que estes dados são inválidos em um banco de dados convencional (como MySQL e PostgreSQL). Ao tentar salvar uma data formatada com dia/mês/ano você receberá um erro como resposta. Como resolver isso? Use o behavior Locale no seu modelo.

Basta adicionar o behavior Locale no modelo que ele fará a conversão de datas e números para o formato americano.

public $actsAs = array('Locale.Locale');

É possível converter automaticamente datas, datas acompanhadas de horas e decimais/floats.

Cenário 2: você já tem os dados do seu usuário armazenados no banco (formato padrão/americano) e quer apresenta-los em um formato local na sua View, o que fazer? Use o Helper Locale em sua view. Primeiro ative o helper no seu controller:

public $helpers = array('Locale.Locale');

Agora basta usa-lo na view:

echo $this->Locale->date($this->data['User']['birthday']);

É possível formatar data, data com hora, data literal (quarta-feira 18 de abril de 2012, por exemplo), decimais como 53,42 e valores monetários ( R$ 53,42 ).

Além do Behavior e do Helper, você pode carregar as libs Localize e Unlocalize em qualquer parte de seu sistema para converter entre os dois diferentes formatos. As libs são estáticas e suportam aninhamento de método, assim você pode fazer:

echo Localize::setLocale('pt_BR')->decimal(12.45); // 12,45

A unica configuração necessária é a definição do locale de sua aplicação, que pode ser feito no próprio bootstrap.php do Cake:

setlocale(LC_ALL, 'pt_BR');

Assim como outros plugins da Radig, você pode consultar os testes incluídos para ver melhor o funcionamento deste.

Se for utilizar, nos avise, será uma grande satisfação ver que o plugin é util para outros.

Há uma versão compatível com o CakePHP 1.3 e outra com o CakePHP 2.x, basta usar o branch correspondente.

Categorias
CakePHP Desenvolvimento Web PHP

[CakePHP] Acl: Problema com Acos “duplicados”

Quando falamos em Acl e CakePHP muitos tem a lembrança de horas lutando contra um monte de código para tentar fazer funcionar a autenticação e permissionamento. Bastam algumas dezenas de projetos e você fica craque em configura-lo.

Porém vez ou outra aparece uma dúvida que te faz perder várias horas debugando e as vezes termina isso sem uma solução razoável.

Trabalhamos muito com Plugins na Radig e um problema que enfrentávamos de vez em quando era o de ter um plugin com o mesmo nome de uma ação de controller. Nestes, quando você verifica a permissão para a ação usando uma sintaxe de caminho parcial, isto é, algo como:

$this->Acl->check('acao', 'Fulano');
$this->Acl->check('Controller/acao', 'Fulano');
$this->Acl->check('Plugin/Controller/acao', 'Fulano');

Um erro é retornado, dizendo que o Aco não pode ser verificado (lembrando que para o exemplo, Plugin teria o mesmo nome de acao).

Isso foi até assunto de um bug reportado para o CakePHP, afirmando que a falha estava no fato das comparações no banco de dados serem, na maioria das vezes, case-insensitive. De fato, como respondeu o Mark Story, uma forma de resolver este “problema” é utilizar no banco de dados um COLLATION que seja de fato case-sensitive. O problema nisso é que a maioria dos conjuntos de caracteres, ao menos no MySQL, são case-insensitive, então você teria de mudar todos os seus banco de dados para corrigir isso.

Porém o usuário nlcO postou uma dica interessante: basta usar o caminho completo do Aco que não haverá conflito, mesmo quando controllers, plugins ou actions tiverem os mesmos nomes. Mas como usar o caminho completo? Basta ver qual é seu Aco raiz (que possuí o parent_id = NULL) e ir incluindo após ele todos os subsequêntes – plugins, controllers e actions, até formar o caminho completo.

No meu exemplo ficaria:

$this->Acl->check('aplicacao/Plugin/Controller/acao', 'Fulano');
Categorias
CakePHP Desenvolvimento Web PHP

HTML5: Problemas com Input type=”number”

Opa, esse é mais um aviso.

Recentemente estava trabalhando em um sistema com CakePHP 2.1 e ao tentar editar um registro onde um dos campos era do tipo float, o valor que estava no banco não era apresentado no formulário, embora a tag input estivesse com o atributo value preenchido corretamente. Isso aconteceu comigo no Chrome 17, no Firefox 10 não houve problema porque ele utiliza input text normal.

Um detalhe importante é que eu utilizo o Helper Locale para formatar os números decimais para meus usuários, assim o que vem do banco como “12.58” vira “12,58” formato que usamos no Brasil. Talvez se usasse ponto como separador de decimais não teria problema – o que não é possível pra mim.

Ao pesquisar um pouco descobri um bug no Chromium relacionado a isso reportado no link http://code.google.com/p/chromium/issues/detail?id=44116 . Não consegui entender o motivo mas foi marcado como Wontfix.

A saída foi sobrescrever o FormHelper para utilizar input do tipo text quando o número vindo é um ponto flutuante/decimal. Se você não trabalha com CakePHP, mas trabalha com números decimais separados por vírgula, a dica continua valendo: utilize input com o tipo text ao invés de number.

Aqui tem um commit onde implementamos a “correção” em um FormHelper que estende o do CakePHP.

Categorias
CakePHP PHP

[CakePHP] Dica Rápida – Usando shell de múltiplas versões

Tirando a poeira disso aqui…

Desde que comecei com CakePHP me sentia frustrado por não conseguir utilizar o shell de diferentes versões sem precisar alterar meu ambiente de trabalho. Na época meu problema era ter projetos rodando a versão 1.2 e outros rodando 1.3.

Ontem me deparei novamente com o problema e cheguei até a sugerir um alias embutido no CakePHP, porém a ideia foi sabiamente rejeitada.

A solução para isso é mais simples do que parece (se você usa Linux e Bash, pelo menos): basta criar uma alias de comando para cada uma das versões do CakePHP.

Como fazer

  1. Abra o arquivo ~/.bashrc (se não existir, crie-o);
  2. Para cada versão do CakePHP você vai criar um alias seguindo este “template”:
alias cake13="~/pastas_ate_chegar_ao_cake/cakephp/cake/console/cake"

Meu arquivo ficou assim:

alias cake13="~/develop/php/cake13/cake/console/cake"
alias cake2="~/develop/php/cake2/lib/Cake/Console/cake"

Agora é só fechar e abrir novamente o terminal e usar os comandos “cake2”, “cake13” ou outro que você tenha criado. Works like a charm ;]

Categorias
CakePHP Desenvolvimento Web Programação Projetos

Mantendo uma base de código organizada e documentada

Uma problemática comum de quem desenvolve sistemas é como manter a documentação em dia, se que isso comprometa os prazos de desenvolvimento.

Digamos que isso é um problema de otimização:

  1. um código bem documentado facilita e muito a sua manutenção;
  2. documentar código leva tempo;
  3. tempo é um recurso escasso em desenvolvimento de software;

Olha o problema… sem tempo, não há documentação e sem documentação você precisará de mais tempo para dar manutenção – oras, mas você já não tinha tempo para documentar, como vai ter mais tempo agora para dar manutenção?

Tentando equacionar esse problema surgiram várias ferramentas que visam facilitar todas as atividades relacionadas ao desenvolvimento.

Como a maior parte do meu tempo dedico ao PHP e CakePHP, tomarei estes como base para as ferramentas, porém várias delas podem ser utilizadas com outras linguagens/frameworks sem grandes problemas ou então possuem similares em outras linguagens.

Padrão de código

A primeira etapa, e talvez a mais importante, seja definir e disponibilizar um conjunto de regras explicando como o código foi escrito.

Este padrão envolve nome de classes, atributos, métodos, comentários, tabelas e colunas do banco de dados, organização de diretórios dentre outras coisas. Até coisas simples como a indentação deve ser padronizada.

Veja alguns guias de codificação para ter um exemplo do que quero dizer:

Versionamento de código

Um dos recursos mais importantes durante o desenvolvimento é a capacidade de se desfazer determinada alteração e manter um registro de todas as alterações feitas durante o desenvolvimento.

Atualmente, minha ferramenta favorita para versionamento é o Git um sistema distribuído de controle de versão. Porém existem vários outros que podem agradar, como o centralizador SVN e os também distribuídos Mercurial e Bazaar.

É muito fácil trabalhar com qualquer um destes sistemas e após conhecer as facilidades que o versionamento de código lhe proporcionam, você terá dificuldade em trabalhar com código sem controle de versão, pode apostar.

Caso você opte por um sistema distribuído, dê uma olhada neste modelo de organização para seu código: A successful Git branch model

Versionamento do Banco de Dados

Por melhor que seja o projeto do seu sistema uma coisa sempre ocorrerá: mudança. E isso envolve mais do que código, muitas vezes alterações na estrutura do banco são necessárias.

Como controlar essas alterações? A resposta é “Migrations”

No CakePHP precisamos de plugins para dar essa capacidade a aplicação, há dois largamente utilizados:

Outros frameworks fornecem suporte “nativo” ao recurso, como o Ruby on Rails e Doctrine para PHP em geral.

Testes unitários

Acredito que todos os frameworks modernos fornecem suporte a criação de testes unitários em seus projetos. Os testes são uma fase importante do design do software e fundamental para documentação de qualidade.

O CakePHP até sua versão 1.3 utiliza o framework de testes SimpleTest, porém passará a utilizar o PHPUnit em sua versão 2.0 (atualmente em desenvolvimento).

Não sabe o que são testes unitários? Bom, segue alguns links sobre o assunto:

Testes são como controle de versão… depois que você usa, não vive sem.

Documentação

Como comentado anteriormente, um passo importante para documentação são os testes unitários. Porém não devem ser o único.

Uma forma muito eficiente de documentação são os blocos de comentários, no PHP, o padrão PHPDoc é o mais utilizado.

Existem várias ferramentas que varrem o código de sua aplicação e identificam esses blocos para gerar a documentação, alguns deles são:

Quando sua documentação é concisa e completa, entender o funcionamento da aplicação passa a ser fácil, independente de quando ela foi criada. Quando isso é aliado aos testes unitários, fazer manutenção passa a ser uma atividade mais fácil e gratificante.

Por fim, é preciso saber o que/quando está errado e como/quando foi corrigido, ajudando na manutenção do histórico e acompanhamento da evolução do software. Para isso temos o tópico a seguir.

Controle de Bugs/Atividades

Como controlar o que, quando e por quem uma determinada atividade deve ser feita? E como verificar por quem e quando determinada funcionalidade foi implementada? O controle de versão pode fornecer parte destas respostas, mas ficar analisando logs normalmente não é muito comodo. A melhor maneira é utilizar uma ferramenta de controle de bugs/atividades.

Utilizo no meu dia-a-dia o excelente Redmine, um sistema simples porém poderoso para controle de tarefas. Suporta diferentes projetos, sub-projetos, integra-se com vários sistemas de versionamento de código, permite criação de wikis para documentação além de vários outros recursos.

Além deste, existem vários outros sistemas como o Bugzilla, Trac, Mantis e PHProjekt.

Conclusão?

Não, não tem conclusão. O texto visa apenas apresentar algumas atividades que juntas ao planejamento e desenvolvimento de software tendem a tornar a vida dos desenvolvedores melhor, seja diminuindo o stress causado por alterações dos requisitos ou manutenção de código mal projetado/escrito, seja tornando o desenvolvimento mais ágil, permitindo mais horas de lazer e descanso e menos fios de cabelo branco.

A intenção nunca foi cobrir todos os tópicos a exaustão, mas sim apresentar alguns exemplos e motivos para adoção de tais ferramentas/ideias. Caso tenha surgido dúvida a respeito de qualquer item, deixe um comentário =]

Sentiu a falta de algum item? Utiliza algo de forma diferente? Deixe um comentário também.

————-
Postado originalmente no blog da Radig