Arquitetura ADR no PHP

Introdução

Construir um sistema com a Arquitetura ADR no PHP oferece várias vantagens. A Action-Domain-Responder (ADR) organiza a aplicação em três componentes: Action, que processa e valida as requisições; Domain, que gerencia as regras de negócio; e Responder, que gera a resposta ao cliente.

Neste artigo, vou demonstrar como implementar a Arquitetura ADR no PHP com um exemplo prático. Portanto, exploraremos a estrutura da ADR, utilizaremos o conceito de Repository para persistência de dados e criaremos uma classe de serviço para encapsular a lógica de negócios.

Instalação e Autoload das Dependências

Para garantir que a aplicação ADR funcione corretamente, você deve instalar as dependências utilizando o Composer.

  • Passo 1: Primeiro, crie o arquivo composer.json com as seguintes configurações:
{
  "name": "paginadoale/usuarios",
  "require": {
    "php": "^8.1",
    "slim/slim": "^4.0",
    "slim/psr7": "^1.6.1",
    "php-di/php-di": "^7.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^9.5|^10.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}
  • Passo 2: Depois de configurar o Composer, execute o comando abaixo para instalar as dependências:
composer install
  • Passo 3: Em seguida, para utilizar o autoload do Composer, adicione a seguinte linha no arquivo index.php:
require_once 'vendor/autoload.php';

Construção da Aplicação

Para facilitar o desenvolvimento, você organizará todas as classes no diretório src. No entanto, ajuste essa estrutura conforme necessário.

Criando a Model User

Para iniciar o projeto, você começará construindo os elementos de persistência, sendo o primeiro deles, a classe Model User. Essa classe representa um usuário, encapsulando informações como ID, nome e e-mail. Então, no diretório src, adicione o arquivo User.php com o seguinte conteúdo:

<?php
declare(strict_types=1);

namespace App;

class User {
    public function __construct(
        private ?int $id = null,       // Propriedade id, opcional
        private string $name,          // Propriedade nome
        private string $email          // Propriedade e-mail
    ) {}

    // O tipo de retorno ?int indica que o id pode ser nulo
    public function getId(): ?int {
        return $this->id;
    }

    public function getName(): string {
        return $this->name;
    }

    public function getEmail(): string {
        return $this->email;
    }

    // Atribui um novo ID ao usuário, retornando o próprio objeto (encadeamento)
    public function setId(?int $id): static {
        $this->id = $id;
        return $this;
    }

    // Define o nome e retorna a instância para encadeamento
    public function setName(string $name): static {
        $this->name = $name;
        return $this;
    }

    // Define o e-mail e retorna a instância para encadeamento
    public function setEmail(string $email): static {
        $this->email = $email;
        return $this;
    }
}

Criando o UserRepository

Em alguns projetos usando outras arquiteturas, o Repository normalmente se refere a uma interface que define um contrato dos métodos a serem implementados por uma classe que será o Adapter. Porém, neste artigo, o Repository será uma classe que gerencia as operações de persitência no banco de dados, fornecendo métodos como criar, atualizar, excluir e buscar registros. Entretanto, visto que o nosso foco é estudar a Arquitetura ADR no PHP, constuir Repository dessa maneira não irá nos distrair do foco, que é entender os elementos necessário em nosso sistema.

Agora que você já criou o modelo, pode implementar o UserRepository. Então, crie um novo arquivo chamado UserRepository.php e adicione o seguinte:

<?php
declare(strict_types=1);

namespace App;

use PDO;
use PDOException;
use Psr\Log\LoggerInterface;

class UserRepository {
    // Definimos as propriedades diretamente no construtor como readonly
    public function __construct(
        private readonly PDO $connection,  // Conexão com o banco de dados (readonly)
        private readonly LoggerInterface $logger // Logger para registrar erros (readonly)
    ) {
        $this->createTable();  // Garante que a tabela de usuários exista
    }

    // Cria a tabela de usuários, se ainda não existir
    private function createTable(): void {
        $query = 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL)';
        try {
            $this->connection->exec($query);
        } catch (PDOException $e) {
            // Registra o erro e lança uma exceção apropriada
            $this->logger->error("Erro ao criar a tabela de usuários: {$e->getMessage()}");
            throw new RuntimeException('Erro ao inicializar o banco de dados.');
        }
    }

    // Insere um novo usuário no banco de dados
    public function create(User $user): void {
        $query = 'INSERT INTO users (name, email) VALUES (:name, :email)';
        try {
            $statement = $this->connection->prepare($query);
            $statement->bindValue(':name', $user->getName());
            $statement->bindValue(':email', $user->getEmail());
            $statement->execute();
        } catch (PDOException $e) {
            $this->logger->error("Erro ao criar usuário: {$e->getMessage()}");
            throw new RuntimeException('Erro ao criar o usuário.');
        }
    }

    // Atualiza os dados de um usuário existente
    public function update(User $user): void {
        $query = 'UPDATE users SET name = :name, email = :email WHERE id = :id';
        try {
            $statement = $this->connection->prepare($query);
            $statement->bindValue(':name', $user->getName());
            $statement->bindValue(':email', $user->getEmail());
            $statement->bindValue(':id', $user->getId(), PDO::PARAM_INT);
            $statement->execute();
        } catch (PDOException $e) {
            $this->logger->error("Erro ao atualizar usuário: {$e->getMessage()}");
            throw new RuntimeException('Erro ao atualizar o usuário.');
        }
    }

    // Remove um usuário do banco de dados com base no ID
    public function delete(int $userId): void {
        $query = 'DELETE FROM users WHERE id = :id';
        try {
            $statement = $this->connection->prepare($query);
            $statement->bindValue(':id', $userId, PDO::PARAM_INT);
            $statement->execute();
        } catch (PDOException $e) {
            $this->logger->error("Erro ao deletar usuário: {$e->getMessage()}");
            throw new RuntimeException('Erro ao deletar o usuário.');
        }
    }

    // Busca um usuário pelo ID e retorna um objeto User ou null
    public function findById(int $userId): ?User {
        $query = 'SELECT * FROM users WHERE id = :id';
        try {
            $statement = $this->connection->prepare($query);
            $statement->bindValue(':id', $userId, PDO::PARAM_INT);
            $statement->execute();
            $row = $statement->fetch(PDO::FETCH_ASSOC);

            return $row
                ? new User($row['name'], $row['email'], (int) $row['id'])
                : null;
        } catch (PDOException $e) {
            $this->logger->error("Erro ao buscar usuário pelo ID: {$e->getMessage()}");
            throw new RuntimeException('Erro ao buscar o usuário.');
        }
    }

    // Retorna todos os usuários como um array de objetos User
    public function findAll(): array {
        $query = 'SELECT * FROM users';
        try {
            $statement = $this->connection->query($query);

            // Utiliza FETCH_FUNC para construir os objetos User diretamente
            return $statement->fetchAll(PDO::FETCH_FUNC, fn($name, $email, $id) => new User($name, $email, (int) $id));
        } catch (PDOException $e) {
            $this->logger->error("Erro ao listar usuários: {$e->getMessage()}");
            throw new RuntimeException('Erro ao listar usuários.');
        }
    }
}

Os métodos do UserRepository realizam a criação, atualização, exclusão e busca de usuários. Com essa divisão, os controllers podem focar na manipulação das requisições, enquanto o repositório gerencia a interação com o banco de dados.

Por fim, você adicionará a classe de serviço UserService, que encapsula a lógica de negócios, aproximando a aplicação de uma estrutura mais robusta.

Classe de Serviço

Você deve organizar a lógica de negócios em classes de serviço, que encapsulam ações e regras específicas do domínio. Elas atuam como intermediárias entre Controllers e Repositories, coordenando operações que respondem às solicitações dos usuários. Essa abordagem torna o código mais limpo e fácil de manter. Além disso, isolar a lógica de negócios facilita sua reutilização e simplifica os testes automatizados. Agora, crie a classe UserService no arquivo UserService.php com o conteúdo necessário para gerenciar operações relacionadas aos usuários.

<?php
declare(strict_types=1);

namespace App;

class UserService
{
    // O repositório de usuários é injetado como readonly no construtor
    public function __construct(
        private readonly UserRepository $userRepository  // Injeção de dependência com readonly
    ) {}

    // Cria um novo usuário e delega a persistência ao UserRepository
    public function createUser(string $name, string $email): void {
        $user = new User($name, $email);
        $this->userRepository->create($user);
    }

    // Atualiza os dados de um usuário existente
    public function updateUser(int $id, string $name, string $email): void {
        $user = $this->userRepository->findById($id);
        if ($user !== null) {  // Verifica se o usuário existe
            $user->setName($name)
                 ->setEmail($email);  // Encadeamento para definir dados
            $this->userRepository->update($user);
        }
    }

    // Exclui um usuário com base no ID fornecido
    public function deleteUser(int $id): void {
        $this->userRepository->delete($id);
    }

    // Retorna um usuário com base no ID ou null se não encontrado
    public function getUser(int $id): ?User {
        return $this->userRepository->findById($id);
    }

    // Retorna todos os usuários cadastrados
    public function getUsers(): array {
        return $this->userRepository->findAll();
    }
}

A injeção de dependência no UserService permite que o UserRepository seja injetado diretamente e tratado como readonly, garantindo imutabilidade e segurança. Além disso, ao criar ou atualizar usuários, o serviço utiliza o repositório para persistir ou alterar os dados. Da mesma forma, o método de exclusão delega a remoção de usuários ao repositório, e os métodos de busca retornam um usuário ou uma lista. Assim, essa abordagem simplifica a separação de responsabilidades, tornando o código mais modular, fácil de manter e testar, e também facilita a modificação do repositório sem impactar o controlador.

Agora que a classe de serviço está implementada, você pode prosseguir com a criação dos controladores e a integração com o framework de roteamento.

Adicionando os Responders

Na arquitetura ADR, os Responders desempenham um papel essencial. Eles processam os resultados das ações do Controller e enviam a resposta ao cliente. Dessa forma, encapsulam a lógica de formatação em uma resposta HTTP, permitindo que o Controller se concentre exclusivamente nas ações de negócio, sem lidar com a formatação.

Neste exemplo, você utilizará os Responders para processar as respostas geradas por cada ação do Controller. Cada Responder é responsável por formatar e devolver os dados corretamente ao cliente. Para começar, crie um arquivo separado para cada classe de Responder, conforme as instruções fornecidas.

UserCreateResponder
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Response;

class UserCreateResponder
{
    // Este método recebe a resposta e os dados do usuário, então formata a resposta
    public function __invoke(Response $response, array $userData): Response {
        $responseData = [
            'message' => 'User created successfully',
            'user' => $userData,
        ];

        // Escreve o JSON no corpo da resposta
        $response->getBody()->write(json_encode($responseData, JSON_THROW_ON_ERROR));

        // Retorna a resposta com o cabeçalho correto e o status HTTP 201
        return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
    }
}
UserUpdateResponder
&lt;?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Response;

class UserUpdateResponder
{
    // Este método formata a resposta após uma atualização de usuário
    public function __invoke(Response $response, User $user): Response {
        $responseData = [
            'message' => 'User updated successfully',
            'user' => [
                'name' => $user->getName(),
                'email' => $user->getEmail(),
            ],
        ];

        // Escreve o JSON no corpo da resposta
        $response->getBody()->write(json_encode($responseData, JSON_THROW_ON_ERROR));

        // Retorna a resposta com o status HTTP 200 e cabeçalhos
        return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
    }
}
UserDeleteResponder
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Response;

class UserDeleteResponder
{
    // Este método formata a resposta após a exclusão de um usuário
    public function __invoke(Response $response): Response {
        $responseData = [
            'message' => 'User deleted successfully',
        ];

        // Escreve o JSON no corpo da resposta
        $response->getBody()->write(json_encode($responseData, JSON_THROW_ON_ERROR));

        // Retorna a resposta com o status HTTP 204, sem conteúdo
        return $response->withHeader('Content-Type', 'application/json')->withStatus(204);
    }
}
UserShowResponder
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Response;

class UserShowResponder
{
    // Este método formata a resposta ao exibir um usuário específico
    public function __invoke(Response $response, User $user): Response {
        $responseData = [
            'user' => [
                'name' => $user->getName(),
                'email' => $user->getEmail(),
            ],
        ];

        // Escreve o JSON no corpo da resposta
        $response->getBody()->write(json_encode($responseData, JSON_THROW_ON_ERROR));

        // Retorna a resposta com o status HTTP 200
        return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
    }
}
UserListResponder
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Response;

class UserListResponder
{
    // Este método formata a resposta ao listar vários usuários
    public function __invoke(Response $response, array $users): Response {
        // Mapeia os usuários para o formato de resposta esperado
        $userArray = array_map(static fn($user) => [
            'name' => $user->getName(),
            'email' => $user->getEmail(),
        ], $users);

        $responseData = [
            'users' => $userArray,
        ];

        // Escreve o JSON no corpo da resposta
        $response->getBody()->write(json_encode($responseData, JSON_THROW_ON_ERROR));

        // Retorna a resposta com o status HTTP 200
        return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
    }
}

Cada Responder é responsável por receber dados do Controller, formatá-los em JSON e escrever a resposta. A função json_encode() com a flag JSON_THROW_ON_ERROR garante que qualquer erro de codificação JSON lance uma exceção. Além disso, os Responders retornam códigos de status HTTP adequados, como:

  • 201 Created para criação,
  • 200 OK para atualização e exibição,
  • 204 No Content para exclusões.

No UserListResponder, usamos array_map() para converter a lista de objetos User em um formato adequado, o que simplifica o código e melhora a legibilidade.

4 – Controllers: Lidando com as Ações

Na arquitetura ADR, os Controllers coordenam o fluxo das requisições. Eles recebem pedidos dos clientes, acionam os serviços e repositórios necessários e, em seguida, utilizam os Responders para enviar as respostas. Cada ação, como criar ou atualizar, está associada a um Controller específico. Além disso, os Controllers garantem que a lógica de negócios seja tratada pelos serviços, enquanto os Responders formatam e retornam os dados ao cliente. Agora, vamos analisar a implementação dos Controllers em uma aplicação de gerenciamento de usuários.

UserCreateController
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Request;
use Slim\Psr7\Response;

class UserCreateController
{
    // O serviço de usuários é injetado via construtor
    public function __construct(
        private readonly UserService $userService  // Injeção do serviço de usuários
    ) {}

    // Método invocado quando o Controller é chamado
    public function __invoke(Request $request, Response $response): Response {
        // Decodifica os dados JSON recebidos da requisição
        $requestData = json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);

        // Cria o usuário utilizando o serviço
        $this->userService->createUser($requestData['name'], $requestData['email']);

        // Prepara os dados para o Responder
        $userData = [
            'name' => $requestData['name'],
            'email' => $requestData['email'],
        ];

        // Chama o Responder para formatar a resposta e retornar
        return (new UserCreateResponder())($response, $userData);
    }
}
UserUpdateController
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Request;
use Slim\Psr7\Response;

