PrestaShop Developer Conference
PrestaShop Developer Conference
Paris, France - 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

This section provides a list of the most significant changes in PrestaShop 9.0. While this list is not exhaustive, it aims to give developers a clear overview of the important updates. If you notice any missing or incorrect information, please help us improve by creating an issue on our GitHub repository.

PHP support

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

Node support

The minimum version to build PrestaShop assets is now the version 20.

Core changes

Symfony Upgrade

PrestaShop 9.0 has been upgraded to rely on Symfony 6.4. This is a significant leap from the previous version, 8.x, which was based on Symfony 4.4. As a result, version 9 includes all the breaking changes related to the Symfony Framework. For more details about these changes in the core, you can refer to the PrestaShop GitHub issue. Additionally, you can consult the Symfony migration guides (see links below).

Symfony 6.4 is the latest LTS version and will receive bug fixes until November 2026, as well as security fixes until November 2027.

Removed dependencies

We have followed Symfony’s recommendations and made changes to our dependencies. Instead of using the symfony/symfony dependency, we now manually include each sub-package. Additionally, due to the minimum PHP version requirement of 8.1, we had to upgrade or remove certain dependencies.

During this process, we took the opportunity to clean up dependencies that are no longer used in the core, regardless of whether they are from Symfony or not. If you rely on these dependencies in your modules, you will need to integrate them into your module’s dependencies.

Library name Reason for removal/Replacement
guzzlehttp/guzzle Replaced by Symfony HTTP client in the core
league/tactician-bundle Replaced by Symfony Messenger component
pear/archive_tar No longer used
sensio/framework-extra-bundle We now favor annotations in the core
soundasleep/html2text No longer used
swiftmailer/swiftmailer Replaced by Symfony Mailer component in the core
symfony/inflector No longer used
symfony/notifier No longer used
symfony/rate-limiter No longer used
symfony/semaphore No longer used
symfony/uid No longer used
symfony/workflow No longer used

Mailer Migration

We have migrated from Swift Mailer to Symfony Mailer. This change includes several improvements and aligns with Symfony’s latest recommendations.

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:

Name Old version New version
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 4.4 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, we relied on one Kernel used for the back office. In PrestaShop 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, etc.

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 App 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 examples

To clear the cache of the Admin API for its prod environment:

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

