Back-End

13 mar, 2017

Command, Rotas e o Symfony Microkernel

Publicidade

Salve, pessoal. Há tempos quero escrever sobre estrutura de um projeto novo… Daí vem o Mr Paul Jones e começa um movimento quanto a padronização de pacotes. Pois bem, juntei isso a fazer um exemplo um pouco diferente utilizando o Symfony MicroKernel. Vamos fazer uma pequena aplicação juntando as duas coisas.

PDS/Skeleton

Como o próprio projeto descreve: “é um padrão de esqueleto de sistema de arquivos adequado para todos os pacotes PHP”. É uma iniciativa do pmjones (autor do pacote Aura e do livro “Modernizando Aplicações Legadas em PHP”, que está sendo traduzido pelo meu amigo Rogerio Prado de Jesus). Basicamente este padrão define uma estrutura padrão para seu pacote/projeto PHP e ao introduzir esse pacote no seu composer.json você obtém um validador e um “inicializador”, além de “dizer” para todos que você é aderente a esse padrão.

O projeto pode ser acompanhado em http://php-pds.com/.

Então, na raiz do nosso projeto, vamos adicionar nosso composer.json

{
  "name": "duodraco/project-skeleton",
  "description": "Dummy PHP Project Skeleton based upon Symfony Microkernel + PDS/Skeleton (Teaching Purposes)",
  "minimum-stability": "stable",
  "license": "MIT",
  "authors": [
    {
      "name": "Anderson Casimiro",
      "email": "duodraco@gmail.com"
    }
  ],
  "require": {
    "php":"^7.0",
    "symfony/symfony": "^3.2",
    "sensio/framework-extra-bundle": "^3.0"
  },
  "require-dev": {
    "pds/skeleton": "^1.0"
  },
  "autoload":{
    "psr-4":{
      "Duodraco\\":"src/Duodraco"
    }
  }
}

E rodar o composer update.Isso já vai instalar o necessário para as outras etapas. Assim que concluido vamos executar o vendor/bin/pds-skeleton generate . que vai gerar a estrutura de pastas e arquivos sugeridas pelo padrão pds/skeleton:

/sandbox/project-skeleton> ./vendor/bin/pds-skeleton generate .
Created /sandbox/project-skeleton/bin
Created /sandbox/project-skeleton/config
Created /sandbox/project-skeleton/docs
Created /sandbox/project-skeleton/public
Created /sandbox/project-skeleton/resources
Created /sandbox/project-skeleton/src
Created /sandbox/project-skeleton/tests
Created CHANGELOG.md
Created CONTRIBUTING.md
Created LICENSE.md

Agora vamos codificar um pouco.

Symfony MicroKernel

Na versão 2.8 do Symfony foi introduzida a trait MicroKernel. Essa trait encapsula toda a complexidade do startup do Symfony e permite a customização de diversos itens como diretório dos logs, cache, bundles (sim, bundles 😀 ), entre outros. O problema: 10 entre 10 “exemplos” que achei mostram uma aplicação “one-file” e uma evolução usando annotations. Vamos a um outro exemplo, deixando separados os arquivos de configuração, um bootstrap, rotas e usando uma implementação do padrão Command para a execução da Rota — em outras palavras, desconstruindo o Symfony :P.

Vamos a um arquivo básico de entrypoint web, que apontará para o bootstrap.php. O diretório public deve ser utilizado como Document Root segundo o pds/skeleton . Em public/index.php encontraremos:

<?php
require __DIR__.'/../bootstrap.php';

E no ./bootstrap.php, temos:

<?php
const BASE_PATH = __DIR__;
$loader = require __DIR__ . '/vendor/autoload.php';
use Duodraco\Foundation\Kernel;
use Symfony\Component\HttpFoundation\Request;
$kernel = new Kernel('prod', true);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Aqui estamos instanciando o Kernel, criando um objeto de Request padrão do HttpKernel do Symfony, iniciando o processamento desse Request e terminando a aplicação.

Outra regra do pds/skeleton é que devemos usar o diretório src para nosso código fonte. Lembram que no composer.json colocamos um autoload apontando para src/Duodraco? Então criemos estes diretórios e sob o ultimo criemos Foundation, Service e Command. Sob o Foundation vamos colocar nosso Kernel:

<?php
namespace Duodraco\Foundation;
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Routing\RouteCollectionBuilder;
use Symfony\Component\HttpKernel\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
    use MicroKernelTrait;
    public function registerBundles()
    {
        $bundles = [
            new FrameworkBundle(),
            new SensioFrameworkExtraBundle()
        ];
        return $bundles;
    }
    protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
    {
        $loader->load(BASE_PATH . '/config/config.yml');
    }
    protected function configureRoutes(RouteCollectionBuilder $routes)
    {
        $routes->import(BASE_PATH . '/resources/routes/main.yml');
    }
    public function getCacheDir()
    {
        return sys_get_temp_dir() . '/base-app/cache';
    }
    public function getLogDir()
    {
        return sys_get_temp_dir() . '/base-app/logs';
    }
}

O código é autoexplicativo, como deve ser: em registerBundles configuramos os bundles a ser carregados. Em configureContainer configuramos o Container de Injeção de Dependência e parâmetros da aplicação; nesse caso estamos apontando para um arquivo yaml. Em configureRoutes configuramos nosso roteador e mais uma vez estou me valendo de um arquivo yaml para a configuração de rotas. Em getCacheDir e getLogDir estou colocando para fins didáticos tanto o cache como o log em diretórios na área temporária do sistema de arquivos — crianças, não façam isso em casa (produção), sério!

Vamos para nossos arquivos de configuração. O pds/skeleton diz que devemos colocar nossos arquivos de configuração em config na raiz do projeto. O nosso está em config/config.yml. O que não pode faltar, para configurar o Kernel aqui é o secret, usado para as validações de CSRF do HttpKernel.

framework:
    secret: ItsAK1nd0fM@gik...

Agora vamos para a configuração da nossa rota de estudo (também conhecida como index ou raiz :P). O diretório resources serve para colocarmos outros recursos da nossa aplicação que não se enquadrem nos outros diretórios. Como não julgo uma definição de rota, ou um template, como uma configuração, creio que este diretório seja o ideal para receber nosso yaml de rotas. Aqui fiz uma mudança ao padrão do Symfony para nossa próxima parte. Este arquivo fica em resources/routes/main.yml.

index:
  path: '/'
  defaults:
    _controller: 'Duodraco\Command\Main::go'
    service: 'Duodraco\Service\PeopleProvider'
  requirements: { }
  methods: [GET]

Criando a Rota usando Command

Antes de mais nada vamos entender este Design Pattern. No padrão Command um objeto encapsula tudo o que é necessário para executar um método em outro objeto. O que vamos fazer é deixar um objeto com a unica responsabilidade de receber a Request e retornar o Response. A lógica para obter o conteúdo do Response fica a cargo de outro objeto, que nesse caso vamos chamar de Service. Em alguns artigos esse outro objeto pode receber o nome de Commandee ou Receiver — mas seu papel não muda. Para que seja facil adicionar outros Commands e Services futuramente, vamos adicionar duas classes em src/Foundation/Command:

<?php
namespace Duodraco\Foundation\Command;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
abstract class Command extends Controller
{
    /** @var Service */
    protected $service;
    
    final public function go(Request $request, array $attributes = []) : Response
    {
        $this->service = $this->buildService($request->attributes->get("service"));
        return $this->execute($request, $attributes);
    }
    
    protected function buildService(string $serviceName): Service
    {
        $reflectionClass = new \ReflectionClass($serviceName);
        $service = $reflectionClass->newInstanceArgs([$this->container]);
        if($service instanceof Service){
            return $service;
        }
        throw new \Exception("Service not configured");
    }
    
    abstract public function execute(Request $request, array $attributes): Response;
}

Esse é o Command abstrato. Aqui estamos somente estendendo o Controller padrão do Symfony e adicionando a lógica para obter o Service do arquivo de rotas e injetá-lo no objeto. Também declaramos o método execute() para que seja implementado na realização do Command.

<?php
namespace Duodraco\Foundation\Command;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
abstract class Service
{
    use ContainerAwareTrait;
 
    public function __construct(Container $container)
    {
        $this->container = $container;
    }
}

Aqui temos a base de nosso Service. Apenas injetamos a ContainerAwareTrait para facilitar e implementamos o construtor.

Agora vamos ao Command. Ele simplesmente implementa o método execute chamando o método peopleToFollow do Service e populando o JsonResponse.

<?php
namespace Duodraco\Command;
use Duodraco\Foundation\Command\Command;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class Main extends Command
{
    public function execute(Request $request, array $attributes): Response
    {
        $response = $this->service->peopleToFollow();
        return new JsonResponse($response);
    }
}

Abaixo vemos a implementação do Service PeopleProvider:

<?php
namespace Duodraco\Service;
use Duodraco\Foundation\Command\Service;
class PeopleProvider extends Service
{
    public function peopleToFollow(): array
    {
        return [
            "@gabidavila" => "Gabriela D'Ávila",
            "@eminetto" => "Elton Minetto",
            "@abdala" => "Abdala Cerqueira",
            "@dianaarnos" => "Diana Arnos"
        ];
    }
}

Veja que se quisermos obter essa lista de um Banco de Dados, trabalhar melhor uma camada de persistência e hidratação, fazer uma requisição REST, enfim… podemos só modificar esse ponto e o Command continua o mesmo.

Agora atualizamos o Composer e testemos no servidor Built-in do PHP:

/sandbox/project-skeleton> composer.phar update -o
/sandbox/project-skeleton> php -S localhost:8080 -t public/

E voilá:

“Ok, qual a vantagem de dar essa volta toda com essa parada de Command?”

Primeiramente: como eu disse lá em cima, o Command tem só uma responsabilidade. A ideia é que tenhamos um Command por rota. Segundamente: temos uma classe que pode ser testada tranquilamente, item a item e que pode ser reaproveitada por muitos Command. Terceiramente: temos controle “do quê chama o quê” no yaml de rotas, permitindo todas as combinações que quisermos.

Conclusão

Temos uma pequena fundação de sistema, simples mas poderorsa. Podemos adicionar os Bundles que precisarmos, outras bibliotecas, usar o Dependency Injection Container… sob o novo padrão de pacotes.

Ah, esse projeto está no Github 😉

Dúvidas, sugestões, pitacos e afins? Deixe seu comentário! Gostou? Recomende e compartilhe 🙂