Qué se rompe realmente al llevar un módulo a PrestaShop 9
Llevamos migrando nuestro catálogo de más de 150 módulos a PrestaShop 9 desde la versión 9.0 publicada en junio de 2025, y la respuesta honesta es esta: se rompe más de lo que sugiere el registro de cambios. Algunos módulos quedaron listos en una tarde. Unos cuantos nos llevaron casi una semana, porque los controladores de administración se habían escrito contra el contenedor global de servicios y ese patrón ha desaparecido.

Es el documento que construimos internamente durante la migración, con cada cambio incompatible que nos encontramos de verdad, el código antes y después, y las partes en las que todavía no estamos 100% seguros de que la migración esté completa. Si mantienes módulos (para clientes, para Addons o para tus propias tiendas), léelo antes de decirle a nadie que tu módulo es "compatible con PS 9". Hemos aprendido por las malas que "se instala sin un error fatal" no significa lo mismo.
Ordena tu módulo por riesgo antes de tocar código
El mayor error que cometimos en nuestras tres primeras migraciones fue reescribir primero las partes fáciles y descubrir las difíciles el día antes del lanzamiento. No hagas eso. Mira qué tipo de módulo es y dónde está la mayor superficie de cambio.
| Tipo de módulo | Dónde suele romperse en PS 9 | Qué probar primero |
|---|---|---|
| Controlador de administración Symfony moderno | Contenedor con ámbito limitado, cableado de servicios, atributos de ruta, atributos de seguridad | Abrir cada controlador en modo depuración y confirmar que los servicios inyectados por constructor se resuelven de verdad |
| AdminController heredado (Bootstrap / jQuery) | URL con token, puntos de entrada AJAX, acciones masivas, ayudantes de listas | Ejecutar filtros de lista, acciones de fila, modales, puntos de entrada AJAX, botones de exportación y acciones masivas, todos |
| Módulo de front office / tema | Supuestos de plantilla solo válidos para Classic, diferencias de marcado en Hummingbird | Tarjeta de producto, carrito, proceso de compra, cuenta de cliente y cada hook del front en Classic y Hummingbird |
| Compra, pago o envío | Supuestos sobre carrito, transportista, descuento y flujo de pedido alterados por los sistemas de 9.1 | Realizar pedidos de prueba reales: invitado, cuenta, con descuentos, varios transportistas |
| Integración / ERP / exportación | Webservice heredado frente a nueva AdminAPI; advertencias estrictas de PHP 8.5 | Hacer una prueba rápida en 8.2, 9.0 y 9.1 antes de cambiar el contrato de integración |
| Correo electrónico / notificación | SwiftMailer ha desaparecido; Symfony Mailer renderiza plantillas de otra forma | Enviar cada tipo de correo transaccional y revisar cabeceras, variables y adjuntos |
Nuestros módulos del flujo de compra fueron los que más tiempo llevaron. Cualquier cosa que tocara la página de pedidos en administración o la selección de transportista tuvo que revisarse línea por línea. El "pequeño módulo de utilidad que solo añade un banner" nos llevó media mañana.
Actualización de mayo de 2026: el objetivo real ahora es 9.1, no 9.0
Cuando empezamos esta migración, la respuesta era "compatible con 9.0". Un año después, 9.1 es lo que instalan los comerciantes, y 9.0 es un peldaño del que la gente se aleja al actualizar. Si tu matriz de CI sigue fijada a 9.0, estás probando el PrestaShop de ayer.
Se supone que 9.1 es compatible hacia atrás con 9.0, y en gran parte lo es. Pero añade compatibilidad con PHP 8.5, incluye Hummingbird como tema predeterminado en instalaciones nuevas, introduce nuevos hooks de ciclo de vida y configuración, añade comandos CLI para miniaturas / búsqueda / traducciones, y trae sistemas de multienvío y descuentos bajo feature flags. Cualquiera de esos cambios puede romper en silencio un módulo que funcionaba bien en 9.0.
La versión de mantenimiento 9.1.3 de mayo de 2026 incorporó actualizaciones de dependencias de Symfony y Twig sin cambios en el código del núcleo: el tipo de versión de parche que "no debería afectar a los módulos" y luego sí lo hace, la primera vez que alguien pasa un valor que no es string a un filtro de Twig. Prueba en la rama 9.x mantenida, no solo en la etiqueta original 9.0. En la tabla siguiente, el salto de Symfony 4.4 a 6.4 corresponde a la migración de 8.x a 9.0; 9.1 actualiza sus dependencias.
Qué cambió entre 9.0 y 9.1, lo que volvemos a probar
| Área | Qué cambió | Qué volvemos a probar |
|---|---|---|
| Entorno de ejecución | 9.0 requiere PHP 8.1+ y admite hasta 8.4; 9.1 añade y recomienda 8.5 | Ejecutar el módulo en la versión de PHP más baja y más alta que declara tu composer.json |
| Pila Symfony | Symfony 4.4 → 6.4 LTS, aplican los cambios incompatibles de Symfony 5 y 6 | Herencia de controladores, anotaciones, eventos, definiciones de servicios, paquetes de framework eliminados |
| Hooks y CLI | 9.1 añade hooks de ciclo de vida, un hook de actualización de configuración y comandos CLI para miniaturas / búsqueda / exportación de traducciones | Instalación, activación, desactivación, actualización, guardado de configuración, regeneración de miniaturas, reindexación de búsqueda, exportación de traducciones |
| Hummingbird | Tema predeterminado en instalaciones nuevas de 9.1; las tiendas actualizadas conservan su tema | Salida del front office tanto en Classic como en Hummingbird, cada plantilla de producto, carrito, proceso de compra y cuenta |
| Descuentos | 9.1 introduce una interfaz y arquitectura de descuentos rediseñadas sobre las reglas de carrito | Cualquier cosa que lea, cree, combine, valide o muestre reglas de carrito, cupones y promociones |
| Base de datos / pedidos | 9.1 trae cambios de esquema para multienvío y el nuevo sistema de descuentos | Código de exportación de pedidos, factura, preparación, transportista y ERP contra fixtures de pedidos reales |
| Parches de seguridad | 9.1.3 actualizó dependencias de Symfony y Twig | Pasada rápida de regresión tras cada parche, sobre todo si tocas YAML, XML, Twig o destinatarios de correo |
El salto a Symfony 6.4: por qué hubo que cambiar tanto
Symfony 4.4 llegó al fin de su vida útil en noviembre de 2023. PrestaShop 8.x ya se distribuía sobre una versión de framework sin soporte, algo incómodo para cualquiera que leyera sus registros de errores PHP. Saltar directamente a 6.4 LTS (con soporte hasta noviembre de 2027) fue la decisión correcta, pero se saltó dos versiones mayores del framework en una sola versión, y todos los cambios incompatibles de Symfony 5 y 6 llegan a la vez.
Las implicaciones prácticas:
- PHP 8.1 como mínimo, cualquier capa de compatibilidad con PHP 7.x en tu módulo debe desaparecer. 8.2 a 8.4 son compatibles con 9.0; 9.1 añade 8.5.
- Los controladores son servicios reales, el patrón de "contenedor global, coger lo que necesites" ha muerto. Usa la inyección por constructor o por método, o la vía de migración documentada mediante el localizador de servicios; el contenedor que recibe tu controlador tiene ámbito limitado.
- Atributos, no anotaciones,
@Route,@AdminSecurity,@DemoRestrictedpasan todos a la sintaxis de atributos de PHP 8.sensio/framework-extra-bundleya no existe. - Bibliotecas incluidas eliminadas, Guzzle, SwiftMailer, Tactician. Si las usabas, debes incluirlas tú mismo o migrar.
- Node 20 como mínimo para módulos con paso de compilación. 9.1 compila contra Node 20.19.5, así que fija tus herramientas en lugar de cruzar los dedos con el Node que haya en la máquina de desarrollo.
Cambio incompatible n.º 1: controladores. El contenedor global ha desaparecido
Este fue el cambio que más tiempo nos consumió. En PS 8 podíamos obtener cualquier servicio desde cualquier controlador con $this->get('service.name'). En PS 9, el contenedor entregado al controlador tiene ámbito limitado: solo contiene los servicios que has declarado explícitamente, y la llamada desnuda a $this->get() lanza una excepción o devuelve null según el servicio.
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,
];
}
}
Los cambios principales:
FrameworkBundleAdminControllerestá obsoleto. UsaPrestaShopAdminController.- La inyección por constructor sustituye a
$this->get()para todo lo que el controlador necesita siempre. - La inyección por método cubre las dependencias específicas de una acción.
getSubscribedServices()es la vía de escape para un controlador migrado a medias. Úsala para mantenerlo funcionando mientras refactorizas; no la entregues como estado final.
Algo que nos mordió: si tu servicio decora un servicio del núcleo, el autowiring de la versión decorada deja de funcionar en cuanto un tercer módulo de la cadena no se autoconfigura correctamente. Nos pasó en un módulo que decora el calculador del carrito y pasamos medio día mirando un services.yml perfectamente correcto antes de darnos cuenta de que el problema era otro módulo instalado. La decoración de servicios en PS 9 no perdona el orden.
Cambio incompatible n.º 2: las anotaciones pasan a ser atributos PHP 8
La dependencia sensio/framework-extra-bundle ha desaparecido. Todas las anotaciones de rutas y seguridad deben convertirse a atributos nativos. Es un cambio casi mecánico, pero tiene una trampa.
Rutas
// 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
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
El namespace cambia de Security\Annotation\ a Security\Attribute\. Buscar y reemplazar resuelve la mayor parte. Si se te escapa, tu ruta se ejecuta sin restricciones de seguridad, lo cual es peor que un fallo: el módulo parece funcionar y expone discretamente una acción de administración sin autorización. Haz grep en tu base de código buscando Security\Annotation después de la migración para confirmar que no queda ninguno.
Listeners de eventos
// 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 incompatible n.º 3: definiciones de servicios. YAML sigue funcionando, PHP es más limpio
Los servicios YAML siguen funcionando en PS 9 y los mantuvimos en los módulos donde el YAML ya era correcto. El trabajo nuevo va a configuración PHP con autowiring, porque las restricciones que Symfony 6.4 impone al contenedor de servicios son más fáciles de cumplir cuando la configuración está en el mismo lenguaje que los servicios.
Antes (YAML en PS 8)
# 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 (configuración PHP en PS 9 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%');
};
Una vez activado el autowiring, solo escribes definiciones explícitas para parámetros escalares, enlaces de interfaces cuando existen varias implementaciones y clases de terceros que no pueden cablearse automáticamente. Si llevas diez años escribiendo definiciones de servicios a mano por costumbre, esta parte parece sospechosa. Funciona. Confía en ello.
Cambio incompatible n.º 4: bibliotecas que ya no vienen incluidas
PrestaShop 9 dejó de incluir cuatro bibliotecas que habíamos visto en docenas de módulos antiguos. Si tu composer.json las requiere como dependencia de primer nivel, no pasa nada: se instalan para tu módulo. Si asumías que siempre estaban disponibles porque PS 8 las traía, tienes trabajo pendiente.
| Eliminada | Sustitución | Esfuerzo de migración |
|---|---|---|
guzzlehttp/guzzle | symfony/http-client | Medio, forma de API diferente |
swiftmailer/swiftmailer | symfony/mailer | Medio, nueva API de objeto Email |
league/tactician-bundle | symfony/messenger | Alto, modelo de bus de comandos diferente |
pear/archive_tar | Inclúyelo en tu módulo o usa ext-zip | Bajo |
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);
}
Algo que la documentación no grita lo suficiente: el cifrado SSL para correo ha desaparecido en PS 9. Solo quedan TLS o sin cifrado. Si la tienda de un cliente estaba configurada con SSL en su relay SMTP, el correo saliente deja de funcionar silenciosamente después de la actualización. Hemos visto una actualización a 9.0 completarse sin problemas y luego los clientes dejaron de recibir confirmaciones de pedido durante dos días antes de que alguien lo notara. Revisa la configuración de correo en cada tienda que actualices.
Cambio incompatible n.º 5: los servicios Context tipados sustituyen al singleton
El singleton heredado Context::getContext() sigue funcionando en PS 9: está obsoleto, no eliminado. El nuevo patrón son servicios Context tipados inyectados por el contenedor, y funciona en lugares donde el singleton nunca funcionó bien (comandos CLI, workers asíncronos, cualquier sitio donde el Context heredado no se inicializara desde el front controller).
// 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();
}
El conjunto completo de servicios de contexto en PS 9: EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext, LegacyControllerContext. El singleton heredado se queda por ahora y tu código existente sigue ejecutándose. Estamos migrando módulo a módulo, no de una sola vez, y eso nos ha funcionado bien.
Cambio incompatible n.º 6: la autenticación de administración ahora es completamente Symfony
El flujo de inicio de sesión del back office pasó a Symfony Security. Si tu módulo lee datos de empleados desde Context::$cookie (y hemos visto muchos módulos antiguos que lo hacen), esa ruta no es fiable en PS 9.
// 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 incompatible n.º 7: hooks eliminados
Varios hooks del flujo de inicio de sesión en administración han desaparecido sin sustitución directa.

actionAdminLoginControllerBeforeactionAdminLoginControllerLoginBefore/actionAdminLoginControllerLoginAfteractionAdminLoginControllerForgotBefore/actionAdminLoginControllerForgotAfteractionAdminLoginControllerResetBefore/actionAdminLoginControllerResetAfter
Para personalizar el formulario de inicio de sesión, los nuevos hooks son:
actionBackOfficeLoginForm, modifica el constructor del formulario de inicio de sesiónactionEmployeeRequestPasswordResetForm, modifica el formulario de restablecimiento de contraseña
También se eliminan todos los hooks actionAdminProductsController*, porque la página de producto heredada ya no existe. Si tu módulo añadía campos extra a la antigua página de producto mediante uno de esos hooks, tienes que reescribir esa parte contra la página de producto Symfony, que es un trabajo bastante mayor que cambiar el nombre de un hook.
Cambio incompatible n.º 8: las plantillas del front office ahora usan datos de Presenter
Varias páginas del front (categoría, fabricante, proveedor, tienda) pasaron a clases Presenter. Las variables Smarty que exponen tienen una forma distinta. Si tu módulo sobrescribe o amplía una de estas plantillas, las referencias a variables deben cambiar.
{* 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'}
Cada página afectada tiene un hook actionPresent* correspondiente para que puedas modificar los datos presentados en lugar de sobrescribir la plantilla. Usa ese hook siempre que puedas: sobrevive a cambios de tema y a reescrituras de plantillas en Hummingbird, y ya hemos pasado bastante tiempo arreglando sobrescrituras de plantillas tras revisiones de Hummingbird como para no querer ser dueños de ellas.
Cambio incompatible n.º 9: se acabaron los puntos de entrada PHP independientes en la raíz
Sigues necesitando el archivo PHP principal del módulo. El patrón que ahora es problemático es un ajax.php o callback.php invocable directamente en la raíz del módulo e incluyendo config/config.inc.php a mano. Esos puntos de entrada se saltan el enrutamiento, los tokens, la detección SSL, el contexto de cliente y el resto del ciclo de vida normal de la petición, y las herramientas de seguridad de PS 9 los tratan como una mala señal.
// 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 incompatible n.º 10: comandos de consola y separación en múltiples kernels
PS 9 divide la aplicación en tres kernels Symfony: admin, admin-api y front. Los comandos de consola aceptan --app-id para apuntar a un kernel concreto.
# 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
Si tu módulo incluye un comando de consola, asegúrate de que está registrado en el kernel correcto. Teníamos un comando CLI para reindexar la búsqueda registrado sin especificar kernel: funcionaba bien en PS 8, pero en PS 9 ni siquiera se descubría hasta que añadimos la etiqueta adecuada.
Qué no cambió (las partes que no tuvimos que reescribir)
No todo se rompió. Estos patrones sobreviven intactos en PS 9.
- El sistema de hooks,
hookDisplayHeader()yhookActionProductUpdate()funcionan como antes; los hooks eliminados de inicio de sesión y administración de productos enumerados arriba ya no funcionan. Consulta nuestra referencia de hooks para ver los patrones que sobrevivieron al salto de versión. - Plantillas Smarty del front. La migración a Twig es solo para administración. Smarty en el front office se mantiene.
- Descriptor del módulo,
$this->name,$this->version,$this->ps_versions_compliancyno cambian. Db::getInstance(), las consultas directas a la base de datos para las tablas propias de tu módulo siguen funcionando.- ObjectModel para tablas de módulo, tus subclases ObjectModel personalizadas para tablas específicas del módulo siguen funcionando. La obsolescencia afecta a los ObjectModels de entidades del núcleo, no a los tuyos.
- API de configuración,
Configuration::get()/Configuration::updateValue()no cambian.
La matriz de pruebas que ejecutamos de verdad
Una instalación en verde en PS 9 no equivale a compatibilidad. Lo aprendimos con un módulo que pasó nuestra prueba rápida, se entregó a dos tiendas de clientes y luego falló cuando el comerciante intentó usar el botón de acción masiva. La lista siguiente es la matriz que ejecutamos en cada módulo comercial antes de marcarlo como "compatible con 9.x".
| Objetivo | Por qué importa | Cómo se ve un "aprobado" |
|---|---|---|
| PrestaShop 8.2 en PHP 8.1 / 8.2 | Muchas tiendas en producción se quedarán en PS 8 mientras prueban 9 | La compilación estable existente sigue instalándose, actualizándose y ejecutándose sin ruido de deprecaciones que bloquee el trabajo normal |
| PrestaShop 9.0 en PHP 8.1–8.4 | Esto prueba la migración de versión mayor: Symfony 6.4, PHP 8.1 mínimo, bibliotecas eliminadas | Cada controlador, hook, tarea cron, punto de entrada AJAX y ruta de correo funciona en un 9.0 limpio |
| Rama PrestaShop 9.1 | El objetivo práctico actual: lo que instalan de verdad los comerciantes | El módulo sobrevive a Hummingbird por defecto, cambios de descuentos/pedidos y las últimas versiones de parche |
| Temas Classic y Hummingbird | Las instalaciones nuevas de 9.1 usan Hummingbird; las tiendas actualizadas conservan temas derivados de Classic | Los hooks del front office se renderizan sin diseño roto, selectores ausentes, contenido duplicado ni errores JS |
| Instalación nueva y actualización desde una versión anterior del módulo | El SQL de actualización y los valores de configuración por defecto fallan de forma distinta a una instalación nueva | Ambos caminos producen las mismas tablas finales, valores de configuración, pestañas, hooks y permisos |
| Tienda con modo depuración activado | Las advertencias ocultas en producción se convierten después en tickets de soporte | Sin índices indefinidos, advertencias de propiedades dinámicas, deprecaciones ni excepciones del contenedor durante el uso normal |
La fila "instalación nueva y actualización" no es negociable. La mayoría de los peores errores que hemos entregado a lo largo de los años estaban en la ruta de actualización: la instalación nueva siempre funcionaba porque probábamos la instalación nueva. La tienda que se rompía era la de un cliente existente, cuya base de datos tenía valores que nuestro SQL de actualización no contemplaba.
Nuestra lista de comprobación de migración
La guardamos en un archivo markdown en cada repositorio de módulo. Imprímela, pégala, haz con ella lo que quieras.
- Versión de PHP, probar en 8.1+. Eliminar capas de compatibilidad con PHP 7.x.
- Anotaciones a atributos, buscar y reemplazar
Security\Annotation\porSecurity\Attribute\. Convertir@Routea#[Route]. Hacer grep para encontrar supervivientes. - Clase base del controlador, de
FrameworkBundleAdminControlleraPrestaShopAdminController. - Sustituir
$this->get(), mover a inyección por constructor o por método. - Bibliotecas eliminadas, Guzzle, SwiftMailer, Tactician, pear/archive_tar. Incluirlas o sustituirlas.
- Definiciones de servicios, confirmar que el autowiring funciona.
bin/console debug:container MyService --app-id=admin. - Variables de plantilla, probar las sobrescrituras de plantillas del front office contra la nueva forma de Presenter.
- Hooks eliminados, hacer grep de los hooks obsoletos de inicio de sesión y producto.
- Puntos de entrada PHP independientes, mover cualquier archivo AJAX o callback directo a
ModuleFrontController. - Actualizar compatibilidad, configurar
ps_versions_compliancypara incluir 9.x:
$this->ps_versions_compliancy = [
'min' => '8.0.0',
'max' => '9.99.99',
];
Una advertencia honesta sobre el último punto: solo fijamos el límite superior en 9.99.99 en módulos donde hemos ejecutado la matriz completa de pruebas contra la última versión 9.1. Ponerlo en un módulo al que solo has hecho una prueba rápida es la mentira que rompe tiendas de clientes.
Dar soporte a PS 8 y PS 9 desde la misma base de código
En módulos comerciales no podemos publicar dos versiones y esperar que los clientes elijan la correcta. El patrón que nos ha funcionado es una comprobación de versión en tiempo de ejecución con dos rutas de código, y una clase base de compatibilidad.
// 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 servicios, puedes mantener services.yml (PS 8) y services.php (PS 9) en el mismo módulo. PrestaShop carga el archivo PHP si existe y recurre a YAML si no.
Este patrón de doble base de código vive en nuestro paquete compartido prestashop-compat y lo ampliamos a medida que encontramos diferencias nuevas. Cada método obsoleto o eliminado en una futura versión de PS se envuelve, y el wrapper detecta la versión en tiempo de ejecución. Tools::displayPrice() es el ejemplo canónico: desapareció en 9.0, fue sustituido por el formateador consciente de la configuración regional y está envuelto en \MyPrestaRocks\Compat\PriceFormatter::format(), así solo lo arreglamos en un sitio.
Restricciones de Composer que publicamos de verdad
Resuelve las dependencias contra la versión de PHP más baja que afirmas soportar, y luego prueba por separado contra la más alta. Si no, composer elegirá felizmente bibliotecas que necesitan 8.4 y tu afirmación de "8.1+" será ficción.
{
"require": {
"php": ">=8.1 <8.6",
"symfony/http-client": "^6.4"
},
"config": {
"platform": {
"php": "8.1.0"
}
}
}
matrix:
include:
- prestashop: "8.2"
php: "8.1"
- prestashop: "9.0"
php: "8.4"
- prestashop: "9.1"
php: "8.5"
theme: "hummingbird"
Qué le diríamos a un desarrollador que empieza su primera migración a PS 9
PrestaShop 9 es una plataforma mejor que 8. Symfony 6.4 está al día, los servicios de contexto tipados son más agradables de trabajar que el singleton, el autowiring elimina una clase de errores que solíamos tener, y la admin-api junto al Webservice heredado da a los integradores un camino hacia adelante. El coste es real: entre media jornada para un pequeño módulo de utilidad y una semana para cualquier cosa que toque la página de producto en administración o el flujo de compra.
Hay dos cosas en las que insistiríamos, después de haber hecho esto catorce veces. No afirmes compatibilidad con 9.x por esperanza. Ejecuta la matriz completa de pruebas o marca el módulo como "probado en 9.0, 9.1 en curso" hasta que lo hayas ejecutado de verdad en 9.1 con Hummingbird. Hemos visto módulos de la categoría "debería ir bien, no toca la administración" acabar disparando un hook que ya no existe. Y no intentes hacerlo todo a la vez. Elige un módulo, haz la migración de principio a fin, apunta lo aprendido y el segundo módulo llevará la mitad de tiempo. El quinto ya es rutina.
Empieza por la documentación oficial de cambios en 9.0 y las notas para desarrolladores de 9.1 para la lista canónica de impactos en módulos. Prueba en staging antes de acercarte a una tienda de cliente. Y si usas uno de nuestros módulos y encuentras algo que no esté cubierto aquí, escríbenos: probablemente ya lo hemos resuelto en uno de los otros 147.
Preguntas frecuentes
¿Puedo seguir extendiendo FrameworkBundleAdminController en PrestaShop 9?
Sigue resolviéndose porque PrestaShop mantiene una capa de compatibilidad, pero está obsoleto y previsto para eliminación en PrestaShop 10. Así que un módulo que renderiza bien en 9 mientras todavía lo extiende tiene una fecha de caducidad conocida. Trata "hoy funciona" como una prórroga, no como un aprobado. Los nuevos controladores de administración deberían extender PrestaShopAdminController con inyección por constructor; migra los antiguos antes de que PS 10 te obligue.
¿Sigue funcionando Context::getContext(), o tengo que reescribirlo todo a los servicios de contexto tipados?
El singleton heredado sigue funcionando en 9. Está obsoleto, no eliminado, así que tu código existente sigue ejecutándose. No necesitas una reescritura de golpe. Migramos módulo a módulo hacia los servicios tipados (EmployeeContext, ShopContext, LanguageContext, CurrencyContext y el resto), normalmente cuando ya estamos en un archivo arreglando otra cosa. El único lugar que conviene migrar antes es el código que se ejecuta fuera de una petición normal del front controller, comandos CLI y workers, donde el singleton nunca se inicializaba de forma fiable.
¿Cuál es la razón más habitual por la que un módulo se instala limpiamente en 9 y luego acaba con un error fatal?
Una biblioteca del núcleo eliminada que el módulo asumía que siempre estaba disponible: Guzzle, SwiftMailer o Tactician. El módulo carga, la página de configuración se renderiza, y solo muere la primera vez que se ejecuta la ruta de código relevante (un cron, un envío de correo, una llamada HTTP). Si tu composer.json no incluye la biblioteca en el propio vendor/ del módulo, tendrás un fatal de clase no encontrada en el peor momento posible. Haz grep de use GuzzleHttp, Swift_ y League\Tactician antes de publicar.
¿Debo apuntar a 9.0 o a 9.1 en mi matriz de CI?
9.1: es lo que instalan ahora los comerciantes, y 9.0 es un peldaño del que la gente se aleja al actualizar. Prueba en la rama 9.x mantenida (9.1.3 en el momento de escribir esto), no solo en la etiqueta original 9.0, porque las versiones de parche incorporan subidas de dependencias de Symfony y Twig que "no deberían" afectar a los módulos y a veces lo hacen. Si tu matriz sigue fijada a 9.0, estás probando el PrestaShop de ayer.
¿Es seguro fijar el máximo de ps_versions_compliancy en 9.99.99?
Solo en un módulo donde hayas ejecutado de verdad la matriz completa de pruebas contra la última versión 9.1, tanto en Classic como en Hummingbird. Fijar un límite superior amplio en un módulo al que solo has hecho una prueba rápida es la afirmación que rompe tiendas de clientes en la siguiente versión menor. Si has verificado 9.0 pero no 9.1, dilo explícitamente en lugar de taparlo con un techo optimista.
Lecturas relacionadas
- PrestaShop 1.7 vs 8 vs 9: ¿qué versión deberías usar?
- Qué cambió en PrestaShop 9 y por qué importa la compatibilidad de módulos
- PrestaShop 9.1: Hummingbird, multitransportista e impacto en módulos
- Hooks y overrides de PrestaShop, referencia para desarrolladores
- Guía de la API Webservice de PrestaShop
- Lista de comprobación de endurecimiento de seguridad en PrestaShop
- Guía de resolución de problemas y recuperación de PrestaShop