Desacoplando frameworks no PHP - o Padrão Adapter

O que é e como utilizar o Design Pattern Adapter no PHP para desacoplar micro-frameworks da aplicação.

Desacoplando frameworks no PHP - o Padrão Adapter
Photo by Mourizal Zativa on Unsplash

Com a quantidade de frameworks fullstack e de opinião forte - como Laravel, Yii, CodeIgniter e Symfony uma ressalva para esse último - no universo PHP, às vezes parece impossível que consigamos desacoplar o framework de nossa aplicação.

Por sorte, temos os chamados micro-frameworks - como Slim, Lumen, Phalcon e podemos considerar, também o Symfony nessa lista - o que torna essa tarefa um pouco menos árdua.

Nesse artigo iremos desacoplar a utilização de um framework utilizando o Padrão de Projeto Adapter e ainda, "chavear" entre três frameworks diferentes - Slim, Symfony e Lumen - com pouquíssima alteração no código. Não perca!

O que é um padrão de projeto?

Em resumo um padrão de projeto (ou design pattern) é uma solução conhecida, validada e reutilizável para um problema comum.

O Padrão Adapter

O Design Pattern Adapter é um dos 23 padrões de projeto presentes no livro  "Design Patterns: Elements of Reusable Object-Oriented Software" - Gang Of Four que se enquadra na categoria de padrões de Estrutura.

O objetivo principal do padrão adapter é converter a interface de uma classe em outra, permitindo que classes com interfaces incompatíveis trabalhem em conjunto. Veremos esse padrão em ação mais adiante.

Tá na hora do código!

Sobre o código: código de exemplo consiste em uma API que retorna uma listagem fixa de 3 livros. Utilizaremos PHP como linguagem de programação, porém é possível aplicar as técnicas em qualquer linguagem.

Nosso desafio: desacoplar o framework da aplicação possibilitando a substituição do mesmo com pouca ou nenhuma alteração significativa no código.

Conhecendo as implementações

Antes de utilizarmos o padrão adapter, vamos conhecer as diferentes implementações afim de identificarmos os pontos em comum entre elas. Vamos começar com a implementação do Slim criando a classe SlimHttp.

Nos exemplos iremos suprimir alguns trechos do código para facilitar a leitura

<?php declare(strict_types=1);
...
class SlimHttp
{
  public App $app;

  public function __construct()
  {
    $this->app = AppFactory::create();
    $this->app->addErrorMiddleware(true, false, false);
    $this->app->get('/books', function(ServerRequestInterface $request, ResponseInterface $response) {
      $books = [
        (object) ['title' => 'Clean Code'],
        (object) ['title' => 'Refactoring'],
        (object) ['title' => 'Implementing Domain-Driven Design'],
      ];
      $response->getBody()->write(json_encode($books));
      return $response->withHeader('Content-Type', 'application/json');
    });
    $this->app->run();
  }
}
SlimHttp.php

O que estamos fazendo nesse código:

  1. Construindo a aplicação Slim
  2. Criando uma rota /books que retorna uma lista de livros em formato JSON
  3. Iniciando a aplicação

Vamos criar uma nova implementação, semelhante ao Slim, mas utilizando o Lumen, essa é nossa classe LumenHttp:

<?php declare(strict_types=1);
...
class LumenHttp
{
  public Application $app;

  public function __construct()
  {
    $this->app = new Application();
    $this->app->router->addRoute('GET', '/books', function (Request $request, Response $response) {
      $books = [
        (object) ['title' => 'Clean Code'],
        (object) ['title' => 'Refactoring'],
        (object) ['title' => 'Implementing Domain-Driven Design'],
      ];
      $response->setContent(json_encode($books));
      $response->header('Content-Type', 'application/json');
      $response->send();
    });
    $this->app->run(); 
  }
}
LumenHttp.php

Conseguimos identificar alguns padrões entre elas, os principais são:

  1. Precisamos definir as rotas
  2. Precisamos executar a aplicação para que as rotas sejam processadas

Poderíamos utilizar ambas as classes normalmente em nossa aplicação simplesmente instanciando o framework desejado, nosso public/index.php seria mais ou menos assim:

<?php declare(strict_types=1);
...
require __DIR__ . '/../vendor/autoload.php';
...
new SlimHttp();
// OU
// new LumenHttp();
public/index.php

Nossa aplicação funcionaria normalmente sem grandes alterações, atingimos nosso objetivo, certo? Errado!

Alguns problemas de implementar dessa maneira:

  1. Precisaríamos definir todas as rotas novamente a cada novo framework;
  2. Precisaríamos replicar as configurações de uma rota nova para todas as implementações dos framework

Tudo isso seria insustentável no longo prazo, portanto vamos construir e implementar o padrão adapter.

Construindo o padrão

Agora que já temos características em comum entre elas, vamos definir a interface do nosso adaptador, que chamaremos de Http:

<?php declare(strict_types=1);
...
interface Http
{
  public function route(string $method, string $url, callable $callback);
  public function run();
}
Http.php

Nossa interface é bem simples e define apenas dois métodos:

  1. route que recebe o método, a url e uma função de callback e é responsável pela definição das rotas
  2. run que é responsável por executar a aplicação

Lembrando que o nome, assinatura e quantidade de métodos pode variar conforme a necessidade.

Implementando o Padrão Adapter

Para que possamos utilizar efetivamente o padrão Adapter precisamos fazer alguns ajustes em nossas implementações iniciais, vamos começar alterando a classe SlimHttp

<?php declare(strict_types=1);
...
class SlimHttp implements Http
{
  public App $app;

  public function __construct()
  {
    $this->app = AppFactory::create();
    $this->app->addErrorMiddleware(true, false, false);
  }

  public function route(string $method, string $url, callable $callback)
  {
    $method = strtolower($method);
    $this->app->$method($url, function(ServerRequestInterface $request, ResponseInterface $response) use($callback) {
      $result = $callback($request->getQueryParams(), $request->getParsedBody());
      $response->getBody()->write(json_encode($result));
      return $response->withHeader('Content-Type', 'application/json');
    });
  }

  public function run()
  {
    $this->app->run();
  }
}

Agora que implementamos uma interface, precisamos definir as funções conforme o contrato estabelecido, nesse caso, somos obrigados a definir os métodos route e run.

Além disse fizemos algumas alterações na implementação do método route para que a construção da rota pelo framework ficasse genérica.

Vamos repetir esse procedimento em nossa classe LumenHttp:

<?php declare(strict_types=1);
...
class LumenHttp implements Http
{
  public Application $app;

  public function __construct()
  {
    $this->app = new Application();
  }

  public function route(string $method, string $url, callable $callback)
  {
    $this->app->router->addRoute(strtoupper($method), $url, function (Request $request, Response $response) use($callback) {
      $result = $callback($request->query->all(), $request->request->all());
      $response->setContent(json_encode($result));
      $response->header('Content-Type', 'application/json');
      $response->send();
    });
  }

  public function run()
  {
    $this->app->run(); 
  }
}

Com exceção de alguns detalhes de implementação que são características do Lumen, a implementação dos métodos route e run são bem semelhantes.

Agora vamos atualizar nosso entrypoint public/index.php para utilizar o padrão adapter que acabamos de construir.

<?php declare(strict_types=1);
...
require __DIR__ . '/../vendor/autoload.php';
...
$http = new SlimHttp();

$http->route('get', '/books', function($params, $body) {
  $books = [
    (object) ['title' => 'Clean Code'],
    (object) ['title' => 'Refactoring'],
    (object) ['title' => 'Implementing Domain-Driven Design'],
  ];
  return $books;
});

$http->run();
public/index.php

O que estamos fazendo:

  1. Construindo um objeto $http correspondente ao framework que queremos utilizar
  2. Definindo as rotas
  3. Executando a aplicação

Com isso se quisermos utilizar o Lumen, só precisamos alterar a construção do $http para $http = new LumenHttp() e todo o resto funciona normalmente.

Mas e se eu quiser utilizar um terceiro framework?

Utilizando o padrão adapter precisamos apenas seguir os mesmos passos para os dois primeiros:

  1. Criar uma classe que implementa a interface Http
  2. Trocar a implementação no public/index.php

Vamos ver na prática o que teríamos que fazer no nosso código para utilizar o Symfony:

  1. Começamos criando a classe SymfonyHttp que implementa a interface Http
<?php declare(strict_types=1);
...
class SymfonyHttp implements Http
{
  public $routes;
  public RequestContext $context;

  public function __construct()
  {
    $this->routes = new RouteCollection();
    $this->context = new RequestContext();
  }

  public function route(string $method, string $url, callable $callback)
  {
    $this->routes->add('books', new Route(
      '/books',
      ['handler' => function(Request $request) use($callback) {
        $result = $callback($request->query->all(), $request->request->all());
        $response = new Response(json_encode($result));
        $headers = new ResponseHeaderBag(['Content-Type' => 'application/json']);
        $response->headers = $headers;
        $response->send();
      }],
      [],
      [],
      '',
      [],
      [strtoupper($method)]
    ));
  }

  public function run()
  {
    $request = Request::createFromGlobals();
    $this->context->fromRequest($request);
    $matcher = new UrlMatcher($this->routes, $this->context);
    $parameters = $matcher->match($this->context->getPathInfo());
    $parameters['handler']($request);
  }
}
SymfonyHttp.php

2. Trocar a implementação no public/index.php

<?php declare(strict_types=1);
...
require __DIR__ . '/../vendor/autoload.php';
...
$http = new SymfonyHttp();

$http->route('get', '/books', function($params, $body) {
  $books = [
    (object) ['title' => 'Clean Code'],
    (object) ['title' => 'Refactoring'],
    (object) ['title' => 'Implementing Domain-Driven Design'],
  ];
  return $books;
});

$http->run();
public/index.php

E pronto nossa aplicação deve estar funcionando conforme o esperado mas utilizando o Symfony.

Conclusão

Vimos nesse artigo como o Padrão de Projeto Adapter pode nos ajudar quando precisamos variar implementações de diferentes bibliotecas ou frameworks que servem a um mesmo propósito (e são relativamente semelhantes entre si).

No entanto para frameworks mais opinativos (como Laravel ou Yii) esse tipo de troca é inviável uma vez que eles possuem diversas dependências e estruturas exclusivas do framework e que servem justamente para estabelecer um padrão entre todos os usuário daquele framework.

O código na íntegra está disponível no github e é parte de um projeto pessoal de um mini e-commerce utilizando Clean Code e Clean Architecture e que está ainda se encontra desenvolvimento:

GitHub - virb30/cc-ca-php
Contribute to virb30/cc-ca-php development by creating an account on GitHub.