Services

Symfony Services

You have the ability to modify the Symfony container configuration from a module. This means that

  • You have the possibility to define your own Symfony services from your modules.
  • You have the possibility to modify existing Symfony services declaration from your modules, which is somehow an Override mechanism

Create and declare a new Symfony service

First we strongly advise you to use namespaces in your module, this can be done thanks to composer.

Setup composer

You need setup composer in your module before create the services. Create the file yourmodule/composer.json and paste:

{
    "name": "<your name>/<nmodule name>",
    "description": "<module description>",
    "authors": [
        {
            "name": "<your name>",
            "email": "<your email>"
        }
    ],
    "require": {
        "php": ">=5.6.0"
    },
    "autoload": {
        "psr-4": {
            "<YourNamespace>\\": "src/"
        },
        "exclude-from-classmap": []
    },
    "config": {
        "preferred-install": "dist",
        "prepend-autoloader": false
    },
    "type": "prestashop-module",
    "author": "<???>",
    "license": "<???>"
}

In YourNamespace add your namespace. Then in console in your module root run command composer dump-autoload. This will generate a vendor folder contain an autoload.php file which allows the use of your namespace.

The convention for namespaces used by PrestaShop is PrestaShop\\Module\\ModuleName as our base namespace, you can either follow this convention or adapt it to your business YourCompany\\YourModuleName.

You can also use composer to include some dependencies in your module, you can find more information about composer on Composer page.

Disable prepend-autoloader

It is required for you to disable the prepend autoloader feature. By default the module dependencies would be defined before the core ones, which would result in overriding them. If you want the PrestaShop core system to work correctly you must not override its dependencies. Which is why you need to always add in your composer.json file:

    "config": {
        "prepend-autoloader": false
    }

Don’t forget to publish your vendor folder

When using composer the autoload file generated by composer is required for your module to work correctly. So before creating your module archive and releasing it don’t forget to run composer dump-autoload (or composer install if you have included dependencies) so that they will be included in your module.

Define your service

At first you will need to create a class for your service of course:

// 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"

This will then allow you to get your service from the Symfony container, like in your modern controllers:

// 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(),
        ]);
    }
}

If you need more details about dependency injection and how services work in the Symfony environment we recommend you to read their documentation about the Service Container.

Override an existing Symfony service

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:

Override the 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.

Decorate the service

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 :

 my_own_b2b_feature_service:
    class: 'YourCompany\YourModule\YourService'
    decorates: 'prestashop.core.b2b.b2b_feature'

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 still available under the name prestashop.core.b2b.b2b_feature.inner

The decoration strategy can be very useful if:

  • you want some areas of your code to use the new service, some others to use the old service
  • you want to use the old service in the implementation of the new service

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:

// 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.

Finding the right service

The Symfony command php ./bin/console debug:container will provide you with a list of all the registered services.

Maintaining compatibility

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:

    /**
     * @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:

    /**
     * @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 recommand you use them as well !

Services in Legacy environment

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 office

Accessing your services

You can then access your services from any legacy controllers (in which the container is automatically injected):

// modules/yourmodule/controllers/front/Demo.php
class YourModuleDemoModuleFrontController extends ModuleFrontController {
    public function display()
    {
        ...
        $yourService = $this->get('your_company.your_module.front.your_service');
        ...
    }
}
// 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:

// modules/yourmodule/yourmodule.php
class yourmodule {
    public function getContent()
    {
        ...
        // The controller here is the ADMIN one so only admin services are accessible
        $yourService = $this->context->controller->getContainer()->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->context->controller->getContainer()->get('your_company.your_module.front.your_service');
        ...
    }
}

Environments

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 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 No Yes Yes Doctrine, services defined in <PS_ROOT_DIR>/config/services/front folder

Define a service on both front and admin

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 }