Dependency injection что это
Перейти к содержимому

Dependency injection что это

  • автор:

Краткое введение во внедрение зависимостей: что это и когда это необходимо использовать

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

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

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

Зависимость или зависимое — означает полагаться на что-то. Это все равно что, если сказать, что мы слишком много полагаемся на мобильные телефоны — это означает, что мы зависим от них.

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

Когда класс A использует некоторую функциональность из класса B, тогда говорят, что класс A зависим от класса B.

В Java, прежде чем мы сможем использовать методы других классов, нам необходимо для начала создать экземпляры этого класса (то есть класс А должен создать экземпляр класса В).

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

Так почему следует использовать внедрение зависимостей?

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

Без использования внедрения зависимостей

Перед вами класс Car, отвечающий за создание всех объектов зависимостей. Теперь, что если мы решим избавиться колес компании MRFWheels и хотим использовать колеса от Yokohama в будущем?

Нам нужно будет воссоздать объект класса Car с новой зависимостью от Yokohama. Но при использовании внедрении зависимостей мы можем изменить колеса во время выполнения программы (потому что зависимости можно внедрять во время выполнения, а не во время компиляции).

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

Это делает наш класс автомобилей независимым от создания объектов таких как колеса, аккумулятор и т.д.

Существует три основных типа внедрения зависимостей:

  1. constructor injection: все зависимости передаются через конструктор класса.
  2. setter injection: разработчик добавляет setter-метод, с помощью которого инжектор внедряет зависимость
  3. interface injection: зависимость предоставляет инжектору метод, с помощью которого инжектор передаст зависимость. Разработчики должны реализовать интерфейс, предоставляющий setter-метод, который принимает зависимости

Внедрение зависимостей ответственно за:

  1. Создание объектов;
  2. Представление о том, какие классы требуются этим объектам;
  3. И предоставление зависимостей этим объектам.

Если есть какие-либо изменения в объектах, то DI смотрит на него, и он не должен относиться к классу с использованием этих объектов.

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

Инверсия управления — концепция, лежащая в основе внедрения зависимости

Это означает, что класс не должен конфигурировать свои зависимости статистически, а должен быть сконфигурирован другим классом извне.

Это пятый принцип S.O.L.I.D из пяти основных принципов объектно-ориентированного программирования и разработки от дяди Боба, в котором говорится, что класс должен зависеть от абстракции, а не от чего-то конкретного (простыми словами, жестко закодированного).

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

Замечание: Если вы хотите узнать больше о принципах SOLID от дяди Боба вы можете перейти по этой ссылке.

Преимущества использования внедрения зависимостей

  1. Помогает в модульном тестировании
  2. Количество шаблонного кода сокращается, поскольку инициализация зависимостей выполняется компонентом инжектора;
  3. Расширение приложения становится еще проще;
  4. Помогает уменьшить связность кода, что важно при разработке приложений.

Недостатки использования внедрения зависимостей

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

Тем не менее вы вполне можете реализовать внедрение зависимостей самостоятельно без использования сторонних библиотек и фреймворков или используя их.

Библиотеки и фреймворки, реализующие внедрение зависимостей

  • Spring (Java)
  • Google Guice (Java)
  • http://square.github.io/dagger/ (Java and Android)
  • Castle Windsor (.NET)
  • Unity(.NET)

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

  • Java Dependency Injection — DI Design Pattern Example Tutorial — JournalDev
  • Using dependency injection in Java — Introduction — Tutorial — Vogella
  • Inversion of Control Containers and the Dependency Injection pattern — Martin Fowler

Надеюсь, это вам помогло!

Dependency injection

Представляемый вашему вниманию перевод открывает серию статей от Jakob Jenkov, посвященных внедрению зависимостей, или DI. Примечательна серия тем, что в ней автор, анализируя понятия и практическое применение таких понятий как «зависимость», «внедрение зависимостей», «контейнер для внедрения зависимостей», сравнивая паттерны создания объектов, анализируя недостатки конкретных реализаций DI-контейнеров (например, Spring), рассказывает, как пришел к написанию собственного DI-контейнера. Таким образом, читателю предлагается познакомиться с довольно цельным взглядом на вопрос управления зависимостями в приложениях.

В данной статье сравнивается подход к настройке объектов изнутри и извне (DI). По смыслу настоящая статья продолжает статью Jakob Jenkov Understanding Dependencies, в которой дается определение самому понятию «зависимости» и их типам.

Серия включает в себя следующие статьи
  • Understanding Dependencies
  1. Dependency Injection
  2. Dependency Injection Containers
  3. Dependency Injection Benefits
  4. When to use Dependency Injection
  5. Is Dependency Injection Replacing the Factory Patterns?

Внедрение зависимостей

«Внедрение зависимостей» — это выражение, впервые использованное в статье Мартина Фаулера Inversion of Control Containers and the Dependency Injection Pattern. Это хорошая статья, но она упускает из виду некоторые преимущества контейнеров внедрения зависимостей. Также я не согласен с выводами статьи, но об этом — в следующих текстах.

Объяснение внедрения зависимостей

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

UPD: после обсуждения представленных автором фрагментов кода с flatscode и fogone, я принял решение скорректировать спорные моменты в коде. Изначальный замысел был в том, чтобы не трогать код и давать его таким, каков он написан автором. Оригинальный авторский код в спорных местах закомментирован с указанием «в оригинале», ниже дается его исправленная версия. Также оригинальный код можно найти по ссылке в начале статьи.

public class MyDao < //в оригинале: protected DataSource dataSource = private DataSource dataSource = new DataSourceImpl("driver", "url", "user", "password"); //data access methods. public Person readPerson(int primaryKey) >

Этот DAO (Data Access Object), MyDao нуждается в экземпляре javax.sql.DataSource для того, чтобы получить подключения к базе данных. Подключения к БД используются для чтения и записи в БД, например, объектов Person.

Заметьте, что класс MyDao создает экземпляр DataSourceImpl, так как нуждается в источнике данных. Тот факт, что MyDao нуждается в реализации DataSource, означает, что он зависит от него. Он не может выполнить свою работу без реализации DataSource. Следовательно, MyDao имеет «зависимость» от интерфейса DataSource и от какой-то его реализации.

Класс MyDao создает экземпляр DataSourceImpl как реализацию DataSource. Следовательно, класс MyDao сам «разрешает свои зависимости». Когда класс разрешает собственные зависимости, он автоматически также зависит от классов, для которых он разрешает зависимости. В данном случае MyDao завсист также от DataSourceImpl и от четырех жестко заданных строковых значений, передаваемых в конструктор DataSourceImpl. Вы не можете ни использовать другие значения для этих четырех строк, ни использовать другую реализацию интерфейса DataSource без изменения кода.

Как вы можете видеть, в том случае, когда класс разрешает собственные зависимости, он становится негибким в отношении к этим зависимостям. Это плохо. Это значит, что если вам нужно поменять зависимости, вам нужно поменять код. В данном примере это означает, что если вам нужно использовать другую базу данных, вам потребуется поменять класс MyDao. Если у вас много DAO-классов, реализованных таким образом, вам придется изменять их все. В добавок, вы не можете провести юнит-тестирование MyDao, замокав реализацию DataSource. Вы можете использовать только DataSourceImpl. Не требуется много ума, чтобы понять, что это плохая идея.

Давайте немного поменяем дизайн:

public class MyDao < //в оригинале: protected DataSource dataSource = null; private final DataSource dataSource; public MyDao(String driver, String url, String user, String password)< this.dataSource = new DataSourceImpl(driver, url, user, password); >//data access methods. public Person readPerson(int primaryKey) >

Заметьте, что создание экземпляра DataSourceImpl перемещено в конструктор. Конструктор принимает четыре параметра, это — четыре значения, необходимые для DataSourceImpl. Хотя класс MyDao все еще зависит от этих четырех значений, он больше не разрешает зависимости сам. Они предоставляются классом, создающим экземпляр MyDao. Зависимости «внедряются» в конструктор MyDao. Отсюда и термин «внедрение (прим. перев.: или иначе — инъекция) зависимостей». Теперь возможно сменить драйвер БД, URL, имя пользователя или пароль, используемый классом MyDao без его изменения.

Внедрение зависимостей не ограничено конструкторами. Можно внедрять зависимости также используя методы-сеттеры, либо прямо через публичные поля (прим. перев.: по поводу полей переводчик не согласен, это нарушает защиту данных класса).

Класс MyDao может быть более независимым. Сейчас он все еще зависит и от интерфейса DataSource, и от класса DataSourceImpl. Нет необходимости зависеть от чего-то, кроме интерфейса DataSource. Это может быть достигнуто инъекцией DataSource в конструктор вместо четырех параметров строкового типа. Вот как это выглядит:

