29.12.2025 15 мин чтения

От хаоса к контролю: как ServiceLocator в Bitrix спасает от спагетти-кода и позволяет управлять зависимостями

ServiceLocator и Dependency Injection в Bitrix — это не просто модные термины, а реальные инструменты для решения проблем legacy-кода. В статье разбираем, как перестать писать спагетти-код с жестко зашитыми зависимостями и начать строить масштабируемую архитектуру.

Кирилл Новожилов

Кирилл Новожилов

Автор

От хаоса к контролю: как ServiceLocator в Bitrix спасает от спагетти-кода и позволяет управлять зависимостями

Введение: Ночной кошмар разработчика

Представьте классическую ситуацию: три часа ночи, вы пытаетесь отладить странный баг в обработчике события OnSaleOrderSaved. Внутри метода вы видите вызов global $USER, статический вызов MyOldHelper::doSomething(), а где-то в глубине хелпера внезапно всплывает прямая работа с $_SESSION. Вы тянете за одну ниточку, и разваливается всё: тесты (которых нет) не написать, зависимости зашиты намертво, а любое изменение превращается в игру «увернись от регрессии».

Знакомо? Это — классический «спагетти-код», который годами был визитной карточкой проектов на Битриксе. Мы привыкли к тому, что зависимости — это нечто невидимое, размазанное по всему коду в виде глобальных переменных и синглтонов. Но современный PHP 8.1+ диктует другие правила: нам нужна предсказуемость, типизация и тестируемость.

С выходом версии main 20.5.400 в ядре D7 появился инструмент, который незаметно, но фундаментально изменил правила игры — Service Locator. Это не просто очередной класс в ядре, это мост между «старым добрым» Битриксом и миром современного корпоративного ПО.

В этой статье мы пройдем путь от архитектурного хаоса до чистого, расширяемого кода. Мы разберем всё: от конфигов до юнит-тестов, и даже заглянем в исходники ядра, чтобы понять, как эта магия работает на самом деле. Если вы готовы перестать быть «программистом на Битриксе» и стать инженером — этот лонгрид для вас.

Что вы узнаете из этой статьи:

  • Почему классические подходы к созданию объектов убивают поддерживаемость кода
  • Как работает DI-контейнер в Bitrix Framework
  • Три способа регистрации сервисов и когда какой использовать
  • Автоматическое разрешение зависимостей через рефлексию
  • Интеграция с компонентами, агентами и событиями
  • Антипаттерны, которых стоит избегать

Для кого эта статья:

Для разработчиков уровня Middle, которые хотят писать поддерживаемый и тестируемый код, понимают базовые принципы ООП и готовы выйти за рамки «просто работает».

Часть 1: Проблема и решение

Когда зависимости становятся проблемой

Представьте типичный класс в вашем проекте:

        class OrderService
{
    public function createOrder(array $data)
    {
        $logger = new FileLogger('/var/log/orders.log');
        $emailSender = new SmtpEmailSender();
        $paymentGateway = new PaymentGateway('api_key_12345');
        
        $orderId = $this->saveOrder($data);
        
        $logger->log("Order created: " . $orderId);
        $paymentGateway->charge($data['amount']);
        $emailSender->send($data['email'], 'Order confirmation');
        
        return $orderId;
    }
}

    

На первый взгляд всё выглядит нормально. Но давайте посмотрим, что здесь не так:

Проблема 1: Жёсткая связанность

Класс OrderService намертво привязан к конкретным реализациям. Хотите использовать другой логгер? Придётся менять код класса. Нужен другой email-сервис? Снова лезть в код.

Проблема 2: Тестирование — это боль

Как протестировать этот код? Каждый тест будет реально отправлять письма, писать в файлы, дёргать платёжный API. Замокать зависимости? Забудьте — они создаются внутри метода.

Проблема 3: Скрытые зависимости

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

Проблема 4: Конфигурация размазана

Пути к файлам, API-ключи, настройки — всё захардкожено прямо в коде. Нужно поменять путь к логу? Ищите по всему проекту.

Решение: Dependency Injection

Dependency Injection (внедрение зависимостей) — это паттерн, при котором объект не создаёт свои зависимости самостоятельно, а получает их извне. Звучит просто, но это меняет всё:

        class OrderService
{
    private LoggerInterface $logger;
    private EmailServiceInterface $emailService;
    private PaymentGatewayInterface $paymentGateway;
    
