PrestaShop Developer Conference
PrestaShop Developer Conference
November 6, 2024
Let's talk code, commerce and open source.

Notice: You are browsing the documentation for PrestaShop 9, which is currently in development.

You might want to look at the current version, PrestaShop 8. Read the current version of the documentation

Notable changes in PrestaShop 9.0

PHP support

PrestaShop 9.0 now requires PHP 8.1 minimum, with support added for PHP 8.2 and 8.3.

Core changes

Symfony Upgrade

PrestaShop V9 has been upgraded to rely on Symfony 6.4, V8 was previously based on Symfony 4.4 so it’s quite a big leap. As such the V9 includes all the breaking changes related to the Symfony Framework, you can see more details about the changes in the core here and if you can also check Symfony migration guides (see below). Symfony 6.4 is the latest LTS version and will be supported until November 2026 for bug fixes, and November 2027 for security fixes.

Removed dependencies

We followed Symfony recommendations and no longer use the symfony/symfony dependency but instead each sub package manually, the minimum version of PHP 8.1 also forced us to upgrade or removed some dependencies. We used this occasion to clean the dependencies that are no longer used in the core (whether they are from symfony or not), if you need these dependencies in your modules you will have to integrate them in your module’s dependencies.

  • guzzlehttp/guzzle (replaced by Symfony HTTP client in the core)
  • league/tactician-bundle (replaced by Symfony Messenger component)
  • pear/archive_tar
  • sensio/framework-extra-bundle (we now favor annotations in the core)
  • soundasleep/html2text
  • swiftmailer/swiftmailer (replaced by Symfony Mailer component in the core)
  • symfony/inflector
  • symfony/notifier
  • symfony/rate-limiter
  • symfony/semaphore
  • symfony/uid
  • symfony/workflow

Mail SSL encryption was dropped

When we migrated from Swift Mailer to Symfony Mailer, we noticed that SSL support was not an option in ESMTP transport. SSL is an old and outdated encryption type, and for security reasons, it will no longer be allowed.

The remaining choices are “TLS encryption” or “No encryption”.

Upgraded dependencies

