Como usar o padrão de repositório em um aplicativo Laravel

October 01, 2021
Escrito por
Oluyemi Olususi
Contribuidor
As opiniões expressas pelos colaboradores da Twilio são de sua autoria
Revisado por

Um repositório pode ser definido como uma camada de abstração entre o domínio e as camadas de mapeamento de dados, uma que fornece uma via de mediação entre ambos por meio de uma interface semelhante a uma coleção para acessar objetos de domínio.

Estruturas PHP modernas, como Laravel e Symfony, interagem com bancos de dados via Mapeadores objeto-relacionais (ORMs); a Symfony usa o Doctrine como seu ORM padrão e a Laravel usa o Eloquent.

Ambos adotam abordagens diferentes em como a interação com o banco de dados funciona. Com o Eloquent, os modelos são gerados para cada tabela de banco de dados, formando a base de interação. O Doctrine, no entanto, usa o padrão de Repositório onde cada Entidade tem um repositório correspondente contendo funções auxiliares para interagir com o banco de dados. Embora o Laravel não forneça essa funcionalidade de forma imediata, é possível usar o padrão de Repositório em projetos Laravel.

Um dos principais benefícios do padrão de Repositório é que ele nos permite usar o Princípio de inversão de dependência (ou o código para abstrações, não concretizações). Isso torna nosso código mais resistente às alterações, como se uma decisão fosse tomada mais tarde para mudar para uma fonte de dados que não é suportada pelo Eloquent.

Ele também ajuda a manter o código organizado e a evitar duplicação, pois a lógica relacionada ao banco de dados é mantida em um só lugar. Embora este benefício não seja imediatamente aparente em projetos pequenos, fica mais visível em projetos de grande escala que precisam ser mantidos durante muitos anos.

Neste artigo, mostrarei a você como implementar o padrão de Repositório em seus aplicativos Laravel. Para isso, criaremos uma API para gerenciar pedidos recebidos de clientes de uma empresa.

Pré-requisitos

Primeiros passos

Crie um novo projeto Laravel e use cd no diretório usando os comandos a seguir.

laravel new order_api
cd order_api

Configure o banco de dados

Para este tutorial, usaremos o MySQL como o nosso banco de dados. Para fazer isso, no arquivo .env, atualize os parâmetros relacionados ao banco de dados conforme mostrado abaixo.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=order_api
DB_USERNAME=<YOUR_DATABASE_USERNAME>
DB_PASSWORD=<YOUR_DATABASE_PASSWORD>

Por fim, usando seu aplicativo de gerenciamento de banco de dados preferido, crie um novo banco de dados chamado order_api.

Gere os dados iniciais para o banco de dados

Estamos criando um aplicativo de gerenciamento de pedidos, então vamos criar o modelo para ele executando o seguinte comando.

php artisan make:model Order -a

O argumento -a permite que o Artisan saiba que queremos criar um arquivo de migração, umpropagador, uma fábrica e um controlador para o modelo Order.

O comando acima criará cinco novos arquivos:

  • Um controlador em app/Http/Controllers/OrderController.php
  • Uma fábrica de banco de dados em database/factories/orderFactory.php
  • Um arquivo de migração em database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php
  • Um modelo localizado em app/Models/Order.php
  • Um arquivo propagador em database/seeders/OrderSeeder.php e

Em database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php, atualize a função up para corresponder ao seguinte.

public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->text('details');
        $table->string('client');
        $table->boolean('is_fulfilled')->default(false);
        $table->timestamps();
    });
 }

Conforme especificado no arquivo de migração, a tabela order terá as seguintes colunas:

  1. Um ID. Esta será a chave primária da tabela.
  2. Os detalhes do pedido.
  3. O nome do cliente que fez o pedido.
  4. Se a pedido foi atendido ou não.
  5. Quando o pedido foi criado e atualizado, created_at e updated_at, fornecidos pela função de timestamps.

Em seguida, vamos atualizar a OrderFactory para que ele possa usá-la para gerar uma ordem fictícia para propagar o banco de dados. Em database/factories/OrderFactory.php, atualize a função definition para que corresponda ao seguinte.

public function definition() 
{
    return [
        'details'       => $this->faker->sentences(4, true),
        'client'         => $this->faker->name(),
        'is_fulfilled' => $this->faker->boolean(),
    ];
}

Em seguida, abra odatabase/seeders/OrderSeeder.php e atualize a função run para corresponder ao seguinte.

public function run() 
{
    Order::factory()->times(50)->create();
}

Isso usa OrderFactory para criar 50 pedidos no banco de dados.

Don't forget to add this import:

use AppModelsOrder;

Em src/database/seeders/DatabaseSeeder.php, adicione o seguinte à função run.

$this->call(
    [
        OrderSeeder::class
    ]
);

Isso executa o comando QuoteSeeder quando o comando db:seed do Artisan é executado.

