kromacie/l5repository

Package to manage data-objects queries with friendly repository pattern oriented way in Laravel/Lumen 5.7

1.3.0 2019-09-22 18:44 UTC

This package is not auto-updated.

Last update: 2024-04-29 17:03:39 UTC


README

pipeline status coverage report License: MIT

You don't like messed up controllers? With this package, you can simply separate your query usage to one line of code. This package bases on Repository Pattern solution and it allows you to run your queries from one of your repository method or specially created action. Additionally, it provides caching mechanism to relive your related database storage.

Content

  1. Requirements
  2. Setup
  3. Configuration
  4. Usage

Requirements

This package has been written with PHP 7.2 for Laravel 5.7 and Lumen 5.7, so I don't guarantee that on olders versions this will work stable.

For this solution you can only use tagged based caching mechanism like Redis/Memcache.

Setup

To download this package just write in your project terminal:

$ composer require kromacie/l5repository

When the package will be downloaded, register the service provider.

For Lumen:

$app->register(\Kromacie\L5Repository\RepositoryServiceProvider::class);

For Laravel this should be discovered automatically but if not, just register this in config/app.php using the same namespace as above.

Next, copy or provide configuration file. You can find this in "vendor/kromacie/l5repository/config" directory.

Configuration

This package provides configuration for all necessary cases.


return array(
    /*
     * Here youe should add classnames of all created cachable repositories
     * to enable finding all related queries with any of these repositories, it allows to
     * automatically flush cache when model is creating, deleting or updating.
     */
    'repositories' => [

    ],
    /*
     * You can change repository resolver here to use your own if you need other caching mechanism
     */
    'resolver' => \Kromacie\L5Repository\RepositoryResolver::class,

    'cache' => [
        /*
         * Here is cache prefix for all created keys and tags. You should set something unique
         * to avoid overwriting your cache keys by other values. It may cause some unexpected errors.
         */
        'prefix' => 'l5repository',

        /*
         * Set the default store for all repositories
         * Remember, you can ovverride this in your repository or by setting this directly for method
         */
        'store' => 'redis',

        /*
         * Set the default time in minutes for all repositories. It cant be forever to not
         * choke your cache memory. Preffered time should be something like 10 minutes, becouse
         * if your database will grow up, it may fast overfill your memory.
         */
        'time' => 10,

        /*
         * Set if scopes from repositories should be cached as one of tag and part of builded cache key.
         * If you are using parameters in your scopes, it should be enabled. Otherwise, it will return
         * last cached query for your repository method regardless of set parameters.
         */
        'scopes' => true,
        
        /*
         * Set true if u want to enable caching, or false if u want to be disabled.
         * You can disable/enable this directly for repository or method.
         */
        'enabled' => true,
    ]
);

Usage

Basics

Assuming that you need to create a new repository for Users, just create a new class named UserRepository (or whatever you want). It needs to extend AbstractRepository class.

namespace App\Http\Repositories;

use Kromacie\L5Repository\Repositories\AbstractRepository;
use App\Http\Models\User;

class UserRepository extends AbstractRepository
{

    public static function getClass(): String
    {
        return User::class;
    }

}

Abstract repository implements abstract static function named getClass() and this is good place to define which Model this repository is going to use.

Initially nothing spacial happend. Just class and one method. But in the background you can find some basic methods implemented by AbstractRepository.

  • public function get(array $columns = ['*'])
  • public function sum(string $column)
  • public function count(string $columns = '*')
  • public function exists()
  • public function first(array $columns = ['*'])
  • public function delete()
  • public function updateOrCreate(array $attributes)
  • public function updateOrInsert(array $attributes)
  • public function insert(array $values)
  • public function insertGetId(array $values, string $sequence = null)
  • public function update(array $values)
  • public function create(array $attributes)
  • public function paginate(int $perPage = null, array $columns = ['*'], string $pageName = 'page', int $page = null)
  • public function perform(ActionInterface $action)

Almost all of these methods have the same usage as methods provided by Eloquent Model. But here is something new. Method named perform.

Using action

For the best explanation, lets create another class - Action named UserLogin.

namespace App\Http\Actions;

use Kromacie\L5Repository\Contracts\ActionInterface;
use Kromacie\L5Repository\Repositories\AbstractRepository;

class UserLogin implements ActionInterface
{

