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.

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();
}
}
O que estamos fazendo nesse código:
- Construindo a aplicação Slim
- Criando uma rota
/books
que retorna uma lista de livros em formato JSON - 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();
}
}
Conseguimos identificar alguns padrões entre elas, os principais são:
- Precisamos definir as rotas
- 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();
Nossa aplicação funcionaria normalmente sem grandes alterações, atingimos nosso objetivo, certo? Errado!
Alguns problemas de implementar dessa maneira:
- Precisaríamos definir todas as rotas novamente a cada novo framework;
- 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();
}
Nossa interface é bem simples e define apenas dois métodos:
route
que recebe o método, a url e uma função de callback e é responsável pela definição das rotasrun
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();
O que estamos fazendo:
- Construindo um objeto $http correspondente ao framework que queremos utilizar
- Definindo as rotas
- 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:
- Criar uma classe que implementa a interface
Http
- 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:
- Começamos criando a classe
SymfonyHttp
que implementa a interfaceHttp
<?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);
}
}
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();
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: