How to Use the Repository Pattern in a Laravel Application

October 01, 2021
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

A repository can be defined as a layer of abstraction between the domain and data mapping layers, one that provides an avenue of mediation between both, via a collection-like interface for accessing domain objects.

Modern PHP frameworks, such as Laravel and Symfony, interact with databases via Object-relational mappers (ORMs); Symfony uses Doctrine as its default ORM and Laravel uses Eloquent.

Both take different approaches in how database interaction works. With Eloquent, Models are generated for each database table, forming the basis of interaction. Doctrine, however, uses the Repository pattern where each Entity has a corresponding repository containing helper functions to interact with the database. While Laravel doesn't provide this functionality out of the box, it is possible to use the Repository pattern in Laravel projects.

A key benefit of the Repository pattern is that it allows us to use the Principle of Dependency Inversion (or code to abstractions, not concretions). This makes our code more robust to changes, such as if a decision was made later on to switch to a data source that isn't supported by Eloquent.

It also helps with keeping the code organized and avoiding duplication, as database-related logic is kept in one place. While this benefit is not immediately apparent in small projects, it becomes more observable in large-scale projects which have to be maintained for many years.

In this article, I will show you how to implement the Repository pattern in your Laravel applications. To do that, we will build an API to manage orders received from clients for a company.

Prerequisites

Getting started

Create a new Laravel project and cd into the directory using the following commands.

laravel new order_api
cd order_api

Set up the database

For this tutorial, we'll use MySQL as our database. To do that, in the .env file, update the database-related parameters as shown below.

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>

Finally, using your preferred database management application, create a new database called order_api.

Generate the initial data for the database

We are building an order management application, so let's create the model for it by running the following command.

php artisan make:model Order -a

The -a argument lets Artisan know that we want to create a migration file, seeder, factory, and controller for the Order model.

The command above will create five new files:

  • A controller in app/Http/Controllers/OrderController.php
  • A database factory in database/factories/orderFactory.php
  • A migration file in database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php
  • A model located in app/Models/Order.php
  • A seeder file in database/seeders/OrderSeeder.php and

In database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php, update the up function to match the following.

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

As specified in the migration file, the order table will have the following columns:

  1. An ID. This will be the table's primary key.
  2. The details of the order.
  3. The name of the client who placed the order.
  4. Whether or not the order has been fulfilled.
  5. When the order was created and updated, created_at and updated_at, provided by the timestamps function.

Next, let's update OrderFactory so that it can generate a dummy order to seed the database with. In database/factories/OrderFactory.php, update the definition function to match the following.

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

Next, open database/seeders/OrderSeeder.php and update the run function to match the following.

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

This uses OrderFactory to create 50 Orders in the database.

Don't forget to add this import:

use AppModelsOrder;

In src/database/seeders/DatabaseSeeder.php, add the following to the run function.

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

This runs the QuoteSeeder when Artisan's db:seed command is run.

Finally, run your migrations and seed the database using the following command.

php artisan migrate --seed

If you open the orders table, you'll see the newly seeded orders.

List of Orders

Create the Repository

Before we create a repository for the Order model, let's define an interface to specify all the methods which the repository must declare. Instead of relying directly on the repository class, our controller (and any order component we may build in the future) will depend on the interface.

This makes our code flexible because, should it become necessary to make a change in the future, the controller remains unaffected. For instance, if we decided to outsource order management to a 3rd party application, we can build a new module that conforms to OrderRepositoryInterface's signature and swap the binding declarations and our controller will work exactly as expected - without touching a single line of code in the controller.

In the app directory, create a new folder called Interfaces. Then, in the Interfaces, create a new file called OrderRepositoryInterface.php and add the following code to it.

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

Next, in the app folder, create a new folder called Repositories. In this folder, create a new file called OrderRepository.php and add the following code to it.

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

Apart from the flexibility provided by the interface, encapsulating queries in this manner has the added advantage that we don't have to duplicate queries throughout the application.

If, in the future, we decide to retrieve only unfulfilled orders in the getAllOrders() function, we would only have to make a change in one place instead of tracking down all the places where Order::all() is declared while risking missing some.

Creating the controllers

With our repository in place, let's add some code to our controller. Open app/Http/Controllers/OrderController.php and update the code to match the following.

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

The code injects an OrderRepositoryInterface instance via the constructor and uses the relevant object's methods in each controller method.

First, within the index() method, it calls the getAllOrders() method defined in the orderRepository to retrieve the list of orders and returns a response in JSON format.

Next, the store() method calls the createOrder() method from the orderRepository to create a new order. This takes the details of the order that needs to be created as an array and returns a successful response afterward.

Within the show() method in the controller, it retrieves the unique order Id from the route and passes it to the getOrderById() as a parameter. This fetches the details of the order with a matching Id from the database and returns a response in JSON format.

Then, to update the details of an already created order, it calls the updateOrder() method from the repository. This takes two parameters: the unique id of the order and the details that need to be updated.

Lastly, the destroy() method retrieves the unique id of a particular order from the route and calls the deleteOrder() method from the repository to delete it.

Adding the routes

To map each method defined in the controller to specific routes, add the following code to 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;

Bind the interface and the implementation

The last thing we need to do is bind OrderRepository to OrderRepositoryInterface in Laravel's Service Container; we do this via a Service Provider. Create one using the following command.

php artisan make:provider RepositoryServiceProvider

Open app/Providers/RepositoryServiceProvider.php and update the register function to match the following.

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;

Finally, add the new Service Provider to the providers array in config/app.php.

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

Test the application

Run the application using the following command.

php artisan serve

By default, the served application will be available at http://127.0.0.1:8000/. Using Postman or cURL, we can make requests to our newly created API.

Run the following command to test the /api/orders endpoint using cURL:

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

The response was formatted to JSON using jq.

You will see JSON output similar to the example below in your terminal, which was truncated to aid in readability.

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

That's how to use the Repository Pattern in a Laravel application

In this article, we learned about the Repository pattern and how to use it in a Laravel application. We've also seen some of the benefits it offers a large-scale project - one of which is loosely coupled code where we are coding to abstractions, not concrete implementations.

I will end on a note of caution, however. For small projects, this approach will feel like a lot of work and boilerplate code for returns that may not be immediately apparent. It is, therefore,  important that you properly consider the scale of the project before adopting this approach.

The entire codebase for this tutorial is available on GitHub. Feel free to explore further. Happy coding!

Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.

A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.