    public function __construct(
        LoggerInterface $logger,
        EmailServiceInterface $emailService,
        PaymentGatewayInterface $paymentGateway
    ) {
        $this->logger = $logger;
        $this->emailService = $emailService;
        $this->paymentGateway = $paymentGateway;
    }
    
    public function createOrder(array $data)
    {
        $orderId = $this->saveOrder($data);
        
        $this->logger->log("Order created: " . $orderId);
        $this->paymentGateway->charge($data['amount']);
        $this->emailService->send($data['email'], 'Order confirmation');
        
        return $orderId;
    }
}

    

Что изменилось?

  • Зависимости явные — всё, что нужно классу, объявлено в конструкторе
  • Слабая связанность — работаем с интерфейсами, а не конкретными реализациями
  • Тестируемость — легко подменить зависимости моками
  • Гибкость — можем использовать разные реализации без изменения кода

Но появляется новая проблема: кто-то должен создать все эти зависимости и передать их в конструктор. Вот тут на сцену выходит ServiceLocator.

Часть 2: ServiceLocator в Bitrix

Что это такое

ServiceLocator в Bitrix — это центральный реестр, куда вы регистрируете сервисы, а потом получаете их по имени:

        $serviceLocator = \Bitrix\Main\DI\ServiceLocator::getInstance();
$orderService = $serviceLocator->get('mycompany.orders.orderService');

    

Сервис создаётся один раз при первом обращении и переиспользуется дальше (singleton).

Ключевые особенности:

Особенность Описание
PSR-11 Реализует стандарт ContainerInterface
Lazy initialization Сервисы создаются только при первом обращении
Singleton Один экземпляр на весь запрос
Autowire Автоматическое разрешение зависимостей через рефлексию
Защита от циклов Обнаруживает циклические зависимости

Чем лучше, чем просто new?

        // Плохо: жёсткая связь, нельзя подменить, конфигурация в коде
$sender = new SmtpEmailSender('smtp.mail.ru', 465);

// Лучше: централизованная конфигурация, можно подменить
$sender = ServiceLocator::getInstance()->get('mycompany.orders.email');

    
Критерий new ServiceLocator
Конфигурация Размазана по коду В одном месте
Подмена реализации Менять везде Поменял в конфиге — работает везде
Тестирование Нужны хаки Подменяешь сервис в локаторе
Lazy loading Объект создаётся сразу Объект создаётся при первом get()
Singleton Реализуешь сам Из коробки

Простой пример: нужно переключить отправку писем с SMTP на API. С new — искать и менять в 50 местах. С ServiceLocator — поменял одну строку конфигурации.

Часть 3: Регистрация сервисов

Структура модуля

Скачать полный исходный код демо-модуля можно в нашем Telegram канале. Подписывайтесь, чтобы не пропустить новые материалы!

Основной способ регистрации — декларативный, в файле .settings.php модуля:

        local/modules/mycompany.orders/
├── .settings.php              # Конфигурация сервисов
├── lib/
│   ├── Contracts/             # Интерфейсы
│   │   ├── LoggerInterface.php
│   │   ├── EmailServiceInterface.php
│   │   └── NotificationServiceInterface.php
│   └── Services/              # Реализации
│       ├── FileLogger.php
│       ├── EmailService.php
│       ├── TelegramService.php
│       └── OrderService.php

    

После подключения модуля через Loader::includeModule() все сервисы автоматически регистрируются в ServiceLocator.

Способ 1: className + constructorParams

Самый простой вариант — указываем класс и параметры конструктора:

        // .settings.php
return [
    'services' => [
        'value' => [
            'mycompany.orders.logger' => [
                'className' => \MyCompany\Orders\Services\FileLogger::class,
                'constructorParams' => [
                    $_SERVER['DOCUMENT_ROOT'] . '/local/logs/orders.log',
                ],
            ],
        ],
        'readonly' => true,
    ],
];

    

Когда использовать: для простых сервисов со скалярными параметрами (строки, числа).

Использование:

        \Bitrix\Main\Loader::includeModule('mycompany.orders');

$locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
$logger = $locator->get('mycompany.orders.logger');
$logger->log('Заказ создан', ['order_id' => 123]);

    