To 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 access component
    • search form
    • notifications center
    • employee dropdown
    • the multistore header (when multistore is enabled)
    • the page toolbar (which includes breadcrumbs 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:

Here you’ll find the execution workflow of legacy controllers

Symfony workflow:

flowchart TD A[Special Routing Matching URL] -->|Check URL with LegacyRouterChecker::check| B{Is valid?} B -->|Yes| C[Instantiate Controller & call AdminController::init] B -->|No| Z[Invalid URL] C --> D[Set attributes on Symfony Request] D --> E[LegacyController::legacyPageAction] E --> F[LegacyController::initController] F --> G[Check permissions based on detected controller & action] G --> H[SmartyVariablesFiller::fillDefault] H --> I[AdminController::setMedia] I --> J[AdminController::postProcess] J -->|redirect_after defined| K[Perform HTTP redirection] J -->|redirect_after not defined| L{ajax query parameter defined?} L -->|Yes| M[LegacyController::renderAjaxController] L -->|No| N[LegacyController::renderPageContent] M --> O[Execute AdminController::initContent] O --> P[Execute AdminController::displayAjax] P --> Q[Catch output data & return as response] N --> R[Execute AdminController::initContent] R --> S[Fetch AdminController::template content] S --> T[Call Cookie::write] T --> U[Get 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.

Back office 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 a Session

If you need to persist some custom Employee data, we no longer recommend using the Context::$cookie. 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 useful features, such as flash bags for storing temporary data. You can find more information 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 internal subscriber.

Related PRs:

Symfony context refactorization

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 its 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 back office pages to Symfony 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
  • the page no longer uses HelperForm and HelperList, but Symfony form components and Grid components

Here is the list of migrated pages in v9:

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

Changes in the product page

Edition of features on the new product page, use a new optimized data structure in the form. You may need to adapt your module if you rely on the old structure.

Breaking Changes

trans method

Following PrestaShop/PrestaShop#30415, the function trans() does not escape anymore strings. In PrestaShop 8, 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#31900, 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:

Page Details
Category New hook available, actionPresentCategory
Supplier Different template structure and new hooks
Manufacturer Different template structure and new hooks
Store New hook available actionPresentStore

Stronger types / change of prototypes

Change of behaviour/rules or removed code

The list below is one of the most important changes in PrestaShop 9.0, it includes changes in the behaviour of the core, rules that have been removed or changed, and some code that has been removed. It is very important to check this list to understand the changes in the core as it may impact your solutions.

Each change links to the pull request that introduced it, so you can find more details about the modification.

Change Details
Customization quantity feature has been removed The customization quantity is now the one from the cart_product row. In the pull request you can find more details about the changes, also related to themes.
Refactor AdminModulesControllers and remove obsolete features n/a
Invalid characters are being saved as Social titles n/a
AbstractCategoryType constructor changed n/a
HTTPs check in BackOffice is now based on Symfony Request::isSecure instead of legacy Tools::usingSecureMode n/a
PrestaShopAutoload has been removed in favor of prestashop/autoload If you rely on classes/PrestaShopAutoload.php class, you need to adapt your solutions
Form Types/Extensions have been migrated to be autowired and rely on FQCN service names: If you decorated or override some services modified in the pull request, you may need to adapt your solutions.
- https://github.com/PrestaShop/PrestaShop/pull/31138
- https://github.com/PrestaShop/PrestaShop/pull/31193
- https://github.com/PrestaShop/PrestaShop/pull/31391
Enable/disable module on mobile feature was removed Module configuration to prevent display on mobile devices is no longer possible. This feature has been removed due to issues with cache and low reliability. You can still achieve the same result by implementing this mechanism on a module level.
Standardize filterManufacturerContent hook Parameters inside filterManufacturerContent hook have changed.
Legacy images format no longer supported n/a
PrestaShopBundle\Kernel\ModuleRepository was removed along with its Factory n/a
Product::getDefaultCategory always returns an int n/a
Remove high DPI images feature HighDPI images feature wasn’t used in PrestaShop 8 and has been removed in PrestaShop 9. You can still achieve the same results by creating more image types and using sourceset
Remove Category menu thumbnail feature n/a
Symfony Parameter prestashop.addons.categories was removed n/a
UpdateHookStatusCommand parameter is no the new expected status n/a
Replaced SwiftMailer by Symfony Mailer n/a
Replaced TactitianBundle by Symfony Messenger n/a
Removed Advanced Stock Management remains Advanced stock management functionality has been partially removed and deprecated for a whole lifetime of 1.7, but not completely removed. This PR removes all the logic from the code along with everything related - supply orders, warehouses, old stock management etc.
Remove shop activity during install n/a
Remove Multi Address delivery This change may have an impact on third-party solutions,especially ones that modify checkout process in PrestaShop. Address IDs from the cart_product and customization tables are no longer relevant and you should not rely on them.
Remove non responsive component in BO n/a
Lazy load Product feature in FO By default, Product::getProductsProperties will no longer contain features data.
Removed cover_image_id from Product lazy array Removes cover_image_id property that was used to override id_image. You can still alter the cover by using standard presenter hook. It also adds a new functionality that allows to choose behavior of cover image for products with combinations.
Removed attribute_price from Product::getProductProperties n/a
Doctrine dependencies updated This change brings a lot of backward incompatible changes. All details are available inside a pull request. It is really important to understand those changes to make your solutions compatible.
FrontController::addJqueryUi no only adds the requested component This performance optimization may result in a different loading of jQuery UI components.
Removed code from old product page The old product page has been removed in version 9, this brings a lot of backward incompatible changes, especially if you used some of the components of the old product page.
ObjectModel fields definition can contain translatable wording This change introduced a new approach to translating error messages related to ObjectModel fields.
Image feature flag has been removed n/a
Forbid sensitive files in modules directory This pull request has been merged following discussion about securing PrestaShop modules better. It will require the adaptation of your modules and switching from calling .php files directly to using ModuleFrontController. You can check all the details inside the discussion and pull request, as well as related pull requests from native modules which you can use as an example implementation.
Remove legacy tab system n/a
Upgrade jquery to latest version, drop polyfills n/a
EmployeeId has strict type, OrderStatusForViewing has new constructor parameters TBD
RedirectTargetProvider and ProductRedirectTarget moved into generic namespace n/a
Changed return types of all ean13 properties in CQRS ean13 has been replaced with gtin which may require adjustments to third-party solutions
ModuleManagerBuilder and ThemeManager internal properties no longer public n/a
Admin API, lots of experimental code renamed n/a
Empty value no longer allowed for redirect_type in product tables n/a
PHPStan Doctrine extension introduced that impact all existing Entities n/a

Removal of deprecated code

Rebranding changes

  • Breaking changes in the PrestaShop Back Office due to rebranding
    • New Fonts: The adoption of IBM Plex Sans and Material Symbols is part of the rebranding effort. These fonts are included in both the default theme and also the new-theme through the UI Kit.
    • Removal of postcss.config.js: The postcss.config.js file, which used the deprecated postcss-cssnext package, has been removed. This package is no longer needed.
    • Removal of SCSS Files: The file admin-dev/themes/default/scss/modules/_colors.scss and the directory admin-dev/themes/default/scss/modules/colors/ have been removed. These are no longer used in the back office. Update your stylesheets to avoid relying on these outdated resources.

Back office rebranding

By the end of 2023, PrestaShop introduced a new brand identity to reinforce its strengths and enhance credibility and trust. The PrestaShop back office was one of the final components to undergo rebranding, with the release of PrestaShop 9 providing an ideal opportunity for this update.

Anatomy of PrestaShop back office

The PrestaShop back office consists of two themes. Both themes have been modified to achieve a complete rebranding:

Theme: default

  • Used for legacy controllers
  • Based on Bootstrap 3
  • Now includes _root.scss file from the PrestaShop UI Kit to expose CSS variables from the UI Kit

Theme: new-theme

  • Used for Symfony controllers
  • Based on Bootstrap 4
  • Utilizes the PrestaShop UI Kit