class UserUpdateController
{
    public function __construct(
        private readonly UserService $userService  // Injeção do serviço de usuários
    ) {}

    public function __invoke(Request $request, Response $response, array $args): Response {
        // Decodifica os dados recebidos da requisição
        $requestData = json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);

        // Obtém o ID do usuário a ser atualizado
        $userId = (int) $args['id'];

        // Verifica se o usuário existe antes de atualizar
        $existingUser = $this->userService->getUser($userId);
        if ($existingUser === null) {
            // Retorna uma resposta de erro se o usuário não for encontrado
            $response->getBody()->write(json_encode(['error' => 'User not found'], JSON_THROW_ON_ERROR));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
        }

        // Atualiza o usuário e obtém os dados atualizados
        $this->userService->updateUser($userId, $requestData['name'], $requestData['email']);
        $updatedUser = $this->userService->getUser($userId);

        // Retorna a resposta formatada pelo Responder
        return (new UserUpdateResponder())($response, $updatedUser);
    }
}
UserDeleteController
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Request;
use Slim\Psr7\Response;

class UserDeleteController
{
    public function __construct(
        private readonly UserService $userService  // Injeção do serviço de usuários
    ) {}

    public function __invoke(Request $request, Response $response, array $args): Response {
        // Obtém o ID do usuário a ser excluído
        $userId = (int) $args['id'];

        // Verifica se o usuário existe antes de excluí-lo
        $existingUser = $this->userService->getUser($userId);
        if ($existingUser === null) {
            // Retorna uma resposta de erro se o usuário não for encontrado
            $response->getBody()->write(json_encode(['error' => 'User not found'], JSON_THROW_ON_ERROR));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
        }

        // Exclui o usuário usando o serviço
        $this->userService->deleteUser($userId);

        // Retorna a resposta formatada pelo Responder
        return (new UserDeleteResponder())($response);
    }
}
UserShowController
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Request;
use Slim\Psr7\Response;

