Библиотека для внедрения зависимостей (PSR-11)
Реализация стандарта PSR-11 для работы с контейнером зависимостей в PHP.
🎥 Видеообзор
🛠 Как это работает
-
Конфигурация
Описываете зависимости в конфигурационном файле. -
Генерация
Библиотека создаёт PHP-класс, реализующийContainerInterface. -
Использование
Подключаете готовый класс в приложении и работаете с контейнером.
Если в конфигурации есть ошибки или зависимости не могут быть разрешены - библиотека сообщит об этом на этапе генерации.
✅ Преимущества
-
Простота
Работаете с готовым классом через стандартный интерфейс. -
Оптимизация
Не требует дополнительного кэширования - всё работает с opcache. -
Скорость
Нет runtime-накладных расходов: контейнер готов к использованию. -
Гибкость
Можно генерировать код при деплое, во время сборки или при первом запросе.
⚠️ Что стоит учесть
- Требуется сборка
Нужно организовать этап генерации кода, что необычно для типичного PHP-проекта.
Начало работы
Установка
composer require cekta/di
Настройка проекта
1. Создайте структуру проекта (опционально)
Создайте папку для сгенерированного кода:
mkdir runtime
Обновите composer.json, добавив автозагрузку:
"autoload": {
"psr-4": {
"App\\": "src/",
"App\\Runtime\\": "runtime/"
}
}
Выполните:
composer dumpautoload
2. Создайте тестовый класс
src/Example.php:
<?php
declare(strict_types=1);
namespace App;
class Example
{
}
3. Настройте класс проекта
src/Project.php:
<?php
declare(strict_types=1);
namespace App;
use Cekta\DI\Compiler;
use Psr\Container\ContainerInterface;
use RuntimeException;
class Project
{
private string $container_file;
private string $container_fqcn = 'App\\Runtime\\Container';
public function __construct(private array $env)
{
$this->container_file = realpath(__DIR__ . '/..') . '/runtime/Container.php';
}
public function createContainer(): ContainerInterface
{
if (!class_exists($this->container_fqcn)) {
throw new RuntimeException("$this->container_fqcn не найден");
}
return new ($this->container_fqcn)($this->params());
}
public function compile(): void
{
$content = (new Compiler(
containers: [Example::class],
params: $this->params(),
fqcn: $this->container_fqcn,
))->compile();
if (file_put_contents($this->container_file, $content, LOCK_EX) === false) {
throw new RuntimeException("Не удалось сгенерировать $this->container_file");
}
chmod($this->container_file, 0777);
}
private function params(): array
{
return [];
}
}
4. Создайте скрипт генерации
/bin/build.php:
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
$project = new \App\Project($_ENV);
$project->compile();
5. Сгенерируйте контейнер
php bin/build.php
6. Проверьте работу
/app.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
$project = new \App\Project($_ENV);
$container = $project->createContainer();
var_dump($container->get(App\Example::class));
Запустите:
php app.php
Ожидаемый вывод:
object(App\Example)#1 (0) {
}
7. Настройте автоматическую генерацию (опционально)
Добавьте в composer.json:
{
"scripts": {
"post-autoload-dump": ["php ./bin/build.php"]
},
"config": {
"optimize-autoloader": true
}
}
Теперь при обновлении автозагрузки контейнер будет генерироваться автоматически:
composer dumpautoload
Примеры реализации
Дальнейшие шаги
После настройки используйте библиотеку следующим образом:
- Изменяйте конфигурацию в классе
App\Project - Генерируйте контейнер:
php bin/build.phpилиcomposer dumpautoload - Используйте контейнер в приложении:
$container->get(Service::class)
Готово к использованию!
FQCN (Полное имя класса)
Значение по умолчанию: \App\Container
Что такое FQCN?
FQCN (Fully Qualified Class Name) - это полное имя класса, включая пространство имён (namespace).
Как используется в библиотеке
В конфигурационном классе при создании компилятора:
new Compiler(
containers: [ ... ],
params: [ ... ],
fqcn: 'Your\\Namespace\\Container', // ← здесь
)
Указанный FQCN будет у сгенерированного класса.
Особенности
- Если namespace не указан - класс генерируется в глобальном пространстве имён
- Рекомендуется использовать пространства имён для избежания конфликтов
- Убедитесь, что FQCN соответствует PSR-4 автозагрузке в composer.json
Containers (Контейнеры)
По умолчанию: []
Что такое Containers?
Это список зависимостей, которые будут доступны через методы контейнера:
get()- получение зависимостиhas()- проверка наличия (возвращаетtrueтолько для зависимостей из этого списка)
Какие зависимости указывать
В список включайте основные точки входа вашего приложения:
- HTTP-обработчики
- Контроллеры
- Middleware
- Консольные команды
- Другие основные сервисы
Как это работает
На этапе компиляции библиотека:
- Анализирует все зависимости из списка
- Пытается создать их автоматически
- Рекурсивно разрешает все вложенные зависимости
Если какая-то зависимость не может быть разрешена - вы получите ошибку на этапе генерации.
🔧 Autowiring в конструкторе
Autowiring - это автоматическое внедрение зависимостей на основе аргументов конструктора.
Как это работает:
- Библиотека анализирует каждый аргумент конструктора
- Для определения имени зависимости:
- Если у аргумента указан тип (type-hint) и это не встроенный тип (
string,intи т.д.) - используется имя типа - Иначе используется имя аргумента (без
$)
- Если у аргумента указан тип (type-hint) и это не встроенный тип (
- Процесс повторяется рекурсивно для всех найденных зависимостей
Особенности:
- Глубина вложенности не ограничена
- Autowiring применяется только к зависимостям без специальной конфигурации в
paramsилиalias
📋 Пример autowiring
Структура классов:
<?php
declare(strict_types=1);
namespace App;
class Example
{
public function __construct(
private A $a,
private B $b,
) {
}
}
class B
{
public function __construct(private C $c)
{
}
}
class A
{
}
class C
{
}
Конфигурация:
new \Cekta\DI\Compiler(
containers: [\App\Example::class],
// params и alias не указаны - работает autowiring
)->compile();
Результат:
- Создаётся
Example, которому автоматически передаютсяAиB - Для создания
Bавтоматически создаётсяC - Все зависимости разрешены без дополнительной конфигурации
💡 Важно
- Только то, что в списке
Методhas()вернётtrueтолько для зависимостей изcontainers. - Проверка на этапе компиляции Все ошибки (недоступные зависимости, циклические ссылки) обнаруживаются до запуска приложения.
- Комбинируйте с другими методами
Autowiring можно комбинировать с явной конфигурацией через params и alias. - Начинайте с простого
Добавляйте зависимости в список постепенно, проверяя работу на каждом этапе.
Params (Параметры)
По умолчанию: []
Назначение
Параметры позволяют задавать конкретные значения для аргументов зависимостей:
- Встроенные типы (
string,int,arrayи т.д.) - Конкретные экземпляры объектов
- Значения по умолчанию
📌 Основной синтаксис
{Имя аргумента без $} => {значение аргумента}
📋 Пример использования
Конфигурация:
class Example
{
public function __construct(private string $username, private string $password){}
}
new \Cekta\DI\Compiler(
containers: [Example::class],
params: [
'username' => 'my default username',
'password' => 'my default password',
],
fqcn: 'App\\Runtime\\Container',
)->compile();
Использование:
$container = new \App\Runtime\Container([
'username' => 'my current username', // Переопределяем!
'password' => 'my current password',
]);
$example = $container->get(Example::class);
// $example->username = 'my current username'
// $example->password = 'my current password'
Важно: Значения из runtime имеют приоритет над значениями, заданными при компиляции.
Обязательные параметры
Если параметры используются при компиляции, они становятся обязательными при создании контейнера:
// Вызовет исключение - отсутствуют обязательные параметры
$container = new \App\Runtime\Container([]);
// Правильно:
$container = new \App\Runtime\Container([
'username' => '...',
'password' => '...',
]);
🎯 Параметры для конкретных зависимостей
Синтаксис:
{ClassName}${argumentName} => {значение аргумента}
Пример:
class Example {
public function __construct(private string $username, private string $password) {}
}
class Example2 {
public function __construct(private string $username, private string $password) {}
}
new \Cekta\DI\Compiler(
containers: [Example::class, Example2::class],
params: [
'username' => 'default_username',
'password' => 'default_password',
Example2::class . '$username' => 'special_username', // Только для Example2
],
)->compile();
Приоритеты:
ClassName$argumentName(наивысший)- Общие параметры
- Autowiring (если не указано явно)
🔄 Работа с одинаковыми типами
Используйте имя для точного указания аргумента, если у нескольких аргументов одинаковый тип.
class Example {
public function __construct(
private A $a1,
private A $a2, // Только этот переопределяем
private A $a3,
) {}
}
new \Cekta\DI\Compiler(
containers: [Example::class],
params: [
Example::class . '$a2' => new B(), // B extends A
],
)->compile();
🕐 Lazy-параметры (отложенные значения)
Если параметр реализует Lazy interface то он будет загружаться в runtime.
Для чего:
- Динамические вычисления значений
- Фабрики и билдеры
- Работа с legacy-кодом
Интерфейс Lazy:
interface Lazy {
public function resolve(ContainerInterface $container): mixed;
}
Пример с LazyClosure:
new \Cekta\DI\Compiler(
containers: [Example::class, 'db_type', 'db_path'],
params: [
'username' => $env['DB_USERNAME'] ?? null,
'password' => $env['DB_PASSWORD'] ?? null,
'dsn' => new \Cekta\DI\LazyClosure(
function (ContainerInterface $c) {
return "{$c->get('db_type')}:{$c->get('db_path')}";
}
),
'db_type' => $env['DB_TYPE'] ?? 'sqlite',
'db_path' => $env['DB_PATH'] ?? './db.sqlite',
],
)->compile();
Рекомендация: Если параметры используются внутри LazyClosure, добавьте их в containers для гарантии доступности.
Alias (Псевдонимы)
По умолчанию: []
Механизм псевдонимов позволяет заменять одну зависимость другой. Это полезно для:
- Выбора реализации интерфейса - указание, какой конкретный класс использовать для интерфейса.
- Выбора наследника абстрактного класса - определение конкретной реализации абстрактного класса.
- Замены зависимостей - использование подклассов вместо родительских классов.
- Сокращения имен - создание коротких псевдонимов для длинных имен классов
📋 Базовый пример
<?php
declare(strict_types=1);
namespace App;
class Example {
public function __construct(
public I $i,
public Base $base,
) {}
}
interface I {}
class R1 implements I {}
class R2 implements I {}
class Base {}
class E1 extends Base {}
class E2 extends Base {}
new \Cekta\DI\Compiler(
containers: [Example::class],
alias: [
I::class => R2::class, // Для I используем R2
Base::class => E1::class, // Для Base используем E1
'short-name' => Example::class, // Создаем короткий псевдоним
],
fqcn: 'App\\Runtime\\Container',
);
Использование:
<?php
declare(strict_types=1);
namespace App;
$container = new \App\Runtime\Container([]);
$example = $container->get(Example::class);
$example->i instanceof R2; // true
$example->base instanceof E1; // true
$example === $container->get('short-name'); // true
Важно: Псевдонимы устанавливаются на этапе компиляции. Для их изменения требуется повторная генерация контейнера.
🎯 Псевдонимы для конкретных зависимостей
Можно задавать псевдонимы, которые применяются только к определенным аргументам.
Синтаксис:
{ClassName}${argumentName} => {target}
Пример:
<?php
declare(strict_types=1);
namespace App;
class Example {
public function __construct(
public I $i,
public I $i2, // Этот аргумент получит особую реализацию
public I $i3,
) {}
}
interface I {}
class R1 implements I {}
class R2 implements I {}
new \Cekta\DI\Compiler(
containers: [Example::class],
alias: [
I::class => R2::class, // По умолчанию для I используем R2
Example::class . '$i2' => R1::class, // Но для $i2 в Example используем R1
],
fqcn: 'App\\Runtime\\Container',
);
Использование:
<?php
declare(strict_types=1);
namespace App;
$container = new \App\Runtime\Container([]);
$example = $container->get(Example::class);
$example->i instanceof R2; // true
$example->i2 instanceof R1; // true - переопределен!
$example->i3 instanceof R2; // true
💡 Приоритеты применения
При разрешении зависимостей учитывается следующий порядок приоритетов:
- Конкретный аргумент (
ClassName$argumentName) - наивысший приоритет - Общий псевдоним - применяется ко всем использованиям класса/интерфейса
- Autowiring - если псевдоним не задан, используется стандартное разрешение
🚀 Типичные сценарии использования
1. Выбор реализации интерфейса
alias: [
LoggerInterface::class => FileLogger::class,
CacheInterface::class => RedisCache::class,
]
2. Работа с абстрактными классами
alias: [
AbstractRepository::class => UserRepository::class,
AbstractService::class => UserService::class,
]
3. Сокращение длинных имен
alias: [
'users' => \App\Domain\User\Services\UserManagementService::class,
'auth' => \App\Domain\Auth\Services\AuthenticationService::class,
]
4. Замена реализации в определенном месте
alias: [
MailerInterface::class => SmtpMailer::class,
NewsletterService::class . '$mailer' => TestMailer::class, // Только для NewsletterService
]
⚠️ Важные замечания
- Изменения требуют перекомпиляции - псевдонимы фиксируются при генерации контейнера
- Совместимость типов - заменяемый класс должен быть совместим по типу (реализовывать интерфейс или наследовать класс)
- Проверка на этапе компиляции - ошибки несовместимости типов обнаруживаются при генерации кода
🔄 Совместное использование с другими механизмами
Псевдонимы хорошо работают в сочетании с:
- Params - для передачи конкретных значений
- Autowiring - для автоматического разрешения оставшихся зависимостей
- Lazy-зависимостями - для отложенного создания объектов
Пример комплексного использования:
new \Cekta\DI\Compiler(
containers: [Controller::class],
alias: [
RepositoryInterface::class => DatabaseRepository::class,
Controller::class . '$logger' => FileLogger::class,
],
params: [
'database.dsn' => 'mysql:host=localhost;dbname=app',
Controller::class . '$debug' => true,
],
);
Псевдонимы - мощный инструмент для гибкой настройки зависимостей, позволяющий легко менять реализации без изменения основного кода
Управление жизненным циклом зависимостей
Библиотека предоставляет три типа жизненного цикла для зависимостей, что особенно полезно в долгоживущих процессах:
- Приложениях на RoadRunner или FrankenPHP
- Фоновых workers
- Консольных командах, обрабатывающих множество задач
Вы можете управлять жизненным циклом любых зависимостей: containers, params, alias и autowiring.
🔄 Типы жизненных циклов
1. Scoped (Область видимости) ⭐ По умолчанию
- Зависимость создаётся один раз в пределах одного контейнера
- При создании нового экземпляра контейнера создаётся новая копия зависимости
- Идеально для обработки отдельных запросов
2. Singleton (Одиночка)
- Зависимость создаётся один раз на всё время выполнения скрипта
- Все экземпляры контейнера получают один и тот же объект
- Используется через параметр singletons в конфигурации
3. Factory (Фабрика)
- Каждый запрос создаёт новый экземпляр зависимости
- Используется через параметр factories в конфигурации
📋 Конфигурация
<?php
declare(strict_types=1);
namespace App;
class Scoped {}
class Singleton {}
class Factory {}
new \Cekta\DI\Compiler(
containers: [
Scoped::class,
Singleton::class,
Factory::class,
],
fqcn: 'App\\Runtime\\Container',
singletons: [Singleton::class], // Singleton-зависимости
factories: [Factory::class], // Factory-зависимости
// Scoped-зависимости не указываются явно (используются по умолчанию)
)->compile();
🧪 Пример использования
<?php
declare(strict_types=1);
namespace App;
function testLifecycle(string $className) {
$container1 = new \App\Runtime\Container();
$container2 = new \App\Runtime\Container();
$a = $container1->get($className);
$b = $container1->get($className); // Второй запрос к тому же контейнеру
$c = $container2->get($className); // Запрос к другому контейнеру
echo "Внутри одного контейнера: " . ($a === $b ? "одинаковый" : "разный") . "\n";
echo "Между разными контейнерами: " . ($a === $c ? "одинаковый" : "разный") . "\n";
echo "---\n";
}
echo "Scoped (по умолчанию):\n";
testLifecycle(Scoped::class);
echo "Singleton:\n";
testLifecycle(Singleton::class);
echo "Factory:\n";
testLifecycle(Factory::class);
Результат:
Scoped (по умолчанию):
Внутри одного контейнера: одинаковый
Между разными контейнерами: разный
---
Singleton:
Внутри одного контейнера: одинаковый
Между разными контейнерами: одинаковый
---
Factory:
Внутри одного контейнера: разный
Между разными контейнерами: разный
🎯 Сравнение жизненных циклов
| Тип | Внутри одного контейнера | Между разными контейнерами | Когда использовать |
|---|---|---|---|
| Scoped ⭐ | Один объект | Разные объекты | Обработка запросов, пользовательские сессии |
| Singleton | Один объект | Один объект | Конфигурация, подключения к БД, кеши |
| Factory | Разные объекты | Разные объекты | Stateless-сервисы, DTO, временные данные |
⚠️ Важные замечания
- Scoped по умолчанию - если не указан другой тип, используется Scoped
- Конфликты приоритетов - нельзя указать один класс одновременно как Singleton и Factory
- Производительность - Factory создаёт наибольшую нагрузку, Singleton - наименьшую
- Потокобезопасность - Singleton должен быть потокобезопасным в многопоточных средах
🚀 Пример для долгоживущего приложения
<?php
new \Cekta\DI\Compiler(
containers: [
HttpController::class,
UserRepository::class,
EmailService::class,
],
singletons: [
Database::class, // Одно подключение
RedisCache::class, // Общий кеш
Config::class, // Конфигурация
],
factories: [
HttpRequest::class, // Новый для каждого запроса
UserSession::class, // Новый для каждого пользователя
],
);
Правильное управление жизненным циклом позволяет:
- Экономить ресурсы (Singleton)
- Изолировать данные (Scoped)
- Предотвращать утечки памяти (Factory)
- Легко масштабировать приложение
Приоритет загрузки зависимостей
Библиотека предоставляет несколько способов определения зависимостей. Важно понимать, в каком порядке они применяются - это помогает избежать неожиданного поведения и правильно настроить контейнер.
🎯 Порядок приоритетов (от высшего к низшему)
- Params для конкретной зависимости
ClassName$argumentNameв разделе params - Alias для конкретной зависимости
ClassName$argumentNameв разделе alias - Params общего вида Общие параметры по имени аргумента
- Alias общего вида Общие псевдонимы по имени класса/интерфейса
- Autowiring в конструктор Автоматическое разрешение по типу аргумента
🧪 Пример, демонстрирующий приоритеты
Конфигурация:
<?php
declare(strict_types=1);
namespace App;
class Example {
public function __construct(
public \stdClass $std_class
) {
}
}
// Создаём различные объекты для тестирования
$std_param = new \stdClass();
$std_param->name = 'params';
$std_param_custom = new \stdClass();
$std_param_custom->name = 'params custom';
$std_alias_custom = new \stdClass();
$std_alias_custom->name = 'alias custom';
$std_alias = new \stdClass();
$std_alias->name = 'alias';
new \Cekta\DI\Compiler(
containers: [Example::class],
params: [
Example::class . '$std_class' => $std_param_custom, // Приоритет 1
\stdClass::class => $std_param, // Приоритет 3
'std_alias_custom' => $std_alias_custom, // Поддержка для приоритета 2
'std_alias' => $std_alias, // Поддержка для приоритета 4
],
alias: [
Example::class . '$std_class' => 'std_alias_custom', // Приоритет 2
\stdClass::class => 'std_alias', // Приоритет 4
],
fqcn: 'App\\Runtime\\Container',
)->compile();
Использование:
<?php
declare(strict_types=1);
namespace App;
$container = new \App\Runtime\Container();
$example = $container->get(Example::class);
echo $example->std_class->name; // Результат: "params custom"
🔍 Как это работает в деталях
При запросе Example::class контейнер ищет зависимость для аргумента $std_class в следующем порядке:
| Приоритет | Способ | В нашем примере |
|---|---|---|
| 1 | Example::class . '$std_class' в params | $std_param_custom |
| 2 | Example::class . '$std_class' в alias | Ссылается на 'std_alias_custom' |
| 3 | Общие params для \stdClass::class | $std_param |
| 4 | Общие alias для \stdClass::class | Ссылается на std_alias |
| 5 | Autowiring | Попытка создать \stdClass автоматически |
Итог: Выигрывает наивысший приоритет - параметр для конкретной зависимости.
⚠️ Важные замечания
- Конкретные настройки всегда побеждают - используйте ClassName$argument для точного контроля
- Порядок в конфигурации не имеет значения - приоритеты фиксированы
- Autowiring - последний вариант - используется только если нет других указаний
- Проверка на этапе компиляции - библиотека предупредит о невозможности разрешить зависимость
Понимание приоритетов позволяет гибко настраивать зависимости, переопределяя их там, где нужно, и полагаясь на автоматику в остальных случаях.
Автоматическая конфигурация зависимостей
Иногда вручную перечислять все containers в конфигурации неудобно. Гораздо проще, когда инструмент сам находит точки входа в приложение.
🎯 Когда это нужно?
Автоматическая конфигурация полезна, когда:
- Много однотипных зависимостей - контроллеры, команды, middleware
- Часто добавляются новые точки входа - не нужно каждый раз обновлять конфигурацию
- Хочется меньше ручной работы - автоматизация экономит время
🔍 Как это работает?
Точки входа обычно имеют отличительные признаки:
| Тип | Признак |
|---|---|
| HTTP-обработчики | Реализуют RequestHandlerInterface |
| Middleware | Реализуют MiddlewareInterface |
| Консольные команды | Наследуются от Command (Symfony) |
| Фоновые задачи | Собственные интерфейсы или атрибуты |
Важно: На текущий момент cekta/di не занимается сканированием проекта.
Это инструмент для генерации контейнера на основе готовой конфигурации.
Вы можете легко создать сканирование самостоятельно или использовать подходящий пакет.
🚀 Практический пример
Шаг 1: Подготовка автозагрузчика
Включите оптимизацию автозагрузчика в composer.json:
{
"config": {
"optimize-autoloader": true
}
}
Обновите автозагрузчик:
composer dumpautoload
Теперь в файле /vendor/composer/autoload_classmap.php содержится массив всех классов вашего проекта.
Шаг 2: Создаём сканер классов
/src/ProjectDiscovery.php:
<?php
declare(strict_types=1);
namespace App;
use ReflectionClass;
use Throwable;
class ProjectDiscovery
{
private array $implements = [];
private array $containers = [];
public function __construct(
private readonly iterable $classes,
) {}
public function containerImplement(string $interface, array $exclude = []): self
{
$this->implements[$interface] = $exclude;
return $this;
}
public function loadContainers(): array
{
foreach ($this->classes as $className) {
try {
$class = new ReflectionClass($className);
} catch (Throwable) {
continue;
}
$this->checkImplement($class);
}
return array_unique($this->containers);
}
private function checkImplement(ReflectionClass $class): void
{
foreach ($this->implements as $interface => $excludes) {
if ($class->implementsInterface($interface) && !in_array($class->getName(), $excludes)) {
$this->containers[] = $class->getName();
}
}
}
}
Шаг 3: Используем сканер в сборке
/bin/build.php:
<?php
declare(strict_types=1);
namespace App;
use Cekta\DI\Compiler;
// Интерфейс-маркер для автоматического добавления
interface MarkerForContainer {}
// Классы, которые хотим добавить автоматически
class A implements MarkerForContainer {}
class B implements MarkerForContainer {}
// Получаем все классы проекта
$classMap = require __DIR__ . '/../vendor/composer/autoload_classmap.php';
$classes = array_keys($classMap);
// Сканируем и находим нужные классы
$discover = new ProjectDiscovery($classes);
$discover->containerImplement(MarkerForContainer::class);
$containers = $discover->loadContainers();
// Генерируем контейнер
$code = (new Compiler(
containers: $containers,
fqcn: 'App\\Runtime\\Container',
))->compile();
file_put_contents(__DIR__ . '/../runtime/Container.php', $code);
Шаг 4: Использование
app.php:
<?php
declare(strict_types=1);
namespace App;
$container = new \App\Runtime\Container([]);
// Все классы, реализующие MarkerForContainer, теперь доступны
$a = $container->get(A::class);
$b = $container->get(B::class);
⚙️ Автоматизация процесса
Добавьте в composer.json:
{
"scripts": {
"post-autoload-dump": [
"php ./bin/build.php"
]
}
}
Теперь после каждой установки/обновления пакетов контейнер будет обновляться автоматически, проверим:
/src/C.php - Создадим новый класс
<?php
declare(strict_types=1);
namespace App;
class C implements MarkerForContainer {}
Обновим список классов проекта и Container.php
composer dumpautoload # Или composer install/update
app.php:
<?php
declare(strict_types=1);
namespace App;
$container = new \App\Runtime\Container([]);
// Все классы, реализующие MarkerForContainer, теперь доступны
$a = $container->get(A::class);
$b = $container->get(B::class);
$c = $container->get(C::class); // теперь доступен и наш новый класс
💡 Лучшие практики
- Используйте интерфейсы-маркеры - создавайте специализированные интерфейсы для разных типов зависимостей
- Исключайте ненужные классы - используйте параметр $exclude для исключения абстрактных классов или тестов
- Предпочитайте autowiring - большая часть кода в вашем проекте не требует настроек!
Автоматическая конфигурация значительно упрощает поддержку больших проектов. Начальная настройка требует некоторых усилий, но затем она экономит время и снижает вероятность ошибок при добавлении новых зависимостей.
Service Locator с использованием ContainerInterface
В некоторых сценариях необходимо передавать сам контейнер зависимостей как зависимость. Это полезно при реализации паттерна Service Locator, когда сервисы создаются лениво или динамически.
🎯 Зачем это нужно?
- Динамическое создание сервисов - когда не все зависимости известны на этапе компиляции
- Ленивая загрузка - создание объектов только по необходимости
- Плагины и расширения - динамическое подключение дополнительных компонентов
- Частичная инициализация - загрузка только необходимых частей приложения
📋 Пример реализации Service Locator
<?php
declare(strict_types=1);
namespace App;
interface Handler {
public function handle(): int;
}
class A implements Handler {
public function handle(): int {
return 0;
}
}
class B implements Handler {
public function handle(): int {
return 0;
}
}
class ServiceLocator
{
private array $map = [
'a' => A::class,
'b' => B::class,
// можно добавлять другие сервисы
];
public function __construct(
private \Psr\Container\ContainerInterface $container
) {}
public function locate(string $key): Handler
{
if (!array_key_exists($key, $this->map)) {
throw new \InvalidArgumentException("Сервис '$key' не найден");
}
return $this->container->get($this->map[$key]);
}
}
🔧 Настройка контейнера
Шаг 1: Конфигурация компилятора
new \Cekta\DI\Compiler(
containers: [
ServiceLocator::class,
],
params: [
\Psr\Container\ContainerInterface::class => new \Cekta\DI\LazyClosure(
function (\Psr\Container\ContainerInterface $container) {
return $container; // Возвращаем сам контейнер
}
),
],
fqcn: 'App\\Runtime\\Container',
)->compile();
Шаг 2: Использование в приложении
<?php
declare(strict_types=1);
namespace App;
// Создаём контейнер с параметром ContainerInterface
$container = new \App\Runtime\Container([
\Psr\Container\ContainerInterface::class => new \Cekta\DI\LazyClosure(
function (\Psr\Container\ContainerInterface $container) {
return $container;
}
),
]);
// Получаем ServiceLocator
$locator = $container->get(ServiceLocator::class);
// Используем для динамического получения сервисов
$handlerA = $locator->locate('a'); // instanceof A
$handlerB = $locator->locate('b'); // instanceof B
🔄 Как это работает
- На этапе компиляции мы объявляем, что
ContainerInterfaceбудет предоставляться через параметр - В параметре используем
LazyClosure, который при вызове возвращает сам контейнер - ServiceLocator получает контейнер через внедрение зависимости
- При вызове
locate()ServiceLocator использует контейнер для создания нужного сервиса
⚠️ Предостережения
- Избегайте злоупотребления - Service Locator может скрывать реальные зависимости
- Тестируемость - сервисы, получающие контейнер, сложнее тестировать
- Явные зависимости предпочтительнее - по возможности указывайте зависимости явно в конструкторе
Service Locator с передачей контейнера - мощный инструмент для сложных сценариев, но используйте его осознанно, отдавая предпочтение явному внедрению зависимостей там, где это возможно.
Преобразование аргументов в имена зависимостей
Библиотека анализирует типы аргументов конструктора, чтобы определить имя зависимости для каждого аргумента. Вот как это работает для разных типов данных.
🎯 Основные правила
| Тип аргумента | Имя зависимости | Пример |
|---|---|---|
| Builtin-тип (string, int, bool и др.) | Имя аргумента без $ | $username → username |
| Класс/Интерфейс | Полное имя класса с namespace | A $arg1 → App\A |
| Nullable builtin | Имя аргумента без $ | ?string $username → username |
| Nullable класс | Имя класса с ? в начале | ?A $arg1 → ?App\A |
| Variadic builtin | ... + имя аргумента | int ...$numbers → ...numbers |
| Variadic класс | ... + имя класса | A ...$items → ...App\A |
🔍 Подробное объяснение
1. Builtin-типы (isBuiltin() = true)
class Example {
public function __construct(private string $username) {}
}
→ Имя зависимости: username
2. Классы и интерфейсы (isBuiltin() = false)
namespace App;
class Example {
public function __construct(private A $arg1) {}
}
class A {}
→ Имя зависимости: App\A
3. Nullable-типы
- Builtin:
?string $username→username - Класс:
?A $arg1→?App\A
4. Variadic-аргументы (…)
- Builtin:
int ...$numbers→...numbers - Класс:
?A ...$items→...?App\A
Важно: Для variadic-аргументов необходимо передавать массив, где каждый элемент соответствует указанному типу.
⚠️ Union|Intersection|DNF types
class Example {
public function __construct(
string|int $arg1,
int|string $arg2,
A&B $arg2,
(A&B)|string $arg3,
) {}
}
→ Зависимость считается non-builtin. В качестве имени используется тип аргумента.
string|intиint|string→ приводятся к одному имени, имя формируется по внутренним правилам PHP **(нормализация порядка типов).- Если возникает ошибка, смотрите в исключении точное имя, которое пытается найти библиотека
- Для таких аргументов
Значения по умолчанию игнорируются
class Example {
public function __construct(string $username = 'default') {}
}
→ Значение ‘default’ игнорируется. Вы должны явно указать зависимость через params или alias.
🧪 Примеры для разных случаев
class UserService {
public function __construct(
private string $dsn, // → 'dsn'
private Logger $logger, // → 'Logger'
private ?Config $config = null // → '?Config'
) {}
}
class EventDispatcher {
public function __construct(
EventListener ...$listeners // → '...EventListener'
) {}
}
// В params нужно передать массив:
// '...EventListener' => [new Listener1(), new Listener2()]
Понимание правил преобразования аргументов помогает правильно настроить зависимости и избежать ошибок при компиляции контейнера.
Обнаружение бесконечной рекурсии зависимостей
❓ Что такое бесконечная рекурсия зависимостей?
Это ситуация, когда зависимости циклически ссылаются друг на друга, создавая бесконечную цепочку
- A зависит от B
- B зависит от C
- C зависит от A (возвращаемся к началу)
🎯 Типичный пример
Хотите логировать всё в базу данных, но для подключения к БД тоже нужен логгер:
- Логгер требует подключение к БД
- Подключение к БД требует логгер
Библиотека обнаруживает такие проблемы на этапе компиляции и сообщает об ошибке.
🧪 Пример проблемы
<?php
declare(strict_types=1);
namespace App;
class A {
public function __construct(public B $b) {}
}
class B {
public function __construct(public A $a) {}
}
new \Cekta\DI\Compiler(
containers: [A::class],
)->compile();
💥 Ошибка компиляции
При попытке скомпилировать такой контейнер вы получите исключение:
php bin/build.php
Fatal error: Uncaught Cekta\DI\Exception\InfiniteRecursion:
Infinite recursion detected for `App\A`, stack: App\A, App\B
in /app/vendor/cekta/di/src/Compiler.php:99
Stack trace:
...
🔍 Что показывает ошибка?
Сообщение об ошибке содержит всю цепочку зависимостей:
Infinite recursion detected for **App\A**- проблема обнаружена при создании класса Astack: App\A, App\B- полный путь циклической зависимости- Начинаем с A
- Переходим к B
- Возвращаемся к A (цикл замкнулся)
Обнаружение бесконечной рекурсии на этапе компиляции - мощная защита от ошибок, которые иначе проявились бы только в runtime. Используйте эту возможность для создания стабильных и надежных приложений.
