Cómo utilizar el patrón de repositorio en una aplicación de Laravel

October 01, 2021
Redactado por
Oluyemi Olususi
Colaborador
Las opiniones expresadas por los colaboradores de Twilio son propias.
Revisado por

Un repositorio se puede definir como una capa de abstracción entre las capas de dominio y de mapeo de datos, una que proporciona un camino de mediación entre ambos, a través de una interfaz similar a una colección para acceder a los objetos de dominio.

Las estructuras de PHP modernas, como Laravel y Symfony, interactúan con bases de datos a través de Mapeo relacional de objetos (ORM); Symfony utiliza Doctrine como su ORM predeterminado y Laravel utiliza Eloquent.

Ambos utilizan diferentes enfoques en el funcionamiento de la interacción de la base de datos. Con Eloquent, se generan modelos para cada tabla de la base de datos, lo que conforma la base de la interacción. Sin embargo, Doctrine utiliza el patrón de Repositorio en el que cada Entidad tiene un repositorio correspondiente que contiene funciones auxiliares para interactuar con la base de datos. Si bien Laravel no proporciona esta funcionalidad de forma predeterminada, es posible utilizar el patrón de Repositorio en proyectos de Laravel.

Un beneficio clave del patrón de Repositorio es que nos permite utilizar el Principio de la Inversión de Dependencias (o codificar en abstracciones, no en concreciones). Esto hace que nuestro código sea más sólido en cuanto a los cambios como, por ejemplo, si se tomó una decisión más adelante para cambiar a una fuente de datos que no es compatible con Eloquent.

También ayuda a mantener el código organizado y evitar la duplicación, ya que la lógica relacionada con la base de datos se mantiene en un solo lugar. Si bien este beneficio no es inmediatamente evidente en proyectos pequeños, se vuelve más observable en proyectos a gran escala que se deben mantener durante muchos años.

En este artículo, te mostraré cómo implementar el patrón de Repositorio en tus aplicaciones de Laravel. Para hacerlo, desarrollaremos una API para administrar los pedidos de los clientes que recibe una empresa.

Requisitos previos

Cómo empezar

Crea un nuevo proyecto de Laravel y un cd en el directorio mediante los siguientes comandos.

laravel new order_api
cd order_api

Configura la base de datos

Para este tutorial usaremos MySQL como nuestra base de datos. Para hacerlo, en el archivo .env , actualiza los parámetros relacionados con la base de datos como se muestra a continuación.

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 último, mediante el uso de la aplicación de administración de base de datos preferida, crea una nueva base de datos llamada order_api.

Genera los datos iniciales para obtener la base de datos

Estamos creando una aplicación de administración de pedidos, así que vamos a crear el modelo para obtenerla ejecutando el siguiente comando.

php artisan make:model Order -a

El argumento -a permite a Artisan saber que queremos crear un archivo de migración , semilla, fábrica y controlador para el modelo de Pedido .

El comando anterior creará cinco nuevos archivos:

  • Un controlador en app/HTTP/Controllers/OrderController.php
  • Una base de datos de fábrica en database/factories/orderFactory.php
  • Un archivo de migración en database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php
  • Un modelo ubicado en app/Models/Order.php
  • Un archivo semilla en database/seeders/OrderSeeder.php y

En database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php, actualiza la función up para que coincida con la siguiente.

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();
    });
 }

Como se especifica en el archivo de migración, la tabla order tendrá las siguientes columnas:

  1. Un ID. Esta será la clave principal de la tabla.
  2. Los detalles del pedido.
  3. El nombre del cliente que realizó el pedido.
  4. Si el pedido se completó o no.
  5. Creación y actualización del pedido, created_at y updated_at, proporcionadas por la función timestamps .

A continuación, actualicemos OrderFactory para que pueda generar una simulación de pedido con la que inicializar la base de datos. En database/factories/OrderFactory.php, actualiza la función definition para que coincida con la siguiente.

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

A continuación, abre database/seeders/OrderSeeder.php y actualiza la función run para que coincida con la siguiente.

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

Esto utiliza OrderFactory para crear 50 pedidos en la base de datos.

Don't forget to add this import:

use AppModelsOrder;

En src/database/seeders/DatabaseSeeder.php, agrega lo siguiente a la función run .

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

Esto ejecuta el QuoteSeeder db:seed cuando se ejecuta el comando de Artisan.

Por último, ejecuta tus migraciones e inicia la base de datos con el siguiente comando.

php artisan migrate --seed

Si abres la tabla de pedidos , verás los pedidos recién realizados.

Lista de Pedidos

Crea el Repositorio

Antes de crear un repositorio para el modelo Order , definamos una interfaz para especificar todos los métodos que el repositorio debe declarar. En lugar de depender directamente de la clase de repositorio, nuestro controlador (y cualquier componente de pedido que podamos desarrollar en el futuro) dependerá de la interfaz.