Some dependencies are still present but were upgraded which comes with their own breaking changes, please refer to each dependency changelog to understand them in details if you depend on these dependencies:

  • api-platform/core: 2.7.6 -> 3.2.13
  • composer/installers: 1.12.0 -> 2.2.0
  • friendsofsymfony/jsrouting-bundle: 2.8.0 -> 3.2.1
  • lcobucci/jwt: 3.4.6 (special patch from https://github.com/PrestaShop/jwt.git) -> 5.0.0 (no need for fork version anymore)
  • mobiledetect/mobiledetectlib: 2.8.41 -> 3.74.0
  • pelago/emogrifier: 5.0.1 -> 7.0.0
  • Symfony dependencies all upgraded from 4.4 to 6.4
  • twig/twig: 3.4.3 -> 3.8.0
  • doctrine/dbal: 2.13.8 -> 3.6.5
  • doctrine/lexer: 1.2.3 -> 2.1.1
  • doctrine/orm: 2.12.1 -> 2.15.5
  • doctrine/deprecations: 0.5.3 -> 1.1.3
  • ’egulias/email-validator’: 3.2.6 -> 4.0.1

Symfony controllers

In Symfony 6.4, the controllers must now be defined as services. The impact on the existing controllers is mainly around the concept of Dependency Injection and how services are injected or accessed in the controllers. The container passed to the controllers is no longer the “global container” that contains all the existing services in the application. Instead, it injects a dedicated container optimized for the controller based on the services that are injected into it.

The issue is that most of PrestaShop’s Symfony controllers do not rely on injection but instead use the $this->get('service_name') method to access any service. This is no longer possible in modern controllers because of the mentioned optimization related to container build (and the get method was even removed). To avoid a very big breaking change, we modified the PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController, so that it can fetch services from the controller’s container and from the global one, but this is a little hack that goes against Symfony recommendations, as such this base class is already deprecated and will be removed in PrestaShop 10.0.

This gives developers some time to refacto their controllers. They should now rely on the new PrestaShopBundle\Controller\Admin\PrestaShopAdminController, which doesn’t implement the mentioned hack in order to force a proper implementation of the controllers. You can read more details about Controllers as services, Service Subscribers & Locators or Service container in general.

As an example, you have three main possibilities to inject a service into your controller:

<?php

use PrestaShopBundle\Controller\Admin\PrestaShopAdminController;

class MyController extends PrestaShopAdminController
{
    /**
     * You can inject your service systematically in the constructor, useful if the service is used by several actions of your controller.
     * 
     * @param MyCustomService $myCustomService
     */
    public function __construct(
      private readonly MyCustomService $myCustomService,
    ) {
    }
    
    /**
     * You can inject a service only on one method, useful if the service is used only in one action. It is more optimized than injecting it all the time and never using it.
     * 
     * @param MySpecificService $specificService
     * @return JsonResponse
     */
    public function indexAction(MySpecificService $specificService): JsonResponse
    {
        $generalData = $this->myCustomerService->getData();
        $specificData = $specifiService->getSpecificData();

        // You can use the get method to fetch a service that was added in the getSubscribedServices method
        $toolingService = $this->container->get(ToolingService::class);
        
        return new JsonResponse($toolingService->format($generalData + $specificData));
    }
    
    /**
     * You can define a list of registered services, they will be accessible via the $this->container->get method, this
     * way of injecting service is interesting if implemented on a base controller class for generic services that
     * may be used by many controllers (like the router, the translator, ...) so you don't need to inject them manually
     * in each controller class.
     * 
     * @return array
     */
    public static function getSubscribedServices(): array
    {
        return parent::getSubscribedServices() + [
            ToolingService::class => ToolingService::class,
        ];
    }
}

Define your controller as a service

You can already anticipate and define your controllers as services, it is also compatible with previous versions of Symfony and PrestaShop:

services:
  MyNamespace\MyController:
    # Using autowire will inject services in the constructors automatically
    autowire: true
    # Using autoconfigure with controllers that extend AbstractController will handle method parameter injection and subscribed services
    autoconfigure: true
    # If your controller class doesn't extend AbstractController you should add this tag manually so you benefit from method parameters injection
    tags: ['controller.service_arguments']

PrestaShopAdminController::getSubscribedServices

The new base controller class uses the getSubscribedServices method to give easy access to some commonly used PrestaShop services, like the ConfigurationInterface, HookDispatcherInterface, TranslatorInterface, … You can see the full list of subscribed services in the class, this list will probably grow piece by piece but only for common generic services.

Thanks to this, you can use $this->container->get(ConfigurationInterface::class)->get('my_config') to access the configuration service, or alternatively use the helper method $this->getConfiguration()->get('my_config'). The PrestaShopAdminController class comes with other helpful helper methods that are commonly used in controllers.

Kernel modifications

Until PrestaShop 8.0, we relied on one Kernel used for the back office. In Symfony 9.0, we expanded the usage of the Symfony framework, especially for two new features:

  • the new Admin API based on API Platform and OAuth authentication
  • an experimental feature that gives access to a Symfony container in the front office (in opposition to the custom-built container available today). This one is not very much used yet, but it’s a first stone for the future front office migration

Those two new environments have mechanisms and configuration different from those in the back office. To separate these configurations cleanly, they each have their dedicated kernel class and their own configuration. This allows to cleanly define different routing, security configurations, dedicated services, listeners, … Each kernel has a unique applicationId that allows to dynamize its configuration and cache folder (since the services are not the same, each kernel needs its own cache folder). The appId parameter has also been added globally to the bin/console tool. They all share the common app/config/config.yml configuration file, but each one extends it in its own config folder.

Environment Kernel class Config folder Cache folder Endpoint Application ID
Back office AdminKernel app/config/admin var/cache/{dev,prod,test}/admin /admin-dev/index.php admin
Admin API AdminAPIKernel app/config/admin-api var/cache/{dev,prod,test}/admin-api /admin-api/index.php admin-api
Front office (experimental) FrontKernel app/config/front var/cache/{dev,prod,test}/front /index.php front

Console usage

Clear the cache of the Admin API for its prod environment

php bin/console cache:clear --env=prod --app-id=admin-api

Display event listeners for the back office in dev environment (default value of app-id is admin for retro compatibility)

php bin/console debug:event-dispatcher kernel.request --env=dev

Related PRs

Symfony layout

All the back office pages share a common layout, composed of a few elements:

  • the <head> element that includes all the CSS and JS (among other things)
  • the side navigation menu
  • the header of the page which is itself composed of:
    • quick accesses
    • search form
    • notifications center
    • employee dropdown
    • the multistore header (when multistore is enabled)
    • the page toolbar (which includes breadcrumb and top action buttons)
  • the footer (which only contains a displayBackOfficeFooter display hook)

Until PrestaShop 8.1, all these common elements were handled by legacy code, so even on the migrated pages, there was always a background legacy controller based on AdminController in charge of building the layout data and rendering it. Once the layout was rendered the central content of Symfony pages was included in the middle of it. It means no page was completely free from the legacy controllers, which would ultimately block the end of migration, while they are intended to disappear completely.

In PrestaShop 9.0 all this layout part is now fully handled by Symfony, we use Twig components to render each element independently. The code is, therefore, easier to understand, a component class is responsible for fetching/building the data, while the actual rendering is based on Twig. See our Layout components for more details.

On legacy pages, we follow the same principle. Symfony is now in charge of rendering all the layout, and we use some Legacy layout components that follow the same architecture, but render different Twig templates to fit with the old default theme (that is based on old Bootstrap and includes old legacy helpers like HelperForm and HelperList).

What changes for my module pages?

We refactored this layout with maximum backward compatibility in mind. The HTML layout itself had minimum changes (in both migrated and legacy pages). We tried to keep the same hooks in both the PHP code and the Twig templates. We even introduced a fake legacy controller in migrated pages that has no logic but is kept mostly as a DTO to contain things like CSS files and JS files, because that’s where most modules add their content. We still had to change the controller workflow. Many of the functions in AdminController are no longer useful as they are used to initialize the layout variables (now handled by Twig components). Some methods also render or write output directly, which we prevent as we need to get the rendered content as a string to integrate it correctly inside the new layout. This means some internal methods had to be split to avoid unwanted usage.

Legacy workflow:

  • Dispatcher::dispatch would detect the legacy controller and instantiate it
  • AdminController::run was executed by the Dispatcher, which was split into
    • AdminController::init
    • AdminController::checkAccess would handle the check of the legacy token in the url
    • AdminController::setMedia is in charge of defining the JS/CSS files to be loaded in the page
    • AdminController::postProcess processes the posted data
      • if after this method AdminController::redirect_after is defined then perform an HTTP redirection
    • AdminController::initHeader
    • AdminController::initContent
    • AdminController::initFooter
    • Check if AdminController::ajax, one of the two is executed
      • AdminController::displayAjax (or specific displayAjax{action} method)
      • AdminController::display
        • init many variables used by the layout templates (meta title, toolbar, JS definitions, …)
        • render page, header, footer and sets them as Smarty variables
        • fetch the Smarty AdminController::template to get the content of the page
        • calls smartyOutputContent to echo/output the whole page (layout + content)

Here you’ll find the execution workflow of legacy controllers

Symfony workflow:

  • A special routing matching URLs like ?controller=LegacyAdminController checks if the URL is valid thanks to LegacyRouterChecker::check
    • the query parameter must match an existing legacy controller class
    • the controller object is instantiated and AdminController::init is called, this is important to know if the controller is anonymous, this is the proper moment to define AdminController::allowAnonymous
    • a few attributes are set on the Symfony Request so they can be reused later in the workflow by other components/services
  • LegacyController::legacyPageAction is a generic controller responsible for executing the legacy controller, it doesn’t execute AdminController::run but splits it into several steps
    • AdminController::checkAccess is not called, the token in URLs is a Symfony CSRF token now and is checked by TokenizedUrlListener
    • LegacyController::initController
      • the permission is checked based on the detected controller and action, unless AdminController::viewAccess has been overridden in which case it is used to check authorization
      • SmartyVariablesFiller::fillDefault is in charge of building most of the generic smarty variables previously handled by AdminController::initHeader
      • AdminController::setMedia is in charge of defining the JS/CSS files to be loaded in the page
      • AdminController::postProcess processes the posted data
        • if after this method AdminController::redirect_after is defined then perform an HTTP redirection
    • Check if ajax query parameter is defined, one of the two is executed
      • LegacyController::renderAjaxController executes AdminController::initContent the appropriate AdminController::displayAjax method, it catches all the output data into a string buffer (because most legacy ajax methods simply echo their content) and returns it as a response
      • LegacyController::renderPageContent
        • executes AdminController::initContent
        • fetch the Smarty AdminController::template to get the content of the page
        • calls Cookie::write which was previously called via smartyOutputContent
        • get the modal content via AdminController::renderModal

The two workflows should render the same result. Many methods from the legacy workflow are not executed anymore because they lost their purpose, but in case you override one of those methods, be aware that they are no longer called directly, so there are breaking changes:

  • AdminController:run
  • AdminController::initHeader
  • AdminController::initContent
  • AdminController::initFooter
  • AdminController:display

Also, be aware that many Smarty variables are no longer defined because they were only used to render the layout.

Despite the changes in the workflow and the fact we no longer depend on the Dispatcher class, we maintained these hooks:

  • actionDispatcher
  • actionDispatcherBefore
  • actionDispatcherAfter

Related PRs:

For more details about the changes, you can check the content of the Symfony layout Epic.

BO login and authorization migrated to Symfony

The back office login page has been migrated to Symfony. Along with this change, the authorization system in the back office is now also based on Symfony, which implies several things:

  • we no longer depend on the legacy Context::$cookie, the session is kept on the server side, and very few data are kept on the browser side. For retro-compatibility, we still populate the legacy cookie so that you can read it. While you can use Context::$cookie for your custom data, making changes to the data previously used for PrestaShop authorization will no longer have the desired effect and may even result in instability, so it’s not recommended
  • the PrestaShopBundle\Security\Admin\EmployeeProvider and PrestaShopBundle\Service\DataProvider\UserProvider now return a PrestaShopBundle\Entity\Employee instance, their responsibility is to return a Symfony\Component\Security\Core\User\UserInterface anyway (and they still do) but in case you depended on the child class know that PrestaShopBundle\Security\Admin\Employee no longer exist
  • if you need to get the logged-in user, you can use the Symfony Symfony\Bundle\SecurityBundle\Security service, but we recommend you use the EmployeeContext internal PrestaShop service (see explanation about new Contexts)

Storing custom data in Session

If you need to persist some custom Employee data, we don’t recommend using the Context::$cookie anymore, instead you can use the Session from Symfony and update its attributes:

// In Symfony controllers, you can get the session from the Request parameter injected by Symfony
$session = $request->getSession();

// In other services you will need to get the current request via the RequestStack service
$request = $requestStack->getCurrentRequest();
if ($request) {
  $session = $request->getSession();
}

// You can then get/set a custom attribute, but you should always check that the session object is indeed available. Some requests do not rely on the session, and it may never be created
if ($session) {
  // Be careful to use a custom and UNIQUE attribute name
  $myData = $session->getAttribute('my_custom_data');
  // The session object will be automatically serialized at the end of the request, and automatically unserialized at the beginning of the next requests
  $session->setAttribute('my_custom_data', 'updated value');
}
The Symfony session has other interesting features (like flash bags for unique usage data). You can read more about its usage in the Session documentation.

Removed hooks:

  • actionAdminLoginControllerBefore
  • actionAdminLoginControllerLoginBefore
  • actionAdminLoginControllerLoginAfter
  • actionAdminLoginControllerForgotBefore
  • actionAdminLoginControllerForgotAfter
  • actionAdminLoginControllerResetBefore
  • actionAdminLoginControllerResetAfter

Hooks kept for backward compatibility:

  • actionAdminLoginControllerSetMedia so you can add some custom assets on the login page
  • displayAdminLogin so you can add custom HTML on the login page

New hooks

  • actionBackOfficeLoginForm to modify the login form (the form builder is passed via the form_builder hook parameter)
  • actionEmployeeRequestPasswordResetForm to modify the request password form (the form builder is passed via the form_builder hook parameter)

The login logic, however, is not handled by PrestaShop’s internal code so there is no handler that interprets the submitted data (and that’s why there are so few new hooks). Instead we rely on Symfony form login authentication system, so it’s Symfony’s internal system that interprets the data submitted, validates it and authenticate the employee to the back office.

If you need to integrate your code with the authentication process you should now rely on the Authentication events described in their documentation, you can also inspire yourself from our own internal subscriber.

Related PRs:

Symfony context refacto

A significant amount of code in the core relies on the legacy Context class, which stores certain data as a singleton. However, this legacy implementation has many drawbacks, so refactoring was initiated in PrestaShop 9.0 to replace this usage. You can find more details about the reason behind this and the implementation in the related ADR about Context refactoring.

You should now favor using the modern split context services:

  • PrestaShop\PrestaShop\Core\Context\ApiClientContext
  • PrestaShop\PrestaShop\Core\Context\CountryContext
  • PrestaShop\PrestaShop\Core\Context\CurrencyContext
  • PrestaShop\PrestaShop\Core\Context\EmployeeContext
  • PrestaShop\PrestaShop\Core\Context\LanguageContext
  • PrestaShop\PrestaShop\Core\Context\LegacyControllerContext
  • PrestaShop\PrestaShop\Core\Context\ShopContext

We still have some work to do to replace all the usages of the legacy context with the modern ones, but the new code will stop using the legacy ones. We recommend that developers rely on the new ones from now on.

Migrated pages

The migration of pages imply several breaking changes, we won’t detail all of them for each page but here is a summary:

  • the legacy URL is no longer reachable, you are automatically redirected to the migrated page
  • the legacy controller is removed along with its associated smarty templates
  • the legacy hooks related to controller workflow are no longer called
  • the page no longer uses smarty but twig

Here is the list of migrated pages in v9:

  • International > Locations > States
  • Shop parameters > Order settings > Statuses
  • Orders > Shopping Carts
  • Sell > Catalog > Attributes
  • Sell > Catalog > Feature
  • Sell > Catalog > Products (new version of the page introduce in PrestaShop V8 is now the only available one)

Breaking Changes

trans method

Following PrestaShop/PrestaShop#30415 the function trans() does NOT escape anymore strings. In v8 you could pass parameters like htmspecialcharacters or addslashes to trans() to perform additional escape, but it’s been removed. It also affects the l function in Smarty.

Since PrestaShop/PrestaShop#30415 the trans method always keep the behavior it had with the (now removed) _raw parameter, meaning the content is not modified anymore. You need to use htmlspecialchars on your parameters provided on input and on the returned string if you need to modify it.

Some front controller now use Presenter classes

This changes the data passed to the smarty templates:

Stronger types / change of prototypes

Change of behaviour/rules or removed code

Removal of deprecated code