Чат в Telegram

Laravel: контейнер внедрения зависимостей

31.10.2020 в 15:41
489
+4

Раньше было лучше.

Опустим вступительную часть и начнем. Возьмем код типового php проекта без фреймворка.

final class UsersController
{
    private Redirector $redirector;
    private FlashMessagesRenderer $flashes;
    private LoggerInterface $logger;

    public function __construct(Redirector $redirector, FlashMessagesRenderer $flashes, LoggerInterface $logger)
    {
        $this->redirector = $redirector;
        $this->flashes = $flashes;
        $this->logger = $logger;
    }

    public function register(Request $request, UserCreator $users): Response
    {
        try {
            $users->create($request->dto());

            $this->flashes->success('Вы успешно создали аккаунт.');
        } catch (\Throwable $throwable) {
            $this->logger->log('error', $throwable->getMessage(), ['exception' => $throwable]);

            $this->flashes->error('Ошибка, попробуйте позже');
        }

        return $this->redirector->back();
    }
}

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

  1. На фронт-контроллер приходит запрос
  2. Находим роут для этого запроса
  3. Находим контроллер для этого роута
  4. Запускаем контроллер
  5. Рендерим ответ

Когда мы переходим к 4-му этапу, в самописных фреймворках обычно делают так:

$controllerClass = $route->getController();
$action = $route->getControllerAction();

$controller = new $controllerClass();

$controller->$action();

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

Давайте переведем код из начала статьи в нормальный для типовых проектов вид:

final class UsersController
{
    public function register(): Response
    {
        try {
            UserCreator::create($_POST);

            FlashMessagesRenderer::success('Вы успешно создали аккаунт.');
        } catch (\Throwable $throwable) {
            Logger::log('error', $throwable->getMessage(), ['exception' => $throwable]);

            FlashMessagesRenderer::error('Ошибка, попробуйте позже')
        }

        return Redirector::back();
    }
}

Вот это уже другое дело, запахло духом старой школы. На первый взгляд может показаться, что в этом коде меньше бойлерплейта, его меньше писать и как будто бы легче поддерживать. Да, конкретно в этом листинге проблем не так много, как в сервисах, которые этот контроллер использует. Вы можете себе представить, как выглядят UserCreator, Logger, а FlashMessagesRenderer? UserCreator создает юзера, значит, для работы ему нужна база данных, а базе данных нужно подключение. Логгеру так же нужны настройки, чтобы понимать, куда писать логи и как оформлять логи. А что делает FlashMessagesRenderer? Пишет короткоживущее сообщение в сессию, откуда наш шаблонизатор, если он у нас есть (не считая самого php, разумеется), достанет его и отрендерит.

Дерево наших зависимостей может выглядеть так:

final class DatabaseManager
{
    private $pdo;

    private function __construct()
    {
       $config = require_once __DIR__ . '/../../../../../../../../../../../../../../db.php';
       $this->pdo = new \PDO($config['dsn'], $config['user'], $config['password']);        
    }

    public function instance()
    {...}
}

final class UserCreator
{
    public function create(array $params)
    {
        DatabaseManager::instance()->prepare(...);
    }
} 

Как только у нас поменяется путь до конфига базы данных, нам потребуется поправить класс DatabaseManager. Если повезет, вам не придется менять класс UserCreator. Тем не менее, при таком подходе вы тем чаще будете изменять свои классы, чем больше они знают друг о друге или о низкоуровневой конфигурации, как в случае с различными подключениями. Иногда это порождает каскадные изменения огромного масштаба. И это не единственная проблема. Другая проблема заключается в том, что при таком подходе вы не сможете тестировать свой код. Наверно, я изрядно фантазирую, полагая, что люди, которые пишут такой код, пишут к нему еще и тесты, и все же. Если вы оставите код таким и попробуете написать тесты, то ваши тесты будут ходить в реальную (локальную) базу и забивать ее тестовыми данными. А как вы будете тестировать контроллеры, если у вас нет сессии?

