Better implementation of Laravel repositories for Eloquent and Scout!

single-image

In the tutorial I will present how to create your own implementation of this pattern correctly in an ultra flexi way. Invites you to read.

If you are familiar with repositories, e.g. Symfony, you will miss them at Laravel. Laravel provides Query Scopes which are a phenomenal tool for speeding up query building, moreover, if you think about it more deeply, this mechanism does not conflict with the Repositories design pattern.

About Repository Design Pattern.

Repository is class witch is used to mediate between the domain and data mapping layers using a collection-like interface for accessing domain objects. They encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.

[Design patterns in PHP]

Assumptions for the project

In the article below I will try to introduce you to Repositories that:

  • They fulfill the principle of one responsibility, unlike many other models available on the internet.
  • They are easily testable.
  • They allow you to use Eloquent, Scout or any other Builder using one common interface, which you can additionally expand!

Creating RepositoryServiceProvider

I decided to take this step because we will gather all the functions of the Service Container, and they will be at least two, in one place.

Firstly create file:

<?php


namespace App\Providers;


use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
    }
}

Secondly register provider in config/app.php:

 'providers' => [
     /*
     * Application Service Providers...
     */
     //...
     App\Providers\RepositoryServiceProvider::class,
 ],

Creating interfaces

The biggest problem with creating adapters for Scout and Eloquent was the creation of a common interface allowing smooth work on repositories. Unfortunately, native Laravel does not provide this option. To get a common interface we will use the Dekorator design pattern and the possibilities of extending the ServiceContainer proposed by Laravel. In a moment everything will become clear, so we create classes in turn.

The first step will be to create an interface for scopes that will be used as our classes to create individual queries:

<?php


namespace App\Repositories;


use App\Repositories\Decorators\BuilderInterface;

interface ScopeInterface
{
    public function build(): BuilderInterface;
}

As you can see, our interface contains only one build() method in which we will return our target BuilderInterface – that is, we will return a creation there that will allow us to do with queries everything that the native Builder classes in Larvel can do

Next, we create a creation that I called BuilderInterface:

<?php


namespace App\Repositories\Decorators;


interface BuilderInterface
{
    public function get($columns = []);

    public function first($columns = []);
}

As you can see, the interface has three methods, but it should definitely be developed by you according to your intentions (e.g. I often use the firstOrFail() method so I will gladly implement it into the interface).

Last but not the worst interface, supporting interface for maintaining RepositoryInterface order

<?php


namespace App\Repositories;


use App\Repositories\Decorators\BuilderInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

interface RepositoryInterface
{
    public function setScope(ScopeInterface $query): void;

    public function builder(): BuilderInterface;
    
    public function get(): Collection;

    public function first(): ?Model;
}

As you can see again we will want to add other methods besides get() and first().

Implementation

This time we will start with the implantation of Repositories:

<?php


namespace App\Repositories;


use App\Repositories\Decorators\BuilderInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

class Repository implements RepositoryInterface
{
    /**
     * @var ScopeInterface
     */
    private $scope;

    public function setScope(ScopeInterface $scope): void
    {
        $this->scope = $scope;
    }
    
    public function builder(): BuilderInterface
    {
        return $this->scope->build();
    }

    public function get(): Collection
    {
        return $this->scope->build()->get();
    }

    public function first(): ?Model
    {
        return $this->scope->build()->first();

    }
}

The next step is to create two decorators that will decorate our builders. It is very important that when extending, they fulfill everything in the constructor of the parent constructor.

The next step is to create two decorators that will decorate our builders. It is very important that when extending, they fulfill everything in the constructor of the parent constructor.

Rest is a fairly simple implementation – repetitive get and first methods used in Eloquent and Scout:

<?php


namespace App\Repositories\Decorators;


use Illuminate\Database\Eloquent\Builder;

class EloquentBuilderDecorator extends Builder implements BuilderInterface
{
    // we don't have to implement nothing atm
}
<?php


namespace App\Repositories\Decorators;


use Laravel\Scout\Builder;

class ScoutBuilderDecorator extends Builder implements BuilderInterface
{

    public function __construct(Builder $builder)
    {
        parent::__construct($builder->model, $builder->query, $builder->callback);

        $this->wheres = $builder->wheres;

    }
    public function get($columns = [])
    {
        return parent::get();
    }

    public function first($columns = [])
    {
        return parent::first();
    }

}

Now the time has come for some magic, or how could this decorator start working while maintaining full compatibility with Laravel Builders used throughout the application?

Using the extend() service container method we created earlier will be used to ScoutBuilderDecorator. So let’s get back to RepositoryServiceProvider and leave it in this form:

<?php


namespace App\Providers;


use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->extend(\Laravel\Scout\Builder::class, function ($service, $app) {
            return new \App\Repositories\Decorators\ScoutBuilderDecorator($service);
        });
    }
}

For EloquentBuilderDecorator we need to create trait for our models, by overriding newEloquentBuilder() function we are getting access to our extended Builder in model:

<?php


namespace App\Repositories\Decorators;


trait EloquentBuilderDecorable
{
    public function newEloquentBuilder($query)
    {
        return new EloquentBuilderDecorator($query);
    }
}

Then we create implementations for repositories that seems simple and logical:

<?php


namespace App\Repositories;


use App\Repositories\Decorators\BuilderInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

class Repository implements RepositoryInterface
{
    /**
     * @var ScopeInterface
     */
    private $scope;

    public function setScope(ScopeInterface $scope): void
    {
        $this->scope = $scope;
    }

    public function builder(): BuilderInterface
    {
        return $this->scope->build();
    }

    public function get(): Collection
    {
        return $this->scope->build()->get();
    }

    public function first(): ?Model
    {
        return $this->scope->build()->first();

    }
}

Creating scopes

We are already done with 100% of logic. Now I will show You how to create scopes and use it.

Lets make first very simple scope:

<?php

namespace App\Scopes\User;

use App\Repositories\Decorators\BuilderInterface;
use App\Repositories\ScopeInterface;
use App\User;

class SearchScope implements ScopeInterface
{
    /**
     * @var string
     */
    private $search;

    public function build(): BuilderInterface
    {
        $builder = User::where('name', $this->search);

        return $builder;
    }

    public function setSearch(string $search): void
    {
        $this->search = $search;
    }
}

Usage

I will show you how to use our repositories in example of controller:

<?php

namespace App\Http\Controllers\User;


use App\Repositories\Repository;
use App\Scouts\User\SearchScope;

class SearchController
{
    public function search(SearchScope $scope, Repository $repository)
    {
        $scope->setSearch('foobar');
        $repository->setScope($scope);
        $user = $repository->first(); // Model or null
    }
}

As you see use of Repository is very intuitive and very flexi. You can always add new methods to RepositoryInterface such as FirstOrCreate() or whatever you need.

But common work with repositories gonna be to create your scopes (wich follow the single responsibility rule!) setting them to repository and getting your results.

I will show more examples and tests in next articles.


In addition, I wonder whether to create a package from repository decorators and the repository class itself. Let me know if you think this might be useful.


Please leave me comment if you have any question. If you liked this post, go to my instagram and follow me to stay up to date!

You may like