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

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

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

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

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

Цель класса Manager

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

Скрытый класс Менеджер

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

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

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

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

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

namespace Illuminate\Support;

use Closure;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;

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

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

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

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

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

    /**
     * Создание нового экземпляра менеджера
     *
     * @param  \Illuminate\Contracts\Container\Container  $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 App\Managers;

use App\Transport\Car\Car;
use App\Transport\Car\CarWheels;
use App\Transport\Car\FuelEngine;
use App\Transport\Car\Chassis;
use App\Transport\Bicycle\Bicycle;
use App\Transport\Bicycle\BicycleWheel;
use App\Transport\Bicycle\Pedals;
use App\Transport\Bicycle\Frame;
use App\Transport\StoreWithdrawal\StoreWithdrawal;
use Illuminate\Support\Manager;

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

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

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

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

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

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

namespace App\Http\Controllers;

use App\Package;
use App\Managers\TransportManager;

class DeliveryController extends Controller
{
    /**
     * Начать процесс доставки посылки
     * 
     * @param  \App\Http\Controllers\Request $request
     * @param  \App\Package $package
     * @param  \App\Managers\TransportManager $transport
     * @return \Illuminate\Http\RedirectResponse
     */
    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
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.