Service Container

Concept Core builds the application around a DI container (Dependency Injection): dependencies are declared once, and classes receive them via constructor or factories. Implementation — League Container with PSR-11 support.

See also: Service providers, controllers → autowiring, request lifecycle.

Creating the container

Bootstrap chain (see setup):

// 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']);

In App::__construct(), the following are registered immediately:

  • ReflectionContainer — autowiring (delegate)
  • ContainerInterface::class → the container itself (shared)
  • App::class → application instance
  • PathManager — paths from bootstrap/paths.php
  • Whoops — early error handler (before providers)
$this->container = new Container();
$this->container->delegate(new ReflectionContainer(cacheResolutions: true));
$this->container->add(ContainerInterface::class, fn () => $this->container)->setShared(true);

Service providers

Provider list — in bootstrap/providers/app.php. Each class implements League\Container\ServiceProvider\ServiceProviderInterface and registers services in register().

return [
    ConfigServiceProvider::class,
    LocaleServiceProvider::class,
    TelemetryServiceProvider::class,
    ErrorHandlerServiceProvider::class,
    HttpServiceProvider::class,
    SessionServiceProvider::class,
    // ...
    ComponentsServiceProvider::class,  // last — component routes and views
];

Order matters: e.g. ConfigServiceProvider before HttpServiceProvider (router reads config/routes), TwigServiceProvider before components that add Twig paths.

Details — Service providers. Components can add providers via ComponentsServiceProvider.

register() and boot()

Bootable providers in core: HttpServiceProvider, DatabaseServiceProvider, ErrorHandlerServiceProvider, ComponentsServiceProvider, DebugLoggerServiceProvider. Order in bootstrap/providers/*.php determines which services are already available in each provider’s boot().

provides() method

If a provider declares provides(string $id): bool, League Container can defer loading it until one of the listed services is needed (deferred providers).

Registering services (bindings)

Closure factory

Most common approach in Concept Core:

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

setShared(true) — one instance per request (singleton within the container). Without setShared, each get() creates a new object.

Ready-made object

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

Interface → implementation

Depend on contracts in code; bind the implementation in the provider:

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

// In application class
public function __construct(private readonly LoggerInterface $logger) {}

Autowiring

If there is no explicit add() for a class, ReflectionContainer kicks in: constructor analysis, recursive get() for type hints.

  • Controllers, middleware, FormRequest, application services — usually without manual registration
  • Eloquent models — also resolved if the constructor allows it

Method injection limits in controllers — only FormRequest, PSR-7, URL parameters. Everything else — in the constructor.

League Container → Dependency Injection

Who calls get()

LocationWhat is resolved
League Route + RouteStrategy Controller [Class, 'method'], middleware, FormRequest
Service provider Dependencies in closure function () use ($container)
App::run() Router, ServerRequestInterface
CLI bin/console.php ConsoleApplication, commands from the container

RouteStrategy passes the container to the route strategy ($strategy->setContainer($container) in HttpServiceProvider). Documentation: Routing, Middleware.

Updating the request in the container

After a route match, RouteStrategy adds URL parameters to the request and overwrites the binding (third argument true — replace):

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

Therefore UrlGenerator, ViewResponseFactory, and ResponseFactory::back() see the current request with {id} attributes and SAFE_BACK_URL.

Core services

Typical bindings from Concept Core providers (by contract or class):

ServiceProvider
ConfigInterfaceConfigServiceProvider
TelemetryCollectorTelemetryServiceProvider
ServerRequestInterface, RouterHttpServiceProvider
UrlGeneratorInterface, ResponseFactoryInterfaceHttpServiceProvider
ViewResponseFactoryInterfaceHttpServiceProvider
SessionInterface, FlashBagInterfaceSessionServiceProvider
ViewInterfaceTwigServiceProvider
ValidatorInterfaceValidationServiceProvider
DatabaseInterface, CapsuleManagerDatabaseServiceProvider
CasterInterfaceCastingServiceProvider
LoggerInterfaceLogServiceProvider
ComponentRegistryComponentsServiceProvider

Twig environment is assembled inside ViewInterface: extensions from config/view.php, paths from components — see Views.

Custom service

  1. Create a class (preferably interface + implementation).
  2. Register in an application or component ServiceProvider (providers() on the component class).
  3. Inject the type into a controller / service constructor.
// App/Providers/BillingServiceProvider.php
public function register(): void
{
    $this->getContainer()
        ->add(BillingGatewayInterface::class, function () {
            return new StripeBillingGateway(/* ... */);
        })
        ->setShared(true);
}

Add the provider to bootstrap/providers/app.php or the component providers array.

Anti-patterns

Service locator

Do not pass ContainerInterface into controllers and domain services to call $container->get() “as needed”. This hides dependencies.

Full discussion — Controllers → service locator.

When getContainer() is appropriate

  • Inside a service provider (register() / boot())
  • Infrastructure: ResponseFactory (URL generator), SeederManager, error handler
  • Bootstrap, CLI entrypoint, tests with binding overrides

has() and get()

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

PSR-11: get() throws NotFoundExceptionInterface if the service is not registered. In providers, has(ConfigInterface::class) is often checked first to avoid circular dependencies during early bootstrap.

Bootstrap telemetry

Core providers record FRAMEWORK_SERVICE_AWAKENING when creating key services (if telemetry.enabled). Read via TelemetryCollector (Telemetry).

The container is the “warehouse” of dependencies; business logic stays in services and controllers. For new integrations (payment gateway, cache), add a binding + interface rather than get() calls from a dozen places.