class UserShowController
{
    public function __construct(
        private readonly UserService $userService  // Injeção do serviço de usuários
    ) {}

    public function __invoke(Request $request, Response $response, array $args): Response {
        // Obtém o ID do usuário a ser exibido
        $userId = (int) $args['id'];

        // Busca o usuário através do serviço
        $user = $this->userService->getUser($userId);

        // Verifica se o usuário foi encontrado
        if ($user === null) {
            // Retorna uma resposta de erro se o usuário não for encontrado
            $response->getBody()->write(json_encode(['message' => 'User not found'], JSON_THROW_ON_ERROR));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
        }

        // Retorna a resposta formatada pelo Responder
        return (new UserShowResponder())($response, $user);
    }
}
UserListController
<?php
declare(strict_types=1);

namespace App;

use Slim\Psr7\Request;
use Slim\Psr7\Response;

class UserListController
{
    public function __construct(
        private readonly UserService $userService  // Injeção do serviço de usuários
    ) {}

    public function __invoke(Request $request, Response $response): Response {
        // Obtém a lista de todos os usuários através do serviço
        $users = $this->userService->getUsers();

        // Retorna a resposta formatada pelo Responder
        return (new UserListResponder())($response, $users);
    }
}

    No Controller, o UserService é injetado com a propriedade readonly, garantindo que ele não seja alterado após a criação da instância. Os Controllers delegam a formatação das respostas para os Responders, assegurando que os dados sejam fornecidos no formato correto (JSON), com os cabeçalhos adequados. Essa organização mantém o código modular e claro, já que cada Controller cuida de uma ação específica, facilitando a manutenção e separação de responsabilidades entre lógica de negócios e formatação de respostas.

    Adicionando o Front-Controller

    O Front-Controller centraliza o processamento de requisições na aplicação e direciona cada uma para o Controller apropriado. Além disso, ele gerencia o roteamento e a injeção de dependências, configurando o ambiente da aplicação. No arquivo index.php, o Front-Controller organiza e distribui as requisições, garantindo o tratamento adequado. Assim, ele facilita o controle das requisições e a manutenção do sistema. Então, esta é a implementação que precisamos neste momento:

    <?php
    declare(strict_types=1);
    
    require_once 'vendor/autoload.php';
    
    use App\UserCreateController;
    use App\UserDeleteController;
    use App\UserListController;
    use App\UserRepository;
    use App\UserService;
    use App\UserShowController;
    use App\UserUpdateController;
    use DI\Container;
    use Slim\Factory\AppFactory;
    
    //////////////////////////////
    // Configuração do Container //
    //////////////////////////////
    
    // Cria uma instância do container de dependências (DI Container)
    $container = new Container();
    
    // Define as configurações e dependências do container
    $container->set(UserRepository::class, function (): UserRepository {
        // Retorna a instância do UserRepository
        return new UserRepository(new PDO('sqlite:database.db')); // Injeção manual de PDO
    });
    
    // Define o serviço de usuário, injetando o UserRepository
    $container->set(UserService::class, function (Container $container): UserService {
        // Recupera o UserRepository do container e passa para o UserService
        return new UserService($container->get(UserRepository::class));
    });
    
    ///////////////////////////////
    // Inicialização do Slim App //
    ///////////////////////////////
    
    // Cria uma instância da aplicação Slim a partir do container
    $app = AppFactory::createFromContainer($container);
    
    ////////////////////////
    // Definição de Rotas //
    ////////////////////////
    
    // Define a rota para criar usuários (POST)
    $app->post('/users', UserCreateController::class);
    
    // Define a rota para atualizar usuários (PUT)
    $app->put('/users/{id}', UserUpdateController::class);
    
    // Define a rota para excluir usuários (DELETE)
    $app->delete('/users/{id}', UserDeleteController::class);
    
    // Define a rota para exibir um usuário específico (GET)
    $app->get('/users/{id}', UserShowController::class);
    
    // Define a rota para listar todos os usuários (GET)
    $app->get('/users', UserListController::class);
    
    //////////////////
    // Execução App //
    //////////////////
    
    // Executa o aplicativo Slim para processar as requisições
    $app->run();
    

    O arquivo vendor/autoload.php carrega automaticamente as dependências do Composer. O container gerencia a injeção de dependências, inicializando o UserRepository com uma conexão PDO e, em seguida, configurando o UserService para injetá-lo. As rotas do Slim são claramente definidas, mapeando requisições HTTP para os Controllers correspondentes, como UserCreateController, e lidando com métodos como POST e GET. Por fim, o método $app->run() executa o aplicativo, processando e direcionando as requisições conforme configurado nas rotas.

    Testando cada endpoint

    Agora que você configurou corretamente os endpoints, chegou o momento de testá-los. Usaremos o cURL para verificar se os endpoints estão funcionando conforme o esperado.

    Primeiro, vamos realizar o teste com p endpoint de criação de usuário (UserCreateController):

    curl -X POST -H "Content-Type: application/json" \
        -d '{"name":"John Doe", "email":"johndoe@example.com"}' \
        http://localhost/users
    

    Este comando envia uma requisição POST para o endpoint de criação de usuários. Esperamos que a resposta seja um status HTTP 201 Created, e que retorne os detalhes do usuário criado.

    Agora vamos realizar um teste com o endpoint de atualização de usuário (UserUpdateController):

    curl -X PUT -H "Content-Type: application/json" \
        -d '{"name":"Mr John Doe", "email":"johndoe@example.com"}' \
        http://localhost/users/1
    

    Neste caso, você envia uma requisição PUT para atualizar o usuário com ID 1. A resposta retornará com um status HTTP 200 OK, indicando que a atualização do usuário foi realizada com sucesso.

    Continuando com os testes, vamos realizar um teste com o endpoint de exibição de usuário (UserShowController):

    curl http://localhost/users/1
    

    Esse comando testa o endpoint de exibição de usuários com o método GET. Ele retornará as informações do usuário com ID 1. A resposta esperada é um status HTTP 200 OK, junto com os dados do usuário.

    Testando o endpoint de exclusão de usuário (UserDeleteController):

    curl -X DELETE http://localhost/users/1
    

    Aqui, você envia uma requisição DELETE para remover o usuário com ID 1. A resposta retornará com um status HTTP 204 No Content, confirmando que a exclusão foi realizada com sucesso.

    Então o proximo teste será com o endpoint de listagem de usuários (UserListController):

    curl http://localhost/users
    

    Esse comando lista todos os usuários da aplicação. Ele retornará um status HTTP 200 OK, junto com um array que contém os detalhes dos usuários registrados.

    Lembre-se de substituir http://localhost pelo endereço correto do seu servidor, caso você esteja testando em um ambiente diferente. Esses testes garantirão que todos os endpoints estão funcionando adequadamente.

    Testes Unitários

    Além dos testes de integração com cURL, é igualmente importante escrever testes unitários para garantir que cada componente funcione corretamente. Com isso, os testes unitários permitem identificar e corrigir erros em partes específicas do código, enquanto adicionam uma camada extra de confiança e estabilidade ao sistema.

    Aqui está um exemplo de teste unitário para o endpoint de criação de usuários, usando o PHPUnit, uma biblioteca popular para testes em PHP.

    Criando o teste unitário

    Primeiro, certifique-se de instalar o PHPUnit em seu projeto. Em seguida, crie um arquivo de teste chamado UserCreateControllerTest.php no diretório tests e adicione o seguinte código:

    <?php
    declare(strict_types=1);
    
    namespace Tests\App;
    
    use App\UserCreateController;
    use App\UserService;
    use PHPUnit\Framework\TestCase;
    use Slim\Psr7\Factory\ResponseFactory;
    use Slim\Psr7\Factory\ServerRequestFactory;
    use Slim\Psr7\Factory\StreamFactory;
    
    class UserCreateControllerTest extends TestCase
    {
        public function testCreateUser(): void
        {
            // Cria um mock do UserService, simulando o comportamento do serviço
            $userServiceMock = $this->getMockBuilder(UserService::class)
                ->disableOriginalConstructor()
                ->getMock();
            $userServiceMock->expects($this->once())
                ->method('createUser');
    
            // Instancia o UserCreateController com o mock do UserService
            $controller = new UserCreateController($userServiceMock);
    
            // Cria uma requisição Slim com os dados JSON
            $requestFactory = new ServerRequestFactory();
            $jsonBody = '{"name": "John", "email": "john@example.com"}';
            $streamFactory = new StreamFactory();
            $stream = $streamFactory->createStream($jsonBody);
            $request = $requestFactory->createServerRequest('POST', '/users')
                ->withHeader('Content-Type', 'application/json')
                ->withBody($stream);
    
            // Cria uma resposta Slim
            $response = (new ResponseFactory())->createResponse();
    
            // Executa o método __invoke do controller
            $result = $controller($request, $response);
    
            // Verifica se o status de retorno é o esperado (201 Created)
            $this->assertEquals(201, $result->getStatusCode());
        }
    }
    

      Criação do Mock:

      Primeiramente, você cria um mock do UserService utilizando o método getMockBuilder(). Esse mock simula o comportamento do serviço real, evitando assim a necessidade de criar um usuário no banco de dados durante os testes.

      Além disso, você espera que o método createUser() seja chamado uma única vez durante o teste. Para garantir essa verificação, utilize o método expects($this->once()).

      Simulação da Requisição:

      Em seguida, você utiliza o Slim para criar uma requisição POST simulada, que inclui os dados necessários (nome e e-mail) no formato JSON. Dessa maneira, esses dados são enviados ao controller, permitindo que você verifique se o comportamento está correto.

      Verificação da Resposta:

      Depois de executar o método __invoke do controller, você verifica se o status da resposta é 201 Created, o que confirma que o usuário foi criado com sucesso.

      Execução do Teste:

      Por fim, para rodar o teste, você deve executar o seguinte comando no terminal:

      vendor/bin/phpunit tests
      

      Esse comando executa todos os testes no diretório tests e, em seguida, exibe os resultados. Com isso, você consegue verificar se o endpoint de criação de usuários está funcionando conforme o esperado.

      Expansão dos Testes

      Você pode expandir este teste a fim de cobrir uma variedade maior de cenários. Por exemplo, você pode testar o comportamento caso a criação de um usuário falhe, ou simular diferentes tipos de dados de entrada. Conforme você cobre mais cenários, o código se torna naturalmente mais robusto e confiável.

      Além disso, os testes unitários e de integração desempenham um papel fundamental para garantir tanto a qualidade quanto a estabilidade do seu código. Esses testes ajudam a identificar problemas antes que eles alcancem a produção, o que facilita, de maneira significativa, a manutenção e a evolução do software. Portanto, ao integrar tanto os testes manuais com cURL quanto os testes automatizados com PHPUnit, você certamente cria um código ainda mais seguro e, ao mesmo tempo, testável.

      Conclusão

      Neste artigo, exploramos a Arquitetura ADR no PHP e aprendemos como construir uma aplicação PHP utilizando essa abordagem. Por meio de um exemplo prático de uma aplicação CRUD para o gerenciamento de usuários, explicamos a estrutura fundamental e a função de cada componente da arquitetura ADR.

      Ao adotar a Arquitetura ADR em seus projetos PHP, você segue boas práticas de desenvolvimento, o que torna suas aplicações PHP mais fáceis de entender, manter e testar. Além disso, a clareza dessa abordagem é crucial para projetos que precisam crescer de maneira saudável, sem comprometer a qualidade do código.

      Espero que este artigo tenha ajudado você a compreender melhor a Arquitetura ADR e como aplicá-la em seus projetos PHP. Agora, é o momento de colocar esse conhecimento em prática e aproveitar os muitos benefícios que essa abordagem oferece.

      Referências

      Paul M. Jones. “Action-Domain-Responder” [Online]. Disponível em: github.com/pmjones/adr

      Paul M. Jones. “Action Domain Responder” [Online]. Disponível em: pmjones.io/adr

      Deixe um comentário

      O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

      Rolar para cima