Одна из лучших вещей в написании кода – очевидность хороших практик, ведь если им не следовать, возникает раздражение. Очень надоедает, когда вам нужно писать одну и ту же вещь снова и снова. Когда вы чувствуете себя недовольным из-за повторения одних и тех же вещей, наступает время абстракции.
В типичном приложении вы, вероятно, имеете множество Репозиториев для работы с вашей системой хранения. Когда вы используете Laravel, вы проводите много времени работая с Eloquent. Тем не менее, поверьте мне, когда у вас есть множество Репозиториев, вам быстро надоедает многократно писать одни и те же методы для доступа к данным.
В этом руководстве я хочу рассмотреть некоторые паттерны для абстракции основных методов, которые вы больше не будете повторять в каждой реализации ваших Репозиториев. Я также покажу, как мы можем использовать гибкость Eloquent и его Query Builder для написания действительно красивых методов для доступа к данным.
Конечно же, если вы используете паттерн Репозиторий, теоретически, вы можете воспользоваться любым типом постоянного хранилища. Если вы не пользуетесь Eloquent, множество вещей из этого руководства не сильно будут вас касаться. Тем не менее, если вы используете Eloquent, тогда, я надеюсь, вы сможете сделать свои Репозитории лучше.
Структура Репозитория
В Laravel типичная структура для паттерна Репозиторий представляет собой наличие конкретных реализаций репозиториев для различных типов хранилищ, связанных с общим интерфейсом. Это позволяет вам отбросить конкретную реализацию для хранилища без изменения своего кода.
Например, у вас есть интерфейс UserRepository:
interface UserRepository {}
И реализация EloquentUserRepository:
class EloquentUserRepository implements UserRepository {}
Конкретная реализация будет связываться с главным интерфейсом через IoC контейнер Laravel, используя сервис-провайдер:
/**
* Register
*/
public function register()
{
$this->app->bind('Cribbb\Repositories\User\UserRepository', function($app)
{
return new EloquentUserRepository( new User );
});
}
В EloquentUserRepository
мы потом сможем связать модель с классом, сделав её свойством класса:
class EloquentUserRepository implements UserRepository {
/**
* @var Model
*/
protected $model;
/**
* Constructor
*/
public function __construct(User $model)
{
$this->model = $model;
}
}
Теперь, когда у нас есть экземпляр модели в классе, мы сможем выполнять запросы через модель и возвращать данные:
/**
* Return all users
*
* @return Illuminate\Database\Eloquent\Collection
*/
public function all()
{
return $this->model->all();
}
Надеюсь, это всё вам уже знакомо.
Абстрагируем повторяющуюся логику
Когда вы работаете с Репозиториями, у вас скорее всего будут неоднократно повторяться некоторые методы. Например, у вас, скорее всего, должен быть метод в каждом Репозитории, который возвращает все сущности или находит конкретную по id.
Когда вы обнаруживаете повторения в коде, вам нужно абстрагировать его и затем обратиться к этой абстракции.
Это значит, что мы создадим AbstractEloquentRepository
и затем будем использовать этот класс как каркас(от переводчика: план, проект, в общем, как чертёж дома, из которого мы строим дом), который мы будем наследовать.
abstract class AbstractEloquentRepository {
/**
* Return all users
*
* @return Illuminate\Database\Eloquent\Collection
*/
public function all()
{
$this->model->all();
}
}
Затем мы можем расширить абстрактный класс, чтобы он впитал в себя все основные методы, общие для всех Репозиториев:
class EloquentUserRepository extends AbstractEloquentRepository implements UserRepository {}
Теперь нам не нужно реализовывать все методы в EloquentUserRepository
, потому как они автоматически унаследованы из класса AbstractEloquentRepository
.
Каркас для ленивой загрузки(Eager Loading)
Ленивая загрузка очень полезна, когда вам нужно уменьшить кол-во запросов к базе данных при загрузке связей сущности. Ленивая загрузка – это просто способ оптимизации запросов путём определения данных, которые вы хотите получить раньше времени.
Главный метод, доступный в Репозитории – это поиск сущности по id, например:
/**
* Find an entity by id
*
* @param int $id
* @return Illuminate\Database\Eloquent\Model
*/
public function getById($id)
{
return $this->model->find($id);
}
Реализация хороша, если вам нужно вернуть сущность, но что, если вам нужно вернуть ещё и её связи с помощью ленивой загрузки? Единственный способ реализовать это — создать ещё один метод:
/**
* Find an entity by id and include the posts
*
* @param int $id
* @return Illuminate\Database\Eloquent\Model
*/
public function getByIdWithPosts($id)
{
return $this->model->find($id)->with(array('posts'))->first();
}
Чёрт, нам нужно будет реализовать кучу повторяющейся логики для реализации всех связей сущности.
Вместо этого повторения логики мы просто можем создать каркас запроса, где укажем, какую именно связь нужно загрузить с помощью ленивой загрузки:
/**
* Make a new instance of the entity to query on
*
* @param array $with
*/
public function make(array $with = array())
{
return $this->model->with($with);
}
В методе getById
мы принимаем параметр $with
, в котором можем указать, какие связи нам нужны при вызове этого метода:
/**
* Find an entity by id
*
* @param int $id
* @param array $with
* @return Illuminate\Database\Eloquent\Model
*/
public function getById($id, array $with = array())
{
$query = $this->make($with);
return $query->find($id);
}
Конечно, это может распространяться не только на метод getById
. Если вы добавите параметр $with
в любой метод вашего Репозитория и создадите заглушку запроса, используя метод make, вы сможете иметь доступ к ленивой загрузке без повторения какой-либо логики.
Получение по ключу и значению
Ещё один из основных методов в Репозиториях – получение всех сущностей на основе ключа и значения.
Например, если вы хотите получить пользователя по его email:
/**
* Find a user by their email address
*
* @param string $email
* @return Illuminate\Database\Eloquent\Model
*/
public function getByEmail($email)
{
return $this->model->where('email', '=', $email)->first();
}
Тем не менее, если вам нужно будет искать сущности в Репозитории, используя множество различных комбинаций ключей и значений, то у вас появится множество повторяющейся логики.
Вместо этого вы можете просто принимать ключ и значение в общем методе. Я на самом деле предпочитаю создавать 2 метода, один для одиночной сущности и один для множества сущностей:
/**
* Find a single entity by key value
*
* @param string $key
* @param string $value
* @param array $with
*/
public function getFirstBy($key, $value, array $with = array())
{
$this->make($with)->where($key, '=', $value)->first();
}
/**
* Find many entities by key value
*
* @param string $key
* @param string $value
* @param array $with
*/
public function getManyBy($key, $value, array $with = array())
{
$this->make($with)->where($key, '=', $value)->get();
}
Теперь вы можете использовать эти 2 метода для поиска по любому ключу и значению.
Пагинация
Пагинация – ещё одна действительно общая вещь, которую вы, скорее всего, повторяли в Репозиториях. В этом руководстве я покажу метод ручного создания пагинации в Laravel.
Это ещё один метод, который вы обычно повторяете и который может быть абстрагирован из конкретных репозиториев:
/**
* Get Results by Page
*
* @param int $page
* @param int $limit
* @param array $with
* @return StdClass Object with $items and $totalItems for pagination
*/
public function getByPage($page = 1, $limit = 10, $with = array())
{
$result = new StdClass;
$result->page = $page;
$result->limit = $limit;
$result->totalItems = 0;
$result->items = array();
$query = $this->make($with);
$model = $query->skip($limit * ($page - 1))
->take($limit)
->get();
$result->totalItems = $this->model->count();
$result->items = $model->all();
return $result; }
Получение данных, у которых есть конкретная связь
Следующий основной метод, который может быть в Репозитории — когда вам нужно получить результаты, имеющие конкретную связь.
Например, вы хотите получить записи блога, у которых есть комментарии.
В типичном примере это может быть в вашем EloquentPostRepository
:
/**
* Get Posts with Comments
*
* @return Illuminate\Database\Eloquent\Collection
*/
public function getPostsWithComments()
{
$this->model->has('comments')->get();
}
Вот ещё один хороший кандидат для абстракции из конкретного репозитория:
/**
* Return all results that have a required relationship
*
* @param string $relation
*/
public function has($relation, array $with = array())
{
$entity = $this->make($with);
return $entity->has($relation)->get();
}
Заключение
Во многих больших проектах вы можете обнаруживать повторения логики. Когда вы чувствуете, что что-то начинает повторяться слишком часто, обычно это значит, что наступило время для абстракции путём создания основных методов, которые подходят для всех репозиториев и тем самым уменьшают сложность наших репозиториев и повторение кода в них.
Когда вы решаете абстрагировать кусок логики, вы должны быть уверены, что в этом есть смысл. Обычно абстракция делает ваш код более сложным, чем нужно. Есть грань между очень сильной абстракцией и недостаточной абстракцией.