Внедрение зависимостей — это многократное использование кода. Это шаблон проектирования, направленный на то, чтобы сделать повторно используемый код высокого уровня путем отделения создания / конфигурации объекта от использования.
Рассмотрим следующий код:
<?php class Test { protected $dbh; public function __construct(\PDO $dbh) { $this->dbh = $dbh; } } $dbh = new PDO('mysql:host=localhost;dbname=test', 'username', 'password'); $test = new Test($dbh)
Как видите, вместо создания объекта PDO внутри класса, мы создаем его вне класса и передаем как зависимость — через метод конструктора. Таким образом, мы можем использовать драйвер по своему выбору вместо необходимости использовать драйвер, определенный внутри класса.
Наш собственный Алехандро Жервасио фантастически объяснил концепцию DI , и Фабьен Потенциер также осветил ее в серии .
Однако у этого шаблона есть один недостаток: когда число зависимостей растет, многие объекты необходимо создавать / настраивать, прежде чем передавать в зависимые объекты. Мы можем получить кучу шаблонного кода и длинную очередь параметров в наших методах конструктора. Введите Dependency Injection контейнеры!
Контейнер Dependency Injection — или просто DI-контейнер — это объект, который точно знает, как создать сервис и обработать его зависимости.
В этой статье мы продемонстрируем концепцию дальше с новичком в этой области: диско .
Для получения дополнительной информации о контейнерах внедрения зависимостей, смотрите другие наши посты на эту тему здесь .
Поскольку фреймворки являются отличными примерами развертывания DI-контейнеров, мы закончим статью, создав базовую фреймворк на основе HTTP с помощью Disco и некоторых компонентов Symfony .
Установка
Для установки Disco мы используем Composer как обычно:
composer require bitexpert/disco
Чтобы протестировать код, мы будем использовать встроенный веб-сервер PHP:
php -S localhost:8000 -t web
В результате приложение будет доступно по http://localhost:8000
из браузера. Последний параметр -t
определяет корень документа — где находится файл index.php
.
Начиная
Disco — это контейнер DI, совместимый с container_interop. Несколько спорно, диско является аннотацией на основе ДИ контейнера.
Обратите внимание, что пакет container_interop
состоит из набора интерфейсов для стандартизации функций объектов контейнера. Чтобы узнать больше о том, как это работает, см. Учебник, в котором мы создаем наш собственный контейнер инъекции зависимостей SitePoint, также основанный на взаимодействии контейнеров.
Чтобы добавить сервисы в контейнер, нам нужно создать класс конфигурации . Этот класс должен быть помечен аннотацией @Configuration
:
<?php /** * @Configuration */ class Services { // ... }
Каждый контейнерный сервис должен быть определен как открытый или защищенный метод внутри класса конфигурации. Диско называет каждый сервис Бином , который происходит из культуры Java.
Внутри каждого метода мы определяем, как должен быть создан сервис. Каждый метод должен быть помечен @Bean
который подразумевает, что это сервис, и аннотациями @return
которые отмечают тип возвращаемого объекта.
Это простой пример класса конфигурации Disco с одним «Bean»:
<?php /** * @Configuration */ class Configuration { /** * @Bean * @return SampleService */ public function getSampleService() { // Instantiation $service = new SampleService(); // Configuration $service->setParameter('key', 'value'); return $service; } }
Аннотация @Bean
принимает несколько параметров конфигурации для указания характера службы. Должен ли это быть одноэлементный объект, лениво загруженный (если объект является ресурсоемким), или даже его состояние сохраняется в течение времени жизни сеанса, определяется этими параметрами.
По умолчанию все сервисы определены как одноэлементные сервисы.
Например, следующий компонент Bean создает одиночный загруженный сервис с отложенной загрузкой:
<?php // ... /** * @Bean({"singleton"=true, "lazy"=true}) * @return \Acme\SampleService */ public function getSampleService() { return new SampleService(); } // ...
Диско использует ProxyManager для ленивой загрузки сервисов. Он также использует его для добавления дополнительных поведений (определенных аннотациями) в методы класса конфигурации.
После того, как мы создадим класс конфигурации, нам нужно создать экземпляр AnnotationBeanFactory
, передав ему класс конфигурации. Это будет наш контейнер.
Наконец, мы регистрируем контейнер в BeanFactoryRegistry
:
<?php // ... use \bitExpert\Disco\AnnotationBeanFactory; use \bitExpert\Disco\BeanFactoryRegistry; // ... // Setting up the container $container = new AnnotationBeanFactory(Services::class, $config); BeanFactoryRegistry::register($container);
Как получить услугу из контейнера
Поскольку Disco совместим с container/interop
, мы можем использовать методы get()
и has()
для объекта контейнера:
// ... $sampleService = $container->get('sampleService'); $sampleService->callSomeMethod();
Сфера обслуживания
HTTP — это протокол без сохранения состояния, то есть при каждом запросе все приложение загружается и все объекты воссоздаются. Однако мы можем влиять на срок службы службы, передавая соответствующие параметры в аннотацию @Bean
. Одним из этих параметров является scope
. Область действия может быть либо request
либо session
.
Если область действия — session
, состояние службы будет сохраняться в течение времени жизни сеанса. Другими словами, в последующих HTTP-запросах последнее состояние объекта извлекается из сеанса.
Давайте поясним это на примере. Рассмотрим следующий класс:
<?php class sample { public $counter = 0; public function add() { $this->counter++; return $this; } }
В приведенном выше классе значение $counter
увеличивается каждый раз при вызове метода add()
; Теперь давайте добавим это в контейнер с областью действия, установленной на session
:
// ... /** * @Bean({"scope"="session"}) * @return Sample */ public function getSample() { return new Sample(); } // ...
И если мы используем это так:
// ... $sample = $container->get('getSample'); $sample->add() ->add() ->add(); echo $sample->counter; // output: 3 // ...
ственного// ... $sample = $container->get('getSample'); $sample->add() ->add() ->add(); echo $sample->counter; // output: 3 // ...
При первом запуске будет три. Если мы снова запустим скрипт (чтобы сделать еще один запрос ), значение будет шесть (вместо трех). Вот как состояние объекта сохраняется в запросах.
Если для области задано значение request
, в последующих HTTP-запросах это значение всегда будет равно трем.
Параметры контейнера
Контейнеры обычно принимают параметры из внешнего мира. С помощью Disco мы можем передать параметры в контейнер в виде ассоциативного массива:
// ... $parameters = [ // Database configuration 'database' => [ 'dbms' => 'mysql', 'host' => 'localhost', 'user' => 'username', 'pass' => 'password', ], ]; // Setting up the container $container = new AnnotationBeanFactory(Services::class, $parameters); BeanFactoryRegistry::register($container);
Чтобы использовать эти значения внутри каждого метода класса конфигурации, мы используем аннотации @Parameters
и @parameter
:
<?php // ... /** * @Bean * @Parameters({ * @parameter({"name"= "database"}) * }) * */ public function sampleService($database = null) { // ... }
Дискотека в действии
В этом разделе мы собираемся создать базовую структуру на основе HTTP. Структура создаст ответ на основе информации, полученной из запроса .
Для создания ядра нашей платформы мы будем использовать некоторые компоненты Symfony .
HTTP-ядро
Сердце нашего каркаса. Предоставляет основы запроса / ответа.
Http Foundation
Хороший объектно-ориентированный слой вокруг PHP-глобальных суперглобалей.
маршрутизатор
Согласно официальному сайту: сопоставляет HTTP-запрос с набором переменных конфигурации — подробнее об этом ниже.
Диспетчер событий
Эта библиотека позволяет подключаться к различным этапам жизненного цикла запроса / ответа, используя слушателей и подписчиков.
Чтобы установить все эти компоненты:
composer require symfony/http-foundation symfony/routing symfony/http-kernel symfony/event-dispatcher
Как правило, мы будем хранить код Framework
пространстве имен Framework
.
Также зарегистрируем автозагрузчик PSR-4 . Для этого мы добавляем следующее отображение пространства имен в путь под ключом psr-4
в composer.json
:
// ... "autoload": { "psr-4": { "": "src/" } } // ...
В результате все пространства имен будут искать в каталоге src/
. Теперь мы запускаем composer dump-autoload
чтобы это изменение вступило в силу.
В оставшейся части статьи мы напишем код нашей платформы вместе с фрагментами кода, чтобы прояснить некоторые концепции.
Ядро
Основой любого фреймворка является его ядро. Здесь запрос обрабатывается в ответ .
Мы не собираемся создавать ядро с нуля. Вместо этого мы расширим класс Kernel
только что установленного компонента HttpKernel .
<?php // src/Framework/Kernel.php namespace Framework; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; class Kernel extends HttpKernel implements HttpKernelInterface { }
Поскольку базовая реализация прекрасно работает для нас, мы не будем переопределять какие-либо методы, а вместо этого будем просто полагаться на унаследованную реализацию.
Маршрутизация
Объект Route
содержит путь и обратный вызов , который вызывается (с помощью контроллера разрешения ) каждый раз, когда сопоставляется маршрут (с помощью URL Matcher ).
URL matcher — это класс, который принимает коллекцию маршрутов (мы вскоре обсудим это) и экземпляр RequestContext для поиска активного маршрута.
Объект контекста запроса содержит информацию о текущем запросе.
Вот как маршруты определяются с помощью компонента Routing :
<?php // ... use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; $routes = new RouteCollection(); $routes->add('route_alias', new Route('path/to/match', ['_controller' => function(){ // Do something here... }] ));
Чтобы создать маршруты, нам нужно создать экземпляр RouteCollection
(который также является частью компонента Routing
), а затем добавить к нему наши маршруты.
Чтобы сделать синтаксис маршрутизации более выразительным, мы создадим класс построителя маршрутов вокруг RouteCollection
.
<?php // src/Framework/RouteBuilder.php namespace Framework; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; class RouteBuilder { protected $routes; public function __construct(RouteCollection $routes) { $this->routes = $routes; } public function get($name, $path, $controller) { return $this->add($name, $path, $controller, 'GET'); } public function post($name, $path, $controller) { return $this->add($name, $path, $controller, 'POST'); } public function put($name, $path, $controller) { return $this->add($name, $path, $controller, 'PUT'); } public function delete($name, $path, $controller) { return $this->add($name, $path, $controller, 'DELETE'); } protected function add($name, $path, $controller, $method) { $this->routes->add($name, new Route($path, ['_controller' => $controller], ['_method' => $method])); return $this; } }
Этот класс содержит экземпляр RouteCollection
. В RouteBuilder
для каждого HTTP-глагола есть метод, который вызывает add()
. Мы сохраним наши определения маршрутов в файле src/routes.php
:
<?php // src/routes.php use Symfony\Component\Routing\RouteCollection; use Framework\RouteBuilder; $routeBuilder = new RouteBuilder(new RouteCollection()); $routeBuilder ->get('home', '/', function() { return new Response('It Works!'); }) ->get('welcome', '/welcome', function() { return new Response('Welcome!'); });
Фронт-контроллер
Точкой входа любого современного веб-приложения является его фронт-контроллер. Это файл PHP, обычно называемый index.php
. Это где автозагрузчик класса включен, и приложение загружается.
Все запросы проходят через этот файл и отправляются отсюда на соответствующие контроллеры. Поскольку это единственный файл, который мы собираемся предоставить общественности, мы помещаем его в наш корневой веб-каталог, оставляя остальной код снаружи.
<?php //web/index.php require_once __DIR__ . '/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\EventListener\RouterListener; use Symfony\Component\HttpKernel\Controller\ControllerResolver; // Create a request object from PHP's global variables $request = Request::createFromGlobals(); $routes = include __DIR__.'/../src/routes.php'; $UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext()); // Event dispatcher & subscribers $dispatcher = new EventDispatcher(); // Add a subscriber for matching the correct route. We pass UrlMatcher to this class $dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack())); $kernel = new Framework\Kernel($dispatcher, new ControllerResolver()); $response = $kernel->handle($request); // Sending the response $response->send();
В приведенном выше коде мы создаем экземпляр объекта Request
на основе глобальных переменных PHP.
<?php // ... $request = Request::createFromGlobals(); // ...
Далее мы загружаем файл routes.php
в $routes
. Определение правильного маршрута является обязанностью класса UrlMatcher
, поэтому мы создаем его, передавая коллекцию маршрутов вместе с объектом RequestContext
.
<?php // ... $routes = include __DIR__.'/../src/routes.php'; $UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext()); // ...
Чтобы использовать экземпляр UrlMatcher
, мы передаем его RouteListener
событий RouteListener
.
<?php // ... // Event dispatcher & subscribers $dispatcher = new EventDispatcher(); // Add a subscriber for matching the correct route. We pass UrlMatcher to this class $dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack())); // ...
Каждый раз, когда запрос попадает в приложение, событие запускается и вызывается соответствующий прослушиватель, который, в свою очередь, обнаруживает правильный маршрут, используя переданный ему UrlMatcher
.
Наконец, мы создаем экземпляр ядра, передавая Dispatcher и экземпляр Controller Resolver — через его конструктор:
<?php // ... $kernel = new Framework\Kernel($dispatcher, new ControllerResolver()); $response = $kernel->handle($request); // Sending the response $response->send(); // ...
Время дискотеки
До сих пор нам приходилось выполнять множество экземпляров (и конфигураций) во фронт-контроллере, начиная с создания объекта контекста запроса, средства сопоставления URL-адресов, диспетчера событий и его подписчиков, и, конечно, самого ядра.
Пришло время позволить Disco соединить все эти части для нас.
Как и прежде, мы устанавливаем его с помощью Composer:
composer require bitexpert/Disco;
Затем мы создаем класс конфигурации и определяем службы, которые нам понадобятся во фронт-контроллере:
<?php // src/Framework/Services.php use bitExpert\Disco\Annotations\Bean; use bitExpert\Disco\Annotations\Configuration; use bitExpert\Disco\Annotations\Parameters; use bitExpert\Disco\Annotations\Parameter; /** * @Configuration */ class Services { /** * @Bean * @return \Symfony\Component\Routing\RequestContext */ public function context() { return new \Symfony\Component\Routing\RequestContext(); } /** * @Bean * * @return \Symfony\Component\Routing\Matcher\UrlMatcher */ public function matcher() { return new \Symfony\Component\Routing\Matcher\UrlMatcher($this->routeCollection(), $this->context()); } /** * @Bean * @return \Symfony\Component\HttpFoundation\RequestStack */ public function requestStack() { return new \Symfony\Component\HttpFoundation\RequestStack(); } /** * @Bean * @return \Symfony\Component\Routing\RouteCollection */ public function routeCollection() { return new \Symfony\Component\Routing\RouteCollection(); } /** * @Bean * @return \Framework\RouteBuilder */ public function routeBuilder() { return new \Framework\RouteBuilder($this->routeCollection()); } /** * @Bean * @return \Symfony\Component\HttpKernel\Controller\ControllerResolver */ public function resolver() { return new \Symfony\Component\HttpKernel\Controller\ControllerResolver(); } /** * @Bean * @return \Symfony\Component\HttpKernel\EventListener\RouterListener */ protected function listenerRouter() { return new \Symfony\Component\HttpKernel\EventListener\RouterListener( $this->matcher(), $this->requestStack() ); } /** * @Bean * @return \Symfony\Component\EventDispatcher\EventDispatcher */ public function dispatcher() { $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); $dispatcher->addSubscriber($this->listenerRouter()); return $dispatcher; } /** * @Bean * @return Kernel */ public function framework() { return new Kernel($this->dispatcher(), $this->resolver()); } }
Похоже, много кода; но на самом деле это тот же код, который ранее находился во фронт-контроллере.
Перед использованием класса нам нужно убедиться, что он был загружен автоматически, добавив его в ключ files
в нашем файле composer.json
:
// ... "autoload": { "psr-4": { "": "src/" }, "files": [ "src/Services.php" ] } // ...
А теперь на наш фронт контроллер.
<?php //web/index.php require_once __DIR__ . '/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class); \bitExpert\Disco\BeanFactoryRegistry::register($container); $routes = include __DIR__.'/../src/routes.php'; $kernel = $container->get('framework') $response = $kernel->handle($request); $response->send();
Теперь наш фронтальный контроллер может дышать! Все экземпляры выполняются Disco, когда мы запрашиваем услугу.
Но как насчет конфигурации?
Как объяснялось ранее, мы можем передавать параметры в виде ассоциативного массива в класс AnnotationBeanFactory
.
Чтобы управлять конфигурацией в нашей среде, мы создаем два файла конфигурации, один для разработки и один для производственной среды.
Каждый файл возвращает ассоциативный массив, который мы можем загрузить в переменную.
Давайте держать их в каталоге Config
:
// Config/dev.php return [ 'debug' => true; ];
И для производства:
// Config/prod.php return [ 'debug' => false; ];
Чтобы обнаружить среду, мы определим среду в специальном текстовом файле , так же как мы определяем переменную среды:
ENV=dev
Для разбора файла мы используем PHP dotenv , пакет, который загружает переменные среды из файла (по умолчанию имя файла .env
) в .env
PHP $_ENV
. Это означает, что мы можем получить значения с помощью PHP-функции getenv () .
Чтобы установить пакет:
composer require vlucas/phpdotenv
Затем мы создаем наш файл .env
каталоге Config/
.
Config / .env
ENV=dev
Во фронт-контроллере мы загружаем переменные окружения, используя PHP dotenv:
<?php //web/index.php // ... // Loading environment variables stored .env into $_ENV $dotenv = new Dotenv\Dotenv(__DIR__ . '/../Config'); $dotenv->load(); // Load the proper configuration file based on the environment $parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php'; $container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters); \bitExpert\Disco\BeanFactoryRegistry::register($container); // ...
В предыдущем коде мы сначала указываем каталог, в котором находится наш файл .env
, затем вызываем load()
для загрузки переменных среды в $_ENV
. Наконец, мы используем getenv()
чтобы получить правильное имя файла конфигурации.
Создание Контейнерного Строителя
Есть еще одна проблема с кодом в его текущем состоянии: всякий раз, когда мы хотим создать новое приложение, мы должны создать экземпляр AnnotationBeanFactory
в нашем фронт-контроллере ( index.php
). В качестве решения мы можем создать фабрику, которая создает контейнер при необходимости.
<?php // src/Factory.php namespace Framework; class Factory { /** * Create an instance of Disco container * * @param array $parameters * @return \bitExpert\Disco\AnnotationBeanFactory */ public static function buildContainer($parameters = []) { $container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters); \bitExpert\Disco\BeanFactoryRegistry::register($container); return $container; } }
Эта фабрика имеет статический метод buildContainer()
, который создает и регистрирует контейнер Disco.
Вот как это улучшает наш фронт-контроллер:
<?php //web/index.php require_once __DIR__ . '/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; // Getting the environment $dotenv = new Dotenv\Dotenv(__DIR__ . '/../config'); $dotenv->load(); // Load the proper configuration file based on the environment $parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php'; $request = Request::createFromGlobals(); $container = Framework\Factory::buildContainer($parameters); $routes = include __DIR__.'/../src/routes.php'; $kernel = $container->get('framework') $response = $kernel->handle($request); $response->send();
Это выглядит намного аккуратнее, не так ли?
Класс приложения
Мы можем сделать шаг вперед с точки зрения удобства использования и абстрагировать оставшиеся операции (во фронт-контроллере) в другой класс. Давайте назовем этот класс Application
:
<?php namespace Framework; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; class Application { protected $kernel; public function __construct(HttpKernelInterface $kernel) { $this->kernel = $kernel; } public function run() { $request = Request::createFromGlobals(); $response = $this->kernel->handle($request); $response->send(); } }
Application
зависит от ядра и работает как оболочка вокруг него. Мы создаем метод с именем run()
, который заполняет объект запроса и передает его ядру для получения ответа.
Чтобы сделать его еще круче, давайте добавим и этот класс в контейнер:
<?php // src/Framework/Services.php // ... /** * @Bean * @return \Framework\Application */ public function application() { return new \Framework\Application($this->kernel()); } // ...
И это новый вид нашего фронт-контроллера:
<?php require_once __DIR__ . '/../vendor/autoload.php'; // Getting the environment $dotenv = new Dotenv\Dotenv(__DIR__ . '/../config'); $dotenv->load(); // Load the proper configuration file based on the environment $parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php'; // Build a Disco container using the Factory class $container = Framework\Factory::buildContainer($parameters); // Including the routes require __DIR__ . '/../src/routes.php'; // Running the application to handle the response $app = $container->get('application') ->run();
Создание слушателя ответа
Мы можем использовать эту платформу сейчас, но все еще есть возможности для улучшения. В настоящее время мы должны возвращать экземпляр Response
в каждом контроллере, в противном случае ядро создает исключение:
<?php // ... $routeBuilder ->get('home', '/', function() { return new Response('It Works!'); }); ->get('welcome', '/welcome', function() { return new Response('Welcome!'); }); // ...
Тем не менее, мы можем сделать его необязательным и разрешить отправлять обратно чистые строки. Для этого мы создаем специальный класс подписчика, который автоматически создает объект Response
если возвращаемое значение является строкой.
Подписчики должны реализовать интерфейс Symfony\Component\EventDispatcher\EventSubscriberInterface
. Они должны реализовать метод getSubscribedMethods()
в котором мы определяем события, на которые мы заинтересованы подписаться, и их прослушиватели событий.
В нашем случае нас интересует событие KernelEvents::VIEW
. Событие происходит, когда ответ должен быть возвращен.
Вот наш класс подписчика:
<?php // src/Framework/StringResponseListener namespace Framework; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\KernelEvents; class StringResponseListener implements EventSubscriberInterface { public function onView(GetResponseForControllerResultEvent $event) { $response = $event->getControllerResult(); if (is_string($response)) { $event->setResponse(new Response($response)); } } public static function getSubscribedEvents() { return array(KernelEvents::VIEW => 'onView'); } }
Внутри метода слушателя onView
мы сначала проверяем, является ли ответ строкой (а не объектом- Response
), а затем создаем объект-ответ, если требуется.
Чтобы использовать подписчика, нам нужно добавить его в контейнер как защищенный сервис:
<?php // ... /** * @Bean * @return \Framework\StringResponseListener */ protected function ListenerStringResponse() { return new \Framework\StringResponseListener(); } // ...
Затем мы добавляем его в диспетчерскую службу:
<?php // ... /** * @Bean * @return \Symfony\Component\EventDispatcher\EventDispatcher */ public function dispatcher() { $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); $dispatcher->addSubscriber($this->listenerRouter()); $dispatcher->addSubscriber($this->ListenerStringResponse()); return $dispatcher; } // ...
Отныне мы можем просто возвращать строку в наших функциях контроллера:
<?php // ... $routeBuilder ->get('home', '/', function() { return 'It Works!'; }) ->get('welcome', '/welcome', function() { return 'Welcome!'; }); // ...
Каркас готов сейчас.
Вывод
Мы создали базовый HTTP-фреймворк с помощью Symfony Components и Disco. Это просто базовая структура запросов / ответов, в которой отсутствуют какие-либо другие концепции MVC, такие как модели и представления, но она позволяет реализовать любые дополнительные архитектурные шаблоны, которые мы можем пожелать.
Полный код доступен на Github .
Disco является новичком в игре DI-контейнера и, по сравнению со старыми, в ней отсутствует исчерпывающая документация. Эта статья была попыткой обеспечить плавное начало для тех, кто может найти этот новый вид DI-контейнера интересным.
Вы склеиваете компоненты своего приложения с DI-контейнерами? Если да, то какие? Вы дали дискотеке попробовать? Дайте нам знать!