Контейнер сервісів

Concept Core будує застосунок навколо DI-контейнера (Dependency Injection): залежності оголошуються один раз, а класи отримують їх через конструктор або фабрики. Реалізація — League Container з підтримкою PSR-11.

Див. також: Service providers, контролери → автовайринг, життєвий цикл запиту.

Створення контейнера

Ланцюжок bootstrap (див. налаштування):

// public/index.php
$app = require bootstrap/app.php;
$app->run();

// bootstrap/app.php
$app = App::create($rootPath, $paths);
$app->registerServiceProviders([$rootPath . '/bootstrap/providers/app.php']);

У App::__construct() одразу реєструється:

  • ReflectionContainer — автовайринг (delegate)
  • ContainerInterface::class → сам контейнер (shared)
  • App::class → екземпляр застосунку
  • PathManager — шляхи з bootstrap/paths.php
  • Whoops — ранній обробник помилок (до providers)
$this->container = new Container();
$this->container->delegate(new ReflectionContainer(cacheResolutions: true));
$this->container->add(ContainerInterface::class, fn () => $this->container)->setShared(true);

Service providers

Список провайдерів — у bootstrap/providers/app.php. Кожен клас реалізує League\Container\ServiceProvider\ServiceProviderInterface і реєструє сервіси в методі register().

return [
    ConfigServiceProvider::class,
    LocaleServiceProvider::class,
    TelemetryServiceProvider::class,
    ErrorHandlerServiceProvider::class,
    HttpServiceProvider::class,
    SessionServiceProvider::class,
    // ...
    ComponentsServiceProvider::class,  // останнім — маршрути й views компонентів
];

Порядок має значення: наприклад, ConfigServiceProvider перед HttpServiceProvider (роутер читає config/routes), TwigServiceProvider перед компонентами, які додають Twig paths.

Детально — Провайдери сервісів. Компоненти можуть додавати провайдери через ComponentsServiceProvider.

register() і boot()

  • register() — оголошення bindings ($container->add(...))
  • boot() — eager-логіка одразу після register() цього провайдера (див. Провайдери → register і boot)

Bootable-провайдери в core: HttpServiceProvider, DatabaseServiceProvider, ErrorHandlerServiceProvider, ComponentsServiceProvider, DebugLoggerServiceProvider. Порядок у bootstrap/providers/*.php визначає, які сервіси вже доступні в boot() кожного з них.

Метод provides()

Якщо провайдер оголошує provides(string $id): bool, League Container може відкладати його завантаження, поки не знадобиться один із перелічених сервісів (deferred providers).

Реєстрація сервісів (bindings)

Closure-фабрика

Найпоширеніший спосіб у Concept Core:

$container->add(ConfigInterface::class, function () use ($container) {
    $pathManager = $container->get(PathManager::class);
    // ...
    return new Config($nhConfig);
})->setShared(true);

setShared(true) — один екземпляр на запит (singleton у межах контейнера). Без setShared кожен get() створює новий об’єкт.

Готовий об’єкт

$container->add(PathManager::class, new PathManager($rootPath, $paths));

Інтерфейс → реалізація

У коді залежте від контракту; у провайдері прив’яжіть реалізацію:

$container->add(LoggerInterface::class, function () use ($container) {
    return new Logger(/* ... */);
})->setShared(true);

// У класі застосунку
public function __construct(private readonly LoggerInterface $logger) {}

Автовайринг

Якщо для класу немає явного add(), спрацьовує ReflectionContainer: аналіз конструктора, рекурсивний get() для type-hint.

  • Контролери, middleware, FormRequest, сервіси застосунку — зазвичай без ручної реєстрації
  • Моделі Eloquent — теж резолвяться, якщо конструктор дозволяє

Обмеження method injection у контролерах — лише FormRequest, PSR-7, параметри URL. Усе інше — у конструктор.

League Container → Dependency Injection

Хто викликає get()

МісцеЩо резолвиться
League Route + RouteStrategy Контролер [Class, 'method'], middleware, FormRequest
Service provider Залежності у closure function () use ($container)
App::run() Router, ServerRequestInterface
CLI bin/console.php ConsoleApplication, команди з контейнера

RouteStrategy передає контейнер у стратегію маршруту ($strategy->setContainer($container) у HttpServiceProvider). Документація: Routing, Middleware.

Оновлення запиту в контейнері

Після збігу маршруту RouteStrategy додає параметри URL у запит і перезаписує binding (третій аргумент true — replace):

$container->add(ServerRequestInterface::class, $request, true);

Тому UrlGenerator, ViewResponseFactory і ResponseFactory::back() бачать актуальний запит із атрибутами {id} та SAFE_BACK_URL.

Основні сервіси ядра

Типові binding з провайдерів Concept Core (за контрактом або класом):

СервісПровайдер
ConfigInterfaceConfigServiceProvider
TelemetryCollectorTelemetryServiceProvider
ServerRequestInterface, RouterHttpServiceProvider
UrlGeneratorInterface, ResponseFactoryInterfaceHttpServiceProvider
ViewResponseFactoryInterfaceHttpServiceProvider
SessionInterface, FlashBagInterfaceSessionServiceProvider
ViewInterfaceTwigServiceProvider
ValidatorInterfaceValidationServiceProvider
DatabaseInterface, CapsuleManagerDatabaseServiceProvider
CasterInterfaceCastingServiceProvider
LoggerInterfaceLogServiceProvider
ComponentRegistryComponentsServiceProvider

Twig environment збирається всередині ViewInterface: extensions з config/view.php, paths з компонентів — див. Представлення.

Власний сервіс

  1. Створіть клас (краще інтерфейс + реалізація).
  2. Зареєструйте в ServiceProvider застосунку або компонента (providers() у класі компонента).
  3. Інжектуйте тип у конструктор контролера / сервісу.
// App/Providers/BillingServiceProvider.php
public function register(): void
{
    $this->getContainer()
        ->add(BillingGatewayInterface::class, function () {
            return new StripeBillingGateway(/* ... */);
        })
        ->setShared(true);
}

Додайте провайдер у bootstrap/providers/app.php або в масив providers компонента.

Антипатерни

Service locator

Не передавайте ContainerInterface у контролери й доменні сервіси, щоб викликати $container->get() «за потреби». Це приховує залежності.

Повний розбір — Контролери → service locator.

Коли getContainer() доречний

  • Усередині service provider (register() / boot())
  • Інфраструктура: ResponseFactory (URL generator), SeederManager, error handler
  • Bootstrap, CLI entrypoint, тести з підміною binding

has() і get()

if ($container->has(LoggerInterface::class)) {
    $logger = $container->get(LoggerInterface::class);
}

PSR-11: get() кидає NotFoundExceptionInterface, якщо сервіс не зареєстрований. У провайдерах часто спочатку перевіряють has(ConfigInterface::class), щоб уникнути циклічних залежностей на ранній стадії bootstrap.

Телеметрія bootstrap

Провайдери ядра записують FRAMEWORK_SERVICE_AWAKENING при створенні ключових сервісів (якщо telemetry.enabled). Читання — через TelemetryCollector (Телеметрія).

Контейнер — «склад» залежностей; бізнес-логіка лишається в сервісах і контролерах. Нову інтеграцію (платіжний шлюз, кеш) додавайте binding + інтерфейс, а не виклики get() з десятка місць.