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 instancePathManager— paths frombootstrap/paths.phpWhoops— 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()
register()— binding declarations ($container->add(...))-
boot()— eager logic immediately after this provider’sregister()(see Service providers → 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()
| Location | What 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):
| Service | Provider |
|---|---|
ConfigInterface | ConfigServiceProvider |
TelemetryCollector | TelemetryServiceProvider |
ServerRequestInterface, Router | HttpServiceProvider |
UrlGeneratorInterface, ResponseFactoryInterface | HttpServiceProvider |
ViewResponseFactoryInterface | HttpServiceProvider |
SessionInterface, FlashBagInterface | SessionServiceProvider |
ViewInterface | TwigServiceProvider |
ValidatorInterface | ValidationServiceProvider |
DatabaseInterface, CapsuleManager | DatabaseServiceProvider |
CasterInterface | CastingServiceProvider |
LoggerInterface | LogServiceProvider |
ComponentRegistry | ComponentsServiceProvider |
Twig environment is assembled inside ViewInterface:
extensions from config/view.php, paths from components — see
Views.
Custom service
- Create a class (preferably interface + implementation).
-
Register in an application or component
ServiceProvider(providers()on the component class). - 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.