PrestaShop 9: que ha cambiado internamente y como preparar tus módulos

PrestaShop 9 ya está aquí — Y rompe más de lo que crees

PrestaShop 9.0 se lanzó a finales de 2025 con una base de Symfony 6.4 LTS—saltando desde Symfony 4.4, pasando por alto dos versiones mayores del framework en un solo lanzamiento. He migrado 14 módulos de producción a compatibilidad con PS9 hasta ahora. Algunos fueron sobre ruedas. Otros requirieron reescribir controladores de administración completos.

Desarrollador PHP codificando actualizaciones de módulos PrestaShop para compatibilidad con versión 9

Esto no es un resumen de changelog. Esta es la guía de trabajo de un desarrollador sobre lo que realmente se rompe, con código antes-y-después para cada cambio importante. Si mantienes módulos de PrestaShop—ya sea para el marketplace de Addons, para clientes o para tus propias tiendas—esta es la referencia de migración que me habría gustado que existiera cuando empecé.

El salto a Symfony 6.4: por qué importa

Symfony 4.4 llegó a su fin de vida en noviembre de 2023. PrestaShop 8.x ya estaba ejecutándose sobre una versión del framework sin soporte. El salto a 6.4 LTS (con soporte hasta noviembre de 2027 para correcciones de seguridad) era necesario pero doloroso.

Las implicaciones clave para los desarrolladores de módulos:

  • PHP 8.1 mínimo — El código PHP 7.x no funcionará. PHP 8.2 o 8.3 recomendados para el mejor equilibrio entre funcionalidades y estabilidad.
  • Controladores como servicios adecuados — El patrón del “contenedor global” está muerto. La inyección de dependencias es ahora obligatoria.
  • Anotaciones reemplazadas por atributos PHP 8@Route, @AdminSecurity, @DemoRestricted todos se convierten a sintaxis de atributos.
  • Varias bibliotecas mayores eliminadas — Guzzle, SwiftMailer y Tactician han desaparecido del core.
  • Node.js 20 mínimo — Para módulos con pasos de compilación.

Cambio importante #1: Controladores — Del contenedor global a la inyección de dependencias

Este es el cambio que más desarrolladores hace tropezar. En PS8, podías llamar a $this->get('service_name') en cualquier controlador de administración para obtener cualquier servicio del contenedor global de Symfony. En PS9, el contenedor pasado a los controladores es un contenedor con alcance que solo incluye servicios que has declarado explícitamente.

Antes (PrestaShop 8.x)

<?php
// modules/mymodule/src/Controller/Admin/MyController.php

use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;

class MyController extends FrameworkBundleAdminController
{
    public function indexAction()
    {
        // Grab any service from the global container
        $productRepository = $this->get('prestashop.core.product.repository');
        $translator = $this->get('translator');
        $configService = $this->get('my_module.config_service');

        return $this->render('@Modules/mymodule/views/templates/admin/index.html.twig', [
            'products' => $productRepository->findAll(),
        ]);
    }
}

Después (PrestaShop 9.x)

<?php
// modules/mymodule/src/Controller/Admin/MyController.php

use PrestaShopBundle\Controller\Admin\PrestaShopAdminController;
use MyModule\Service\ConfigService;
use Symfony\Component\HttpFoundation\Response;

class MyController extends PrestaShopAdminController
{
    // Constructor injection for services you always need
    public function __construct(
        private readonly ConfigService $configService,
    ) {
    }

    // Method injection for request-specific services
    public function indexAction(ProductRepository $productRepository): Response
    {
        return $this->render('@Modules/mymodule/views/templates/admin/index.html.twig', [
            'products' => $productRepository->findAll(),
            'config' => $this->configService->getAll(),
        ]);
    }

    // If you MUST use the service locator pattern (migration bridge)
    public static function getSubscribedServices(): array
    {
        return parent::getSubscribedServices() + [
            ConfigService::class => ConfigService::class,
        ];
    }
}

Cambios clave:

  • FrameworkBundleAdminController está deprecado—usa PrestaShopAdminController
  • La inyección por constructor reemplaza las llamadas a $this->get()
  • La inyección por método funciona para dependencias específicas de acción
  • getSubscribedServices() es un puente de migración—úsalo temporalmente mientras refactorizas, pero migra a inyección por constructor/método

Cambio importante #2: De anotaciones a atributos PHP 8

La dependencia sensio/framework-extra-bundle ha sido eliminada. Todas las anotaciones de enrutamiento y seguridad deben convertirse a atributos nativos de PHP 8.

Enrutamiento

// BEFORE (PS8) — Annotation syntax
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/mymodule/settings", name="mymodule_settings")
 */
public function settingsAction()

// AFTER (PS9) — PHP 8 Attribute syntax
use Symfony\Component\Routing\Annotation\Route;

#[Route('/mymodule/settings', name: 'mymodule_settings', methods: ['GET'])]
public function settingsAction(): Response

Anotaciones de seguridad

// BEFORE (PS8)
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use PrestaShopBundle\Security\Annotation\DemoRestricted;

/**
 * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
 * @DemoRestricted(redirectRoute="mymodule_settings")
 */
public function settingsAction()

// AFTER (PS9)
use PrestaShopBundle\Security\Attribute\AdminSecurity;
use PrestaShopBundle\Security\Attribute\DemoRestricted;

#[AdminSecurity("is_granted('read', request.get('_legacy_controller'))")]
#[DemoRestricted(redirectRoute: 'mymodule_settings')]
public function settingsAction(): Response

Observa el cambio de namespace: Security\Annotation\ se convierte en Security\Attribute\. Es un buscar-y-reemplazar fácil, pero si lo pasas por alto, tus rutas no tendrán restricciones de seguridad—lo cual es peor que un crash.

Event Listeners

// BEFORE (PS8) — YAML service definition + interface
# config/services.yml
services:
    mymodule.event_listener:
        class: MyModule\EventListener\ProductListener
        tags:
            - { name: kernel.event_listener, event: kernel.request }

// AFTER (PS9) — PHP attribute, no YAML needed
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: 'kernel.request')]
class ProductListener
{
    public function __invoke(RequestEvent $event): void
    {
        // ...
    }
}

Cambio importante #3: Definiciones de servicios — De YAML a configuración PHP

Aunque las definiciones de servicios en YAML todavía funcionan en PS9, el enfoque recomendado es la configuración basada en PHP con autowiring. Más importante aún, tus servicios deben estar correctamente configurados para la inyección de dependencias más estricta de Symfony 6.4.

Antes (PS8 YAML)

# modules/mymodule/config/services.yml
services:
    mymodule.config_service:
        class: MyModule\Service\ConfigService
        arguments:
            - '@prestashop.adapter.legacy.configuration'
        public: true

    mymodule.admin_controller:
        class: MyModule\Controller\Admin\MyController
        arguments:
            - '@mymodule.config_service'
        tags:
            - { name: controller.service_arguments }

Después (PS9 configuración PHP con autowiring)

<?php
// modules/mymodule/config/services.php

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
    $services = $container->services();

    $services->defaults()
        ->autowire()
        ->autoconfigure()
        ->public(false);

    // Register all classes in src/ as services
    $services->load('MyModule\\', '../src/')
        ->exclude('../src/{Entity}');

    // Explicit service definition only when needed
    $services->set(MyModule\Service\ConfigService::class)
        ->arg('$shopId', '%prestashop.shop_id%');
};

Con autowiring, Symfony resuelve las dependencias del constructor automáticamente por type-hint. Solo necesitas definiciones explícitas para:

  • Parámetros escalares ($shopId, $apiKey, etc.)
  • Bindings de interfaces (cuando existen múltiples implementaciones)
  • Clases de terceros que no pueden ser autowired

Cambio importante #4: Bibliotecas eliminadas

PrestaShop 9 eliminó varias bibliotecas incluidas. Si tu módulo las usaba, debes incluirlas tú mismo o migrar al reemplazo.

Biblioteca eliminadaReemplazoEsfuerzo de migración
guzzlehttp/guzzlesymfony/http-clientMedio—la API es diferente
swiftmailer/swiftmailersymfony/mailerMedio—nueva API del objeto Email
league/tactician-bundlesymfony/messengerAlto—patrón de command bus diferente
pear/archive_tarIncluir en el módulo o usar ext-zipBajo

De Guzzle a Symfony HttpClient

