Доброго времени, уважаемые господа.
Не так давно столкнулся с явлением дублирующегося и повторяющегося кода при код ревью одного проекта на Laravel.
Суть в следующем: у системы существует некоторая структура внутреннего API для AJAX запросов, по сути возвращающая коллекцию чего-либо из базы (заказы, пользователи, квоты, etc...). Вся суть данной структуры — вернуть JSON с результатами, не более. При код-ревью я насчитал 5 или 6 классов, использующие один и тот же код, разница была лишь в инжекте зависимостей ResourceCollection, JsonResource и непосредственно модели. Такой подход мне показался в корне неверным, и я решил внести свои, как я считаю, правильные изменения в данный код, воспользовавшись мощным DI, который предоставляет нам Laravel Framework.
Итак, как же я пришел к тому, о чем расскажу дальше.
У меня уже примерно полтора года опыта разработки под Magento 2, и впервые столкнувшись с этой CMS, я был в шоке о ее DI. Для тех, кто не знает: в Magento 2 не малая часть системы построена на так называемых «виртуальных типах». То есть, обращаясь к определенному классу, мы не всегда обращаемся к «реальному» классу. Мы обращаемся к виртуальному типу, который был «собран» на основе определенного «реального» класса (пример — Collection для админского грида, собираемый через DI). То есть, мы можем фактически собрать любой класс для использования с нашими зависимостями, просто прописав в DI нечто подобное:
<virtualType name="Vendor\Module\Model\ResourceModel\MyData\Grid\Collection"
type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
<arguments>
<argument name="mainTable" xsi:type="string">vendor_table</argument>
<argument name="resourceModel" xsi:type="string">Vendor\Module\Model\ResourceModel\MyData
</argument>
</arguments>
</virtualType>
Теперь, запросив класс Vendor\Module\Model\ResourceModel\MyData\Grid\Collection, мы получим экземпляр Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult, но с подставленным через DI зависимостями mainTable — «vendor_table» и resourceModel — «Vendor\Module\Model\ResourceModel\MyData».
Сначала, подобный подход мне показался не совсем понятным, не совсем «уместным» и не совсем нормальным, однако спустя год разработки
Возвращаемся к Laravel.
DI Laravel построен на «сервис-контейнере» — сущности, которая управляет биндингами и зависимостями в системе. Таким образом мы можем, например, указать интерфейсу DummyDataProviderInterface вполне себе реализацию этого интерфейса DummyDataProvider.
app()->bind(DummyDataProviderInterface::class, DummyDataProvider::class);
Затем, когда мы запросим DummyDataProviderInterface в сервис-контейнере (например, через конструктор класса), мы получим экземпляр класса DummyDataProvider.
Многие
Laravel может «биндить» не только реальные сущности, как например данный интерфейс, но и создавать так называемые «виртуальные типы» (а.к.а алиасы). И, даже в этом случае, Laravel не обязательно передавать класс, реализующий ваш тип. Метод bind() вторым аргументом может принимать анонимную функцию, с передаваемым туда параметром $app — экземпляр класса приложения. Вообще, сейчас мы больше уходим в контекстный биндинг, где от текущей ситуации зависит то, что мы передадим в реализующий «виртуальный тип» класс.
Предупреждаю, что с таким подходом к построению архитектуры приложений согласны не все, поэтому если вы любитель сотни одинаковых классов — пропустите этот материал.
Итак, для начала определимся, что будет выступать в качестве «реального» класса. На примере проекта, попавшего мне на код-ревью, возьмем ту же ситуацию с запросами ресурсов (по сути CRUD, но немного урезанный).
Посмотрим на реализацию общего Crud-контроллера:
<?php
namespace Wolf\Http\Controllers\Backend\Crud;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Wolf\Http\Controllers\Controller;
class BaseController extends Controller
{
/**
* @var Model
*/
protected $model;
/**
* @var \Illuminate\Http\Resources\Json\ResourceCollection|null
*/
protected $resourceCollection;
/**
* @var \Illuminate\Http\Resources\Json\JsonResource|null
*/
protected $jsonResource;
/**
* BaseController constructor.
* @param Model $model
* @param \Illuminate\Http\Resources\Json\ResourceCollection|null $resourceCollection
* @param \Illuminate\Http\Resources\Json\JsonResource|null $jsonResource
*/
public function __construct(
$model,
$resourceCollection = null,
$jsonResource = null
) {
$this->model = $model;
$this->resourceCollection = $resourceCollection;
$this->jsonResource = $jsonResource;
}
/**
* Display a listing of the resource.
*
* @param Request $request
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function index(Request $request)
{
return $this->resourceCollection::make($this->model->get());
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function show($id)
{
return $this->jsonResource::make($this->model->find($id));
}
}
Я не сильно заморачивался с реализацией, т.к проект находится на стадии, по сути, планирования.
У нас есть два метода, которые должны нам что-либо возвращать: index, возвращающий коллекцию сущностей из базы, и show, возвращающий json-ресурс определенной сущности.
Если бы использовали реальные классы — мы бы каждый раз создавали класс, содержащий в себе 1-2 сеттера, которые задавали бы классы для моделей, ресурсов и коллекций. Представьте себе, десятки файлов, из которых по истине сложная реализация находится только в 1-2. Избежать таких «клонов» мы можем, используя DI Laravel.
Итак, архитектура данной системы будет проста, но надежна как швейцарские часы.
Существует json-файл, который содержит массив «виртуальных типов» с непосредественным указанием на классы, которые будут использованы в качестве коллекций, моделей, ресурсов, etc…
Например, такой:
{
"Wolf\\Http\\Controllers\\Backend\\Crud\\OrdersResourceController": {
"model": "Wolf\\Model\\Backend\\Order",
"resourceCollection": "Wolf\\Http\\Resources\\OrdersCollection",
"jsonResource": "Wolf\\Http\\Resources\\OrderResource"
}
}
Далее, используя биндинг Laravel, мы будем задавать для нашего виртуального типа Wolf\Http\Controllers\Backend\Crud\OrdersResourceController в качестве реализующего класса наш базовый круд-контроллер Wolf\Http\Controllers\Backend\Crud\BaseController (обратите внимание, что класс не должен быть абстрактным, т.к при запросе Wolf\Http\Controllers\Backend\Crud\OrdersResourceController мы должны получить экземпляр Wolf\Http\Controllers\Backend\Crud\BaseController, а не абстрактный класс).
В CrudServiceProvider в метод boot() поместим следующий код:
$path = app_path('etc/crud.json');
if ($this->filesystem->isFile($path)) {
$virtualTypes = json_decode($this->filesystem->get($path), true);
foreach ($virtualTypes as $virtualType => $data) {
$this->app->bind($virtualType, function ($app) use ($data) {
/** @var Application $app */
$bindingData = [
'model' => $app->make($data['model']),
'resourceCollection' => $data['resourceCollection'],
'jsonResource' => $data['jsonResource']
];
return $app->makeWith(self::BASE_CRUD_CONTROLLER, $bindingData);
});
}
}
Константа BASE_CRUD_CONTROLLER содержит имя класса, реализующего логику CRUD-контроллера.
Далеко не идеал, но зато работает :)
Здесь мы проходим по массиву с виртуальными типами и задаем биндинги. Заметьте, что из сервис-контейнера мы получаем только экземпляр модели, а ResourceCollection и JsonResource остаются всего лишь именами классов. Почему так? Модель не обязательно должна принимать в себя атрибуты для заполнения, она вполне может обойтись и без них. А вот коллекции должны принимать в себя какой-либо ресурс, из которого они будут доставать данные и сущности. Поэтому, в BaseController мы используем статические методы collection() и make() соответственно (в принципе, можем добавить динамические геттеры, которые будут класть что либо в ресурс и возвращать нам экземпляр, но это я оставлю вам), которые будут возвращать нам экземпляры этих же коллекций, но с переданными в них данными.
По сути, вы можете в принципе весь биндинг Laravel довести до такого состояния.
Итого, запросив Wolf\Http\Controllers\Backend\Crud\OrdersResourceController мы получим экземпляр контроллера Wolf\Http\Controllers\Backend\Crud\BaseController, но с встроенными зависимостями нашей модели, ресурса и коллекции. Осталось только создать ResourceCollection и JsonResource и можно управлять возвращаемыми данными.