    public function perform(AbstractRepository $repository)
    {
        // TODO: Implement logic of user login
    }
}

This class implements method named perform. So, now we have action which doesn't have any logic. Lets write something.

Scopes

The next step is creating scope. It will be the lowest level of provided logic.

namespace App\Http\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Kromacie\L5Repository\Contracts\ScopeInterface;

class UserLoginData implements ScopeInterface
{

    protected $login;
    protected $password;

    public function __construct($login, $password)
    {
        $this->login = $login;
        $this->password = $password;
    }

    public function scope(Builder $builder)
    {
        $builder->where('login', '=', $this->login)
                ->where('password', '=', $this->password);
    }
}

This class is going to separate all of bad-looking logic inside a reusable component which we have already created. Method named scope will be always handled when our repository performs our Action. So lets implement this.

namespace App\Http\Actions;

use Kromacie\L5Repository\Contracts\ActionInterface;
use Kromacie\L5Repository\Repositories\AbstractRepository;

class UserLogin implements ActionInterface
{

    protected $login;
    protected $password;

    public function __construct($login, $password)
    {
        $this->login = $login;
        $this->password = $password;
    }

    public function perform(AbstractRepository $repository)
    {
        $repository->scope(new UserLoginData($this->login, $this->password));
        
        return $repository->first();
    }
}

Look how simply it looks, just add our scope to repository which we are going to use. But why we need so many classes? It's just for your comfort of work. Small separated classes doing one simple things. In this case, it's just our user login, but if you have more expanded logic of your application, it may become more unreadable.

Controller usage

But now in your controller, you will have just one line of usage.


public function login(UserLoginRequest $request) {

    $login = $request->validated()['login'];
    $password = $request->validated()['password'];
    
    $user = $this->repository->perform(new UserLogin($login, $password));
}

Okay, it's more than one line, but still looks pretty, isn't it?

Using method

But what if i'm lazy and i don't want to create actions for all queries? I can create method in our repository. It will be very similar to our action.

namespace App\Http\Repositories;

use Kromacie\L5Repository\Repositories\AbstractRepository;
use Kromacie\L5Repository\Traits\CachableRepository;
use App\Http\Models\User;
use App\Http\Scopes\UserLoginData;

class UserRepository extends AbstractRepository
{

    use CachableRepository;
    
    public function getCachableMethods(): array
    {
        return [
            'login'
        ];
    }

    public static function getClass(): String
    {
        return User::class;
    }
    
    public function login($login, $password)
    {
        $this->scope(new UserLoginData($login, $password));
        
        return $this->first();
    }

}

And now, usage in the controller will look like:

namespace App\Http\Controllers;

use App\Http\Repositories\UserRepository;
use App\Http\Requests\UserLoginRequest;

class UserController extends Controller 
{
    private $users;
    
    public function __contruct(UserRepository $users)
    {
        $this->users = $users;
    }
    
    public function login(UserLoginRequest $request) {

        $login = $request->validated()['login'];
        $password = $request->validated()['password'];
        
        $user = $this->users->login($login, $password);
    }
    
}

Very similar.

Caching

Some days passed, our code is growing and we can feel first symptoms of database overload. Too many queries in too short part of time. We don't have to search solutions like - "How to cache database queries?" - becouse this package provides tested solution to solve this problem.

And it will be the CachableRepositoryTrait.

So lets implement this and use our repository from previous example.

namespace App\Http\Repositories;

use Kromacie\L5Repository\Repositories\AbstractRepository;
use Kromacie\L5Repository\Traits\CachableRepository;
use App\Http\Models\User;
use App\Http\Actions\UserLogin;

class UserRepository extends AbstractRepository
{

    use CachableRepository;
    
    public function getCachableMethods(): array
    {
        return [
            UserLogin::class
        ];
    }

    public static function getClass(): String
    {
        return User::class;
    }

}

New method is named getCachableMethods(). Here we should define, which methods or actions we need to cache. For this example we want to cache UserLogin class. assuming that we are using api without session to login, we want to limit to minimum numbers of database connections.

Caching configuration

We can configure this Action in 3 ways. In l5repository.php configuration file, repository or directly in getCachableMethod.

Configuration file always has the lowest priority for overwriting config, so if we set configuration for repository, it will be never overwritten by config file. And analogically, if we set configuration for Action, it will be never overwritten becouse it has the highest priority.

