Как стать автором
Обновить

Пишем меньше дублирующего кода, используя биндинг в Laravel

Время на прочтение6 мин
Количество просмотров8.9K
image

Доброго времени, уважаемые господа.

Не так давно столкнулся с явлением дублирующегося и повторяющегося кода при код ревью одного проекта на 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 может «биндить» не только реальные сущности, как например данный интерфейс, но и создавать так называемые «виртуальные типы» (а.к.а алиасы). И, даже в этом случае, 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 и можно управлять возвращаемыми данными.
Теги:
Хабы:
Всего голосов 13: ↑8 и ↓5+3
Комментарии16

Публикации

Истории

Работа

PHP программист
148 вакансий

Ближайшие события