LARAVEL PACKAGE: REPOSITORY DESIGN PATTERN

July 31st, 2019 by Kevin Pimentel

Creating a Laravel Package is a great way to avoid duplicating code. Recently, I implemented the Repository Design Pattern on a Laravel project. The repository pattern helps to keep my controllers tidy and clean for testing. 


With a new project soon kicking off, I’d like to reuse the same exact repository code in the new project. At the same time, you always want to avoid copying and pasting code across projects. 


My solution is to move the repository code into its own Laravel package. With composer and a little autoloading magic, I should be able to load my classes and have access to my make:repository command.


Here is the folder structure that I am aiming for:

lazyelephpant/
    repository/
        src/
	       Console/
		      Command/
  			     Stubs/

You can run composer init to interactively define a composer.json file. 

However, I just copied an old composer.json file from a different package and changed the values. 

¯\_(ツ)_/¯

{
    "name": "lazyelephpant/repository",
    "description": "Laravel package to pull in repository functionality across multiple projects.",
    "type": "library",
    "authors": [
        {
            "name": "Kevin Pimentel",
            "email": "kevin@kevinpimentel.com"
        }
    ],
    "require": {},
    "require-dev": {},
    "autoload": {
        "psr-4": {
            "LazyElePHPant\\Repository\\":"src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "LazyElePHPant\\Repository\\RepositoryServiceProvider"
            ],
            "aliases": {
               
            }
        }
    }
}

Autoloading

The first important step is autoloading:

"autoload": {
    "psr-4": {
        "LazyElePHPant\\Repository\\":"src/"
    }
},

If you aren’t familiar with how the magical wonders of autoloading work: Composer registers a PSR-4 autoloader for the LazyElePHPant namespace. Essentially mapping the string LazyElePHPant to the src/ folder. 


You then need to regenerate the autoload.php file in the vendor/ folder, which can be achieved by running a dump-autoload.

Laravel Package Discovery

In Laravel, you can set packages that you want the framework to discover by visiting the config/app.php and adding it to the providers array. However, we can get our package to do this automatically.

"extra": {
        "laravel": {
            "providers": [
                "LazyElePHPant\\Repository\\RepositoryServiceProvider"
            ],
            "aliases": {
               
            }
        }
    }

By specifying the RepositoryServiceProvider in our extras section of our composer.json file, we can get Laravel to load the service provider automatically.

The Code

In the src/ folder we need to drop our RepositoryInterface and our Repository implementation. These files are currently in use by my current project. I have modified them below to work from within the Laravel package.


RepositoryInterface.php

<?php

namespace LazyElePHPant\Repository;

use Illuminate\Database\Eloquent\Model;

interface RepositoryInterface
{
    public function all($columns = ['*']);

    public function list($orderByColumn, $orderBy = 'desc', $with = [], $columns = ['*']);

    public function create(array $data);

    public function update(array $data, $id);

    public function delete($id);

    public function find($id, $columns = array('*'));

    public function findBy(string $field, mixed $value, $columns = array('*'));

    public function setModel(Model $model);

    public function getModel();
}

Repository.php

<?php

namespace LazyElePHPant\Repository;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App;

abstract class Repository implements RepositoryInterface
{
    /** @var App */
    private $app;

    /** @var Illuminate\Database\Eloquent\Model */
    private $model;

    public function __construct(App $app)
    {
        $this->app = $app;
        $this->makeModel();
    }

    abstract public function model();

    public function makeModel()
    {
        $model = $this->app->make($this->model());

        if (!$model instanceof Model) {
            throw new RepositoryException(
                "Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"
            );
        }

        return $this->model = $model;
    }

    public function all($columns = array('*'))
    {
        return $this->model->get($columns);
    }

    public function list($orderByColumn, $orderBy = 'desc', $with = [], $columns = ['*'])
    {
        return $this->model->with($with)
                           ->orderBy($orderByColumn, $orderBy)
                           ->get($columns);
    }

    public function create(array $data)
    {
        return $this->model->create($data);
    }

    public function update(array $data, $id, $attribute = 'id')
    {
        return $this->model->where($attribute, '=', $id)->update($data);
    }

    public function delete($id)
    {
        return $this->model->destroy($id);
    }

    public function find($id, $columns = array('*'))
    {
        return $this->model->find($id, $columns);
    }

    public function findBy(string $field, mixed $value, $columns = array('*'))
    {
        return $this->model->where($field, $value)
                           ->select($columns)
                           ->first();
    }

    public function setModel(Model $model)
    {
        $this->model = $model;
    }

    public function getModel()
    {
        return $this->model;
    }
}

For more details on the Repository Design Pattern I believe this is the original place where I learned about it.

At this stage our Laravel Package folder structure should be:

lazyelephpant/
    repository/
        composer.json
        src/
            RepositoryInterface.php
            Repository.php
            Console/
	           Command/
		          Stubs/

Stubs

To understand what a stub is you have to dig into the framework. There are mentions of stubs in the documentation but I don’t remember seeing an in-depth explanation of them. Stubs were certainly new to me. I only came across them because I was digging around the framework trying to figure out how php artisan was able to generate controller and model classes. It turns out the answer is Stubs! Who knew?! Not me.


If you’ve ever used:

php artisan make:controller SomeController 

or 

php artisan make:model SomeModel.

A stub is the template that these classes are based on. I looked in the framework and created my own, here’s what they look like:

repository.plain.stub

namespace DummyNamespace;

use LazyElePHPant\Repository\Repository;
use Illuminate\Database\Eloquent\Model;

class DummyClass extends Repository
{
    public function model()
    {
        return Model::class;
    }
}

The plain stub is the template that is used when no model is specified:

php artisan make:repository

However, if you were to specify a model, the repository.stub is the template that would be used:

php artisan make:repository --model=User

repository.stub

namespace DummyNamespace;

use LazyElePHPant\Repository\Repository;
use NamespacedDummyModel;

class DummyClass extends Repository
{
    public function model()
    {
        return DummyModel::class;
    }
}

Idea being that you generate a repository with the User model:

namespace App\Repository;

use LazyElePHPant\Repository\Repository;
use App\User;

class UserClass extends Repository
{
    public function model()
    {
        return User::class;
    }
}

Repository Command

Finally, the command where all the magic happens looks like this:

RepositoryMakeCommand.php

<?php

namespace LazyElePHPant\Repository\Console\Commands;

use Illuminate\Support\Str;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;

class RepositoryMakeCommand extends GeneratorCommand
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $name = 'make:repository';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new repository class';

    /**
     * Build the class with the given name.
     *
     * @param  string  $name
     * @return string
     */
    protected function buildClass($name)
    {
        $stub = parent::buildClass($name);
        $model = $this->option('model');

        return $model ? $this->replaceModel($stub, $model) : $stub;
    }

    /**
     * Replace the model for the given stub.
     *
     * @param  string  $stub
     * @param  string  $model
     * @return string
     */
    protected function replaceModel($stub, $model)
    {
        $model = str_replace('/', '\\', $model);
        $namespaceModel = $this->laravel->getNamespace().$model;

        $stub = (Str::startsWith($model, '\\'))
              ? str_replace('NamespacedDummyModel', trim($model, '\\'), $stub)
              : str_replace('NamespacedDummyModel', $namespaceModel, $stub);

        $stub = str_replace(
            "use {$namespaceModel};\nuse {$namespaceModel};", "use {$namespaceModel};", $stub
        );

        $model = class_basename(trim($model, '\\'));

        $dummyModel = Str::camel($model) === 'user' ? 'model' : $model;

        return str_replace('DummyModel', $model, $stub);
    }

    /**
     * Get the stub file for the generator.
     *
     * @return string
     */
    protected function getStub()
    {
        return $this->option('model')
                    ? __DIR__.'/Stubs/repository.stub'
                    : __DIR__.'/Stubs/repository.plain.stub';
    }

    /**
    * Get the default namespace for the class.
    *
    * @param  string  $rootNamespace
    * @return string
    */
    protected function getDefaultNamespace($rootNamespace)
    {
        return $rootNamespace . '\Repository';
    }

    /**
    * Get the console command options.
    *
    * @return array
    */
    protected function getOptions()
    {
        return [
            ['model', 'm', InputOption::VALUE_OPTIONAL, 'The model that the repository applies to']
        ];
    }
}

Now that we know what all the moving parts are. The package structure looks like this:

lazyelephpant/
    repository/
        composer.json
        src/
            RepositoryInterface.php
            Repository.php
            Console/
	           Command/
                    RepositoryMakeCommand.php
    		           Stubs/
                        repository.stub
                        repository.plain.stub

That’s our Laravel Package structure at this point. Except there is one problem. We are missing the most important piece: The RepositoryServiceProvider.php class.

Laravel Package Service Provider

We need to create a RepositoryServiceProvider.php class in the src/ folder. The RepositoryServiceProvider will need to extend the Laravel ServiceProvider.php class.

<?php

namespace LazyElePHPant\Repository;

use Illuminate\Support\ServiceProvider;

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

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
    	
    }
}

In the documentation we see exactly how to register commands:

<?php

namespace LazyElePHPant\Repository;

use Illuminate\Support\ServiceProvider;
use LazyElePHPant\Repository\Console\Commands\RepositoryMakeCommand;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
    	if ($this->app->runningInConsole()) {
	        $this->commands([
	            RepositoryMakeCommand::class,
	        ]);
	    }
    }

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {

    }
}

Everything is set, now we test!

Testing

To test our new Laravel package locally we dive back into our composer file and add an entry to required:

"lazyelephpant/repository": "dev-master",


"repositories":[
        {
            "type":"path",
            "url":"../lazyelephpant/repository",
            "options":{
                "symlink":true
            }
        }
]

Then run composer update to get the package pulled in.

composer update



Okay! It’s in the vendor folder.

Now lets see if the command works:

It successfully generated the repository class without the model:

<?php

namespace App\Repository;

use LazyElePHPant\Repository\Repository;
use Illuminate\Database\Eloquent\Model;

class TestRepository extends Repository
{
    public function model()
    {
        return Model::class;
    }
}

Now we test that we can assign a model:

It’s science magic.

<?php

namespace App\Repository;

use LazyElePHPant\Repository\Repository;
use App\User;

class TestRepository extends Repository
{
    public function model()
    {
        return User::class;
    }
}

Here’s a simple dump to show that we are in fact retrieving the TestRepository:

use App\Repository\TestRepository;

...

public function __construct(TestRepository $testRepository)
{
    dd($testRepository);
}


Conclusion

I recently read an article, a very well written one, on why Object Oriented Programming is flawed. The argument made was that: when you want to reuse code in other projects you end up having to copy it over. 


I would say that most programming languages use some type of package management system these days. In my opinion it’s easier to make libraries reusable now, more than ever. If you ever find yourself copying code from one project to the next, consider creating yourself package.


GitHub: LazyElePHPant / repository



Kevin Pimentel

There are two types of people in the world: those that code, and those that don’t. I said that! Quote me. My name is Kevin and I’m one of the ones that codes.