“Criando” um micro framework PHP de maneira profissional e escalável

O poder de se trabalhar com componentes

Olá pessoal, tudo bem?

Assim como eu, muitos desenvolvedores PHP trabalham profissionalmente em seu dia-a-dia com algum framework ou plataforma que nos propõe padrões estruturais e organizacionais que nos entregam diversos benefícios como escalabilidade, estabilidade, facilidade de manutenção, baixa curva de aprendizagem da equipe, além de outros incontáveis benefícios.

Se você está iniciando agora com PHP, talvez não seja uma boa leitura pra você, pois trataremos de alguns conceitos de nível intermediário porém vou procurar referenciar ao máximo quando não achar necessário explicar tudo aqui.

Mas por que reinventar rodas?

Em meu dia-a-dia trabalho muito com Magento, me acostumei com a estrutura dos diretórios, organizações e muitos padrões principalmente na versão 2, porém existem muitas outras coisas que preciso fazer e que não têm qualquer ligação com e-commerce como por exemplo algumas automações de rotinas diárias.

E por que não utilizar um micro framework ou full stack disponível no mercado?

Já utilizei vários, sigo utilizando e incentivo o uso para projetos de médio/grande porte, mas tudo depende do que preciso fazer, do tempo que terei que gastar com aprendizagem, etc… O ideal é analisar cada necessidade para decidir o melhor caminho.

Ao longo dos últimos anos tenho sentido necessidade de automatizar tudo e segui por alguns caminhos como:

  • Usar Makefiles;
  • Programar em Shell Script;
  • Programar em Python;
  • Programar em PHP com algumas ferramentas como Robo, Phing, entre outras ferramentas

Todas citadas foram sensacionais e sempre cumpriram o que prometeram, porém eu não estava muito satisfeito em manter as coisas dessa forma ainda.

Venho acompanhando as evoluções dos Frameworks e micro frameworks e um dos que tem chamado muito minha atenção foi o Zend Expressive pela simplicidade, performance e principalmente o poder de escolha em usar somente o mínimo necessário. Analisando sua estrutura, pensei, o que preciso para uma base de um projeto de automação via terminal?

  • Gostaria de ter um gerenciador de configurações;
  • Gostaria de ter um gerenciador de serviços e injeções de dependências;
  • Gostaria de ter uma interface de console/terminal bonita;
  • Gostaria de uma estrutura semelhante a que estou acostumado a trabalhar no Magento, de forma modular onde eu pudesse versionar separadamente cada componente/funcionalidade/conjunto de funcionalidades e pudesse reutilizar isso em outros projetos do mesmo tipo via Composer.

Bem, se você já leu até aqui é porque realmente se interessou, gostaria de compartilhar a boa experiência que tive com isto, então vamos em frente colocar a mão na massa.


Iniciando o projeto

Vamos dar um nome fictício de console-tool (lembrando que poderia ser qualquer nome, na verdade o que estamos fazendo agora é um esqueleto para poder usar como base para qualquer outro projeto), então vamos criar este diretório e inicializar o Composer nele:

O Composer fará algumas perguntas e ao final gerará um arquivo composer.json com conteúdo semelhante o abaixo:

Podemos agora chamar algumas dependências para nosso projeto (poderia na hora do “composer init” também) para suprir as necessidades citadas anteriormente:

Para gerenciador de configurações, usaremos o Zend Config:
$ composer require zendframework/zend-config

Para gerenciador de serviços, injeção de dependências e diversas outras funcionalidades usaremos nesse caso o Zend Service Manager:
$ composer require zendframework/zend-servicemanager

Com estas duas dependências já conseguiremos montar nossa base. Como nesse caso específico a ideia é montar algo que se possa utilizar via terminal, vamos criar uma pasta chamada “bin” e dentro dela um arquivo que iremos chamar aqui de “consoletool” podemos ou não adicionar a extensão .php nesse caso.

$ mkdir bin && touch bin/consoletool

Este arquivo será responsável por executar nossa aplicação, vamos criar mais um arquivo somente para este não ficar muito extenso e também para podermos reutilizar os códigos referentes a autoload da aplicação em outros arquivos executáveis. A partir daqui, usarei estruturas de pastas semelhantes a do Magento, mas você pode fazer da maneira que estiver mais confortável. Por exemplo estou trocando a pasta comumente usada como “src” para “app”. Também não preciso mencionar em todos os casos que as nomenclaturas dos arquivos podem ficar a seu critério.

$ mkdir app && vim app/bootstrap.php

O conteúdo desse arquivo bootstrap.php:

Vamos entender um pouco este arquivo.

Da linha 7 até a linha 16 estamos apenas incluindo o “autoload” do Composer para podermos carregar nossas classes, etc…

Na linha 19, estamos usando a Factory do Zend Config para carregar arquivos de configuração, o bacana é que utilizando esta factory podemos trabalhar com arquivos do tipo xml, yaml, php, ini, json e até JavaProperties por padrão sem precisar de qualquer configuração adicional, além de poder usar outro parser de sua escolha com relativamente pouco trabalho.
Estamos passando como parâmetro para o método glob “app/code/*/*/etc/*.*” por que nesse caso quero carregar as configurações de todos possíveis módulos que eu venha ter e os módulos no Magento são organizados dessa forma: app/code/VendorName/ModuleName
Diretórios que contenham arquivos de configurações são chamados de “etc” no Magento, então nesse caso estou pedindo para o Zend Config buscar por qualquer arquivo de qualquer extensão que esteja dentro do diretório etc de qualquer módulo.

Lembrando que se você criar arquivos de configurações com extensão diferente das que citei, você receberá uma exception.

Na linha 21, carregamos uma configuração global que reescreverá qualquer configuração contida em um módulo e definimos que estas configurações ficarão em app/etc/

Na linha 22 realizamos um merge das configurações dando um replace em qualquer configuração de módulo que existir.

Da linha 24 até a 26, estamos iniciando o Zend Service Manager e passando para ele todas nossas configurações, definimos que as configurações a serem utilizadas pelo Service Manager terão uma key chamada “dependencies”, porém você pode utilizar qualquer uma de seu gosto e logo abaixo estamos criando um novo serviço com um alias “config” e armazenando todas nossas configurações nele, ou seja, se utilizarmos o Service Manager dessa forma:
$serviceManager->get('config');
Obteremos toda configuração de nossa aplicação. Podemos também armazenar essas configurações em cache, mas isso é assunto para um outro post.

Na linha 28 estou retornando o Service Manager para ser armazenado em uma variável ao incluir este arquivo em outro local.

Bem até este ponto, a estrutura de nosso projeto deve estar funcional e conseguimos de dentro de nossos módulos criar serviços via configurações, fabricar objetos, instanciar classes e tudo o que o Service Manager nos entrega bastando apenas incluir o arquivo bootstrap. A este ponto acredito que nosso “Skeleton” estaria pronto para e em condições de… Pois poderíamos por exemplo criar um diretório “public” ou algo do tipo e dentro dele colocar um index.php que chamaria o arquivo bootstrap e adicionar um componente para tratar rotas, etc… enfim… tudo via módulos, este é o ponto de partida. Temos apenas 2 componentes para tornar nosso “skeleton” funcional.


Como o intuito é desenvolver módulos para serem reutilizáveis com este skeleton em diversos outros projetos, devemos nos atentar que não seria uma boa ideia manter as dependências (Composer) de nossos futuros módulos declaradas dentro do arquivo composer.json principal de nosso projeto, caso contrário teríamos que sempre declarar todas as dependências cada vez que fossemos utilizar um módulo e isso seria impossível manter.

O Composer possui uma API legal para criar Plugins, Scripts, etc… porém para esta necessidade específica: “desenvolver módulos com seus composer.json dentro de suas pastas em tempo de desenvolvimento e poder carregar estes arquivos em nossa aplicação principal sem precisar declarar diretamente no composer.json da aplicação principal” existe algo pronto bem bacana:

Composer Merge Plugin

Este cara permitirá nossos módulos terem seus próprios arquivos composer.json dentro da pasta app/code/VendorName/ModuleName em nosso caso.

$ composer require wikimedia/composer-merge-plugin

Precisamos configurar nosso composer.json principal:

Já podemos criar um novo módulo dentro de app/code e declarar nossas dependências, etc… dentro de um arquivo composer.json no diretório raiz do módulo!!!

Aconselho commitar seu projeto, pois o Skeleton está pronto. Sei que falta ainda algo para tratar as configurações dos módulos quando os mesmos forem instalados via Composer, pois estariam dentro da pasta vendor, porém este também é um assunto que ficará para outro post.


Iniciando nosso primeiro módulo

Nosso primeiro módulo será responsável por agregar a funcionalidade de linha de comando para nosso projeto e vamos chamá-lo aqui de Console.

Começamos criando a estrutura de diretórios de nosso módulo, nesse exemplo usarei minhas inicias como VendorName do módulo, mas fique a vontade para usar as suas :)

$ mkdir -p app/code/ROB/Console

Iremos utilizar um componente fantástico do Symfony (aliás, acho fantástico todos os componentes do Symfony) chamado Symfony Console. então o arquivo composer.json de nosso módulo terá o seguinte conteúdo:

No atributo “name”, utilizei meu username do github seguido do nome da aplicação e o nome do módulo, mas o ideal seria você criar um projeto no github e ter um namespace de repente com o nome do projeto e ter algo como: nomedoprojeto/nomedomodulo

No atributo “type” criei também um nome, é bacana você utilizar o nome de sua aplicação seguido de -module, ou algo do tipo que seja bem específico que identifique que realmente é algo para funcionar em sua aplicação, pois dessa maneira futuramente você poderá tratar a forma como seus módulos serão instalados via Composer e o type seria um modo de identificá-los.

Reparem que no autoload psr-4 colocamos o namespace como key e não preenchemos o valor, pois não precisamos especificar um diretório já que este arquivo composer.json está dentro de nosso módulo: app/code/ROB/Console/composer.json

Configurando e executando o Symfony Console

O Symfony Console têm uma API bem simples para ser utilizado, você pode simplesmente instanciar a classe Application passando como parâmetro o nome de sua aplicação e a versão, após isso executar o método addCommands e passar como argumento um array contendo literalmente novas instancias de seus Commands como o exemplo abaixo:

Simples assim, com isso você teria o Symfony Console funcionando em qualquer projeto, porém aqui estamos preocupados com escalabilidade, manutenção, baixo acoplamento, etc… e tratar a funcionalidade de console apenas como um módulo de nosso projeto.

A melhor maneira que encontrei para configurarmos isto em nosso projeto foi criando uma Factory utilizando o Service Manager, mas logo me deparei com algo pronto:

O que ele faz é uma Factory que implementa a FactoryInterface do Service Manager para fabricar o objeto (Aplication) do Symfony Console já com os comandos instanciados, basta que estejam declarados nas configurações e existam.

Vamos à instalação (dentro da pasta do módulo, execute este comando, depois pode apagar a pasta vendor criada dentro do módulo):

$ composer require dwendrich/console-config-resolver

Após a instalação, rode o update do Composer no diretório principal da aplicação para que seja carregada esta nova dependência no projeto.

Agora vamos criar um diretório de configuração dentro de nosso módulo:

$ mkdir -p app/code/ROB/Console/etc

E criar um arquivo config.php (xml, json, yaml, JavaProperties) com o seguinte conteúdo:

Na verdade você poderia retirar daí a parte de configuração referente a name, version e colocá-las em um arquivo de configuração global em app/etc, pois isso pode mudar de projeto para projeto.

Dentro da key commands basta declarar suas classes de Command, como no exemplo ou mesmo instanciando-as diretamente dentro desse array. Então agora em qualquer módulo futuro, podemos criar novos Commands e disponibilizarmos apenas configurando dentro do próprio módulo:

<?php
return [
'ROB\Console' => [ 'commands' => [
CustomCommand::class,
new InstanceOtherCommand('my:command')
]
]
];

Precisamos apenas editar o arquivo que criamos lá no início dentro do diretório “bin” chamado consoletool

Pronto, podemos executar no terminal na raíz de nosso projeto dessa forma:

$ php bin/consoletool

Vale lembrar que é necessário retirar da configuração a classe MyConsoleCommandClass::class já que a mesma não existe, a menos que você a crie.

Que surtirá em um output como este:


Conclusão

Criamos uma estrutura com pouquíssimos componentes, somente o que realmente precisamos para o contexto proposto nesse post. A partir daí poderíamos criar diversos outros módulos, versioná-los de forma separada da aplicação, declarar dependências no composer.json de cada módulo e com o nosso primeiro módulo instalado podemos criar Commands em qualquer outro módulo adicionando tudo via configuração do próprio módulo, sem interferir em nada no esqueleto da aplicação.

Em minha concepção é um caminho bacana a se seguir, pois apesar de talvez enfrentar problemas comuns como incompatibilidade de versões das dependências instaladas pelo Composer nos diversos módulos é uma maneira bem desacoplada e profissional de cada módulo crescer em sua story com seus próprios testes, etc… sem se preocupar com o restante do sistema.

Próximos passos seriam criar módulos como se fossem adaptadores para outras tecnologias, exemplo: módulo Doctrine, módulo de usuários que depende do módulo Doctrine e do módulo Console e assim por diante.

Espero que tenham gostado, pois deu um trabalhinho. Me deixem saber se é interessante continuar escrevendo.

Até a próxima.

    Rafael Ortega Bueno

    Written by

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade