Laravel

Скрытый класс Manager

При создании приложений вам может понадобиться класс, который принимает разные реализации чего-либо. Например, курьеру доставки может понадобиться любой транспорт, чтобы доставить вам вашу посылку: велосипед, автомобиль или даже общественный транспорт.

Хорошо, ситуация понятна. Теперь вам нужен способ создания этих реализаций в зависимости от того, что курьер доставки будет использовать, чтобы добраться до вашего дома. И каждая из этих реализаций транспорта будет имеет свой собственный способ создания. Я имею в виду, что создание автомобиля очень отличается от велосипеда и полностью отличается от общественного транспорта, который может быть как такси, так и автобус, но каждый из них начинает движение с конкретного места и доставляет в точку назначения.

Для такого рода ситуаций есть хороший класс под названием Manager. Он входит в состав Laravel «из коробки» и облегчает большую часть управления множественными драйверами.

Класс Manager был кратко документирован для Laravel 5.0, но последующие релизы не включают его описание, в том числе и из-за того, что его использование стало очень ограниченным.

Цель класса Manager

Он сочетает в себе некоторые принципы шаблона Abstract Factory, позволяя разработчику поместить задачу создания различных классов в один класс, вместо того, чтобы создавать что-то везде с нуля. Дополнительный аккорд заключается в том, что созданные экземпляры также хранятся в классе Manager, поэтому они не создаются каждый раз, а извлекаются.

Класс Manager отвечает за создание конкретной реализации драйвера на основе конфигурации приложения. Например, класс CacheManager может создавать APC, Memcached, File и другие реализации драйверов кеша.

Каждый из этих менеджеров включает в себя метод extend, который можно использовать для простого внедрения в менеджер нового функционала драйверов прямо во время выполнения.

Это означает, что экземпляр драйвера будет жить в памяти столько, сколько потребуется.

Возвращаясь к нашему примеру с курьером, парню, у которого ваша посылка, не нужно знать, как сделать велосипед и ему не нужно учиться этому. Ему нужно просто попросить его у своей компании по доставке и она это велосипед сделает, если не сделала его до этого. Если курьера заменят, то вам не нужно будет учить его снова строить велосипед, за это отвечает компания.

Но класс Manager — это не просто паттерн. Давайте посмотрим что у него внутри:

namespace IlluminateSupport;

use Closure;
use IlluminateContractsContainerContainer;
use InvalidArgumentException;

abstract class Manager
{
    /**
     * Экземпляр контейнера
     *
     * @var IlluminateContractsContainerContainer
     */
    protected $container;

    /**
     * Экземпляр контейнера
     *
     * @var IlluminateContractsContainerContainer
     *
     * @deprecated Use the $container property instead.
     */
    protected $app;

    /**
     * Экземпляр конфигурационного репозитория
     *
     * @var IlluminateContractsConfigRepository
     */
    protected $config;

    /**
     * Зарегистрированные создатели кастомных драйверов
     *
     * @var array
     */
    protected $customCreators = [];

    /**
     * Массив созданных «драйверов»
     *
     * @var array
     */
    protected $drivers = [];

    /**
     * Создание нового экземпляра менеджера
     *
     * @param  IlluminateContractsContainerContainer  $container
     * @return void
     */
    public function __construct(Container $container)
    {
        $this->app = $container;
        $this->container = $container;
        $this->config = $container->make('config');
    }

    /**
     * Получение дефолтного имени драйвера
     *
     * @return string
     */
    abstract public function getDefaultDriver();

    /**
     * Получение экземпляра драйвера
     *
     * @param  string  $driver
     * @return mixed
     *
     * @throws InvalidArgumentException
     */
    public function driver($driver = null)
    {
        $driver = $driver ?: $this->getDefaultDriver();

        if (is_null($driver)) {
            throw new InvalidArgumentException(sprintf(
                'Unable to resolve NULL driver for [%s].', static::class
            ));
        }

        // Если данный драйвера не был создан ранее, то мы создаем экземпляр здесь
        // и кэшируем его, чтобы в следующий раз можно было его быстро вернуть.
        // Если уже есть драйвер с таким именем, то мы просто вернем этот экземпляр.
        if (! isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }

        return $this->drivers[$driver];
    }

    /**
     * Создание нового экземпляра драйвера
     *
     * @param  string  $driver
     * @return mixed
     *
     * @throws InvalidArgumentException
     */
    protected function createDriver($driver)
    {
        // Сначала мы определим, существует ли создатель кастомного драйвера для этого драйвера
        // и, если нет, то проверим на наличие метода создателя дял данного драйвера.
        // Обратные вызовы кастомных создателей позволяют разработчикам легко создавать
        // свои «драйвера» с помощью Замыканий
        if (isset($this->customCreators[$driver])) {
            return $this->callCustomCreator($driver);
        } else {
            $method = 'create'.Str::studly($driver).'Driver';

            if (method_exists($this, $method)) {
                return $this->$method();
            }
        }
        throw new InvalidArgumentException("Driver [$driver] not supported.");
    }

    /**
     * Вызов создателя кастомного драйвера
     *
     * @param  string  $driver
     * @return mixed
     */
    protected function callCustomCreator($driver)
    {
        return $this->customCreators[$driver]($this->container);
    }

    /**
     * Регистрация Замыкания создателя кастомного драйвера
     *
     * @param  string    $driver
     * @param  Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }

    /**
     * Получение всех созданных «драйверов»
     *
     * @return array
     */
    public function getDrivers()
    {
        return $this->drivers;
    }

    /**
     * Динамический вызов экземпляра дефолтного драйвера
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->driver()->$method(...$parameters);
    }
}

Выглядит странно, но не парьтесь. Я проведу быструю экскурсию по всему, что предлагает этот класс.

Повторное использование

Как вы можете видеть, Manager работает с «драйверами», являющимися экземплярами классов, с которыми вы хотите работать. Эти драйверы могут быть объявлены с использованием методов, следующих шаблону create{Name}Driver:

protected function createBicycleDriver()
{
    return $this->container->make(BicycleTransport::class);
}

После создания они не уничтожаются, а живут в пуле, откуда их можно использовать повторно при необходимости.

Например, при использовании базы данных, одно и то же «соединение» используется повторно несколько раз. Это экономит на экземплярах и позволяет избегать создания подключения к базе данных каждый раз.

Как только наш курьер перестанет использовать велосипед и вернет его на завод, то компания будет использовать этот же велосипед для другой доставки, вместо того, чтобы создавать его с нуля.

Расширяемость

Менеджер не привязан только к тем драйверам, которые вы настроили — во время выполнения можно добавлять другие, используя метод extend().

public function boot(TransportManager $manager)
{
    $manager->extend('quad', function ($container) {
        return $container->make(QuadMotorcycle::class);
    });
}

Метод принимает обратный вызов, получаемый контейнером приложения — вы можете создать драйвер, которого нет в Менеджере, при необходимости обращаясь к другим службам. Нет необходимости редактировать или расширять класс.

Курьер может попросить квадроцикл. Поскольку компания не знает, как его сделать, то он передаст инструкции по его созданию, и получит квадроцикл. Это также позволит другим использовать этот же квадроцикл, если они этого захотят.

Переопределение

Вы также можете переопределить драйвер, «расширив» менеджер именем драйвера. Расширенный драйвер будет иметь приоритет над оригиналом.

public function boot(TransportManager $manager)
{
    $manager->extend('bicycle', function ($container) {
        return $container->make(BicycleTransport::class)
            ->include(new Umbrella);
    });
}

Наш курьер может не только запросить у компании велосипед, но и передать инструкции для приклепления зонта к велосипеду в дождливые дни. Если дождя нет, то инструкции не передаются, и он просто получает свой старый надежный велосипед.

Дефолтное поведение

Менеджер всегда будет выдавать дефолтный драйвер, если он не указано иное. Это означает, что все, кто запрашивают драйвер у менеджера, получают дефолтный, без необходимости указания его названия.

Вы можете вызвать дефолтный драйвер, просто вызвав любой метод драйвера, не вызывая driver(). Менеджер автоматически создаст экземпляр дефолтного драйвера и передаст ему вызов.

$manager->do('something')->and('celebrate');

Если говорить простым языком, то наш курьер получит автомобиль, если он не укажет что-то другое.

Использование Менеджера

Итак, Manager — это абстрактный класс, поэтому мы должны расширять его в зависимости от того, чем нужно управлять. В этом примере мы создадим TransportManager, чтобы наши курьеры могли использовать его для доставки посылок.

namespace AppManagers;

use AppTransportCarCar;
use AppTransportCarCarWheels;
use AppTransportCarFuelEngine;
use AppTransportCarChassis;
use AppTransportBicycleBicycle;
use AppTransportBicycleBicycleWheel;
use AppTransportBicyclePedals;
use AppTransportBicycleFrame;
use AppTransportStoreWithdrawalStoreWithdrawal;
use IlluminateSupportManager;

class TransportManager extends Manager
{
    /**
     * Получение дефолтного имени драйвера
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return 'bicycle';
    }

    /**
     * Создает новый драйвер Автомобиля
     *
     * @return AppTransportCarCar
     */
    public function createCarDriver()
    {
        return new Car(new CarWheels, new FuelEngine, new Chassis);
    }

    /**
     * Создает новый драйвер Велосипеда
     *
     * @return AppTransportBicycleBicycle
     */
    public function createBicycleDriver()
    {
        return new Bicycle(new BicycleWheel, new Pedals, new Frame);
    }

    /**
     * Создает новый драйвер для снятия денег
     *
     * @return AppTransportStoreWithdrawalStoreWithdrawal
     */
    public function createStoreWithdrawalDriver()
    {
        return new StoreWithdrawal;
    }
}

Класс очень прост: мы устанавливаем несколько драйверов, используя шаблон create{Name}Driver для каждого метода драйвера. Также мы устанавливаем дефолтный драйвер, в данном случае — bicycle. Это означает, что TransportManager по дефолту будет вызывать метод createBicycleDriver(), если ранее драйвер не вызывался.

В любой части вашего кода можно просто использовать Внедрение Зависимости (Dependency Injection) для получения менеджера и драйвера. Для иллюстрации мы поместим это в Контроллер, отвечающий за запуск процесса доставки.

namespace AppHttpControllers;

use AppPackage;
use AppManagersTransportManager;

class DeliveryController extends Controller
{
    /**
     * Начать процесс доставки посылки
     * 
     * @param  AppHttpControllersRequest $request
     * @param  AppPackage $package
     * @param  AppManagersTransportManager $transport
     * @return IlluminateHttpRedirectResponse
     */
    public function deliver(Request $request, Package $package, TransportManager $transport)
    {
        // Показать статус посылки, если
        if ($package->inTransit()) {
            return redirect()->route('package.delivery.status', $package);
        }

        // Валидация адреса доставки посылки
        $request->validate([
            'address' => 'required|string|max:255',
        ]);

        // Доставка посылки с помощью дефолтного драйвера
        $transport->send($package)->to($request->input('address'));

        // Показать, что доставляем посылку
        return redirect()->route('package.delivery.status', $package);
    }
}

Посмотрите строку 29: мы начинаем доставку с дефолтным драйвером всего одной красивой строкой. Нет необходимости создавать новый транспорт или даже иметь дело с тем же экземпляром транспорта в других частях кода.

И, если вы хотите использовать для доставки другой транспорт, то это делается буквально несколькими символами:

$manager->driver('anything')
    ->send($package)
    ->to($request->input('address'));

Нужны драйверы? Нет проблем, добавьте их в класс Manager. Кроме того, вы можете переопределить метод driver(), чтобы не сохранять экземпляр в пуле, особенно если вы хотите иметь больше контроля над памятью. Я имею в виду, что не все должно быть сохранено в контейнере, например одноразовые объекты (throwaway objects), которые выполняют всего один раз, или те, которые должны каждый раз создаваться с нуля — в этих случаях я бы дважды подумал об использовании класса Manager.

/**
 * Получение экземпляра драйвера
 *
 * @param  string  $driver
 * @return mixed
 *
 * @throws InvalidArgumentException
 */
public function driver($driver = null)
{
    $driver = $this->createDriver(
        $driver ?: $this->getDefaultDriver()
    );    if ($driver) {
        return $driver;
    }    throw new InvalidArgumentException(sprintf(
        'Unable to resolve NULL driver for [%s].', static::class
    ));
}

Практически это основы этого класса. Наслаждайтесь, добавляя свои собственные драйверы.

Автор: Italo Baeza Cabrera
Перевод: Demiurge Ash

Источник: laravel.demiart.ru

Похожие записи

Загрузка ....