public class MyDao < //в оригинале: protected DataSource dataSource = null; private final DataSource dataSource; public MyDao(DataSource dataSource)< this.dataSource = dataSource; >//data access methods. public Person readPerson(int primaryKey) >

Теперь класс MyDao больше не зависит от класса DataSourceImpl или от четырех строк, необходимых конструктору DataSourceImpl. Теперь можно использовать любую реализацию DataSource в конструкторе MyDao.

Цепное внедрение зависимостей

Пример из предыдущего раздела немного упрощен. Вы можете возразить, что зависимость теперь перемещена из класса MyDao к каждому клиенту, который использует класс MyDao. Клиентам теперь приходится знать о реализации DataSource, чтобы быть в состоянии поместить его в конструктор MyDao. Вот пример:

public class MyBizComponent < public void changePersonStatus(Person person, String status)< MyDao dao = new MyDao( new DataSourceImpl("driver", "url", "user", "password")); Person person = dao.readPerson(person.getId()); person.setStatus(status); dao.update(person); >>

Как вы можете видеть, теперь MyBizComponent зависит от класса DataSourceImpl и четырех строк, необходимых его конструктору. Это еще хуже, чем зависимость MyDao от них, потому что MyBizComponent теперь зависит от классов и от информации, которую он сам даже не использует. Более того, реализация DataSourceImpl и параметры конструктора принадлежат к разным слоям абстракции. Слой ниже MyBizComponent — это слой DAO.

Решение — продолжить внедрение зависимости по всем слоям. MyBizComponent должен зависеть только от экземпляра MyDao. Вот как это выглядит:

 public class MyBizComponent < //в оригинале: protected MyDao dao = null; private final MyDao dao; public MyBizComponent(MyDao dao)< this.dao = dao; >public void changePersonStatus(Person person, String status) < Person person = dao.readPerson(person.getId()); person.setStatus(status); dao.update(person); >>

Снова зависимость, MyDao, предоставляется через конструктор. Теперь MyBizComponent зависит только от класса MyDao. Если бы MyDao был интерфейсом, можно было бы менять реализацию без ведома MyBizComponent.

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

  • dependencies
  • dependency injection

Dependency Injection простыми словами

В продолжение предыдущей статьи «Dependency injection (внедрение зависимости)» решил добавить ещё несколько примеров. К этому вопросу я вернулся из-за того, что в Albireo потребовалось сделать небольшой сервис-контейнер и я опять окунулся в эту тему.

Внедрение зависимости

Для начала определимся с основами. Есть понятие «внедрение зависимости». Это означает, что какой-то класс для своей работы будет использовать другой класс.

class A <> class B < public function __construct(A $varA) <>>

Здесь класс «B» принимает в качестве параметра класс «А» — то есть внедряется зависимость. Теперь, если нужно создать объект класса «B» вначале нужно создать объект класса «А»:

$a = new A; $b = new B($a);

Такое внедрение происходит через конструктор. В основном именно так это и делается, хотя есть вариант создания зависимости через отдельный set-метод класса «B».

class B < private $a; public function __construct() <>public function set(A $varA) < $this->a = $varA; > >

Соответственно меняется код инстанцирования:

$a = new A; $b = new B; $b->set($a);

Запомним этот момент, а пока рассмотрим понятие контейнера.

Контейнер объектов

Когда много классов, то возникает задача их замены. Например есть класс FileCache, который выполняет кэширование. Но способ кэширования может быть разным. Например это может быть Memcached. Поскольку класс FileCache мы используем в каких-то своих функциях, а это серьёзное ограничение.

function getData()

Если нужно поменять алгоритм кэширования, то придётся менять код этой функции:

function getData() < // $cache = new FileCache(); $cache = new MemcachedCache(); . >

Очевидно, такой вариант малопригодный. Можно, конечно, ввести всякие условия или прописывать их в конфигурации, но тогда код получения класса кэша ещё раз усложнится. Ну и кроме того, что делать если в будущем нужно будет добавить ещё несколько способов кэширования? Придётся опять переписывать код.

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

Именно такой вариант описывает стандарт PSR-11, где есть два метода: get() — для получения и has() — для проверки существования объекта в контейнере.

Работа с классами будет уже примерно так:

// получаем контейнер $services = Services\Services::getInstance(); // получаем нужный класс $cache = $services->get('Cache\Cache'); // указывается полное имя класса

Или этот же код в одну строчку:

$cache = Services\Services::getInstance()->get('Cache\Cache');

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

Вот так это работает в Albireo на примере класса кэша:

// где-то в конфигурации создаём псевдоним Services\Services::getInstance()->setAlias('cache', Cache\Cache::class); // а потом где-то в коде используем: $cache = Services\Services::getInstance()->getAlias('cache');

В других фреймворках это может быть чуть по другому:

$cache = Services\Services::getInstance()->get('cache');

Но здесь возникает вопрос — а как в этом случае хранилище узнает с какими меткам хранятся классы?

Самый простой способ — это явно прописать в классе контейнера методы, которые вызывают нужный класс. Например в CodeIgniter 4 можно вызвать метод контейнера logger() , вместо того, чтобы указывать класс CodeIgniter\Log\Logger . Для системных классов это может сработать, но не будет работать для своих классов. Поэтому поступают по другому.

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

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

Такие зависимости называют сервисами, а хранилище — локатор сервисов (или служб).

Локатор сервисов (service locator)

По сути service locator — это самое обычное хранилище, но с завязкой на какую-то конфигурацию. Это может быть массив, либо php-класс, где прописываются нужные связи.

Работать с таким локатором достаточно просто (пример из CodeIgniter):

$cache= \Config\Services::cache();

Если стоит задача добавить свой сервис (читай метод), то нужно его добавить в файле конфигурации Config\Services.php . Таким образом service locator хорошо подходят для случаев, когда нужно «отвязаться» от конкретного класса, как в изначальном примере с кэшем. В функции мы работаем именно с сервисом, но в конфигурации можем указать любой подходящий класс.

// где-то в конфигурации $container = new Container(); $container->set('db', new Database($options)); // используем в функции $db = $container->get('db');

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

Контейнер Dependency Injection

В начале статьи я показал что такое внедрение зависимости. Теперь, представьте себе, что мы создали service locator и добавили в него класс «B». Проблема в том, что он зависит от класса «А», о чём наше хранилище не в курсе. Если хранилище попытается создать объект класса «B», то он будет неверно работать — вначале нужно каким-то образом создать «А».

В самом простом варианте в get-метод контейнера можно указать дополнительный параметр, который и будет передаваться в создаваемый класс. Пример из Albireo:

$a = new A; $my = Services\Services::getInstance()->get('B', $a); // указываем имя класса и параметр

Но теперь представим себе, что у нас будет ещё какая-то зависимость у класса «A», как в таком случае передавать параметры?

В PHP существует только один способ автоматизировать этот процесс — использовать Reflection API — с его помощью можно получить информацию о php-классах.

Я долго не мог разобраться в чём же разница между Service Locator и Dependency Injection Container и в итоге для себя решил, что первый не может автоматически определять зависимости. Поэтому DI Container — это Service Locator + Reflection API. Из-за того, что многие фреймворки начинали с простого Service Locator, но потом добавили Reflection, при этом сохранили старое название и происходит постоянная путаница в этих понятиях.

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

В основе лежит следующий алгоритм.

В первую очередь речь идёт о хранилище объектов в виде сервисов. Но часто в качестве имени сервиса выступает имя класса. Это делается для того, чтобы не создавать путаницу в именах.

Следующий момент — в обычном локаторе, когда «объект сервиса» ещё не создан, то он проверяется на то, что это возможно и просто создаётся в виде new и отправляется в хранилище. Но в DI-контейнере происходит дополнительная проверка, которая позволяет определить есть ли у создаваемого объекта параметры конструктора. Если есть, то получаются эти параметры и, опять же рекурсивно проверяются уже на свои зависимости.

Поэтому, когда через DI-контейнер нужно будет получить класс «B», он проверит, что у этого класса есть зависимость в виде класса «А». Потом посмотрит, что у «А» нет зависимостей, создаст объект и вернёт его в объект «B» в качестве первого параметра.

Именно так работает DI Container.

Reflection API

Уже ясно, что Reflection API — это ключевой момент. Чтобы понять как он используется, рассмотрим несколько классов.

class A <> class B < public function __construct() <>> class F < public function __construct(A $varA, B $varB, C $varC, string $s = 'abc') <>>