Por fim, execute suas migrações e propague o banco de dados usando o seguinte comando.

php artisan migrate --seed

Se você abrir a tabela de pedidos, verá os pedidos recém-propagados.

Lista de pedidos

Crie o repositório

Antes de criar um repositório para o modelo Order, vamos definir uma interface para especificar todos os métodos que o repositório deve declarar. Em vez de depender diretamente da classe do repositório, nosso controlador (e qualquer componente de pedido que possamos construir no futuro) dependerá da interface.

Isso torna nosso código flexível porque, caso seja necessário fazer uma mudança no futuro, o controlador não é afetado. Por exemplo, se decidimos terceirizar o gerenciamento de pedidos para um aplicativo de terceiras partes, podemos construir um novo módulo que esteja em conformidade com a assinatura da OrderRepositoryInterface e trocar as declarações vinculativas, e nosso controlador funcionará exatamente como esperado, sem tocar em uma única linha de código no controlador.

No diretório app, crie uma nova pasta chamada Interfaces. Em seguida, nas interfaces, crie um novo arquivo chamado OrderRepositoryInterface.php e adicione o seguinte código a ele.

<?php

namespace App\Interfaces;

interface OrderRepositoryInterface 
{
    public function getAllOrders();
    public function getOrderById($orderId);
    public function deleteOrder($orderId);
    public function createOrder(array $orderDetails);
    public function updateOrder($orderId, array $newDetails);
    public function getFulfilledOrders();
}

Em seguida, na pasta app, crie uma nova pasta chamada Repositórios. Nesta pasta, crie um novo arquivo chamado OrderRepository.php e adicione o seguinte código a ele.

<?php

namespace App\Repositories;

use App\Interfaces\OrderRepositoryInterface;
use App\Models\Order;

class OrderRepository implements OrderRepositoryInterface 
{
    public function getAllOrders() 
    {
        return Order::all();
    }

    public function getOrderById($orderId) 
    {
        return Order::findOrFail($orderId);
    }

    public function deleteOrder($orderId) 
    {
        Order::destroy($orderId);
    }

    public function createOrder(array $orderDetails) 
    {
        return Order::create($orderDetails);
    }

    public function updateOrder($orderId, array $newDetails) 
    {
        return Order::whereId($orderId)->update($newDetails);
    }

    public function getFulfilledOrders() 
    {
        return Order::where('is_fulfilled', true);
    }
}

Além da flexibilidade fornecida pela interface, encapsular consultas dessa maneira tem a vantagem adicional de que não é necessário duplicar consultas em todo o aplicativo.

Se, no futuro, decidirmos recuperar apenas pedidos não cumpridos na função getAllOrders(), só teríamos de fazer uma alteração em um lugar, em vez de rastrear todos os lugares onde Order::all() é declarado, arriscando deixar alguns passar.

Criando os controladores

Com nosso repositório em vigor, vamos adicionar uns códigos ao nosso controlador. Abra app/Http/Controllers/OrderController.php e atualize o código para corresponder ao seguinte.

<?php

namespace App\Http\Controllers;

use App\Interfaces\OrderRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class OrderController extends Controller 
{
    private OrderRepositoryInterface $orderRepository;

    public function __construct(OrderRepositoryInterface $orderRepository) 
    {
        $this->orderRepository = $orderRepository;
    }

    public function index(): JsonResponse 
    {
        return response()->json([
            'data' => $this->orderRepository->getAllOrders()
        ]);
    }

    public function store(Request $request): JsonResponse 
    {
        $orderDetails = $request->only([
            'client',
            'details'
        ]);

        return response()->json(
            [
                'data' => $this->orderRepository->createOrder($orderDetails)
            ],
            Response::HTTP_CREATED
        );
    }

    public function show(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');

        return response()->json([
            'data' => $this->orderRepository->getOrderById($orderId)
        ]);
    }

    public function update(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');
        $orderDetails = $request->only([
            'client',
            'details'
        ]);

        return response()->json([
            'data' => $this->orderRepository->updateOrder($orderId, $orderDetails)
        ]);
    }

    public function destroy(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');
        $this->orderRepository->deleteOrder($orderId);

        return response()->json(null, Response::HTTP_NO_CONTENT);
    }
}

O código injeta uma instância OrderRepositoryInterface por meio do construtor e usa os métodos relevantes do objeto em cada método de controlador.

Primeiro, dentro do método index(), ele chama o método getAllOrders() definido no orderRepository para recuperar a lista de pedidos e retorna uma resposta no formato JSON.

Em seguida, o método store() chama o método createOrder() no orderRepository para criar um novo pedido. Isso obtém os detalhes da ordem que precisa ser criada como uma matriz e retorna uma resposta bem-sucedida posteriormente.

Dentro do método show() no controlador, ele recupera o Id exclusivo do pedido na rota e a passa para o getOrderById() como um parâmetro. Isso busca os detalhes do pedido com um ID correspondente do banco de dados e retorna uma resposta no formato JSON.

Em seguida, para atualizar os detalhes de um pedido já criado, ele chama o método updateOrder() no repositório. Isso requer dois parâmetros: o ID exclusivo do pedido e os detalhes que precisam ser atualizados.

Por fim, o método destroy() recupera o ID exclusivo de um pedido específico na rota e chama o método deleteOrder() no repositório para excluí-lo.

Adicionando as rotas

Para mapear cada método definido no controlador para rotas específicas, adicione o seguinte código a routes/api.php.

Route::get('orders', [OrderController::class, 'index']);
Route::get('orders/{id}', [OrderController::class, 'show']);
Route::post('orders', [OrderController::class, 'store']);
Route::put('orders/{id}', [OrderController::class, 'update']);
Route::delete('orders/{id}', [OrderController::class, 'delete']);

Remember to include the import statement for the OrderController.

use App\Http\Controllers\OrderController;

Vincule a interface e a implementação

A última coisa que precisamos fazer é ligar OrderRepository a OrderRepositoryInterface no Contêiner de serviço da Laravel; isso é feito através de um Prestador de serviço. Crie um com o seguinte comando.

php artisan make:provider RepositoryServiceProvider

Abra o app/Providers/RepositoryServiceProvider.php e atualize a função register para corresponder ao seguinte.

public function register() 
{
    $this->app->bind(OrderRepositoryInterface::class, OrderRepository::class);
 }

Remember to include the import statement for OrderRepository and OrderRepositoryInterface.

use App\Interfaces\OrderRepositoryInterface;
use App\Repositories\OrderRepository;

Por fim, adicione o novo prestador de serviços à matriz providers em config/app.php.

'providers' => [
    // ...other declared providers
    App\Providers\RepositoryServiceProvider::class,
];

Teste o aplicativo

Execute o aplicativo novamente usando o seguinte comando.

php artisan serve

Por padrão, o aplicativo fornecido estará disponível em http://127.0.0.1:8000/. Usando Postman ou cURL, podemos fazer solicitações para a nossa API recém-criada.

Execute o seguinte comando para testar o endpoint /api/orders usando cURL:

curl --silent http://localhost:8000/api/orders | jq

The response was formatted to JSON using jq.

Você verá uma saída JSON parecida com o exemplo abaixo em seu terminal, que foi truncado para ajudar na legibilidade.

{
  "data": [
    {
      "id": 1,
      "details": "Sit ullam cupiditate dolorem in. Magnam suscipit eaque occaecati facilis amet illum. Dolor perspiciatis velit laboriosam. Enim fugiat excepturi qui natus incidunt dolorem debitis ut.",
      "client": "Cydney Conn V",
      "is_fulfilled": 0,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    },
    {
      "id": 2,
      "details": "Eum iste eum molestiae est. Voluptatibus veritatis earum commodi. Quod et laboriosam ratione dolor adipisci. Nam et debitis nobis ea sit.",
      "client": "Willow Herzog",
      "is_fulfilled": 1,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    },
    {
      "id": 3,
      "details": "At maxime architecto repellat quidem id. Saepe provident quo eos officiis et tenetur. Et expedita maxime atque. Et consequuntur sequi aperiam possimus odio est ab.",
      "client": "Mr. Peyton Nolan DVM",
      "is_fulfilled": 1,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    }
  ]
}

É assim que se usa o padrão de Repositório em um aplicativo Laravel

Neste artigo, aprendemos sobre o padrão de Repositório e como usá-lo em um aplicativo Laravel. Também vimos alguns dos benefícios que ele oferece a um projeto em grande escala, sendo um deles um código de acoplamento flexível, onde codificamos para abstrações, e não implementações concretas.

No entanto, terminarei com um lembrete de precaução. Para pequenos projetos, essa abordagem vai parecer muito trabalho e um código de modelo para devoluções que podem não ser imediatamente aparentes. Portanto, é  importante que você considere o tamanho do projeto apropriadamente antes de adotar essa abordagem.

A base de código completa deste tutorial está disponível no GitHub. Sinta-se à vontade para explorar mais. Boa codificação!

Oluyemi é um entusiasta de tecnologia com um histórico na Engenharia de Telecomunicações. Com interesse em resolver os problemas do dia a dia encontrados pelos usuários, ele se aventurou na programação e, desde então, dirigiu suas habilidades de solução de problemas para a criação de software para a Web e para dispositivos móveis.

Um engenheiro de software de pilha completa com paixão por compartilhar conhecimento, Oluyemi publicou um bom número de artigos técnicos e conteúdo em vários blogs na Internet. Por ser experiente em tecnologia, seus hobbies incluem experimentar novas linguagens e estruturas de programação.