Вопросов много, ответ один: нам нужна инверсия контроля. Дать нам ее может контейнер внедрения зависимостей.

А сейчас еще лучше

Чего мы хотим, мы хотим, чтобы:

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

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

$request = Request::fromGlobals();

$controller = new UsersController(
    new Redirector(new UrlGenerator(new RouteCollection(...), $request)),
    new FlashMessagesRenderer(new Session())
    new Logger(__DIR__ . '/../var/log/'),
);

$response = $controller->register(
    $request,
        new UserCreator(new DatabaseManager(...))
);

Такая матрешка может быть с каждым контроллером. Остается найти инструмент, который поможет нам узнать, какие зависимости есть у класса или функции, и такой инструмент есть — это рефлексия. Благодаря ей вы можете узнать о входящих зависимостях, возвращаемый тип, в каком файле расположен исследуемый объект и даже phpdoc над ним.

Предположим, у нас есть такой код:

final class SomeB
{
}

final class SomeA
{
    /**
     * @var SomeB
     */
    private $b;

    public function __construct(SomeB $b)
    {
        $this->b = $b;
    }
}

function underReflection(SomeA $a, array $params)
{
}

Нам нужно узнать зависимости функции underReflection, причем не только зависимости первого уровня, но и зависимости SomeA. Это можно сделать следующим образом:

function inspectFunction(ReflectionFunctionAbstract $reflection)
{
    $params = [];

    foreach ($reflection->getParameters() as $parameter) {
        if ($parameter->getClass()) {
            $params[] = inspectObject($parameter->getClass());
        } else {
            $params[] = $parameter->getName();
        }
    }

    return array_merge($params);
}

function inspectObject(ReflectionClass $class, bool $initial = true): array
{
    $params = [];

    if ($initial) {
        $params[] = $class->getName();
    }

    if ($class->getConstructor()) {
        foreach ($class->getConstructor()->getParameters() as $parameter) {
            if ($parameter->getClass()) {
                $params[] = inspectObject($parameter->getClass(), false);
            } else {
                $params[] = $parameter->getName();
            }
        }
    } else {
        $params[] = $class->getName();
    }

    return $params;
}

$params = inspectFunction(new ReflectionFunction('underReflection'));

var_dump($params);

/*
array(2) {
  [0]=>
  array(2) {
    [0]=>
    string(5) "SomeA"
    [1]=>
    array(1) {
      [0]=>
      string(5) "SomeB"
    }
  }
  [1]=>
  string(6) "params"
}
*/

Итак, чтобы контейнер смог собрать зависимости, ему нужно сделать следующее:

  1. Узнать, какой тип объекта пришел: функция (метод объекта и callable тоже являются функциями) или объект.
  2. Если это объект, то необходимо проверить конструктор. Если он есть, рекурсивно спускаемся по нему; если нет, создаем объект и возвращаем его в массив.
  3. В конце с помощью рефлексии создаем объект или функцию и передаем ей аргументы.

Давайте подробнее остановимся на 2-м пункте: а что если зависимостью является интерфейс, скаляр, массив? Тот код, что приведен выше, не сможет решить эту задачу, потому что у него нет хранилища, он может создавать только простые объекты. Чтобы мы могли создать объект под нужный интерфейс или заинжектить скаляр, нам нужно, чтобы в нашем контейнере можно было зарегистрировать такие зависимости: связать интерфейс с реализацией (-ями), сохранить скаляр или настроить контекстный биндинг: мол, когда такому-то объекту потребуется такая-то зависимость, дай ему это.

Создаем контейнер

Добавим пару тестовых классов:

final class SomeA
{
    /**
     * @var SomeB
     */
    private SomeB $b;

    public function __construct(SomeB $b)
    {
        $this->b = $b;
    }

    public function do()
    {
        return $this->b->do();
    }
}

final class SomeB
{
    /**
     * @var SomeC
     */
    private SomeC $c;

    public function __construct(SomeC $b)
    {
        $this->c = $c;
    }

    public function do()
    {
        return $this->c->do();
    }
}

final class SomeC
{
    public function do()
    {
        return 'Injected';
    }
}

Напишем тест:

Тест

Мы хотим получить строку Injected, но сейчас наш тест пока еще падает. Давайте создадим контейнер:

use Psr\Container\ContainerInterface;

final class Container implements ContainerInterface
{
     /**
     * {@inheritdoc}
     */
    public function get($id)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function has($id): bool
    {
        return false;
    }
}

Чтобы наш контейнер был совместим с проектами, где соблюдается psr, можно заимплементить Psr\Container\ContainerInterface, однако это необязательно, в случае чего можно написать адаптер, тем более что psr'овский контракт довольно скудный.

Теперь тест выдает такую ошибку: Error: Call to a member function do() on null. И правда, наш контейнер пока ничего не возвращает, давайте это исправим.

final class Container implements ContainerInterface
{
    private array $bindings = [];
    private array $cachedDependencies = [];

    /**
     * {@inheritdoc}
     */
    public function get($serviceId)
    {
        if (isset($this->cacheDependencies[$serviceId])) {
            return $this->cacheDependencies[$serviceId];
        }

        if (isset($this->bindings[$serviceId])) {
            $binding = $this->bindings[$serviceId];

            if ($binding instanceof \Closure) {
                $this->cacheDependencies[$serviceId] = $binding($this);
            } elseif (is_string($binding) && class_exists($binding)) {
                $this->cacheDependencies[$serviceId] = $this->createService($binding);
            } else {
                $this->cacheDependencies[$serviceId] = $binding;
            }

            return $this->cacheDependencies[$serviceId];
        }

        return $this->createService($serviceId);
    }

    /**
     * {@inheritdoc}
     */
    public function has($serviceId): bool
    {
        return array_key_exists($serviceId, $this->cachedDependencies) || array_key_exists($serviceId, $this->bindings);
    }

    /**
     * @param $serviceId
     *
     * @throws \ReflectionException
     * 
     * @return object
     */
    private function createService($serviceId)
    {
        if (!class_exists($serviceId)) {
            throw new \RuntimeException(sprintf('Unable to resolve service %s', $serviceId));
        }

        $reflectionClass = new \ReflectionClass($serviceId);

        $arguments = [];

        $constructor = $reflectionClass->getConstructor();

        if (null !== $constructor) {
            $arguments = $this->exploreConstructor($constructor);
        }

        $this->cacheDependencies[$serviceId] = $service = $reflectionClass->newInstanceArgs($arguments);

        return $service;
    }

