Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Библиотека для внедрения зависимостей (PSR-11)

Реализация стандарта PSR-11 для работы с контейнером зависимостей в PHP.


🎥 Видеообзор

Смотреть видео


🛠 Как это работает

  1. Конфигурация
    Описываете зависимости в конфигурационном файле.

  2. Генерация
    Библиотека создаёт PHP-класс, реализующий ContainerInterface.

  3. Использование
    Подключаете готовый класс в приложении и работаете с контейнером.

Если в конфигурации есть ошибки или зависимости не могут быть разрешены - библиотека сообщит об этом на этапе генерации.


✅ Преимущества

  • Простота
    Работаете с готовым классом через стандартный интерфейс.

  • Оптимизация
    Не требует дополнительного кэширования - всё работает с 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

Примеры реализации

Дальнейшие шаги

После настройки используйте библиотеку следующим образом:

  1. Изменяйте конфигурацию в классе App\Project
  2. Генерируйте контейнер: php bin/build.php или composer dumpautoload
  3. Используйте контейнер в приложении: $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
  • Консольные команды
  • Другие основные сервисы

Как это работает

На этапе компиляции библиотека:

  1. Анализирует все зависимости из списка
  2. Пытается создать их автоматически
  3. Рекурсивно разрешает все вложенные зависимости

Если какая-то зависимость не может быть разрешена - вы получите ошибку на этапе генерации.

🔧 Autowiring в конструкторе

Autowiring - это автоматическое внедрение зависимостей на основе аргументов конструктора.

Как это работает:

  1. Библиотека анализирует каждый аргумент конструктора
  2. Для определения имени зависимости:
    • Если у аргумента указан тип (type-hint) и это не встроенный тип (string, int и т.д.) - используется имя типа
    • Иначе используется имя аргумента (без $)
  3. Процесс повторяется рекурсивно для всех найденных зависимостей

Особенности:

  • Глубина вложенности не ограничена
  • Autowiring применяется только к зависимостям без специальной конфигурации в params или alias

📋 Пример autowiring

Демонстрация на GitHub

Структура классов:

<?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
  • Все зависимости разрешены без дополнительной конфигурации

💡 Важно

  1. Только то, что в списке
    Метод has() вернёт true только для зависимостей из containers.
  2. Проверка на этапе компиляции Все ошибки (недоступные зависимости, циклические ссылки) обнаруживаются до запуска приложения.
  3. Комбинируйте с другими методами
    Autowiring можно комбинировать с явной конфигурацией через params и alias.
  4. Начинайте с простого
    Добавляйте зависимости в список постепенно, проверяя работу на каждом этапе.

Params (Параметры)

По умолчанию: []

Назначение

Параметры позволяют задавать конкретные значения для аргументов зависимостей:

  • Встроенные типы (string, int, array и т.д.)
  • Конкретные экземпляры объектов
  • Значения по умолчанию

📌 Основной синтаксис

{Имя аргумента без $} => {значение аргумента}

📋 Пример использования

Пример на GitHub

Конфигурация:


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();

Приоритеты:

  1. ClassName$argumentName (наивысший)
  2. Общие параметры
  3. 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 (Псевдонимы)

По умолчанию: []

Механизм псевдонимов позволяет заменять одну зависимость другой. Это полезно для:

  1. Выбора реализации интерфейса - указание, какой конкретный класс использовать для интерфейса.
  2. Выбора наследника абстрактного класса - определение конкретной реализации абстрактного класса.
  3. Замены зависимостей - использование подклассов вместо родительских классов.
  4. Сокращения имен - создание коротких псевдонимов для длинных имен классов

📋 Базовый пример

<?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

💡 Приоритеты применения

При разрешении зависимостей учитывается следующий порядок приоритетов:

  1. Конкретный аргумент (ClassName$argumentName) - наивысший приоритет
  2. Общий псевдоним - применяется ко всем использованиям класса/интерфейса
  3. 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
]

⚠️ Важные замечания

  1. Изменения требуют перекомпиляции - псевдонимы фиксируются при генерации контейнера
  2. Совместимость типов - заменяемый класс должен быть совместим по типу (реализовывать интерфейс или наследовать класс)
  3. Проверка на этапе компиляции - ошибки несовместимости типов обнаруживаются при генерации кода

🔄 Совместное использование с другими механизмами

Псевдонимы хорошо работают в сочетании с:

  • 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();

🧪 Пример использования

Демонстрация на GitHub

<?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, временные данные

⚠️ Важные замечания

  1. Scoped по умолчанию - если не указан другой тип, используется Scoped
  2. Конфликты приоритетов - нельзя указать один класс одновременно как Singleton и Factory
  3. Производительность - Factory создаёт наибольшую нагрузку, Singleton - наименьшую
  4. Потокобезопасность - 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)
  • Легко масштабировать приложение

Приоритет загрузки зависимостей

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

🎯 Порядок приоритетов (от высшего к низшему)

  1. Params для конкретной зависимости
    ClassName$argumentName в разделе params
  2. Alias для конкретной зависимости
    ClassName$argumentName в разделе alias
  3. Params общего вида Общие параметры по имени аргумента
  4. Alias общего вида Общие псевдонимы по имени класса/интерфейса
  5. Autowiring в конструктор Автоматическое разрешение по типу аргумента

🧪 Пример, демонстрирующий приоритеты

Демонстрация на GitHub

Конфигурация:

<?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 в следующем порядке:

ПриоритетСпособВ нашем примере
1Example::class . '$std_class' в params$std_param_custom
2Example::class . '$std_class' в aliasСсылается на 'std_alias_custom'
3Общие params для \stdClass::class$std_param
4Общие alias для \stdClass::classСсылается на std_alias
5AutowiringПопытка создать \stdClass автоматически

Итог: Выигрывает наивысший приоритет - параметр для конкретной зависимости.

⚠️ Важные замечания

  1. Конкретные настройки всегда побеждают - используйте ClassName$argument для точного контроля
  2. Порядок в конфигурации не имеет значения - приоритеты фиксированы
  3. Autowiring - последний вариант - используется только если нет других указаний
  4. Проверка на этапе компиляции - библиотека предупредит о невозможности разрешить зависимость

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

Автоматическая конфигурация зависимостей

Иногда вручную перечислять все containers в конфигурации неудобно. Гораздо проще, когда инструмент сам находит точки входа в приложение.

🎯 Когда это нужно?

Автоматическая конфигурация полезна, когда:

  • Много однотипных зависимостей - контроллеры, команды, middleware
  • Часто добавляются новые точки входа - не нужно каждый раз обновлять конфигурацию
  • Хочется меньше ручной работы - автоматизация экономит время

🔍 Как это работает?

Точки входа обычно имеют отличительные признаки:

ТипПризнак
HTTP-обработчикиРеализуют RequestHandlerInterface
MiddlewareРеализуют MiddlewareInterface
Консольные командыНаследуются от Command (Symfony)
Фоновые задачиСобственные интерфейсы или атрибуты

Важно: На текущий момент cekta/di не занимается сканированием проекта.
Это инструмент для генерации контейнера на основе готовой конфигурации.
Вы можете легко создать сканирование самостоятельно или использовать подходящий пакет.

🚀 Практический пример

Демонстрация на GitHub

Шаг 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); // теперь доступен и наш новый класс

💡 Лучшие практики

  1. Используйте интерфейсы-маркеры - создавайте специализированные интерфейсы для разных типов зависимостей
  2. Исключайте ненужные классы - используйте параметр $exclude для исключения абстрактных классов или тестов
  3. Предпочитайте 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

🔄 Как это работает

  1. На этапе компиляции мы объявляем, что ContainerInterface будет предоставляться через параметр
  2. В параметре используем LazyClosure, который при вызове возвращает сам контейнер
  3. ServiceLocator получает контейнер через внедрение зависимости
  4. При вызове locate() ServiceLocator использует контейнер для создания нужного сервиса

⚠️ Предостережения

  1. Избегайте злоупотребления - Service Locator может скрывать реальные зависимости
  2. Тестируемость - сервисы, получающие контейнер, сложнее тестировать
  3. Явные зависимости предпочтительнее - по возможности указывайте зависимости явно в конструкторе

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

Преобразование аргументов в имена зависимостей

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

🎯 Основные правила

Тип аргументаИмя зависимостиПример
Builtin-тип (string, int, bool и др.)Имя аргумента без $$usernameusername
Класс/ИнтерфейсПолное имя класса с namespaceA $arg1App\A
Nullable builtinИмя аргумента без $?string $usernameusername
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 $usernameusername
  • Класс: ?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 (возвращаемся к началу)

🎯 Типичный пример

Хотите логировать всё в базу данных, но для подключения к БД тоже нужен логгер:

  • Логгер требует подключение к БД
  • Подключение к БД требует логгер

Библиотека обнаруживает такие проблемы на этапе компиляции и сообщает об ошибке.

🧪 Пример проблемы

Демонстрация на GitHub

<?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** - проблема обнаружена при создании класса A
  • stack: App\A, App\B - полный путь циклической зависимости
    • Начинаем с A
    • Переходим к B
    • Возвращаемся к A (цикл замкнулся)

Обнаружение бесконечной рекурсии на этапе компиляции - мощная защита от ошибок, которые иначе проявились бы только в runtime. Используйте эту возможность для создания стабильных и надежных приложений.