adobrovolsky97 / laravel-repository-service-pattern

Laravel Repository-Service Pattern
26 stars 5 forks source link

Laravel 5.x - 11.x Repository-Service Pattern

The repository service pattern in Laravel is a widely used architectural pattern that provides a structured approach to accessing and managing data in an application. It serves as an intermediary layer between the application's business logic and the underlying data storage mechanism, such as a database.

The purpose of using the repository service pattern in Laravel is to decouple the application's business logic from the specific implementation details of data storage. By doing so, it promotes code reusability, maintainability, and testability. The pattern achieves this by defining a set of interfaces or contracts that represent the operations and queries related to data access.

Here are some key purposes and benefits of using the repository service pattern in Laravel:

Overall, the repository service pattern in Laravel provides a structured and flexible approach to managing data access in applications, contributing to better code organization, maintainability, and testability.

Supports soft delete functionality (via trait or custom column).

Supports composite model keys.

Installation

Composer

Execute the following command to get the latest version of the package:

composer require adobrovolsky97/laravel-repository-service-pattern

Methods

Adobrovolsky97\LaravelRepositoryServicePattern\Repositories\Contracts\BaseRepositoryInterface

Adobrovolsky97\LaravelRepositoryServicePattern\Repositories\Contracts\BaseCachableRepositoryInterface

This one supports the same methods, the only difference that it supports caching models & collections

Adobrovolsky97\LaravelRepositoryServicePattern\Services\Contracts\BaseCrudServiceInterface

Usage

Create a Model

Create your model e.g Post

namespace App;

class Post extends Model {

    protected $fillable = [
        'title',
        'author',
        ...
     ];

     ...
}

Create Repository

namespace App;

use Adobrovolsky97\LaravelRepositoryServicePattern\Repositories\BaseRepository;

class PostRepository extends BaseRepository implements PostRepositoryInterface {

    /**
     * Specify Model class name
     *
     * @return string
     */
    protected function getModelClass(): string
    {
        return Post::class;
    }
}

Create Service

namespace App;

use Adobrovolsky97\LaravelRepositoryServicePattern\Repositories\BaseRepository;

class PostService extends BaseCrudService implements PostServiceInerface {

    /**
     * Specify Repository class name
     *
     * @return string
     */
    protected function getRepositoryClass(): string
    {
        return PostRepositoryInteface::class;
    }
}

Link Service to its contract in ServiceProvider

class AppServiceProvider extends ServiceProvider {

    /**
     * Specify Repository class name
     *
     * @return string
     */
    public function register(): void
    {
        $this->app->singleton(PostRepositoryInterface::class, PostRepository::class);
        $this->app->singleton(PostServiceInterface::class, PostService::class);
    }
}

Now the Service is ready for work.

Use methods

namespace App\Http\Controllers;

use App\PostServiceInterface;

class PostsController extends Controller {

    /**
     * @var PostServiceInterface
     */
    protected PostServiceInterface $service;

    public function __construct(PostServiceInterface $service) 
    {
        $this->service = $service;
    }
    ....
}

CRUD Controller Actions Example

Index

public function index(SearchRequest $request): AnonymousResourceCollection
{
    return PostResource::collection($this->service->withTrashed()->getAllPaginated($request->validated(), 25));
}

Show

public function show(int $postId): PostResource
{
    return PostResource::make($this->service->findOrFail($postId));
}

Store

public function store(StoreRequest $request): PostResource
{
    return PostResource::make($this->service->create($request->validated()));
}

Update

public function update(Post $post, UpdateRequest $request): PostResource
{
    return PostResource::make($this->service->update($post, $request->validated()));
}

Destroy

public function destroy(Post $post): JsonResponse
{
    $this->service->delete($post);
    // Or  
    $this->service->softDelete($post); 

    return Response::json(null, 204);
}

Restore

public function restore(Post $deletedPost): PostResource
{
    $this->service->restore($deletedPost);

    return PostResource::make($deletedPost->refresh());
}

Soft Deletes

You need to add at least soft delete column (deleted_at) to the table to start using soft deletes from the service.

Also, it is possible to use it together with SoftDeletes trait

By default soft delete column name is deleted_at, you may override it by defining variable inside your repository

protected $deletedAtColumnName = 'custom_deleted_at';

By default, soft deleted records excluded from the query result data

$posts = $this->service->getAll();
// Those are equivalent
$posts = $this->service->withoutTrashed()->getAll();

Showing only soft deleted records

$posts = $this->service->onlyTrashed()->getAll();

Showing only NOT soft deleted records

$posts = $this->service->withoutTrashed()->getAll();

Loading the Model relationships

$post = $this->service->with(['author'])->withCount(['readers'])->getAll();

Query results filtering

By default filtering will be handled by applyFilterConditions(), but you may probably need to do custom filtering, so override applyFilters method in your repository if you need custom filtering

class PostRepository extends BaseRepository implements PostRepositoryInterface {

   /**
    * Override this method in your repository if you need custom filtering
    * 
    * @param array $searchParams
    * @return Builder
    */
    protected function applyFilters(array $searchParams = []): Builder 
    {
        return $this
            ->getQuery()
            ->when(isset($searchParams['title']), function (Builder $query) use ($searchParams) {
                $query->where('title', 'like', "%{$searchParams['title']}%");
            })
            ->orderBy('id');
    }
}

Find many models by multiple fields

$posts = $this->repository->findMany([
    'field' => 'val' // where('field', '=', 'val')
    ['field', 'val'], // where('field', '=', 'val')
    ['field' => 'val'], // where('field', '=', 'val')
    ['field', '=', 'val'], // where('field', '=', 'val')
    ['field', '>', 'val'], // where('field', '>', 'val')
    ['field', 'like', '%val%'], // where('field', 'like', '%val%')
    ['field', 'in', [1,2,3]], // whereIn('field', [1,2,3])
    ['field', 'not_in', [1,2,3]], // whereNotIn('field', [1,2,3])
    ['field', 'null'], // whereNull($field)
    ['field', 'not_null'], // whereNotNull($field)
    ['field', 'date', '2022-01-01'], // whereDate($field, '=', '2022-01-01')
    ['field', 'date <=', '2022-01-01'], // whereDate($field, '<=', '2022-01-01')
    ['field', 'date >=', '2022-01-01'], // whereDate($field, '>=', '2022-01-01')
    ['field', 'day >=', '01'], // whereDay($field, '>=', '01')
    ['field', 'day', '01'], // whereDay($field, '=', '01')
    ['field', 'month', '01'], // whereMonth($field, '=', '01')
    ['field', 'month <', '01'], // whereMonth($field, '<', '01')
    ['field', 'year <', '2022'], // whereYear($field, '<', '2022')
    ['field', 'year', '2022'], // whereYear($field, '=', '2022')
    ['relation', 'has', function($query) {// your query}], // whereHas('relation', function($query) { // your query}})
    ['relation', 'DOESNT_HAVE', function($query) {// your query}], // whereDoesntHave('relation', function($query) { // your query}})
    ['relation', 'HAS_MORPH', function($query) {// your query}], // whereHasMorph('relation', function($query) { // your query}})
    ['relation', 'DOESNT_HAVE_MORPH', function($query) {// your query}], // whereDoesntHaveMorph('relation', function($query) { // your query}})
    ['field', 'between', [1,5]], // whereBetween('field', [1,5])
    ['field', 'NOT_BETWEEN', [1,5]], // whereNotBetween('field', [1,5])
]);

Caching

If you want to apply caching to your models - extend your entity repository with the BaseCacheableRepository.php

Code Generator

Allows generating Classes/Interfaces/Traits

$template = new ClassTemplate();

$template->setType('class')->setName('ClassName')->setNamespace('Path\\To\\Class');

It is possible define properties, methods, constants, extends, implements, doc block comments, create abstract/final classes, set method body, etc...

Eloquent Model Generation Feature

Command php artisan generate:model {table} - generates the model for the table, define all relations, properties, define doc block with props annotations. (will not override model if the one already exists)

Repository-Service pattern files generation

Command php artisan generate:repository-service {table} generates repository and service for a model (will not override model if the one already exists)

Requests generation

Command php artisan generate:request {tableAndNamespace} {modelNamespace?} will generate a Request instance,

Resource generation

Command php artisan generate:resource {table} generates resource for a particular Model by table name (will not override model if the one already exists)

Api Resource Controller generation

Command php artisan generate:resource-controller {table} will generate a resource controller for a particular Model entity by table name (will not override model if the one already exists)

The controller actions will be based on the Repository-Service laravel pattern package;

CRUD generation

Command php artisan generate:crud {table} will generate all entities mentioned above (will not override model if the one already exists)