Многие слышали или читали про механизм Dependency Injection (DI), но мало кто имеет представление каким образом он реализован под капотом известного фреймворка.
Примеры кода на Laravel 5.8
В документации Laravel говорится, что Service Container - это мощный инструмент для управления зависимостями классов... В чем же заключается так называемая "мощь" и благодаря чему она достигнута. Если ответить максимально просто - благодаря Reflection API.
Концепция рефлексии не принадлежит конкретно PHP, она пришла из Computer Science и в языках программирования была введена Брайаном Смитом (в докторской диссертации 1982 года).[2]
Рефлексия - это процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.[1]
Вместе с рефлексией так же можно слышать термин "интроспекция" (т.е. наблюдение, исследование). Интроспекция является частью рефлексии.
В PHP есть модуль Reflection, который представляет API-интерфейс для интроспекции классов, функций и расширений.[3]
$ php -m [PHP Modules] ... Reflection
Многие библиотеки и фреймворки PHP используют Reflection API в своих целях, например:
Laravel в нашем случае не исключение и его Service Container здесь будет хорошим примером.
В целях упрощения контекста часть логики в коде пропущена или не обсуждается.
Прежде всего важно понимать что представляет из себя класс Illuminate\Foundation\Application
и где он начинает работать. Если мы откроем файл public/index.php
, то увидим что после vendor/autoload.php
сразу подключается bootstrap/app.php
.
/* |-------------------------------------------------------------------------- | Register The Auto Loader |-------------------------------------------------------------------------- | | Composer provides a convenient, automatically generated class loader for | our application. We just need to utilize it! We'll simply require it | into the script here so that we don't have to worry about manual | loading any of our classes later on. It feels great to relax. | */ require __DIR__.'/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- | Turn On The Lights |-------------------------------------------------------------------------- | | We need to illuminate PHP development, so let us turn on the lights. | This bootstraps the framework and gets it ready for use, then it | will load up this application so that we can run it and send | the responses back to the browser and delight our users. | */ $app = require_once __DIR__.'/../bootstrap/app.php';
В bootstrap/app.php
создается класс Application
.
/* |-------------------------------------------------------------------------- | Create The Application |-------------------------------------------------------------------------- | | The first thing we will do is create a new Laravel application instance | which serves as the "glue" for all the components of Laravel, and is | the IoC container for the system binding all of the various parts. | */ $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) );
По сути класс Application
это и есть Laravel, через который запускаются все его компоненты. Далее важно заметить что класс Application
наследует класс Container
.
class Application extends Container implements ApplicationContract, HttpKernelInterface { /** * The Laravel framework version. * * @var string */ const VERSION = '5.8.38';
Illuminate\Foundation\Container
в своей основе как раз и содержит всю логику по работе с зависимостями. Каждый раз перед вызовом класса, в конструкторе или методе которого есть параметры, Container
занимается их разрешением.
Не смотря на то, что Taylor Otwell в Laravel использует компонетны Symfony, для механизма DI он не стал использовать готовые решения а написал свою реализацию. Файлы связанные с логикой Service Container находятся здесь:
/vendor/laravel/framework/src/Illuminate/Container/ BoundMethod.php composer.json Container.php ContextualBindingBuilder.php EntryNotFoundException.php LICENSE.md RewindableGenerator.php
Service Container в Laravel реализует стандарт PSR-11[5]
Теперь предлагаю перейти к наглядному примеру и выяснить каким образом происходит разрешение зависимостей класса. Предположим, у нас есть NewsService
у которого в конструкторе в качестве параметра есть репозиторий NewsRepository
.
namespace App\Services; use App\Pepositories\NewsRepository; class NewsService { private $repository; public function __construct(NewsRepository $repository) { $this->repository = $repository; } // ... }
При вызове NewsService
мы воспользуемся хелпером app()
, чтобы Service Container сам определил наш класс и параметры его конструктора.
$newsService = app(\App\Services\NewsService::class);
app()
внутри себя использует функционал класса Container
. На первый взгляд кажется непонятным почему хелпер может вернуть \Illuminate\Contracts\Foundation\Application
, это объясняется тем, что обращаясь к классу Container
мы работаем с ним не напрямую а через объект Application
, т.к. в конструкторе последнего регистрируется связь класса Container
и объекта Application
через метод instance
.
Есть разница между вызовом
singletone
иinstance
.[4]
if (! function_exists('app')) { /** * Get the available container instance. * * @param string|null $abstract * @param array $parameters * @return mixed|\Illuminate\Contracts\Foundation\Application */ function app($abstract = null, array $parameters = []) { if (is_null($abstract)) { return Container::getInstance(); } return Container::getInstance()->make($abstract, $parameters); } }
Далее мы сосредоточим внимание только над классом Container
и разберем часть методов которые отрабатывают в большинстве ситуаций.
В Container
отработают следующие методы:
make
resolve
getConcrete
build
resolveDependencies
Метод make
примет в качетсве параметра $abstract
адрес нашего класса App\Services\NewsService
и передаст его в метод resolve
, который определит что входящий класс конечный или это интерфейс или абстрактный класс. В нашем случае класс конечный, но если бы мы передали интерфей или абстрактный класс, то нам бы обязательно пришлось на уровне ServiceProvider
связать нашу зависимость через вызов $this->app->bind
чтоб Container
смог его найти, иначе мы получим ошибку.
/** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); }
В методе resolve
отработает getConcrete
и build
.
/** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @param bool $raiseEvents * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { $abstract = $this->getAlias($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) ); // If an instance of the type is currently being managed as a singleton we'll // just return an existing instance instead of instantiating new instances // so the developer can keep using the same objects instance every time. if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // We're ready to instantiate an instance of the concrete type registered for // the binding. This will instantiate the types, as well as resolve any of // its "nested" dependencies recursively until all have gotten resolved. if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } // If we defined any extenders for this type, we'll need to spin through them // and apply them to the object being built. This allows for the extension // of services, such as changing configuration or decorating the object. foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // If the requested type is registered as a singleton we'll want to cache off // the instances in "memory" so we can return it later without creating an // entirely new instance of an object on each subsequent request for it. if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; } if ($raiseEvents) { $this->fireResolvingCallbacks($abstract, $object); } // Before returning, we will also set the resolved flag to "true" and pop off // the parameter overrides for this build. After those two things are done // we will be ready to return back the fully constructed class instance. $this->resolved[$abstract] = true; array_pop($this->with); return $object; }
getConcrete
проверит наш класс на уровень абстракции и вернет его адрес. Тут можно обратить внимание на параметр $this->bindings
, который хранит все зависимости (адреса) на момент регистрации классов через метод bind
. Laravel это делает при каждом запуске.
/** * Get the concrete type for a given abstract. * * @param string $abstract * @return mixed $concrete */ protected function getConcrete($abstract) { if (! is_null($concrete = $this->getContextualConcrete($abstract))) { return $concrete; } // If we don't have a registered resolver or concrete for the type, we'll just // assume each type is a concrete name and will attempt to resolve it as is // since the container should be able to resolve concretes automatically. if (isset($this->bindings[$abstract])) { return $this->bindings[$abstract]['concrete']; } return $abstract; }
build
уже начнет использовать Reflection API. Это фундаментальный метод в котором и происходит интроспекция класса, определяется его конструктор и все входящие параметры.
/** * Instantiate a concrete instance of the given type. * * @param string $concrete * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function build($concrete) { // If the concrete type is actually a Closure, we will just execute it and // hand back the results of the functions, which allows functions to be // used as resolvers for more fine-tuned resolution of these objects. if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } $reflector = new ReflectionClass($concrete); // If the type is not instantiable, the developer is attempting to resolve // an abstract type such as an Interface or Abstract Class and there is // no binding registered for the abstractions so we need to bail out. if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); } $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); // If there are no constructors, that means there are no dependencies then // we can just resolve the instances of the objects right away, without // resolving any other types or dependencies out of these containers. if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } $dependencies = $constructor->getParameters(); // Once we have all the constructor's parameters we can create each of the // dependency instances and then use the reflection instances to make a // new instance of this class, injecting the created dependencies in. try { $instances = $this->resolveDependencies($dependencies); } catch (BindingResolutionException $e) { array_pop($this->buildStack); throw $e; } array_pop($this->buildStack); return $reflector->newInstanceArgs($instances); }
В нашем случае NewsService
имеет конструктор с параметром NewsRepository
, значит он будет проверен на наличие зависимостей и также пропущен через Container
.
/** * Resolve all of the dependencies from the ReflectionParameters. * * @param array $dependencies * @return array * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function resolveDependencies(array $dependencies) { $results = []; foreach ($dependencies as $dependency) { // If this dependency has a override for this particular build we will use // that instead as the value. Otherwise, we will continue with this run // of resolutions and let reflection attempt to determine the result. if ($this->hasParameterOverride($dependency)) { $results[] = $this->getParameterOverride($dependency); continue; } // If the class is null, it means the dependency is a string or some other // primitive type which we can not resolve since it is not a class and // we will just bomb out with an error since we have no-where to go. $results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); } return $results; }
Зависимости constructor условно делятся на 2 типа: class, primitive.
В итоге, в переменную $newsService
будет передан готовый объект и вся цепочка зависимостей классов будет автоматически разрешена.
На первый взгляд логика класса Container
кажется сложной, но по мере ее понимания становится ясно как работает Service Container под капотом Laravel. Если кому-то этого покажется мало, то можно обратить внимание на механику работы Router
и понять как Laravel определяет свои маршруты и обрабатывает входящие параметры для методов контроллера с помощью Reflection API.
Источники: