
Доброго времени, уважаемые господа.
Не так давно столкнулся с явлением дублирующегося и повторяющегося кода при код ревью одного проекта на 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 и можно управлять возвращаемыми данными.
