Мне нравится шаблон репозитория, но во многих случаях он слишком утомителен. Мы используем этот шаблон, потому что он дает нам возможность разорвать зависимость между нашими контроллерами и/или кодом служебного слоя и ORM (обычно Eloquent). К сожалению, это обычно приводит к написанию большого количества лишнего кода.
Во время работы над новым проектом я решил поработать над (частичным) решением моих бед с шаблоном репозитория. Я использовал комбинацию полиморфизма, наследования и динамического программирования, чтобы задействовать полезные части шаблона, ограничивая при этом шаблонные.
Обзор
Основная идея шаблона заключается в создании репозиториев, которые реализуют стандартный интерфейс PHPRepositoryInterface
, который охватывает стандартные методы Eloquent нестатическим бесфасадным способом. Это значит, что мы можем использовать инверсию зависимостей, увеличивая тестируемость нашего приложения. Кроме того, поскольку наш код слоя данных будет скрыт за репозиториями, мы можем изолировать изменения запросов и/или бизнес-логики в этом слое.
Но реализации только функций Eloquent недостаточно. Мне также хотелось бы добавить для репозиториев возможность реализации собственных функций модели. Чтобы сделать это, мы предоставляем каждому репозиторию интерфейс модели, в который мы можем вставить функции модели.
Наконец, вся шаблонная часть скрыта за классом PHPAbstractRepository
, от которого наследуют все репозитории. Мы ограничиваем шаблонную часть, используя динамическое программирование для определения модели Eloquent, которую необходимо вызвать на этапе выполнения.
Реализация
RepositoryInterface
В интерфейсе репозитория мы определяем все стандартные методы Eloquent. Сигнатуры соответствующих методов в интерфейсе и в Illuminate\Database\Eloquent\Model должны точно совпадать.
Примечание: некоторые методы Eloquent были удалены для простоты. Чтобы вернуть их, просто используйте описанный ниже шаблон.
<?php namespace Acme\Repositories;
/**
* RepositoryInterface обеспечивает стандартные функции ЛЮБОГО
* репозитория.
*/
interface RepositoryInterface {
public function create(array $attributes);
public function all($columns = array('*'));
public function find($id, $columns = array('*'));
public function destroy($ids);
}
UserRepositoryInterface
Здесь мы определяем все методы, специфичные для User-версии репозитория. Обратите внимание, что мы наследуем от PHPRepositoryInterface
, поэтому любой класс, реализующий этот интерфейс, должен также предоставлять реализацию методов PHPRepositoryInterface
.
<?php namespace Acme\Repositories;
/**
* UserRepositoryInterface содержит ТОЛЬКО сигнатуры тех методов, которые
* относятся к объекту User.
*
* Обратите внимание, что мы наследуем от RepositoryInterface, поэтому любой класс,
* реализующий этот интерфейс, должен также предоставлять реализацию всех
* стандартных методов eloquent (find, all, и т.д.)
*/
interface UserRepositoryInterface extends RepositoryInterface {
public function findUserByUsername($username);
}
UserRepository
Конкретная реализация PHPUserRepository
. Мы наследуем от PHPAbstractRepository
, чтобы получить реализацию методов PHPRepositoryInterface
, а затем реализуем здесь любые методы определенные в PHPUserRepositoryInterface
.
Обратите внимание на переменную PHP$modelClassName
. Она важна, потому что мы используем её в PHPAbstractRepository
для динамического вызова методов соответствующей модели Eloquent.
<?php namespace Acme\Repositories;
use Acme\Abstracts\Repository as AbstractRepository;
class UserRepository extends AbstractRepository implements UserRepositoryInterface
{
// Вот где происходит "магия":
protected $modelClassName = 'User';
// Этот класс только реализует методы, специфичные для UserRepository
public function findByUserName($username)
{
$where = call_user_func_array("{$this->modelClassName}::where", array($username));
return $where->get();
}
}
AbstractRepository
Наконец, PHPAbstractRepository
выполняет динамическую реализацию различных методов Eloquent.
<?php namespace Acme\Abstracts;
use Acme\Repositories\RepositoryInterface;
/**
* AbstractRepository обеспечивает стандартную реализацию методов, определенных
* в базовом интерфейсе репозитория. Он просто делегирует статические вызовы функций
* нужной модели eloquent на основе $modelClassName.
*/
abstract class Repository implements RepositoryInterface {
protected $modelClassName;
public function create(array $attributes)
{
return call_user_func_array("{$this->modelClassName}::create", array($attributes));
}
public function all($columns = array('*'))
{
return call_user_func_array("{$this->modelClassName}::all", array($columns));
}
public function find($id, $columns = array('*'))
{
return call_user_func_array("{$this->modelClassName}::find", array($id, $columns));
}
public function destroy($ids)
{
return call_user_func_array("{$this->modelClassName}::destroy", array($ids));
}
}
Пример использования
Ниже приведены два примера использования. В первом используется метод Eloquent PHPfind
для получения одной записи. Во втором используется наш метод PHPfindByUsername
для поиска пользователя.
<?php
$repo = new Acme\Repositories\UserRepository;
// вывод имени пользователя с id = 1
echo $repo->find(1)->name;
// вывод количества пользователей, у которых username = johnsmith
echo count($repo->findByUsername('johnsmith'));
Заключение
Такой подход позволяет получить преимущества использования слоя репозитория, не требуя написания большого количества шаблонного кода.
Стандартный процесс добавления новых репозиториев прост.
- Если нам нужны только стандартные функции Eloquent, мы создаем пустой интерфейс (
PHPSomethingRepositoryInterface
), который наследует отPHPRepositoryInterface
. Затем мы создаем классPHPSomethingRepository
, реализуемPHPSomethingRepositoryInterface
, наследуем отPHPAbstractRepository
и добавляем свойствоPHP$modelClassName
. - Если нам надо добавить методы, специфичные для модели, мы следуем алгоритму из предыдущего шага, а затем добавляем сигнатуры пользовательских методов в
PHPSomethingRepositoryInterface
, и конкретные реализации этих методов вPHPSomethingRepository
.