In older PrestaShop modules, business logic was often placed directly inside a controller or even in the main module class.
A typical example might look like this:
$api = new ExternalApiClient();
$result = $api->send($data);
This is manageable while the module is small.
But once you add product imports, external APIs, cron jobs, CLI commands and several Back Office pages, the same dependencies start appearing in multiple places.
Modern PrestaShop development relies increasingly on Dependency Injection (DI) and the Symfony Service Container to solve this problem.
What is a service in PrestaShop?
A service is simply a PHP class responsible for a specific task.
For example:
namespace Ewonta\MyModule\Service;
final class ProductSynchronizer
{
public function synchronize(int $productId): void
{
// Synchronize product data
}
}
Instead of manually creating the class everywhere:
$synchronizer = new ProductSynchronizer();
the class can be registered in the Symfony container.
PrestaShop then provides the service wherever it is required.
This approach is especially useful for:
- API clients;
- product imports;
- synchronisation services;
- price calculations;
- repositories;
- catalogue processing;
- logging;
- external integrations.
What changed in PrestaShop 9.2?
PrestaShop 9.2 introduces an important improvement for module developers.
In addition to the traditional:
config/services.yml
modules can use PHP-based service configuration:
config/services.php
Version-specific configuration files can also be used:
services-9.2.yml
services-9.yml
services.yml
This makes it easier to maintain a module across different PrestaShop versions without filling application code with version checks.
A module can keep service definitions adapted to specific platform generations while keeping the actual PHP classes cleaner.
Example of services.php
A modern service configuration can look like this:
<?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/*'
);
};
With autowiring enabled, Symfony can resolve dependencies automatically.
For example:
final class ProductImporter
{
public function __construct(
private ProductRepository $repository,
private ExternalApiClient $apiClient
) {
}
}
You no longer need to manually instantiate ProductRepository or ExternalApiClient.
The service container resolves and injects them.
Why is this better than using new Service()?
The real benefit becomes visible when the same business logic is used from several entry points.
Imagine that ProductSynchronizer is required by:
Back Office
CLI
Cron
API
If every entry point creates the service manually, each one must also understand all of its dependencies.
For example:
new ProductSynchronizer(
$repository,
$apiClient,
$logger,
$validator
);
If another dependency is added later, several parts of the module may need to be modified.
With Dependency Injection, the dependency is declared once:
public function __construct(
ProductSynchronizer $synchronizer
) {
$this->synchronizer = $synchronizer;
}
The container handles the rest.
This reduces duplication and keeps the code easier to maintain.
Modern controllers in PrestaShop 9
PrestaShop 9 also continues the transition towards modern Symfony-based Back Office controllers.
Instead of relying on service lookups such as:
$this->get('my.service');
new code should prefer explicit dependency injection.
For example:
final class ImportController extends PrestaShopAdminController
{
public function importAction(
ProductImporter $importer
): Response {
$importer->run();
// ...
}
}
This has an important advantage: the dependencies of the controller are immediately visible.
A developer reading the code can quickly understand which services the controller requires.
Back Office and Front Office are different environments
This is an important detail in PrestaShop module development.
The modern Back Office is tightly integrated with Symfony and its service container.
The Front Office can still rely on legacy execution paths, depending on the context.
Because of this, a service registered in one configuration should not automatically be assumed to be available everywhere.
PrestaShop supports dedicated service configuration areas such as:
config/admin/
config/front/
config/webservice/
This allows a module to load only the services required in a particular application context.
It also avoids unnecessarily loading Back Office dependencies on every Front Office request.
Recommended structure for a modern PrestaShop module
A larger module can use a structure similar to this:
mymodule/
├── config/
│ ├── services.php
│ ├── admin/
│ └── front/
├── src/
│ ├── Controller/
│ ├── Service/
│ ├── Repository/
│ ├── Command/
│ └── CommandHandler/
└── mymodule.php
The main module file should not become the place where every business operation is implemented.
Instead, responsibilities can be distributed across dedicated classes.
For example:
ProductImporter
ProductValidator
StockSynchronizer
MarketplaceApiClient
ImportLogger
This makes the module easier to extend and test.
Do not create one huge service
Dependency Injection does not automatically create a good architecture.
It is still possible to create something like:
MyModuleService.php
with several thousand lines of code.
Technically, it is a Symfony service.
Architecturally, it is still a monolith.
A better approach is to separate responsibilities.
For example:
ProductImporter
ImageImporter
StockSynchronizer
PriceCalculator
ProductValidator
ExternalApiClient
Each service should have a clear purpose.
Then the same service can be reused by a controller, CLI command, cron task or another service.
Dependency Injection and CQRS work well together
Dependency Injection also fits naturally with CQRS-based module architecture.
For example:
Controller
↓
Command
↓
CommandHandler
↓
ProductSynchronizer
↓
Repository / API
The Handler does not need to manually build every dependency.
They can be injected through the constructor:
final class UpdateProductHandler
{
public function __construct(
private ProductSynchronizer $synchronizer
) {
}
public function handle(
UpdateProductCommand $command
): void {
$this->synchronizer->synchronize(
$command->getProductId()
);
}
}
This keeps Commands, Handlers and application services clearly separated.
When should a PrestaShop module use Dependency Injection?
A very small module does not need dozens of services.
If the module simply displays a block through a hook, a complex service architecture would probably be unnecessary.
Dependency Injection becomes much more useful when a module contains:
- several Back Office pages;
- Symfony controllers;
- external API integrations;
- product or order imports;
- marketplace synchronisation;
- cron jobs;
- CLI commands;
- complex catalogue processing;
- shared business logic.
The larger the module becomes, the more valuable explicit dependencies and reusable services become.
Dependency Injection is now an important part of modern PrestaShop module development.
PrestaShop 9 continues moving its Back Office towards Symfony-based architecture, while PrestaShop 9.2 gives module developers more flexibility in the way services are configured.
The main idea is simple:
Do not create dependencies everywhere manually.
Register services once.
Inject them where they are required.
For small modules, there is no reason to over-engineer the codebase.
But for modules involving imports, APIs, Back Office interfaces, CLI commands, cron jobs or complex business logic, a proper service architecture can significantly improve maintainability.
It also makes a module easier to extend across future PrestaShop versions.