You have the ability to modify the Symfony container configuration from a module. This means that
First we strongly advise you to use namespaces & autoloading in your module, which can be done thanks to composer. A dedicated chapter is available to learn more about Composer and to set it up.
At first you will need to create a class for your service of course:
<?php
// modules/yourmodule/src/YourService.php
namespace YourCompany\YourModule;
use Symfony\Component\Translation\TranslatorInterface;
class YourService {
/** @var TranslatorInterface */
private $translator;
/** @var string */
private $customMessage;
/**
* @param string $customMessage
*/
public function __construct(
TranslatorInterface $translator,
$customMessage
) {
$this->translator = $translator;
$this->customMessage = $customMessage;
}
/**
* @return string
*/
public function getTranslatedCustomMessage() {
return $this->translator->trans($this->customMessage, [], 'Modules.YourModule');
}
}
Now that your namespace is setup, you can define your services in the config/services.yml
file of your module.
# yourmodule/config/services.yml
services:
_defaults:
public: true
your_company.your_module.your_service:
class: YourCompany\YourModule\YourService
arguments:
- "@translator"
- "My custom message"
# yourmodule/config/services.yml
imports:
- { resource: services.php }
<?php
// yourmodule/config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $configurator) {
};
This will then allow you to get your service from the Symfony container, like in your modern controllers:
<?php
// modules/yourmodule/src/Controller/DemoController.php
namespace YourCompany\YourModule\Controller;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
class DemoController extends FrameworkBundleAdminController
{
public function demoAction()
{
$yourService = $this->get('your_company.your_module.your_service');
return $this->render('@Modules/yourmodule/templates/admin/demo.html.twig', [
'customMessage' => $yourService->getTranslatedCustomMessage(),
]);
}
}
When adding resources with a wildcard to your module namespace, make sure to exclude all index.php
files (this file is used for security; it is redirecting to FO to prevent reading from the directory). If you don’t do it, you might be redirected whenever you open your shop.
your_company.your_module:
resource: '../src/*'
exclude:
- '../src/index.php'
- '../src/*/index.php'
The container definition can be modified by a module, which enables you to override an existing Symfony service being used in PrestaShop.
This is a mechanism similar to PrestaShop standard overrides, but the main benefit is that the php code stays unmodified. This prevents issues linked to code definition or autoloading failures.
As you can read it from the Symfony documentation, there are 2 ways to modify an existing service:
When you choose to override a service, this means that you replace the service by another one. The previous service is not usable anymore. Every other part of the code where this service is used will use the new version.
To do it: you declare your new service using the old service name. So if you want to override the service prestashop.core.b2b.b2b_feature
with your own implementation, you write in config/services.yml
:
prestashop.core.b2b.b2b_feature:
class: 'YourCompany\YourModule\YourService'
That’s done. The service registered under the name prestashop.core.b2b.b2b_feature
is now your service. The previous prestashop.core.b2b.b2b_feature
is gone.
When you choose to decorate a service, this means that you make everybody use your service but you keep the old service available. The previous service has been given a new name and can still be used. Every other part of the code where this service was used will use the new version.
To do it: you declare your new service using the ‘decorates’ keyword. So if you want to decorates the service prestashop.core.b2b.b2b_feature
with my own implementation, you write in config/services.yml
:
mymodule.my_own_b2b_feature_service:
class: 'YourCompany\YourModule\YourService'
decorates: 'prestashop.core.b2b.b2b_feature'
That’s done. The service registered under the name mymodule.my_own_b2b_feature_service
is now your service. The previous prestashop.core.b2b.b2b_feature
implementation is still available under the name mymodule.my_own_b2b_feature_service.inner
.
This means that in your container you can access 3 services now:
mymodule.my_own_b2b_feature_service
your serviceprestashop.core.b2b.b2b_feature
is now an alias for mymodule.my_own_b2b_feature_service
(see service aliases) so the other services which rely on it now use your implementationmymodule.my_own_b2b_feature_service.inner
is the previous implementation, still availableThe decoration strategy can be very useful if:
Indeed sometimes what you want is to modify a small part of the behavior of a class. So why replace it entirely ? You can reuse the existing behavior and modify only the needed part:
<?php
// modules/yourmodule/src/YourService.php
namespace YourCompany\YourModule;
class YourService {
private $decoratedService;
/**
* @param DecoratedService $decoratedService
*/
public function __construct($decoratedService)
{
$this->decoratedService = $decoratedService;
}
/**
* We want to modify the behavior of the function getTranslatedCustomMessage
* without replacing the whole DecoratedService implementation
*
* @return string
*/
public function getTranslatedCustomMessage() {
$unmodifiedOutput = $this->decoratedService->getTranslatedCustomMessage();
$modifiedOutput = $this->modifyTheOutput($unmodifiedOutput);
return $modifiedOutput;
}
}
This is only possible with service decoration, not service override, because the previous service is still available.
The Symfony command php ./bin/console debug:container
will provide you with a list of all the registered services.
What happens, however, if the service you have overriden or decorated is used somewhere else ? You have to make sure your modifications are still compatible with this place in order not to break any existing behavior.
Even worse: what if another part of the code especially requires this class, like this:
<?php
/**
* @param ASpecificClass $service
*/
public function __construct(ASpecificClass $service)
{
// ...
}
Here, this constructor will crash if you provide something else than an instance of ASpecificClass
to it.
In order to avoid this crash, 2 options are available:
PrestaShop classes rely more and more on interfaces. So if this code has been built with the idea of customization/extension in mind, instead of public function __construct(ASpecificClass $service)
you should have:
<?php
/**
* @param MyInterface $service
*/
public function __construct(MyInterface $service)
{
// ...
}
Your new service, which overrides or decorates the previous service, only needs to implement
the interface to be compatible with it.
If however no interface was used here, you probably need to extend
the previous class, ASpecificClass
, instead.
As you can see, interfaces lay the ground for easy extension and customization, that is why we use them more and more in the Core codebase and we recommend you use them as well !
Since 8.1 , modules autoloaders and service configurations loading are now registered before compiler passes. That means that you can now use native Symfony service configuration features in your modules.
Those features are:
As an example, let’s consider a module with the following structure:
config/
services.yml
src/
Collection/
Collection.php
Element.php
ElementInterface.php
And this content:
File: src/Collection/Collection.php
<?php
namespace TestModule\InstanceofConditionals\Collection;
class Collection
{
private $elements = [];
public function __construct(iterable $elements)
{
foreach ($elements as $element) {
$this->addElement($element);
}
}
File: src/Collection/Element.php
<?php
namespace TestModule\InstanceofConditionals\Collection;
interface ElementInterface
{
}
File: src/Collection/ElementInterface.php
<?php
namespace TestModule\InstanceofConditionals\Collection;
class Element implements ElementInterface
{
}
In your module’s config/common.yml
, set the following configuration:
services:
_defaults:
autowire: true
bind:
$elements: !tagged test_module.instance_of.instance_of_tagged
_instanceof:
TestModule\InstanceofConditionals\Collection\ElementInterface:
tags: [ test_module.instance_of.instance_of_tagged ]
This example will tag all classes instances of TestModule\InstanceofConditionals\Collection\ElementInterface
(TestModule\InstanceofConditionals\Collection\Element
in our example) with the tag test_module.instance_of.instance_of_tagged
.
Then, it will bind all services with a $element
variable in its constructor with a test_module.instance_of.instance_of_tagged
service.
In your module’s config/common.yml
, set the following configuration:
services:
_defaults:
autowire: true
bind:
$elements: !tagged test_module.instance_of.manually_tagged
TestModule\InstanceofConditionals\Collection\Element:
class: TestModule\InstanceofConditionals\Collection\Element
tags: [ test_module.instance_of.manually_tagged ]
This example will tag the class TestModule\InstanceofConditionals\Collection\Element
with the tag test_module.instance_of.manually_tagged
.
Then, it will bind all services with a $element
variable in its constructor with a test_module.instance_of.manually_tagged
service.
If we wanted to bind only parameter $element
of class TestModule\InstanceofConditionals\Collection\Collection
with a test_module.instance_of.manually_tagged
tag, we would had configured it this way:
services:
test_module.instance_of.manually_tagged_collection:
class: TestModule\InstanceofConditionals\Collection\Collection
public: true
bind:
$elements: !tagged test_module.instance_of.manually_tagged
TestModule\InstanceofConditionals\Collection\Element:
class: TestModule\InstanceofConditionals\Collection\Element
tags: [ test_module.instance_of.manually_tagged ]
Explore more configuration features in the official Symfony documentation.
Being able to declare services for Symfony environment is a nice feature when you use modern controllers, however when you are on front office or in a legacy page in the back office (meaning a page that has not been migrated yet with Symfony) you can’t access the Symfony container or your services.
Since the version 1.7.6 you can now define your services and access them in the legacy environment. We manage a light
container for this environment (PrestaShop\PrestaShop\Adapter\ContainerBuilder
) which is accessible from legacy containers.
To define your services you need to follow the same principle as Symfony services, but this time you need to place your definition files in sub folders:
config/admin/services.yml
will define the services accessible in the back office (in legacy environment AND Symfony environment)config/front/services.yml
will define the services accessible in the front officeDo not use named arguments for front services definition
For more information read the dedicated section in Naming Conventions.
You can then access your services from any legacy controllers (in which the container is automatically injected):
<?php
// modules/yourmodule/controllers/front/Demo.php
class YourModuleDemoModuleFrontController extends ModuleFrontController {
public function display()
{
...
$yourService = $this->get('your_company.your_module.front.your_service');
...
}
}
<?php
// modules/yourmodule/controllers/admin/demo.php
// Legacy controllers have no namespace
class YourModuleDemoModuleAdminController extends ModuleAdminController {
public function display()
{
...
$yourService = $this->get('your_company.your_module.admin.your_service');
...
}
}
But you can also access them from your module, to display its content or in hooks:
<?php
// modules/yourmodule/yourmodule.php
class yourmodule {
public function getContent()
{
...
// The controller here is the ADMIN one so only admin services are accessible
$yourService = $this->get('your_company.your_module.admin.your_service');
...
}
public function hookDisplayFooterProduct($params)
{
...
// The controller here is the FRONT one so only front services are accessible
$yourService = $this->get('your_company.your_module.front.your_service');
...
}
}
Keep in mind that the legacy container is a light version of the full Symfony container so you won’t have access to all the Symfony components. But you will be able to use the Doctrine service as well as a few core services from PrestaShop.
For more details about available services you can check in <PS_ROOT_DIR>/config/services/
folder which services are available
in admin or front. Be careful and always keep in mind in which context/environment you are calling your service.
Here is a quick summary so that you know where you should define your services:
Definition file | Symfony Container | Front Legacy Container | Admin Legacy Container | Available services |
---|---|---|---|---|
config/services.yml | Yes | No | No | All symfony components and PrestaShopBundle services |
config/admin/services.yml | Yes | No | Yes | Doctrine, services defined in <PS_ROOT_DIR>/config/services/admin folder |
config/front/services.yml | Yes | Yes | No | Doctrine, services defined in <PS_ROOT_DIR>/config/services/front folder |
Sometimes services are only useful in a particular context (back-office or front-office), but sometime you also need them on both (a Doctrine repository is a good example). You could easily define the same services in both environment but it’s very modular and can create errors in case of modifications.
An easy trick is to create a common definition file which will then be included by each environment:
# yourmodule/config/common.yml
services:
_defaults:
public: true
your_company.your_module.common.open_service:
class: YourCompany\YourModule\YourService
arguments:
- '@your_company.your_module.common.open_dependency'
your_company.your_module.common.open_dependency:
class: YourCompany\YourModule\YourServiceDependency
Then you can include this file in the environment you wish (front, admin, Symfony);
# yourmodule/config/services.yml
imports:
- { resource: ./common.yml }
# yourmodule/config/admin/services.yml
imports:
- { resource: ../common.yml }
# yourmodule/config/front/services.yml
imports:
- { resource: ../common.yml }