В старых модулях PrestaShop бизнес-логика нередко находится прямо в контроллере или даже в основном классе модуля:
$api = new ExternalApiClient();
$result = $api->send($data);
Пока модуль небольшой, это работает. Но затем появляется импорт, API, cron, CLI, административная страница — и один и тот же код начинает создаваться в разных местах.
В современном PrestaShop для таких задач используется Dependency Injection (DI) и Symfony Service Container.
Что такое сервис в PrestaShop
Сервис — обычный PHP-класс, выполняющий конкретную задачу.
Например:
namespace Ewonta\MyModule\Service;
final class ProductSynchronizer
{
public function synchronize(int $productId): void
{
// Синхронизация товара
}
}
Вместо постоянного:
$synchronizer = new ProductSynchronizer();
класс регистрируется в Symfony-контейнере, а PrestaShop передаёт его туда, где он нужен.
Это позволяет отдельно организовать API-клиенты, импорт, расчёты, работу с каталогом и другую бизнес-логику.
Что изменилось в PrestaShop 9.2
Это важный момент для разработчиков новых модулей.
Начиная с PrestaShop 9.2 для конфигурации сервисов можно использовать не только привычный:
config/services.yml
но и PHP-конфигурацию:
config/services.php
Кроме того, поддерживаются файлы для конкретных версий:
services-9.2.yml
services-9.yml
services.yml
PrestaShop выбирает первый подходящий файл в следующем порядке:
services.php
services-9.2.yml
services-9.yml
services.yml
Это позволяет модулю иметь разные определения сервисов для разных поколений PrestaShop без большого количества проверок версии внутри PHP-классов. Эта схема официально поддерживается начиная с PrestaShop 9.2.
Пример services.php
Современная конфигурация может выглядеть так:
<?php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return static function (
ContainerConfigurator $container
): void {
$services = $container->services();
$services
->defaults()
->autowire()
->autoconfigure();
$services->load(
'Ewonta\\MyModule\\',
'../src/*'
);
};
Теперь Symfony может автоматически определять зависимости классов.
Например:
final class ProductImporter
{
public function __construct(
private ProductRepository $repository,
private ExternalApiClient $apiClient
) {
}
}
Разработчику не нужно вручную создавать ProductRepository и ExternalApiClient.
Контейнер делает это сам.
Почему это лучше new Service()
Главное преимущество DI проявляется не в количестве строк кода.
Представим, что один ProductSynchronizer используется одновременно:
Back Office
CLI
Cron
API
Если в каждом месте писать:
new ProductSynchronizer(...);
каждая точка входа должна знать все его зависимости.
После добавления Logger или API-клиента придётся изменять несколько частей модуля.
При Dependency Injection зависимость описывается один раз:
public function __construct(
ProductSynchronizer $synchronizer
) {
$this->synchronizer = $synchronizer;
}
Остальное берёт на себя контейнер.
Современные контроллеры PrestaShop 9
В PrestaShop 9 произошёл ещё один важный архитектурный сдвиг: современные Symfony-контроллеры сами являются сервисами.
Поэтому старый подход:
$this->get('my.service');
уже не является правильной основой для новых контроллеров.
В современных контроллерах зависимости рекомендуется получать через constructor injection или method injection. FrameworkBundleAdminController пока сохраняет часть старого поведения ради совместимости, но он уже помечен как устаревший и должен быть удалён в PrestaShop 10.
Для нового кода лучше сразу строить зависимости явно:
final class ImportController extends PrestaShopAdminController
{
public function importAction(
ProductImporter $importer
): Response {
$importer->run();
// ...
}
}
Так сразу видно, какие сервисы нужны контроллеру.
Back Office и Front Office — не одно и то же
Здесь есть важная особенность PrestaShop.
На современных страницах Back Office доступен полноценный Symfony-контейнер.
Front Office всё ещё может работать в legacy-контексте, где набор доступных сервисов отличается.
Поэтому сервис, зарегистрированный только в:
config/services.php
не следует автоматически считать доступным из любого фронтового hook.
Для Front Office PrestaShop поддерживает отдельную конфигурацию:
config/front/
Аналогично существуют отдельные области:
config/admin/
config/front/
config/webservice/
Это позволяет загружать только те зависимости, которые действительно нужны конкретной части магазина.
Как выглядит структура современного модуля
Для достаточно крупного модуля структура может быть такой:
mymodule/
├── config/
│ ├── services.php
│ ├── admin/
│ └── front/
├── src/
│ ├── Controller/
│ ├── Service/
│ ├── Repository/
│ ├── Command/
│ └── CommandHandler/
└── mymodule.php
Основной класс модуля при этом перестаёт быть местом, куда складывается вся логика.
mymodule.php отвечает прежде всего за жизненный цикл и интеграцию модуля с PrestaShop, а бизнес-операции распределяются по отдельным классам.
Не превращайте Service в новый огромный класс
Dependency Injection сам по себе не делает архитектуру хорошей.
Можно создать:
MyModuleService.php
на 4000 строк и зарегистрировать его в контейнере.
Технически это будет Symfony-сервис, но проблема никуда не исчезнет.
Лучше разделять ответственность:
ProductImporter
ProductValidator
ImageImporter
StockSynchronizer
MarketplaceApiClient
ImportLogger
Каждый сервис должен иметь понятную задачу.
Тогда его можно независимо использовать из контроллера, CLI-команды, cron или другого сервиса.
Dependency Injection в современном PrestaShop — уже не просто дополнительная возможность Symfony.
В PrestaShop 9 архитектура контроллеров стала ещё сильнее связана с сервисами и явным внедрением зависимостей, а PrestaShop 9.2 расширил возможности конфигурации модулей через services.php и версионные определения сервисов.
Для небольшого модуля не нужно создавать десятки сервисов ради архитектуры.
Но если модуль содержит Back Office, API, импорт, cron, CLI или сложную бизнес-логику, вынесение этой логики в сервисы делает код заметно проще для развития и поддержки.
Официальная документация: Services — PrestaShop Developer Documentation