Способ 2: constructorParams как Closure

Когда параметры нужно вычислить динамически — например, прочитать из настроек модуля:

        'mycompany.orders.cache' => [
    'className' => BitrixCacheService::class,
    'constructorParams' => static function () {
        // Читаем директорию кеша из настроек модуля
        $cacheDir = \Bitrix\Main\Config\Option::get(
            'mycompany.orders', 
            'cache_dir', 
            '/mycompany/orders'
        );
        return [$cacheDir];
    },
],

    

Когда использовать: для параметров из Option::get(), переменных окружения, вычисляемых значений.

Важно: Closure выполняется только при первом обращении к сервису (lazy initialization). Это значит, что настройки читаются не при загрузке конфига, а когда сервис реально нужен.

Способ 3: constructor для полного контроля

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

        'mycompany.orders.telegram' => [
    'constructor' => static function () {
        $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
        
        return new TelegramService(
            $locator->get('mycompany.orders.logger'),  // Зависимость из контейнера
            \Bitrix\Main\Config\Option::get('mycompany.orders', 'telegram_token', ''),
            \Bitrix\Main\Config\Option::get('mycompany.orders', 'telegram_chat_id', '')
        );
    },
],

    

Когда использовать:

  • Зависимости + конфигурация одновременно
  • Пост-конфигурация объекта (вызов сеттеров после создания)
  • Сложная логика инициализации

Сравнение способов

Ситуация Используйте
Только скалярные параметры constructorParams (массив)
Параметры из Option/Config constructorParams (Closure)
Зависимости + конфигурация constructor (Closure)
Setter injection / инициализация constructor (Closure)

Часть 4: Autowire — автоматическое разрешение зависимостей

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

ServiceLocator умеет сам создавать объекты, анализируя их конструкторы через рефлексию. Если у сервиса все зависимости — это типизированные параметры (классы или интерфейсы), которые уже зарегистрированы или тоже можно создать автоматически, Bitrix сделает всё сам.

Пример:

        class EmailService implements EmailServiceInterface
{
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}

    

Если LoggerInterface зарегистрирован в контейнере, а EmailService нет — при запросе по имени класса он будет создан автоматически:

        // EmailService НЕ зарегистрирован явно
$emailService = $locator->get(EmailService::class);

// ServiceLocator:
// 1. Видит, что EmailService не зарегистрирован
// 2. Создаёт ReflectionClass для анализа конструктора
// 3. Находит параметр LoggerInterface $logger
// 4. Ищет LoggerInterface в контейнере — находит!
// 5. Создаёт EmailService с нужной зависимостью

    

Важное ограничение

Autowire работает только для незарегистрированных классов.

Если вы регистрируете сервис (неважно, под строковым ID или именем класса), autowire не включается. При использовании className без constructorParams Bitrix просто вызовет конструктор без аргументов — и вы получите ошибку, если параметры обязательны.

Для зарегистрированных сервисов с зависимостями используйте constructor с явной передачей параметров:

        // Сервис со скалярными параметрами — constructorParams обязательны
'mycompany.orders.logger' => [
    'className' => FileLogger::class,
    'constructorParams' => ['/path/to/log'],  // Обязательно!
],

// Сервис с зависимостями — используйте constructor
'mycompany.orders.email' => [
    'constructor' => static function () {
        $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
        return new EmailService(
            $locator->get('mycompany.orders.logger')
        );
    },
],

    

Цепочка зависимостей

ServiceLocator умный — он разрешает зависимости рекурсивно и создаёт каждый объект только один раз:

        OrderService
├── LoggerInterface      → FileLogger (создаётся)
├── EmailServiceInterface → EmailService
│   └── LoggerInterface  → FileLogger (переиспользуется!)
└── NotificationServiceInterface → TelegramService
    └── LoggerInterface  → FileLogger (переиспользуется!)

    

Граф зависимостей любой глубины разрешается автоматически, а созданные объекты кешируются (singleton).

Часть 5: Практический пример

Конфигурация модуля

Рассмотрим полноценный пример с цепочкой зависимостей.

Файл .settings.php:

        <?php
use Bitrix\Main\Config\Option;
use MyCompany\Orders\Contracts\LoggerInterface;
use MyCompany\Orders\Services\{FileLogger, EmailService, TelegramService, OrderService};