    /**
     * @param \ReflectionFunctionAbstract $constructor
     *
     * @throws \ReflectionException
     *
     * @return array
     */
    private function exploreConstructor(\ReflectionFunctionAbstract $constructor): array
    {
        $arguments = [];

        foreach ($constructor->getParameters() as $parameter) {
            $class = $parameter->getClass();

            if (null !== $class) {
                $arguments[] = $this->get($class->getName()); // Рекурсия

            } elseif ($parameter->isDefaultValueAvailable()) {
                $arguments[] = $parameter->getDefaultValue();

            } elseif ($this->has($parameter->getName())){
                $arguments[] = $this->get($parameter->getName());

            } else {
                throw new \RuntimeException(sprintf('Unable to create parameter %s', $parameter->getName()));
            }
        }

        return $arguments;
    }

Если мы запустим тест теперь, то он пройдет:
Тест для Container

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

if (isset($this->cacheDependencies[$serviceId])) {
    return $this->cacheDependencies[$serviceId];
}

О биндингах мы пока еще не говорили, но вы должны понимать, что контейнер умеет создавать не только простые объекты, но и сложный граф зависимостей, но для этого ему нужна информация: например, контейнер не сможет создать нужную реализацию интерфейса или не сможет на запрос $container->get('dsn') дать dsn строку, так как не знает, как это разрезолвить. В этом случае вы должны ему помочь (об этом ниже). Если биндингом является Closure, вызываем функцию, передавая туда контейнер. Если биндингом является строка и такой класс существует, создаем объект с помощью рефлексии. Иначе просто достаем наш биндинг (перед нами обычный примитивный тип). Кэшируем все зависимости и возвращаем.

if (isset($this->bindings[$serviceId])) {
     $binding = $this->bindings[$serviceId];

     if ($binding instanceof \Closure) {
        $this->cacheDependencies[$serviceId] = $binding($this);
     } elseif (is_string($binding) && class_exists($binding)) {
        $this->cacheDependencies[$serviceId] = $this->createService($binding);
     } else {
        $this->cacheDependencies[$serviceId] = $binding;
     }

     return $this->cacheDependencies[$serviceId];
}

Если биндинга в контейнере нет, пытаемся создать объект с помощью рефлексии.

return $this->createService($serviceId);

Если вдруг такого класса не существует, выкидываем исключение. Если да, создаем объект рефлексии, проверяем, есть ли у класса конструктор. Если да, раскрываем его и получаем аргументы. Создаем объект с помощью $reflectionClass->newInstanceArgs($arguments);, кэшируем и отдаем.

private function createService($serviceId)
    {
        if (!class_exists($serviceId)) {
            throw new \RuntimeException(sprintf('Unable to resolve service %s', $serviceId));
        }

        $reflectionClass = new \ReflectionClass($serviceId);

        $arguments = [];

        $constructor = $reflectionClass->getConstructor();

        if (null !== $constructor) {
            $arguments = $this->exploreConstructor($constructor);
        }

        $this->cacheDependencies[$serviceId] = $service = $reflectionClass->newInstanceArgs($arguments);

        return $service;
    }

Раскрываем конструктор, пробегаясь по каждому параметру: если параметр — класс, применяем к нему метод get(), рекурсивно спускаясь вниз по графу зависимостей; если у параметра доступно дефолтное значение, используем его; если параметр не является классом, но он есть в контейнере, снова используем метод get(). Иначе выкидываем исключение.

private function exploreConstructor(\ReflectionFunctionAbstract $constructor): array
    {
        $arguments = [];

        foreach ($constructor->getParameters() as $parameter) {
            $class = $parameter->getClass();

            if (null !== $class) {
                $arguments[] = $this->get($class->getName()); // Рекурсия

            } elseif ($parameter->isDefaultValueAvailable()) {
                $arguments[] = $parameter->getDefaultValue();

            } elseif ($this->has($parameter->getName())){
                $arguments[] = $this->get($parameter->getName());

            } else {
                throw new \RuntimeException(sprintf('Unable to create parameter %s', $parameter->getName()));
            }
        }

        return $arguments;
    }

Чтобы мы могли наполнить наш контейнер знаниями, напишем метод bind:

public function bind($serviceId, $definition)
    {
        if ($this->has($serviceId)) {
            unset($this->cacheDependencies[$serviceId], $this->bindings[$serviceId]);
        }

        $this->bindings[$serviceId] = $definition;
    } 

Он крайне простой: если вдруг сервис под таким идентификатором (им может быть полное имя класса или имя примитивного типа) в контейнере есть, очищаем старые зависимости и добавляем в bindings заново. Это может помочь, если вдруг вы хотите на интерфейс фреймворка накинуть свою реализацию.

Напишем простой интерфейс и реализацию к нему:

interface Ops
{
    public function crash(): string;
}

final class SimpleOps implements Ops
{
    public function crash(): string
    {
        return 'Crash';
    }
}

Если мы теперь в тесте попробуем создать объект для интерфейса Ops:

public function testThatContainerInjectDependenciesCorrect(): void
    {
        $container = new Container();

        /** @var Ops $ops */
        $ops = $container->get(Ops::class);

        self::assertEquals('Crash', $ops->crash());
    }

Мы упадем с ошибкой:

RuntimeException: Unable to resolve service ExpressContainer\Tests\Container\Ops

И правда, каким магическим образом контейнер мог узнать о том, какую реализацию интерфейса использовать и есть ли они у него вообще? Конечно никаким. Исправим наш тест:

public function testThatContainerInjectDependenciesCorrect(): void
    {
        $container = new Container();

        $container->bind(Ops::class, SimpleOps::class); // добавили эту строку

        /** @var Ops $ops */
        $ops = $container->get(Ops::class);

        self::assertEquals('Crash', $ops->crash()); // OK
    }

Запустите этот тест и убедитесь, что он проходит.

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

final class Renderer
{
    public function render()
    {
        return 'Crash';
    }
}

interface Ops
{
    public function crash(): string;
}

final class SimpleOps implements Ops
{
    /**
     * @var Renderer
     */
    private Renderer $renderer;

    public function __construct(Renderer $renderer)
    {
        $this->renderer = $renderer;
    }

    public function crash(): string
    {
        return $this->renderer->render();
    }
}

public function testThatContainerInjectDependenciesCorrect(): void
    {
        $container = new Container();

        $container->bind(Ops::class, function (Container $container) {
            return new SimpleOps($container->get(Renderer::class));
        });

        /** @var Ops $ops */
        $ops = $container->get(Ops::class);

        self::assertEquals('Crash', $ops->crash()); // OK

        $container->bind(Ops::class, SimpleOps::class);

        /** @var Ops $ops */
        $ops = $container->get(Ops::class);

        self::assertEquals('Crash', $ops->crash()); // OK

        $container->bind(Ops::class, function () {
            return new SimpleOps(new Renderer());
        });

        /** @var Ops $ops */
        $ops = $container->get(Ops::class);

        self::assertEquals('Crash', $ops->crash()); // OK
    }

final class Renderer
{
    /**
     * @var string
     */
    private string $prefix;

    public function __construct(string $prefix)
    {
        $this->prefix = $prefix;
    }

    public function render()
    {
        return $this->prefix . ' crash';
    }
}

interface Ops
{
    public function crash(): string;
}

final class SimpleOps implements Ops
{
    /**
     * @var Renderer
     */
    private Renderer $renderer;
    /**
     * @var string
     */
    private string $postfix;

    public function __construct(Renderer $renderer, string $postfix)
    {
        $this->renderer = $renderer;
        $this->postfix = $postfix;
    }

    public function crash(): string
    {
        return $this->renderer->render() . ' ' . $this->postfix;
    }
}

public function testThatContainerInjectDependenciesCorrect(): void
    {
        $container = new Container();

        $container->bind(Ops::class, SimpleOps::class);
        $container->bind('prefix', 'Something');
        $container->bind('postfix', 'again');

        /** @var Ops $ops */
        $ops = $container->get(Ops::class);

        self::assertEquals('Something crash again', $ops->crash()); // OK
    }

Итак, мы написали довольно мощный инструмент, который умеет не так много, как контейнер Laravel или Symfony, но его вполне хватит для проектов среднего уровня сложности. Чего не умеет наш контейнер: не умеет ставить теги (тегом называют строку или интерфейс, по которому можно достать ряд объектов), не умеет использовать расширения, не кэшируемый и некоторые другие возможности. Также вполне вероятно, что мы не учли различные ситуации, однако целью статьи было не создать решающий все задачи контейнер, а продемонстрировать идею, лежащую в основе инверсии контроля. Так работают практически все контейнеры, причем не только в php, но и в Java, Golang и многих других языках.

В то же время несмотря на преимущества использования контейнера внедрения зависимостей, у него есть один маленький недостаток: большие возможности. Когда разработчики узнают про автовайринг (автоматическое определение зависимостей), они перестают контролировать зависимости своих объектов. Не создавая объекты руками, они перестают замечать, насколько большим может граф зависимостей одного объекта, а это, безусловно, плохо. Это говорит о плохом дизайне объектов и большом каплинге между компонентами. Если бы вы вручную настраивали все свои зависимости, вы бы, безусловно, внимательнее относились к тому, сколько зависимостей принимает объект.

Контейнер Laravel'а

Давай рассмотрим некоторые из интересных возможностей контейнера Laravel.

Контекстный биндинг

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

final class GithubApi
{
    public function __construct(HttpClient $httpClient, UserService $users, string $baseUri)
    {
    }
}

// container

final class GithubServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->when(GithubApi::class)->needs('$baseUri')->give(config('github.facts.base_uri'));
    }
}

