Начнем с создания папки Repositories в папке app. Вторая папка, которую мы создадим, это папка Interfaces. Она будет находиться прямо в свеже созданной папке Repositories.
В папке Interfaces мы создаем класс BlogRepositoryInterface, который, на данный момент, содержит два метода:
namespace App\Repositories\Interfaces; use App\User; interface BlogRepositoryInterface { public function all(); public function getByUser(User $user); }
Далее создадим BlogRepository, который реализует BlogRepositoryInterface. Будем придерживаться очень простой имплементации.
namespace App\Repositories; use App\Models\Blog; use App\User; use App\Repositories\Interfaces\BlogRepositoryInterface; class BlogRepository implements BlogRepositoryInterface { public function all() { return Blog::all(); } public function getByUser(User $user) { return Blog::where('user_id'. $user->id)->get(); } }
Папка Repositories должна выглядеть следующим образом:
app/ └── Repositories/ ├── BlogRepository.php └── Interfaces/ └── BlogRepositoryInterface.php
Репозиторий в действии
Чтобы начать использовать BlogRepository, мы должны внедрить его в BlogController. Поскольку Репозиторий будет внедрен, то его легко можно заменить другой реализацией. Вот как будет выглядеть контроллер:
namespace App\Http\Controllers; use App\Repositories\Interfaces\BlogRepositoryInterface; use App\User; class BlogController extends Controller { private $blogRepository; public function __construct(BlogRepositoryInterface $blogRepository) { $this->blogRepository = $blogRepository; } public function index() { $blogs = $this->blogRepository->all(); return view('blog')->withBlogs($blogs); } public function detail($id) { $user = User::find($id); $blogs = $this->blogRepository->getByUser($user); return view('blog')->withBlogs($blogs); } }
Как видите, код в контроллере небольшой и, следовательно, читаемый. Вам не нужно десять строк кода, чтобы получить нужный набор данных, все можно сделать одной строкой, благодаря репозиторию. Это также отлично подходит для модульного тестирования, так как методы репозитория могут быть легко смоделированы.
Шаблон проектирования Репозиторий позволяет легко переключаться между источниками данных. В этом примере мы используем базу данных для получения наших блогов. Мы полагаемся на Eloquent, который делает это за нас. Но допустим, где-то в Интернете мы видели отличный API для блогов, и мы хотим им воспользоваться. Все, что нам нужно, это переписать BlogRepository, чтобы он использовал этот API вместо Eloquent.
RepositoryServiceProvider
Вместо внедрения BlogRepository в BlogController мы внедрим BlogRepositoryInterface и затем позволим сервис-контейнеру решать, какой репозиторий будет использоваться. Это можно сделать в методе boot в AppServiceProvider, но я предпочитаю сделать нового провайдера для этого, для сохранения чистоты кода.
php artisan make:provider RepositoryServiceProvider
Вот так выглядит наш RepositoryServiceProvider:
namespace App\Providers; use App\Repositories\BlogRepository; use App\Repositories\Interfaces\BlogRepositoryInterface; use Illuminate\Support\ServiceProvider; class RepositoryServiceProvider extends ServiceProvider { public function register() { $this->app->bind( BlogRepositoryInterface::class, BlogRepository::class ); } }
Обратите внимание, как легко можно поменять BlogRepository на другое хранилище.
Не забудьте добавить RepositoryServiceProvider в список провайдеров в файле config/app.php. После этого нужно очистить кеш конфигурации еще раз.
php artisan config:clear
Критика
Критики шалона репоизоторий говорят о бесполезности данного шаблона для Eloquent моделей. Laravel позволяет созавать так называемые заготовки запросов (scope).
Рассмотрим заготовки запросов.
Заготовки запросов в Laravel, scope
Заготовки запросов в Laravel могут быть локальными и глобальными.
Локальные заготовки
Заготовки позволяют вам повторно использовать логику запросов в моделях. Например, если вам часто требуется получать пользователей, которые сейчас «популярны». Для создания заготовки необходимо начать имя метода модели с префикса scope:
namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Заготовка запроса популярных пользователей. */ public function scopePopular($query) { return $query->where('votes', '>', 100); } /** * Заготовка запроса активных пользователей. */ public function scopeActive($query) { return $query->where('active', 1); } }
Когда заготовка определена, вы можете вызывать методы заготовки при запросах к модели. Но теперь вам не нужно использовать префикс scope. Вы можете даже сцеплять вызовы разных заготовок, например:
$users = App\User::popular()->active()->orderBy('created_at')->get();
Иногда требуется определить заготовку, которая принимает параметры. Для этого необходимо добавить эти параметры в заготовку. Они должны быть определены после параметра $query:
Динамические заготовки:
namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Заготовка запроса пользователей определённого типа. * * @param \Illuminate\Database\Eloquent\Builder $query * @param mixed $type * @return \Illuminate\Database\Eloquent\Builder */ public function scopeOfType($query, $type) { return $query->where('type', $type); } }
Передача параметров осуществляется при вызове метода заготовки:
$users = App\User::ofType('admin')->get();
Глобальные заготовки
Иногда требуется определить заготовку, которая будет применяться для всех выполняемых в модели запросов. По сути так и работает «мягкое удаление» в Eloquent. Глобальные заготовки определяются с помощью комбинации типажей PHP и реализации Illuminate\Database\Eloquent\ScopeInterface.
Сначала определим типаж. В этом примере мы будем использовать встроенный в Laravel SoftDeletes
:
trait SoftDeletes { /** * Загрузка типажа мягкого удаления для модели. * * @return void */ public static function bootSoftDeletes() { static::addGlobalScope(new SoftDeletingScope); } }
Если в модели Eloquent используется типаж, содержащий соответствующий соглашению по названиям bootNameOfTrait метод, тогда этот метод типажа будет вызываться при загрузке модели Eloquent. Это даёт вам возможность зарегистрировать глобальную заготовку, или сделать ещё что-либо необходимое. Заготовка должна реализовывать ScopeInterface
, который содержит два метода: apply()
и remove()
.
Метод apply()
принимает объект конструктора запросов Illuminate\Database\Eloquent\Builder и модель, к которой он применяется, и отвечает за добавление любых дополнительных операторов where, которые необходимы заготовке. Метод remove()
также принимает объект Builder и модель, и отвечает за отмену действий, произведённых методом apply()
. Другими словами, remove()
должен удалить добавленные операторы where (или любые другие). Поэтому для нашей SoftDeletingScope
методы будут такими:
/** * Применение заготовки к указанному конструктору запросов Eloquent. */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); $this->extend($builder); } /** * Удаление заготовки из указанного конструктора запросов Eloquent. */ public function remove(Builder $builder, Model $model) { $column = $model->getQualifiedDeletedAtColumn(); $query = $builder->getQuery(); foreach ((array) $query->wheres as $key => $where) { // Если оператор where ограничивает мягкое удаление данных, мы удалим его из // запроса и сбросим ключи в операторах where. Это позволит разработчику // включить удалённую модель в отношения результирующего набора, который загружается "лениво". if ($this->isSoftDeleteConstraint($where, $column)) { unset($query->wheres[$key]); $query->wheres = array_values($query->wheres); } } }