return [
    'services' => [
        'value' => [
            // 1. Логгер: простая регистрация с параметрами
            'mycompany.orders.logger' => [
                'className' => FileLogger::class,
                'constructorParams' => [
                    $_SERVER['DOCUMENT_ROOT'] . '/local/logs/orders.log',
                ],
            ],

            // 2. Email-сервис: явно указываем зависимость
            'mycompany.orders.email' => [
                'constructor' => static function () {
                    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
                    return new EmailService(
                        $locator->get('mycompany.orders.logger')
                    );
                },
            ],

            // 3. Telegram: constructor для сложной инициализации
            'mycompany.orders.telegram' => [
                'constructor' => static function () {
                    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
                    return new TelegramService(
                        $locator->get('mycompany.orders.logger'),
                        Option::get('mycompany.orders', 'telegram_token', ''),
                        Option::get('mycompany.orders', 'telegram_chat_id', '')
                    );
                },
            ],

            // 4. OrderService: собираем все зависимости
            'mycompany.orders.orderService' => [
                'constructor' => static function () {
                    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
                    return new OrderService(
                        $locator->get('mycompany.orders.logger'),
                        $locator->get('mycompany.orders.email'),
                        $locator->get('mycompany.orders.telegram')
                    );
                },
            ],

            // 5. Псевдоним интерфейса (подробнее ниже)
            LoggerInterface::class => [
                'constructor' => static fn() =>
                    \Bitrix\Main\DI\ServiceLocator::getInstance()->get('mycompany.orders.logger'),
            ],
        ],
        'readonly' => true,
    ],
];

    

Использование в компоненте

        <?php
use Bitrix\Main\Loader;

class OrdersListComponent extends \CBitrixComponent
{
    private $orderService;

    public function onPrepareComponentParams($params)
    {
        Loader::includeModule('mycompany.orders');
        
        $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
        $this->orderService = $locator->get('mycompany.orders.orderService');

        return parent::onPrepareComponentParams($params);
    }

    public function executeComponent()
    {
        $userId = $this->arParams['USER_ID'] ?: $GLOBALS['USER']->GetID();
        $this->arResult['ORDERS'] = $this->orderService->getUserOrders($userId);
        
        $this->includeComponentTemplate();
    }
}

    

Что происходит:

  1. Подключаем модуль — сервисы регистрируются автоматически
  2. Получаем orderService — ServiceLocator создаёт его со всеми зависимостями
  3. Используем сервис — вся бизнес-логика инкапсулирована

Использование в агентах

        class OrderCleanupAgent
{
    public static function run(): string
    {
        \Bitrix\Main\Loader::includeModule('mycompany.orders');
        
        $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
        $orderService = $locator->get('mycompany.orders.orderService');
        
        $deleted = $orderService->cleanupOldOrders();
        
        return static::class . '::run();';
    }
}

    

Использование в обработчиках событий

        $eventManager = \Bitrix\Main\EventManager::getInstance();

$eventManager->addEventHandler('sale', 'OnSaleOrderSaved', function($event) {
    \Bitrix\Main\Loader::includeModule('mycompany.orders');
    
    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
    $telegram = $locator->get('mycompany.orders.telegram');
    
    $order = $event->getParameter('ENTITY');
    $telegram->notify("Заказ #{$order->getId()} сохранён");
});

    

Часть 6: Лайфхак — псевдонимы интерфейсов

Когда у вас много сервисов, зависящих от одного интерфейса, полезно зарегистрировать его как псевдоним:

        // 1. Регистрируем реализацию под строковым ID
'mycompany.orders.logger' => [
    'className' => FileLogger::class,
    'constructorParams' => ['/path/to/log'],
],

// 2. Регистрируем интерфейс как псевдоним
LoggerInterface::class => [
    'constructor' => static fn() =>
        \Bitrix\Main\DI\ServiceLocator::getInstance()->get('mycompany.orders.logger'),
],

    

Что это даёт:

  1. Можно запросить логгер по интерфейсу:
        $logger = $locator->get(LoggerInterface::class);

    
  1. Autowire будет находить реализацию автоматически для сервисов с такой зависимостью в конструкторе.

  2. Единая точка подмены — если завтра захотите заменить FileLogger на SentryLogger, меняете только одну строчку.