В give мы также можем передать замыкание, если зависимость чуть сложнее, чем просто строка: например, вы хотите дать этому объекту другую реализацию интерфейса, так как дефолтная реализация уже существует в контейнере, а забиндить два раза один и тот же интерфейс нельзя. Но если вам хочется иметь сразу несколько реализаций у одного интерфейса, используйте теги.

Теги

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

final class RateExchangerResult
{
}

interface Exchanger
{
    public function supports(): bool;
    public function exchange(): RateExchangerResult;
}

final class SiteParsingExchanger implements Exchanger
{
    public function supports(): bool
    {}

    public function exchange(): RateExchangerResult
    {}
}

final class SomeBankApiExchanger implements Exchanger
{
    public function supports(): bool
    {}

    public function exchange(): RateExchangerResult
    {}
}

final class ChainExchanger
{
    private $exchangers;

    public function __construct(iterable $exchangers)
    {
        $this->exchangers = $exchangers;
    }

    public function exchange(): RateExchangerResult
    {
       foreach($this->exchangers as $exchanger) {
          if ($exchanger->supports()) {
              return $exchanger->exchange();
          }
       }

       throw new CouldNotGetRateExchanges();
    }
}

Нам необходимо заполнить наш ChainExchanger реализациями Exchanger и при добавлении нового не изменять наш класс:

final class ExchangerServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->tag([SiteParsingExchanger::class, SomeBankApiExchanger::class], [Exchanger::class]);

        $this->app->bind(ChainExchanger::class, function () {
            return new ChainExchanger($this->app->tagged(Exchanger::class));
        });
    }
}

Теперь при добавлении нового Exchanger достаточно добавить его в метод tag. Или же вы могли использовать конфиг и тянуть классы оттуда, тогда даже провайдер вам бы не пришлось менять. Многие могут справедливо спросить, а не слишком ли дорогой будет операция создания кучи объектов на каждый запрос, даже на тот запрос, где они не потребуются? Отвечу на этот вопрос кодом из Laravel:

public function tagged($tag)
    {
        if (! isset($this->tags[$tag])) {
            return [];
        }

        return new RewindableGenerator(function () use ($tag) {
            foreach ($this->tags[$tag] as $abstract) {
                yield $this->make($abstract);
            }
        }, count($this->tags[$tag]));
    }

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

Сервис-провайдеры

Laravel использует так называемые сервис-провайдеры (расширения контейнера), которые регистрируют зависимости в контейнере. Использование сервис-провайдеров может помочь вам поделить код на модули, в которых используются свои роуты, свою вьюхи, консольные команды и так далее. Каждый сервис-провайдер должен быть зарегистрирован в файле config/app.php в массиве providers. Но если вы используете в одном модуле несколько провайдеров (например, один для сервисов, другой для эвентов), вы можете зарегистрировать один провайдер в другом:

final class SomeModuleServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->register(SomeModuleEventServiceProvider::class);
    }
}

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

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

'Illuminate\Contracts\Bus\Dispatcher' => 'Illuminate\Bus\BusServiceProvider',

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

protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ]; 

Сначала вызывается метод register, а после — boot. Поэтому Laravel советует использовать в методе boot регистрацию вью-композера, загружать роуты и вьюхи, так как в register еще может не оказаться необходимых для этого зависимостей.

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

Пожалуй, на этом обзор контейнера можно закончить, больше информации советую поискать в документации.

loader
31.10.2020 в 15:41
489
+4
Комментарии (0)
Новый комментарий

Логические задачи с собеседований