// BEFORE — Guzzle
use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.example.com']);
$response = $client->request('GET', '/products', [
    'query' => ['status' => 'active'],
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);
$data = json_decode($response->getBody()->getContents(), true);

// AFTER — Symfony HttpClient
use Symfony\Contracts\HttpClient\HttpClientInterface;

// Inject via constructor
public function __construct(
    private readonly HttpClientInterface $httpClient,
) {}

public function getProducts(string $token): array
{
    $response = $this->httpClient->request('GET', 'https://api.example.com/products', [
        'query' => ['status' => 'active'],
        'auth_bearer' => $token,
    ]);
    return $response->toArray(); // Auto-decodes JSON
}

De SwiftMailer a Symfony Mailer

// BEFORE — SwiftMailer
$message = (new \Swift_Message('Subject'))
    ->setFrom('shop@example.com')
    ->setTo('customer@example.com')
    ->setBody($htmlContent, 'text/html');
$this->get('mailer')->send($message);

// AFTER — Symfony Mailer
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\MailerInterface;

public function __construct(
    private readonly MailerInterface $mailer,
) {}

public function sendNotification(): void
{
    $email = (new Email())
        ->from('shop@example.com')
        ->to('customer@example.com')
        ->subject('Subject')
        ->html($htmlContent);
    $this->mailer->send($email);
}

Importante: El cifrado SSL para email fue eliminado en PS9. Solo quedan TLS o sin cifrado. Actualiza tu configuración de email si estabas usando SSL.

Cambio importante #5: Refactorización del Context

El singleton legacy Context está siendo reemplazado por servicios de contexto tipados. Aunque Context::getContext() todavía funciona en PS9, está deprecado y el nuevo enfoque es más fiable—especialmente en comandos CLI y contextos asíncronos donde el Context legacy no se inicializaba correctamente.

// BEFORE — Legacy Context singleton
$employee = Context::getContext()->employee;
$shop = Context::getContext()->shop;
$language = Context::getContext()->language;
$currency = Context::getContext()->currency;

// AFTER — Typed Context services (inject via constructor)
use PrestaShop\PrestaShop\Core\Context\EmployeeContext;
use PrestaShop\PrestaShop\Core\Context\ShopContext;
use PrestaShop\PrestaShop\Core\Context\LanguageContext;
use PrestaShop\PrestaShop\Core\Context\CurrencyContext;

public function __construct(
    private readonly EmployeeContext $employeeContext,
    private readonly ShopContext $shopContext,
    private readonly LanguageContext $languageContext,
    private readonly CurrencyContext $currencyContext,
) {}

public function someMethod(): void
{
    $employee = $this->employeeContext->getEmployee();
    $shopId = $this->shopContext->getId();
    $langId = $this->languageContext->getId();
    $currency = $this->currencyContext->getCurrency();
}

Servicios de contexto disponibles en PS9: EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext y LegacyControllerContext.

Cambio importante #6: Autenticación y gestión de sesiones

El login del back office ahora está completamente basado en Symfony. Si tu módulo lee datos del empleado desde Context::$cookie, eso no es fiable en PS9.

// BEFORE — Cookie-based auth (unreliable in PS9)
$employeeId = Context::getContext()->cookie->id_employee;
$profile = Context::getContext()->cookie->profile;

// AFTER — Session-based approach
use Symfony\Component\HttpFoundation\RequestStack;
use PrestaShop\PrestaShop\Core\Context\EmployeeContext;

public function __construct(
    private readonly RequestStack $requestStack,
    private readonly EmployeeContext $employeeContext,
) {}

public function getEmployeeData(): array
{
    // For employee identity
    $employee = $this->employeeContext->getEmployee();

    // For custom session data your module stores
    $session = $this->requestStack->getCurrentRequest()?->getSession();
    $myData = $session?->get('my_module_custom_data');

    return [
        'employee_id' => $employee->getId(),
        'profile_id' => $employee->getProfileId(),
        'custom_data' => $myData,
    ];
}

Cambio importante #7: Hooks eliminados

Varios hooks relacionados con el flujo de login de administración han sido eliminados sin reemplazo directo:

Editor de código mostrando el framework Symfony utilizado en la arquitectura de PrestaShop 9

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

Reemplazos para la personalización de formularios:

  • actionBackOfficeLoginForm — Modificar el constructor del formulario de login
  • actionEmployeeRequestPasswordResetForm — Modificar el formulario de restablecimiento de contraseña

Todos los hooks legacy de la página de producto (actionAdminProductsController*) también fueron eliminados ya que la página legacy de producto ya no existe.

Cambio importante #8: Cambios en los datos de plantillas front office

Varias páginas front-end ahora usan clases Presenter, cambiando la estructura de datos disponible en las plantillas Smarty. Si tu módulo sobreescribe o extiende estas plantillas, necesitarás actualizar las referencias a variables.

{* BEFORE (PS8) — Manufacturer page *}
<img src="{$manufacturer.image}" alt="{$manufacturer.name}">
{$manufacturer.nb_products} products

{* AFTER (PS9) — Presenter-based data structure *}
<img src="{$manufacturer.image.medium.url}" alt="{$manufacturer.name}">
{l s='%number% product' sprintf=['%number%' => $manufacturer.nb_products] d='Shop.Theme.Catalog'}

Páginas afectadas: Categoría, Fabricante, Proveedor y Tienda. Cada una tiene un hook actionPresent* correspondiente que permite a los módulos modificar los datos presentados.

Cambio importante #9: Seguridad de módulos — No más archivos PHP raíz

Los módulos ya no pueden contener archivos PHP directamente ejecutables a nivel raíz. Esto afecta a los módulos que usan endpoints AJAX como archivos PHP independientes.

// BEFORE — Direct PHP file (modules/mymodule/ajax.php)
require_once dirname(__FILE__) . '/../../config/config.inc.php';
// Process AJAX request...

// AFTER — ModuleFrontController
// modules/mymodule/controllers/front/ajax.php
class MyModuleAjaxModuleFrontController extends ModuleFrontController
{
    public function displayAjaxProcess()
    {
        $result = $this->module->processAjaxRequest();
        $this->ajaxRender(json_encode($result));
    }
}
// URL: index.php?fc=module&module=mymodule&controller=ajax&action=process

Cambio importante #10: Comandos de consola y Multi-Kernel

PS9 introduce kernels de aplicación separados para admin, admin-api y front. Los comandos de consola ahora aceptan un parámetro --app-id para apuntar al kernel correcto:

# Clear admin cache only
php bin/console cache:clear --env=prod --app-id=admin

# Debug front-office event listeners
php bin/console debug:event-dispatcher kernel.request --app-id=front

# List admin-api routes
php bin/console debug:router --app-id=admin-api

Esto importa para los comandos de consola de módulos—asegúrate de que tu comando está registrado en el contexto de kernel correcto.

Lo que permanece igual (las buenas noticias)

No todo se rompe. Estos patrones siguen funcionando en PS9:

  • El sistema de hookshookDisplayHeader(), hookActionProductUpdate(), etc. siguen funcionando exactamente como antes.
  • Plantillas Smarty — Las plantillas Smarty del front office siguen siendo soportadas. La transición a Twig es solo para administración.
  • Descriptor del módulo$this->name, $this->version, $this->ps_versions_compliancy funcionan de la misma manera.
  • Db::getInstance() — Las consultas directas a la base de datos para tablas específicas del módulo permanecen completamente funcionales.
  • ObjectModel para tablas de módulos — Tus subclases personalizadas de ObjectModel para tablas específicas del módulo siguen funcionando. La deprecación es solo para ObjectModels de entidades del core.
  • API de ConfigurationConfiguration::get() y Configuration::updateValue() no han cambiado.

Lista de verificación de migración

Aquí está la lista de verificación que uso para cada migración de módulo. Imprímela, pégala en tu monitor:

  1. Versión PHP — Probar en PHP 8.1+. Eliminar cualquier hack de compatibilidad con PHP 7.x.
  2. Anotaciones a atributos — Buscar-reemplazar Security\Annotation\ a Security\Attribute\. Convertir @Route a #[Route].
  3. Clase base del controladorFrameworkBundleAdminController a PrestaShopAdminController.
  4. Reemplazar $this->get() — Migrar a inyección por constructor/método.
  5. Verificar bibliotecas eliminadas — Guzzle, SwiftMailer, Tactician. Incluir o reemplazar.
  6. Definiciones de servicios — Verificar que el autowiring funciona. Probar con bin/console debug:container MyService --app-id=admin.
  7. Variables de plantilla — Probar las sobreescrituras de plantillas front-office para estructuras de datos cambiadas.
  8. Hooks eliminados — Verificar si tu módulo usa algún hook de login/producto eliminado.
  9. Archivos PHP raíz — Mover cualquier endpoint AJAX directo a ModuleFrontController.
  10. Actualizar compliancy — Establecer ps_versions_compliancy para incluir 9.x:
$this->ps_versions_compliancy = [
    'min' => '8.0.0',
    'max' => '9.99.99',
];

Compatibilidad dual: soportar PS8 y PS9

La mayoría de los desarrolladores de módulos necesitan soportar ambas versiones durante la transición. Aquí está el patrón que uso:

// Check PrestaShop version at runtime
if (version_compare(_PS_VERSION_, '9.0.0', '>=')) {
    // PS9 path — use new Context services
} else {
    // PS8 path — use legacy Context singleton
}

// For controller base class, use a compatibility layer:
if (class_exists('PrestaShopBundle\Controller\Admin\PrestaShopAdminController')) {
    class MyModuleAdminControllerBase extends PrestaShopAdminController {}
} else {
    class MyModuleAdminControllerBase extends FrameworkBundleAdminController {}
}

class MyActualController extends MyModuleAdminControllerBase
{
    // Your controller code
}

Para las definiciones de servicios, puedes mantener tanto services.yml (PS8) como services.php (PS9)—PrestaShop carga la configuración PHP si está presente, recurriendo a YAML como alternativa.

La conclusión

PrestaShop 9 es una plataforma mejor con una base de framework moderna. El coste de migración es real—estimaría de 2 a 8 horas por módulo dependiendo de la complejidad—pero el resultado es código más mantenible con inyección de dependencias adecuada, seguridad de tipos y acceso al ecosistema completo de Symfony 6.4.

Empieza con la documentación oficial de cambios de PS9 para la lista completa de deprecaciones, y prueba exhaustivamente en un entorno de staging antes de desplegar en producción.

Todos nuestros módulos en mypresta.rocks han sido actualizados para compatibilidad con PS9. Si estás migrando tus propios módulos y te encuentras con algo no cubierto aquí, escríbenos—es probable que ya lo hayamos resuelto.

David Miller lleva construyendo módulos para PrestaShop desde la era de PS 1.6. Mantiene más de 30 módulos de producción en mypresta.rocks.

Artículos Relacionados

  • Qué cambió PrestaShop 9 y por qué importa la compatibilidad de módulos
  • PrestaShop 1.7 vs 8 vs 9: ¿Qué Versión Deberías Usar?
  • Construir módulos para seis versiones de PrestaShop: el reto de la compatibilidad
Compartir esta publicación:
David Miller

David Miller

Más de una década de experiencia práctica con PrestaShop. David desarrolla módulos de comercio electrónico de alto rendimiento centrados en SEO, optimización del checkout y gestión de tiendas....

¿Te gustó este artículo?

Recibe nuestros últimos consejos, guías y actualizaciones de módulos en tu bandeja de entrada.

Comentarios (8)

T
Thomas Mueller 14/02/2026
We just finished migrating our checkout module to PS9. The process was smoother than expected - most hooks work identically. The main pain point was updating our admin controllers from annotations to attributes.
Responder
E
Elena Rodriguez 14/02/2026
Does anyone know if the hook system changes affect actionProductUpdate? We rely heavily on that hook for our inventory sync module.
Responder
D
David Miller 14/02/2026
Elena, actionProductUpdate works the same way in PS9. The hook system is fully backwards-compatible. Your inventory sync module should be fine without changes.
J
Jakub Kowalski 14/02/2026
The migration checklist is super helpful. One thing I would add: test your cron jobs and CLI commands too. We found that some of our module console commands broke because of the Symfony 6.4 DI container changes.
Responder
S
Sophie Laurent 14/02/2026
Great point about CLI commands Jakub! We had a similar issue with our import cron. The fix was updating the service injection in our console command class.
S
Sophie Laurent 14/02/2026
Finally someone explains the API Platform integration clearly. We have been struggling with the old webservice for years. The OAuth2 scoped tokens alone make the upgrade worth it.
Responder
M
Marcus Weber 14/02/2026
Great overview of the PS9 changes! The Doctrine ORM migration is definitely the biggest challenge for us. We have about 15 modules that use ObjectModel extensively. Any tips on a gradual migration path?
Responder
D
David Miller 14/02/2026
Thanks Marcus! For a gradual migration, I recommend starting with read-only Doctrine queries alongside your existing ObjectModel code. That way you can test without risk. Replace ObjectModel writes only after thorough testing.

Dejar un comentario

Cargando...
Volver arriba