Як створити свій власний dependency injection container

  • 26.02.16 9:01 •
  • sfedosimov •
  • # 278049 •
  • Хабрахабр •
  • Переклад •
  • 0 •
  • 12300

- такий же як Forbes, тільки краще.

Привіт всім!
Це вільний переклад статті How to Build Your Own Dependency Injection Container.
Оскільки це мій перший переклад для Хабра, прошу вказувати на помилки, неточності.

Як створити свій власний Dependency Injection Container.


Пошук "dependency injection container" на packagist на даний момент видає більше 95 сторінок результату. З упевненістю можна сказати, що це особливе "колесо" вже винайдено.

Однак, жоден шеф-кухар не вчився готувати, використовуючи тільки готову їжу. Також, і жоден розробник ніколи не вчився програмувати, використовуючи тільки готовий код.

У цій статті ми збираємося вчитися робити простий dependency injection container пакет. Весь написаний в статті код плюс PHPDoc анотації та unit-тести з 100% покриттям доступні на GitHub. Все це так само додано на Packagist.

Плануємо наш Dependency Injection Container


Дозвольте нам почати з планування того, що ми хочемо, щоб наш контейнер робив.
Гарний початок це розділити "Dependency Injection Container" на дві ролі - "Dependency Injection" і "Container".

Два найбільш поширених методу для виконання впровадження залежності через constructor injection або setter injection. Це передача класу залежно через конструктор у вигляді аргументу або виклик методу. Якщо наш контейнер матиме можливість створювати екземпляр і містити сервіси, він буде здатний здійснити обидва ці способи.

Щоб бути контейнером, він повинен мати можливість зберігати та видавати екземпляри сервісів. Це досить тривіальна задача в порівнянні зі створенням сервісу, але це все ще вимагає деякого розгляду. Пакет container-interop забезпечує набір інтерфейсів, які контейнер може реалізувати. Основний інтерфейс ContainerInterface. який визначає два методи - один для отримання сервісу, інший для перевірки чи визначено сервіс.

Досвід інших Dependency Injection Containers


Symfony Dependency Injection Container дозволяє нам визначати сервіси різними способами. У YAML. конфігурація для контейнера може виглядати так:

У PHP, та ж сама конфігурація компонента Symfony Dependency Injection була б схожа на це:

Початок роботи


Перша річ, яку ми повинні зробити, це створити директорію і файл composer.json. який буде використовувати Composer. щоб працював автозавантажувач наших класів. Весь цей файл в даний момент є картою SitePoint \ Container простору імен до директорії src.


Далі, як ми збираємося зробити, наш контейнер буде реалізовувати інтерфейс container-interop. нам потрібно щоб composer завантажив їх і додав в наш composer.json файл:


Поряд з головним інтерфейсом ContainerInterface. пакет container-interop також визначає два інтерфейси для винятків. Перший для загальних винятків при створенні сервісу та інший для винятків, коли запитаний сервіс не був знайдений. Ми також додамо інше виняток до цього списку, для тих ситуацій коли запитаний параметр не був знайдений.

Так як нам не потрібно додавати якусь функціональність, що виходить за рамки пропонованої класом ядра PHP Exception. ці класи досить прості. У той час як вони можуть здатися безглуздими, таке розбиття дозволяє нам легше відловлювати і обробляти їх окремо.

Створимо папку src і в ній такі файли src / Exception / ContainerException.php. src / Exception / ServiceNotFoundException.php і src / Exception / ParameterNotFoundException.php відповідно:

Посилання в контейнері


Symfony клас Reference. розглянутий раніше, дозволив бібліотеці розрізняти PHP значення для безпосереднього використання і аргументи, які необхідно замінити іншими сервісами в контейнері.

Давайте запозичимо цю ідею і створимо два класи для посилань на параметри і сервіси. Так як обидва цих класу будуть зберігати значення об'єктів просто по імені ресурсу, на який вони посилаються, має сенс використовувати базовий абстрактний клас. Таким чином ми не повинні писати один і той же код двічі.

Створіть наступні файли src / Reference / AbstractReference.php. src / Reference / ServiceReference.php і src / Reference / ParameterReference.php відповідно:

клас контейнера


Настав час створити наш контейнер. Ми збираємося почати з базової схеми нашого класу контейнера, і ми будемо додавати методи в нього по ходу справи.

Основна ідея приймати два масиви в конструктор нашого контейнера. Перший масив повинен містити визначення сервісу, а другий визначення параметрів.

У src / Container.php помістіть наступний код:


Все що ми тут зробили, це реалізували інтерфейс ContainerInterface з container-interop і завантажили визначення в якості, до яких можна отримати доступ пізніше. Так само ми створили властивість serviceStore і ініціалізували його порожнім масивом. Коли контейнер попросять створити сервіси, ми збережемо їх у цьому масиві так, щоб вони могли бути відновлені пізніше, без необхідності пересозданія їх.

Зараз дозвольте нам почати писати методи, оголошені в container-interop. Почнемо з get ($ name). додамо наступний метод в клас:


Обов'язково додайте use на початку файлу. Наш get ($ name) метод це проста перевірка на наявність в контейнері визначення сервісу. Якщо його немає буде викинутий ServiceNotFoundException який ми створили раніше. Якщо він є, повертає його. Створює його і зберігає в сховище якщо він не був уже створений.

Поки ми перебуваємо в ньому, нам слід створити метод для вилучення параметрів з контейнера. Беручи параметри, віддані в конструктор з n-мірного асоціативного масиву, нам необхідний спосіб чистого доступу до будь-якого елементу в межах масиву, використовуючи єдиний рядок. Простий спосіб зробити це, використовувати точку як роздільник, так щоб рядок foo.bar посилалася на ключ bar в ключі foo з кореня масиву параметрів.


Зараз ми використовували пару методів, які ми ще не написали. Перший з них has ($ name). який оголошений в container-interop. Це досить простий метод і йому просто потрібно перевірити якщо масив визначень надається конструктору містить $ name сервіс.


Інший метод, який нам належить написати createService ($ name). Це метод буде використовувати визначення надані для створення сервісу. Оскільки ми не хочемо, щоб цей метод був викликаний зовні контейнера, ми зробимо його приватним.

Перша річ, яку потрібно зробити в цьому методі, це деякі розумні перевірки.
Для кожного оголошеного сервісу ми вимагаємо масив, що містить ключ class і необов'язкові ключі arguments і calls. Вони будуть використовуватися для впровадження конструктора і сетера відповідно. Ми можемо так само додати захист від зациклення посилань, перевіривши, спробували ми вже створити сервіс.

Якщо ключ arguments існує, ми хочемо перетворити той масив визначень аргументу в масив PHP значень, який можна зрадити конструктору. Для того щоб зробити це, нам необхідно перетворити довідник об'єктів який ми оголосили раніше в значення, на які посилаються в контейнері. На даний момент ми будемо використовувати цю логіку в методі resolveArguments ($ name, array $ argumentDefinitons).
Ми використовуємо метод ReflectionClass :: newInstanceArgs () для створення сервісу, використовуючи масив arguments. Це впровадження конструктора.

Якщо ключ calls існує, ми хочемо використовувати масив call definitions і застосувати його до сервісу, який ми тільки що створили. Знову ми будемо використовувати логіку в окремому методі, визначеному як initializeService ($ service, $ name, array $ callDefinitions). Це впровадження сетера.


Останній метод виконує впровадження сетера в екземпляр об'єкта сервісу. Щоб зробити це, необхідно перебрати масив визначень при виклику методу. Ключ method використовується для вказівки методу, і необов'язковий ключ arguments може бути використаний для надання аргументів цього методу. Ми можемо використовувати метод, який ми тільки що написали, для перекладу його аргументів на PHP значення.


І зараз ми маємо готовий dependency injection container. Щоб побачити приклади використання, перевірте репозиторій на GitHub.

висновок


Ми вивчили як створити простий dependency injection container. але є безліч контейнерів з чудовими функціями, які наш ще не має.

Деякі dependency injection containers, такі як PHP-DI і Aura.DI надають особливість, яка називається автоматичне підключення. Це де контейнер здогадується які сервіси з контейнера слід впровадити в інші. Щоб зробити це, вони використовують reflection API для пошуку інформації про параметри конструктора.

Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть. будь ласка.

Схожі статті