Esto hace que nuestro código sea flexible porque, en caso de que sea necesario hacer un cambio en el futuro, el controlador no se verá afectado. Por ejemplo, si decidimos externalizar la administración de pedidos a una aplicación de terceros, podemos crear un nuevo módulo que cumpla con la firma de OrderRepositoryInterface e intercambiar las declaraciones vinculantes y nuestro controlador funcionará exactamente como esperamos, sin tocar una sola línea de código en el controlador.

En el directorio de la app, crea una nueva carpeta llamada interfaces. Luego, en las interfaces, crea un nuevo archivo llamado OrderRepositoryInterface.php y agrega el siguiente código.

<?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();
}

A continuación, en la carpeta app , crea una nueva carpeta llamada Repositorios. En esta carpeta, crea un nuevo archivo llamado OrderRepository.php y agrega el siguiente código.

<?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);
    }
}

Además de la flexibilidad proporcionada por la interfaz, encapsular las consultas de esta manera tiene la ventaja adicional de que no tenemos que duplicar consultas a lo largo de la aplicación.

Si, en el futuro, decidimos recuperar solo los pedidos incompletos en la función getAllOrders() , solo tendríamos que hacer un cambio en un lugar en vez de hacer un seguimiento de todos los lugares donde se declara Order::all() mientras arriesgamos perder algunos.

Creación de los controladores

Con nuestro repositorio en marcha, agreguemos un código a nuestro controlador. Abre APP/HTTP/Controllers/OrderController.php y actualiza el código para que coincida con el siguiente.

<?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);
    }
}

El código inyecta una instancia OrderRepositoryInterface a través del constructor y utiliza los métodos del objeto pertinente en cada método de controlador.

En primer lugar, dentro del método index() , avisa al método getAllOrders() definido en el orderRepository para recuperar la lista de pedidos y devuelve una respuesta en formato JSON.

A continuación, el método store() avisa al método createOrder()desde el orderRepository para crear un nuevo pedido. Esto carga los detalles del pedido que se debe crear como un arreglo y devuelve una respuesta exitosa más adelante.

Dentro del método show() en el controlador, recupera el Id de órden único de la ruta y lo pasa a getOrderById() como un parámetro. Esta opción permite obtener los detalles del pedido con un ID coincidente de la base de datos y devuelve una respuesta en formato JSON.

Luego, para actualizar los detalles de un pedido ya creado, avisa al método updateOrder() desde el repositorio. Esto requiere dos parámetros: el ID único del pedido y los detalles que se deben actualizar.

Por último, el método destroy() recupera el ID único de un pedido en particular de la ruta y avisa al método deleteOrder() desde el repositorio para eliminarlo.

Agrega las rutas

Para asignar cada método definido en el controlador a rutas específicas, agrega el siguiente 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;

Vincula la interfaz y la implementación

Lo último que debemos hacer es vincular OrderRepository a OrderRepositoryInterface en el Contenedor de Servicios de Laravel. Lo hacemos a través de un Proveedor de Servicios. Crea uno con el siguiente comando.

php artisan make:provider RepositoryServiceProvider

Abre app/Providers/RepositoryServiceProvider.php y actualiza la función register para que coincida con lo siguiente.

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;

Finalmente, agrega el nuevo Proveedor de Servicios al arreglo de providers en config/app.php.

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

Prueba la aplicación

Ejecuta la aplicación con el siguiente comando.

php artisan serve

Por defecto, la aplicación estará disponible en http://127.0.0.1:8000/. Con Postman o cURL, podemos realizar solicitudes a nuestra API recién creada.

Ejecuta el siguiente comando para probar el punto final /api/orders mediante cURL:

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

The response was formatted to JSON using jq.

Verás una salida JSON similar al siguiente ejemplo en tu terminal, que se truncó para facilitar la lectura.

{
  "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"
    }
  ]
}

Así es como se utiliza el Patrón de Repositorio en una aplicación de Laravel

En este artículo, aprendimos sobre el patrón de Repositorio y cómo usarlo en una aplicación de Laravel. También hemos visto algunos de los beneficios que ofrece un proyecto a gran escala, uno de los cuales es el código acoplado, donde estamos codificando para obtener abstracciones, no implementaciones concretas.

Sin embargo, voy a terminar con una nota de advertencia. En caso de proyectos pequeños, puede que este enfoque se presente como arduo y repetitivo para devoluciones que no sean inmediatamente evidentes. Por lo tanto, es  importante que consideres adecuadamente la escala del proyecto antes de adoptar este enfoque.

Toda la base de código para este tutorial se encuentra disponible en GitHub. No dudes en explorar más a fondo. ¡Feliz codificación!

Oluyemi es un entusiasta de la tecnología que cuenta con experiencia en Ingeniería de Telecomunicaciones. Con un gran interés en resolver los problemas cotidianos de los usuarios, se aventuró en la programación y desde entonces ha dirigido sus habilidades de resolución de problemas en la creación de software tanto para la web como para dispositivos móviles.

Oluyemi, un ingeniero de software de full-stack con pasión por compartir conocimientos, ha publicado una gran cantidad de artículos y contenidos técnicos en varios blogs en Internet. Como amante de la tecnología, sus hobbies incluyen probar nuevos lenguajes de programación y estructuras.