Когда использовать: когда много сервисов зависят от одного интерфейса и вы хотите упростить конфигурацию.

Часть 7: Динамическая регистрация

Иногда нужно зарегистрировать сервис программно — например, подменить реализацию в зависимости от условий.

Регистрация готового экземпляра

        $serviceLocator = \Bitrix\Main\DI\ServiceLocator::getInstance();

// Для тестов — NullLogger
if ($_ENV['APP_ENV'] === 'testing') {
    $serviceLocator->addInstance('mycompany.orders.logger', new NullLogger());
}

    

Ленивая регистрация

        $serviceLocator->addInstanceLazy('mycompany.orders.customCache', [
    'className' => CustomCacheService::class,
    'constructorParams' => ['cache_prefix_' . SITE_ID, 3600],
]);

    

Подмена в зависимости от настроек (когда невозможно настройки запросить в .settings.php)

        // В init.php
$loggerType = Option::get('mycompany.orders', 'logger_type', 'file');

switch ($loggerType) {
    case 'file':
        $logger = new FileLogger($_SERVER['DOCUMENT_ROOT'] . '/local/logs/orders.log');
        break;
    case 'syslog':
        $logger = new SyslogLogger('mycompany.orders');
        break;
    default:
        $logger = new NullLogger();
}

$serviceLocator->addInstance('mycompany.orders.logger', $logger);

    

Часть 8: Реальный кейс — рефакторинг legacy-кода

Давайте возьмём реальную ситуацию и покажем, как применить ServiceLocator для рефакторинга.

Было: типичный legacy-код

        // local/components/mystore/order.create/component.php
if ($_POST['action'] === 'create_order') {
    // Создаём заказ напрямую
    $order = \Bitrix\Sale\Order::create(SITE_ID, $_POST['user_id']);
    // ... добавление товаров ...
    $order->save();
    $orderId = $order->getId();
    
    // Логируем прямо в файл
    file_put_contents(
        $_SERVER['DOCUMENT_ROOT'] . '/local/logs/orders.log',
        date('Y-m-d H:i:s') . " Order $orderId created\n",
        FILE_APPEND
    );
    
    // Отправляем email напрямую
    mail($_POST['email'], 'Заказ оформлен', "Ваш заказ №{$orderId} принят");
    
    // Telegram с захардкоженным токеном
    $token = 'hardcoded_token_123456';
    $chatId = '123456789';
    file_get_contents("https://api.telegram.org/bot{$token}/sendMessage?" . 
        http_build_query(['chat_id' => $chatId, 'text' => "Новый заказ №{$orderId}"])
    );
    
    echo json_encode(['success' => true, 'order_id' => $orderId]);
}

    

Проблемы:

  • Невозможно протестировать — реально отправляет письма и сообщения
  • Жёстко зашитые зависимости — путь к логу, токен Telegram
  • Дублирование — если нужно создать заказ в другом месте, код копируется
  • Нарушение Single Responsibility — компонент делает всё сам

Стало: чистая архитектура с ServiceLocator

Шаг 1: Создаём интерфейсы

        // lib/Contracts/LoggerInterface.php
interface LoggerInterface
{
    public function log(string $message, array $context = []): void;
}

// lib/Contracts/EmailServiceInterface.php
interface EmailServiceInterface
{
    public function send(string $to, string $subject, string $body): bool;
}

// lib/Contracts/NotificationServiceInterface.php
interface NotificationServiceInterface
{
    public function notify(string $message): bool;
}

    

Шаг 2: Создаём сервис с явными зависимостями

        // lib/Services/OrderService.php
class OrderService
{
    public function __construct(
        private LoggerInterface $logger,
        private EmailServiceInterface $emailService,
        private NotificationServiceInterface $notificationService
    ) {}
    
    public function createOrder(array $data): int
    {
        $this->logger->log('Creating order', ['email' => $data['email']]);
        
        // Создаём заказ
        $order = Order::create(SITE_ID, $data['user_id']);
        // ... логика ...
        $order->save();
        $orderId = $order->getId();
        
        $this->logger->log('Order created', ['order_id' => $orderId]);
        
        // Отправляем email
        $this->emailService->send(
            $data['email'],
            'Заказ оформлен',
            "Ваш заказ №{$orderId} принят"
        );
        
        // Уведомляем
        $this->notificationService->notify("Новый заказ №{$orderId}");
        
        return $orderId;
    }
}

    

Шаг 3: Регистрируем в .settings.php

        return [
    'services' => [
        'value' => [
            'mycompany.orders.logger' => [
                'className' => FileLogger::class,
                'constructorParams' => [$_SERVER['DOCUMENT_ROOT'] . '/local/logs/orders.log'],
            ],
            
            'mycompany.orders.email' => [
                'constructor' => static function () {
                    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
                    return new EmailService(
                        $locator->get('mycompany.orders.logger')
                    );
                },
            ],
            
            'mycompany.orders.telegram' => [
                'constructor' => static function () {
                    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
                    return new TelegramService(
                        $locator->get('mycompany.orders.logger'),
                        Option::get('mycompany.orders', 'telegram_token', ''),
                        Option::get('mycompany.orders', 'telegram_chat_id', '')
                    );
                },
            ],
            
            'mycompany.orders.orderService' => [
                'constructor' => static function () {
                    $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
                    return new OrderService(
                        $locator->get('mycompany.orders.logger'),
                        $locator->get('mycompany.orders.email'),
                        $locator->get('mycompany.orders.telegram')
                    );
                },
            ],
        ],
        'readonly' => true,
    ],
];

    

Шаг 4: Используем в компоненте

        // local/components/mystore/order.create/class.php
class OrderCreateComponent extends \CBitrixComponent
{
    public function executeComponent()
    {
        Loader::includeModule('mycompany.orders');
        
        $locator = \Bitrix\Main\DI\ServiceLocator::getInstance();
        $orderService = $locator->get('mycompany.orders.orderService');
        
        if ($this->request->isPost() && $this->request->get('action') === 'create_order') {
            try {
                $orderId = $orderService->createOrder($this->request->getPostList()->toArray());
                $this->arResult['SUCCESS'] = true;
                $this->arResult['ORDER_ID'] = $orderId;
            } catch (\Exception $e) {
                $this->arResult['ERROR'] = $e->getMessage();
            }
        }
        
        $this->includeComponentTemplate();
    }
}

    

Что мы получили

Было Стало
Жёстко зашитые зависимости Конфигурация в одном месте
Невозможно тестировать Легко подменить зависимости моками
Дублирование кода OrderService переиспользуется везде
Скрытые зависимости Всё явно в конструкторе
Токены в коде Настройки в Option

Тестируемость:

        // В тестах
$mockLogger = $this->createMock(LoggerInterface::class);
$mockEmail = $this->createMock(EmailServiceInterface::class);
$mockTelegram = $this->createMock(NotificationServiceInterface::class);

$orderService = new OrderService($mockLogger, $mockEmail, $mockTelegram);
// Тестируем без реальной отправки писем!

    

Часть 9: Антипаттерны

1. Получение зависимостей в каждом методе

        // ❌ Плохо — дублирование, скрытые зависимости
class UserService
{
    public function createUser(array $data): int
    {
        $logger = ServiceLocator::getInstance()->get('logger');
        $validator = ServiceLocator::getInstance()->get('validator');
        // ...
    }
    
    public function updateUser(int $id, array $data): void
    {
        $logger = ServiceLocator::getInstance()->get('logger');
        // Те же зависимости снова!
    }
}

    

Почему плохо:

  • Дублирование кода — в каждом методе одно и то же
  • Скрытые зависимости — нужно читать каждый метод
  • Сложно тестировать — нет единой точки для подмены
        // ✅ Хорошо — зависимости в конструкторе
class UserService
{
    public function __construct(
        private LoggerInterface $logger,
        private ValidatorInterface $validator
    ) {}
    
    public function createUser(array $data): int
    {
        $this->logger->log('Creating user');
        // ...
    }
}

    

Правило: Зависимости получаются из ServiceLocator в одном месте — при создании объекта (в .settings.php или точке входа).

2. Зависимость от конкретных классов

        // ❌ Плохо — жёсткая связанность
public function __construct(FileLogger $logger) {}

// ✅ Хорошо — зависимость от абстракции
public function __construct(LoggerInterface $logger) {}

    

Почему плохо:

  • Нельзя подменить реализацию без изменения кода
  • Нарушение принципа инверсии зависимостей (DIP)
  • Сложно тестировать