Let's look on the example.

/* 
    config/l5repository.php 
*/
'cache' => [
    'prefix' => 'l5repository', // <- this shouldn't be overwritten
    'connection' => 'default',
    'time' => 10,
    'scopes' => true,
    'enabled' => true,
]

/*-------------------------*/
    
class UserRepository extends AbstractRepository
{
    use CacheableRepository;
    
    /*
        These three parameters will overwrite default configuration from file
    */
    protected $cacheScopes = false; 
    protected $store = 'default';
    protected $time = 5; 
    protected $enabled = true;
    
    /*
        But this implementation will always have priority over parameters and config file.
    */
    public function getCachableMethods(): array
    {
        return [
            UserLogin::class => [
                'time' => 15,
                'cacheScopes' => true,
                'enabled' => true,
                'store' => 'default' 
                /* 
                   For example, if u dont want to define any of these parameters, 
                   you can not to do this. Nothing bad will happen.
               * /
            ],
           'getUserById',
           'getUsersByPost' 
        ];
    }

}

Okey, I hope that aspect of how configuration priority works, is clear enough.

Runtime

Now, I want to create PostRepository. I can assume that on my page may be many posts, so I should paginate them. But I don't want to cache all of the post. Only first and second page to not overfill my cache. And how to do this? CachableRepository implements runtime method. And it allows us to overwrite any parameter.

So lets look on this.

Assume that we are in PostRepository.

public function getPosts($page) {

    if($page > 2) {
        $this->runtime('enabled', false);
    }
    
    return $this->paginate(10, ['*'], 'myPage', $page);
}

And, this will cache only first and secound page. Others will be getting directly from database.

Using tags

At this moment, we have still one thing I haven't explained yet.

What if I want to load some relations to my repository? What if something there will change? The solution for this is: TaggableInterface. If I add this to Scope which is using relation and any object using this relation will be deleted, updated or created, all of cached queries tagged with this relation will be flushed.

Example of implementation:

namespace App\Http\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Kromacie\L5Repository\Contracts\ScopeInterface;
use Kromacie\L5Repository\Contracts\TaggableInterface;
use App\Http\Models\Permission;

class WithPermissions implements ScopeInterface, TaggableInterface
{

    protected $login;
    protected $password;

    public function __construct($login, $password)
    {
        $this->login = $login;
        $this->password = $password;
    }
    
    public function tags(): array
    {
        return [
            Permission::class
        ];
    }

    public function scope(Builder $builder)
    {
        $builder->with('permissions');
    }
}

But the requirement is that you have to register a repository which is using this relation. And add class of this repository to config.

/* config/l5repository.php */


'repositories' => [
    \App\Http\Repositories\PermissionRepository::class,
    \App\Http\Repositories\UserRepository::class,
],

So, for this example, deleting, creating and updating models by UserRepository and PermissionRepository which are asociated by tags, will flush their cache.

Generating Repositories

Now, you can automate workflow with this package using code generation.

You can type a command which will create repository, actions for crud and register new repository in config file.

Usage:

php artisan repository:create {name} {--model=} {--crud} {--cache} {--action_dir=}

I will give you a example to help you understand how this command works.

Assume, that you will use this command as below

php artisan repository:create User --model=User --crud --cache

That command should create UserRepository.php file for you and automatically implement method "getClass" with fully qualified namespace for model named User.

Because --cache option is used, UserRepository will use CachableRepository as a Trait and also implement method "getCachableMethods" with empty body.

Because --crud option is used, basic Crud actions will be created for UserRepository in User directory. Default implementation assumes 5 actions:

  • Create
  • Update
  • Delete
  • Show
  • ShowAll

For better understanding, I will give you next, more complex example

php artisan repository:create QuestionHasContentRepository --model=QuestionContent --crud --cache --action_dir=Question/Content

And that comand will create QuestionHasContentRepository.php in your Repositories directory. "getClass" also will be implemented. And now, why I set custom directory for actions? Because name for repository is QuestionHasContent and it will be mapped to directory Question/Has/Content. I don't want "Has" directory, so I can set custom one to prevent unwanted behaviours.

I hope that you will enjoy this package, and it will help you to solve caching problems and get to know how files and classes should be structured.

If you find any issues, please report them for me. I will fix this as fast as possible.