Первый класс не имеет конструктора. Второй имеет конструктор без параметров, а третий имеет кучу зависимостей, причём от несуществующего класса «C». В задачу DI Container будет входить разбор каждого класса. Я приведу сразу готовый кусок кода, с помощью которого можно будет понять как работает Reflection.

$class = 'A'; if (class_exists($class)) < $reflector = new \ReflectionClass($class); $constructor = $reflector->getConstructor(); // если нет конструктора, то можно создать новый объект без параметров // if (is_null($constructor)) return $reflector->newInstance(); // есть конструктор if ($constructor) < $parameters = $constructor->getParameters(); // получили его параметры $dependencies = []; // зависимости // проходимся по параметрам foreach ($parameters as $param) < $depClassName= getClassName($param); // имя класса в подсказке типа if (class_exists($depClassName)) < $dependencies[$param->name] = $depClassName; > else < $dependencies[$param->name] = $depClassName . ' - not found'; > > pr($dependencies); // после того, как все зависимости найдены можно создать объект с параметрами // return $reflector->newInstanceArgs($dependencies); > else < pr($class . ' - constructor not found'); >> else < pr($class . ' - class not found'); >// https://www.php.net/manual/ru/reflectionparameter.getclass.php#108620 function getClassName(ReflectionParameter $param) < preg_match('/\[\s\<\w+?>\s([\w]+)/s', $param->__toString(), $matches); return isset($matches[1]) ? $matches[1] : null; >

Функция getClassName() нужна для совместимости с PHP 8, поскольку разработчики выпилили из него методы получения имени класса. Поэтому используется такой вот «костыль». Функция pr() — это обёртка над print_r() — она есть в Albireo.

После запуска код выведет:

A - constructor not found

В переменной $class мы указываем класс, который нужно проанализировать. У класса «A» нет конструктора, поэтому $reflector->getConstructor() вернул null . Когда у класса нет конструктора, то его можно инстанцировать без параметров с помощью $reflector->newInstance() .

Для класса «B» результат будет пустой массив:

Array ( )

Это означает, что конструктор есть, но у него нет параметров. Определяется это с помощью $constructor->getParameters() . Этот метод возвращает массив доступных параметров.

И теперь класс «F»:

Array ( [varA] => A [varB] => B [varC] => C - not found [s] => string - not found )

Мы получили список параметров и запустили по ним foreach-цикл. Уже внутри цикла получаем имя типа (класса) параметра и проверяем доступен ли этот класс. Соответственно заполняем массив $dependencies , где сохраняем найденные параметры.

Когда параметры найдены, то мы можем выполнить инстанцирование через $reflector->newInstanceArgs($dependencies) и получить готовый объект.

В реальном DI-контейнере каждый параметр проверяется на свои зависимости — рекурсивно это выполняется до тех пор, пока все зависимости не будут разрешены.

Важный момент. Обратите внимание, что параметры конструктора имеют подсказку типа. Если его не указывать, то Reflection не сможет корректно определить зависимость. То же самое происходит, если в качестве типа данных указывать не класс, а string или любой другой вариант. Как известно, в PHP динамические типы данных, но конкретно в этом случае требуется жёсткая типизация.

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

Итого

Современные Dependency Injection-контейнеры достаточно сложны, но и возможности у них большие. Лично я вряд ли рискнул бы создавать свой контейнер Dependency Injection, вместо этого взял бы какой-то готовый, например PHP-DI. И опять же, использование контейнера должно быть оправдано. Если всё делать через сервисы, то это хоть и делает код чуть меньше, но при этом теряется его смысл и понимание. Всё-таки ООП — это в первую очередь оперирование классами, а здесь мы их скрываем. Поэтому контейнеры хороши там, где в этом есть реальная потребность.

Инъекция зависимостей

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

  • Что такое инжекция зависимостей?
  • Глобальное состояние и синглтоны
  • Передача зависимостей
  • Что такое DI-контейнер?
  • Часто задаваемые вопросы

Nette DI

Пакет nette/di предоставляет чрезвычайно продвинутый скомпилированный DI контейнер для PHP.

  • Контейнер DI от Nette
  • Конфигурация
  • Определения сервисов
  • Автоподключение
  • Генерируемые фабрики
  • Создание расширений для Nette DI
  1. Документация
  2. Инъекция зависимостей

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *