ServiceLocator (DI) in Bitrix
SkillFiles & storageCovers DI container Bitrix\Main\DI\ServiceLocator (PSR-11) — registration of services in the services section of a module's .settings.php file, autowire, retrieving dependencies via has()/get(), constructor injection in application services, action-parameter injection in controllers, binding interfaces to implementations. Applied when moving logic to services, avoiding static calls, and wiring dependencies into services and controller actions (not console/event constructors). Key terms — ServiceLocator, DI, services, autowire, PSR-11, dependency injection, container.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the ServiceLocator (DI) in Bitrix skill
What this skill tells your AI
The instructions your AI receives, as published by bxmaximum/bitrix-framework-skills in skills/bitrix-service-locator/SKILL.md and read by ahel’s review.
Bitrix\Main\DI\ServiceLocator is the kernel's PSR-11 container. It should be retrieved via ServiceLocator::getInstance(), but directly in application code only where dependencies cannot be injected the usual way (factories, legacy, static context, console commands, event handlers).
Layer Rules
| Context | Constructor DI via ServiceLocator? | How to get services |
|---|---|---|
| Application / Infrastructure services | Yes | Register in services, autowire constructors |
| Controller action parameters | Yes (autowire) | Type-hint service in the action method |
Controller constructor | No | Engine builds controller with Request only — use action params or init() + ServiceLocator::get() |
| Console commands | No | CLI does new $commandClass() — call ServiceLocator::get() in execute() |
| Event handlers | No | call_user_func_array — resolve inside the handler method |
| Messenger receivers | Yes (must be registered) | Handler FQCN must exist in services |
- Domain does not know about the container.
- Services from
Application//Infrastructure/receive dependencies via constructor. - Controllers receive services via action parameters only (not constructor).
- Console commands and event handlers are not created by the container.
Service Registration
File /local/modules/vendor.module/.settings.php:
<?php
return [
'services' => [
'value' => [
// 1. By string name
'vendor.module.postService' => [
'className' => \Vendor\Module\Application\Service\PostService::class,
],
// 2. By FQCN (preferred — less magic, IDE support)
\Vendor\Module\Application\Service\PostService::class => [
'className' => \Vendor\Module\Application\Service\PostService::class,
],
// 3. Interface → Implementation
\Vendor\Module\Domain\Repository\PostRepositoryInterface::class => [
'className' => \Vendor\Module\Infrastructure\Repository\PostRepository::class,
],
// 4. With constructor parameters
\Vendor\Module\Infrastructure\Http\TelegramClient::class => [
'className' => \Vendor\Module\Infrastructure\Http\TelegramClient::class,
'constructorParams' => static fn () => [
'token' => getenv('TELEGRAM_BOT_TOKEN'),
],
],
// 5. Closure factory (full control over creation)
\Psr\Log\LoggerInterface::class => [
'constructor' => static function (): \Psr\Log\LoggerInterface {
return \Vendor\Module\Infrastructure\Logger\LoggerFactory::create();
},
],
],
'readonly' => true,
],
];
Modes
className— simple registration; the container resolves dependencies via autowire (by FQCN from constructor).className+constructorParams— pass scalar parameters.constructor— full control, returns a finished object.
Global Services
The services section can also be used in /local/.settings.php — registration does not require a module:
'services' => [
'value' => [
'project.featureFlags' => [
'className' => \App\FeatureFlags::class,
],
],
'readonly' => true,
],
Global services are registered first (registerByGlobalSettings). On Loader::includeModule, module services are registered; if has($code) is already true, the module entry is skipped.
Retrieving a Service
Autowire via Constructor (services only)
final class PostService
{
public function __construct(
private readonly \Vendor\Module\Domain\Repository\PostRepositoryInterface $posts,
private readonly \Psr\Log\LoggerInterface $logger,
) {}
}
Simply registering PostService itself is enough — its dependencies will be retrieved from the container by type.
In a Controller (action parameters)
final class Post extends \Bitrix\Main\Engine\Controller
{
public function getAction(
int $id,
\Vendor\Module\Application\Service\PostService $postService,
): array {
return ['post' => $postService->find($id)];
}
}
Do not type-hint custom services in the controller constructor — ControllerBuilder passes Request only.
In a Console Command / Event Handler
Not created by the container. Resolve explicitly:
$service = \Bitrix\Main\DI\ServiceLocator::getInstance()
->get(\Vendor\Module\Application\Service\PostService::class);
Explicit Container Access
$sl = \Bitrix\Main\DI\ServiceLocator::getInstance();
if ($sl->has(PostService::class))
{
/** @var PostService $posts */
$posts = $sl->get(PostService::class);
}
Use only where DI is impossible (init.php, global functions, console execute(), event handlers, old callbacks).
Service Overriding
First registration wins. When a module calls registerByModuleSettings, existing codes are skipped (has() → continue). Another module cannot override a service by registering the same key later with readonly: false.
Override via:
/local/.settings.phpor/local/.settings_extra.php— globalservices(loaded before modules), orServiceLocator::getInstance()->addInstance($code, $object)/addInstanceLazy()at runtime (e.g. ininit.php).
// /local/.settings_extra.php — wins over later module registration for the same key
'services' => [
'value' => [
\Vendor\Blog\Domain\Repository\PostRepositoryInterface::class => [
'className' => \Vendor\Override\Repository\CachedPostRepository::class,
],
],
'readonly' => true,
],
Lifecycle
- Services are singletons per process/request. Do not store per-request state in them; use request scope via method parameters.
- In long-running CLI processes (messenger-consumer), avoid global state and memory leaks.
Antipatterns
- Constructor DI on
Controller/ console command / event handler. $service = new PostService(...);in a controller/command when the service is registered — use action-param DI orServiceLocator::get().ServiceLocator::getInstance()->get(...)in domain classes — they should not know about the container.- Expecting a second module's
servicesentry to override the first registration. - Registering "config" as a service without a wrapper — pass config as an object/DTO rather than an array.
- Mixing global
\Bitrix\Main\Application::getInstance()->...via statics instead of injection.
Checklist
- All application services are in module
servicesor global. - The key matches FQCN where possible (autocompletion + clarity).
- Domain interfaces look at infrastructure implementations only via
ServiceLocator. - Controllers use action-parameter injection; commands/handlers use
ServiceLocator::get(). - Overrides go through global settings /
addInstance, not a later module registration. - No circular dependencies (the container throws an
Exceptionin this case).
Persistent Storage (Since main 25.1100)
PersistentStorageInterface is registered in kernel services. Retrieve via:
$storage = ServiceLocator::getInstance()
->get(\Bitrix\Main\Data\Storage\PersistentStorageInterface::class);
$storage->set('vendor.module.key', $data, 3600);
See skill bitrix-storage for DeferredStorageDecorator and TTL rules.
Signals
- GitHub stars
- 31
- Forks
- 5
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
bitrix-service-locator- Source
- github.com/bxmaximum/bitrix-framework-skills