3. Изменяемое состояние в сервисах

ServiceLocator создаёт синглтоны — один экземпляр на весь запрос. Поэтому сервисы должны быть stateless:

        // ❌ Плохо — состояние может протечь
class UserService
{
    private ?int $currentUserId = null;
    
    public function setCurrentUser(int $id): void
    {
        $this->currentUserId = $id;
    }
    
    public function getCurrentUserOrders(): array
    {
        return $this->findOrders($this->currentUserId);
    }
}

    

Баг в действии:

        $userService = $locator->get(UserService::class);
$userService->setCurrentUser(1);
$orders1 = $userService->getCurrentUserOrders(); // Заказы пользователя 1

// Где-то в другом месте (это ТОТ ЖЕ объект!)
$userService->setCurrentUser(2);

// Обратно в первом месте
$orders1Again = $userService->getCurrentUserOrders(); 
// БАГ! Вернёт заказы пользователя 2!

    
        // ✅ Хорошо — параметры передаются явно
class UserService
{
    public function getUserOrders(int $userId): array
    {
        return $this->findOrders($userId);
    }
}

    

Правило: Сервисы должны быть stateless. Передавайте данные через параметры методов.

4. Ожидание разных экземпляров

        $client1 = $locator->get('api.client');
$client1->setBaseUrl('https://api.service1.com');

$client2 = $locator->get('api.client');
$client2->setBaseUrl('https://api.service2.com');

// СЮРПРИЗ! $client1 === $client2 — это один объект!
echo $client1->getBaseUrl(); // 'https://api.service2.com'

    

Решение — фабрика:

        class ApiClientFactory
{
    public function __construct(private LoggerInterface $logger) {}
    
    public function create(string $apiKey, string $baseUrl): ApiClient
    {
        return new ApiClient($apiKey, $baseUrl, $this->logger);
    }
}

// Использование
$factory = $locator->get('mycompany.orders.apiClientFactory');
$client1 = $factory->create('key1', 'https://api.service1.com');
$client2 = $factory->create('key2', 'https://api.service2.com');
// Теперь это РАЗНЫЕ объекты!

    

Часть 10: Защита от циклических зависимостей

ServiceLocator автоматически обнаруживает циклы и не даст вам выстрелить себе в ногу:

        class OrderService
{
    public function __construct(InvoiceService $invoiceService) {}
}

class InvoiceService
{
    public function __construct(OrderService $orderService) {} // Цикл!
}

    

При попытке получить любой из этих сервисов:

        try {
    $orderService = $locator->get(OrderService::class);
} catch (\Bitrix\Main\DI\Exception\CircularDependencyException $e) {
    // "Cyclic dependency detected for service: 
    //  OrderService -> InvoiceService -> OrderService"
}

    

Как разорвать цикл:

  1. Через события — один сервис отправляет событие, другой обрабатывает
  2. Выделить общую логику — в третий сервис, от которого зависят оба
  3. Пересмотреть архитектуру — возможно, сервисы слишком связаны

Резюме

Способы регистрации

Что делать Как
Простой сервис с параметрами className + constructorParams
Динамические параметры constructorParams как Closure
Зависимости + настройки constructor как Closure
Поддержка autowire Регистрируйте псевдоним интерфейса

Правила хорошего тона

  1. Зависимости в конструкторе — не в методах
  2. Интерфейсы вместо классов — для гибкости и тестируемости
  3. Stateless сервисы — не храните изменяемое состояние
  4. ServiceLocator в точках входа — компоненты, контроллеры, агенты
  5. Фабрики для множественных экземпляров — когда нужны разные объекты

Что даёт ServiceLocator

  • ✅ Централизованная конфигурация
  • ✅ Тестируемость — легко подменить зависимости
  • ✅ Lazy loading — объекты создаются по требованию
  • ✅ Singleton из коробки
  • ✅ Автоматическое разрешение зависимостей

Начните с малого: возьмите один класс из вашего проекта, выделите его зависимости в интерфейсы, зарегистрируйте в ServiceLocator. Почувствуйте, как код становится чище.

Скачать полный исходный код демо-модуля можно в нашем Telegram канале. Подписывайтесь, чтобы не пропустить новые материалы!

Опубликовано 6 часов назад

Похожие статьи