Imaster

Slim e Action-Domain-Responder

Eu tenho um lugar quentinho no meu coração para Slim há um bom tempo, e especialmente, desde que reconheço o padrão Action-Domain-Responder. Neste artigo, vou mostrar como refatorar o aplicativo tutorial Slim para ADR.

Uma coisa agradável sobre o Slim (e a maioria dos outros frameworks de interface de usuário HTTP) é que eles já estão orientados para a “ação”. Ou seja, seus roteadores não presumem uma classe de controlador com muitos métodos de ação. Em vez disso, eles presumem um fechamento de ação ou uma classe invocável de ação única. Então, a parte Ação/Action do Action-Domain-Responder já existe para o Slim. Tudo o que é necessário é retirar os bits estranhos das Ações, para separar mais claramente seus comportamentos dos comportamentos do Domínio e do Respondente.

I.

Comecemos por extrair a lógica do domínio. No tutorial original, as Ações usam dois mapeadores de fonte de dados diretamente e também incorporam alguma lógica de negócios. Podemos criar uma classe de Camada de Serviço chamada TicketService e mover essas operações das Ações para o Domínio. Isso nos dá essa classe:

ticket_mapper = $ticket_mapper;
 $this->component_mapper = $component_mapper;
 }
public function getTickets()
 {
 return $this->ticket_mapper->getTickets();
 }
public function getComponents()
 {
 return $this->component_mapper->getComponents();
 }
public function getTicketById($ticket_id)
 {
 $ticket_id = (int) $ticket_id;
 return $this->ticket_mapper->getTicketById($ticket_id);
 }
public function createTicket($data)
 {
 $component_id = (int) $data['component'];
 $component = $this->component_mapper->getComponentById($component_id);
$ticket_data = [];
 $ticket_data['title'] = filter_var(
 $data['title'],
 FILTER_SANITIZE_STRING
 );
 $ticket_data['description'] = filter_var(
 $data['description'],
 FILTER_SANITIZE_STRING
 );
 $ticket_data['component'] = $component->getName();
$ticket = new TicketEntity($ticket_data);
 $this->ticket_mapper->save($ticket);
 return $ticket;
 }
 }
 ?>

Criamos um objeto contêiner para ele no index.php, assim:

E agora as Ações podem usar o TicketService em vez de executar a lógica do domínio diretamente:

get('/tickets', function (Request $request, Response $response) {
 $this->logger->addInfo("Ticket list");
 $tickets = $this->ticket_service->getTickets();
 $response = $this->view->render(
 $response,
 "tickets.phtml",
 ["tickets" => $tickets, "router" => $this->router]
 );
 return $response;
 });
$app->get('/ticket/new', function (Request $request, Response $response) {
 $components = $this->ticket_service->getComponents();
 $response = $this->view->render(
 $response,
 "ticketadd.phtml",
 ["components" => $components]
 );
 return $response;
 });
$app->post('/ticket/new', function (Request $request, Response $response) {
 $data = $request->getParsedBody();
 $this->ticket_service->createTicket($data);
 $response = $response->withRedirect("/tickets");
 return $response;
 });
$app->get('/ticket/{id}', function (Request $request, Response $response, $args) {
 $ticket = $this->ticket_service->getTicketById($args['id']);
 $response = $this->view->render(
 $response,
 "ticketdetail.phtml",
 ["ticket" => $ticket]
 );
 return $response;
 })->setName('ticket-detail');
 ?>

Um benefício aqui é que agora podemos testar as atividades do domínio separadamente das ações. Podemos começar a fazer algo mais assim como testes de integração, até mesmo teste de unidade, em vez de testes de sistema de ponta a ponta.

II.

No caso do aplicativo tutorial, o trabalho de apresentação é tão simples como não exigir um Respondente separado para cada ação. Uma variação relaxada de uma camada de Respondente é perfeitamente adequada neste caso simples, um em que cada Ação usa um método diferente em um Respondente comum.
Extrair o trabalho de apresentação para um Respondente separado, para que a construção de respostas seja completamente removida da Ação, se parece com isso:

view = $view;
 }
public function index(Response $response, array $data)
 {
 return $this->view->render(
 $response,
 "tickets.phtml",
 $data
 );
 }
public function detail(Response $response, array $data)
 {
 return $this->view->render(
 $response,
 "ticketdetail.phtml",
 $data
 );
 }
public function add(Response $response, array $data)
 {
 return $this->view->render(
 $response,
 "ticketadd.phtml",
 $data
 );
 }
public function create(Response $response)
 {
 return $response->withRedirect("/tickets");
 }
 }
 ?>

Podemos, então, adicionar o objeto TicketResponder ao contêiner em index.php:

E, finalmente, podemos nos referir ao Respondente, em vez de apenas o sistema modelo, nas Ações:

get('/tickets', function (Request $request, Response $response) {
 $this->logger->addInfo("Ticket list");
 $tickets = $this->ticket_service->getTickets();
 return $this->ticket_responder->index(
 $response,
 ["tickets" => $tickets, "router" => $this->router]
 );
 });
$app->get('/ticket/new', function (Request $request, Response $response) {
 $components = $this->ticket_service->getComponents();
 return $this->ticket_responder->add(
 $response,
 ["components" => $components]
 );
 });
$app->post('/ticket/new', function (Request $request, Response $response) {
 $data = $request->getParsedBody();
 $this->ticket_service->createTicket($data);
 return $this->ticket_responder->create($response);
 });
$app->get('/ticket/{id}', function (Request $request, Response $response, $args) {
 $ticket = $this->ticket_service->getTicketById($args['id']);
 return $this->ticket_responder->detail(
 $response,
 ["ticket" => $ticket]
 );
 })->setName('ticket-detail');
 ?>

Agora, podemos testar o trabalho de construção de respostas separadamente do trabalho de domínio.

Algumas observações:

Colocar toda a construção de resposta em uma única classe com vários métodos, especialmente para casos simples como esse tutorial, é bom para começar. Para ADR, não é estritamente necessário ter um Respondente para cada Ação. O que é necessário é extrair os problemas de construção de resposta para fora da Ação.

Mas à medida que a complexidade da lógica de apresentação aumenta (negociação de tipo de conteúdo? cabeçalhos de status? etc.) e, à medida que as dependências se tornam diferentes para cada tipo de resposta a ser construída, você irá desejar ter um Respondente para cada Ação.

Alternativamente, você pode ficar com um único Respondente, mas reduza sua interface para um único método. Nesse caso, você pode achar que o uso de uma Carga de Domínio (em vez de resultados de domínio “nus”) possui alguns benefícios significativos.

III.

Neste ponto, o aplicativo tutorial Slim foi convertido em ADR. Separamos a lógica do domínio para um TicketService e a lógica de apresentação para um TicketResponder. E é fácil ver como cada Ação faz praticamente a mesma coisa:

  • Entrada de Marshals e a transfere para o Domínio
  • Retorna um resultado do Domínio e o transfere para o Respondente
  • Invoca o Respondente para que ele possa construir e retornar a Resposta

Agora, por um caso simples como este, o uso de ADR (ou mesmo MVC webbishy) pode parecer um exagero. Mas os casos simples tornam-se complexos rapidamente e esse caso simples mostra como a separação de problemas de ADR pode ser aplicada à medida que um aplicativo baseado em Slim aumenta em complexidade.

Você está preso a um aplicativo PHP legado? Você deveria comprar o meu livro porque ele lhe dá um guia passo a passo para melhorar sua base de código, tudo isso mantendo-a funcionando o tempo todo.

***

Paul M. Jonses faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://paul-m-jones.com/archives/6639

Powered by WPeMatico