Laravel 5.1 - Repository

1. Giới thiệu về Repository

Repository là một trong các pattern hay được sử dụng trong lập trình hướng đối tượng. Trong Laravel, chúng ta sử dụng repository như một phần trung gian xử lý các tác vụ liên quan tới cơ sở dữ liệu. Sử dụng repository giúp tránh lặp lại code, dễ sử dụng, dễ sửa và đồng thời thể hiện rõ hơn ý nghĩa của các dòng code. Repository

2. Xây dựng repository

Nội dung bài viết được cập nhật ngày: 29/12/2016,

2.1 Repository Interface

Bước đầu tiên trong quá trình xây dựng repository là tạo ra interface cho nó.

<?php
namespace App\Repositories\Contracts;

interface RepositoryInterface
{
    public function __construct();
    public function makeEntity();
    public function orderBy($attr, $dir = 'asc');
    public function all($columns = ['*']);
    public function findBy($attribute, $value, $shouldThrowException = true);
    public function findById($id);
    public function whereIn($attribute, array $values, $columns = ['*']);
    public function where(array $where, $columns = ['*']);
    public function create(array $data);
    public function update($data, $id, $attribute = 'id');
    public function updateOrCreate(array $attributes, array $values = []);
    public function delete($model);
    public function getLatestEntities($limit = null, $eagerLoad = []);
    public function insert(array $data);
    public function batchInsert(array $collection, $returnLastId = true);
    public function resetEntity();
}
2.2 Repository abstract

Để nhắm tránh việc lặp lại code cùng xử lý các tác vụ tương tự nhau trong các repository khác nhau của các đối tượng, ta tạo ra 1 lớp abstract implements interface vừa tạo ở trên. Bài viết chỉ giới thiệu 1 số phương thức, các phương thức còn lại được tổng hợp tại file ở cuối bài.

<?php

namespace App\Repositories;

use Exception;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

/**
 * @property-read string $entityName
 */
abstract class Repository implements RepositoryInterface
{
    /**
     * The entity.
     *
     * @var object
     */
    protected $entity;

    /**
     * Create new repository instance.
     */
    public function __construct()
    {
        $this->makeEntity();
    }

    /**
     * Make entity by specified name.
     *
     * @throws Exception
     */
    public function makeEntity()
    {
        if (! property_exists($this, 'entityName')) {
            throw new Exception('Class'.get_class($this).' must provide an attribute called \'entityName\'');
        }

        $entity = app($this->entityName);

        if (!$entity instanceof Model) {
            throw new Exception($this->entityName.' must be an instance of Illuminate\\Database\\Eloquent\\Model');
        }

        $this->setEntity($entity);
    }

    /**
     * Return the query builder order by the specified attribute
     *
     * @param  string  $attr
     * @param  string  $dir
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function orderBy($attr, $dir = 'asc')
    {
        return $this->entity->orderBy($attr, $dir);
    }

    /**
     * Get all available model instances.
     *
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function all($columns = ['*'])
    {
        return $this->entity->all($columns);
    }

    /**
     * Find a model instance by its attribute.
     *
     * @param  string $attribute
     * @param  mixed $value
     * @param  bool $shouldThrowException
     * @return mixed
     */
    public function findBy($attribute, $value, $shouldThrowException = true)
    {
        $query = $this->entity->where($attribute, $value);

        return $shouldThrowException ? $query->firstOrFail() : $query->first();
    }

    /**
     * Find a model instance by its ID.
     *
     * @param  int $id
     * @return mixed
     */
    public function findById($id)
    {
        return $this->entity->findOrFail($id);
    }

    /**
     * Find entities by their attribute values.
     *
     * @param  string $attribute
     * @param  array  $values
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function whereIn($attribute, array $values, $columns = ['*'])
    {
        return $this->entity->whereIn($attribute, $values)->get($columns);
    }

    /**
     * Find data by multiple fields
     *
     * @param array $where
     * @param array $columns
     *
     * @return mixed
     */
    public function where(array $where, $columns = ['*'])
    {
        $this->applyConditions($where);
        $data = $this->entity->get($columns);
        $this->resetEntity();
        return $data;
    }

    /**
     * Applies the given where conditions to the model.
     *
     * @param array $where
     * @return void
     */
    protected function applyConditions(array $where)
    {
        foreach ($where as $field => $value) {
            if (is_array($value)) {
                list($field, $condition, $val) = $value;
                $this->entity = $this->entity->where($field, $condition, $val);
            } else {
                $this->entity = $this->entity->where($field, '=', $value);
            }
        }
    }

    /**
     * Create new model instance.
     *
     * @param  array  $data
     * @return mixed
     */
    public function create(array $data)
    {
        return $this->entity->create($data);
    }

    /**
     * Update a model instance.
     *
     * @param  array $data
     * @param  int|object $id
     * @param  string $attribute
     * @return mixed
     */
    public function update($data, $id, $attribute = 'id')
    {
        $fillableFields = $this->entity->getFillable();
        $data = array_only($data, $fillableFields);
        $id = is_object($id) ? $id->getKey() : $id;
        $this->entity->where($attribute, $id)->first()->update($data);

        return $this->findBy($attribute, $id);
    }

    /**
     * Update or Create an entity in repository
     *
     * @throws ValidatorException
     *
     * @param array $attributes
     * @param array $values
     *
     * @return mixed
     */
    public function updateOrCreate(array $attributes, array $values = [])
    {
        $entity = $this->entity->updateOrCreate($attributes, $values);
        $this->resetEntity();

        return $entity;
    }

    /**
     * Delete an model instance.
     *
     * @param  int|object $model
     * @return int
     */
    public function delete($model)
    {
        $modelKey = is_object($model) ? $model->getKey() : $model;

        $this->entity->destroy($modelKey);
    }

    /**
     * Get the paginated list of latest model instances.
     *
     * @param  integer $limit
     * @param  array|string  $eagerLoad
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
     */
    public function getLatestEntities($limit = null, $eagerLoad = [])
    {
        $limit = is_null($limit) ? config('common.pagination_per_page') : $limit;

        return $this->entity->with($eagerLoad)->latest()->paginate($limit);
    }

    /**
     * Insert new record for the given model.
     *
     * @param  array  $data
     * @return int
     */
    public function insert(array $data)
    {
        return $this->entity->insert($data);
    }

    /**
     * Batch inserting multiple database records.
     *
     * @param  array  $collection
     * @param  bool $returnLastId
     * @return int|void
     */
    public function batchInsert(array $collection, $returnLastId = true)
    {
        $records = array_map(function ($item) {
            $now = Carbon::now();
            $item[Model::CREATED_AT] = $now;
            $item[Model::UPDATED_AT] = $now;

            return $item;
        }, $collection);

        $this->insert($records);

        if ($returnLastId) {
            return $this->entity->max($this->entity->getKeyName());
        }
    }

    /**
     * Sets the value of entity name.
     *
     * @param Model $entity
     * @return self
     */
    protected function setEntity($entity)
    {
        $this->entity = $entity;

        return $this;
    }

    /**
     * @throws RepositoryException
     */
    public function resetEntity()
    {
        $this->makeEntity();
    }
}
2.3 Repository Command

Ngoài cách tạo repository cho từng đối tượng bằng cách tạo trức tiếp, chúng ta có thể dùng command để tạo ra repository.

Đầu tiên, chúng ta tạo file RepositoryCommand.php và đặt nó vào trong thư mục App\Console\Commands

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use File;
class RepositoryCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:repository
        {name : Repository name}
        {--model= : (Optional) Model name}
    ';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create repository for model';
    private $repositoryPath;
    private $modelPath;
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
        $this->repositoryPath = config('repository.repository_path', 'app/Repositories/');
        $this->modelPath = config('repository.model_path', 'App\Models\\');
    }
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $repositoryContent = '';
        $repositoryName = $this->argument('name');
        $modelName = $this->option('model');
        $repositoryPath = sprintf('%s/%s.php', trim($this->repositoryPath, '/'), $repositoryName);
        if ($repositoryName) {
            $repositoryContent = $this->getRepositoryContent($repositoryName, $modelName);
        }
        if (!File::isFile($repositoryPath)) {
            $this->setRepositoryContent($repositoryPath, $repositoryContent);
        } else {
            $overrideConfirm = $this->ask('Repository is existing. Are you sure you want to override it? (y/n)');
            $yesAnswerList = ['y', 'yes'];
            if (in_array(strtolower($overrideConfirm), $yesAnswerList)) {
                $this->setRepositoryContent($repositoryPath, $repositoryContent);
            }
        }
    }
    protected function setRepositoryContent($repositoryPath, $repositoryContent)
    {
        File::put($repositoryPath, $repositoryContent);
        $this->info('Create repository succeced');
    }
    protected function getRepositoryContent($repositoryName, $modelName = '')
    {
        $repositoryContent = "<?php
namespace App\Repositories;
use App\Repositories\Repository;
class {$repositoryName} extends Repository
{";
        if ($modelName) {
            $repositoryContent .= "
    public function model()
    {
        return \\" . trim($this->modelPath, '\\') . '\\' . $modelName . "::class;
    }";
        }
        $repositoryContent .= "
}";
        return $repositoryContent;
    }
}

Tiếp theo, khai báo RepositoryCommand vừa tạo ở trên vào file App\Console\Kernel.php

<?php
namespace App\Console;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        ....
        \App\Console\Commands\RepositoryCommand::class,
        ....
    ];

Giờ đây chúng ta có thể tạo repository từ command line tương tự như tạo model, controller,.... php artian make:repository tên_của_repository --model=tên_của_model

3. Kết

Sử dụng Repository mang lại nhiều lợi ích. Từ những điều cơ bản như giảm code bị trùng lắp, ứng dụng dễ dàng để mở rộng, kiểm tra và bảo trì. Từ góc độ kiến trúc, controller của bạn sẽ không cần biết dữ liệu được lưu trữ ở đâu hay được sử lý như nào, tất cả đã có repository.

Tham khảo nội dung đầy đủ của các đoạn code trong bài viết ở đây:


All Rights Reserved