Actualización de Pimcore 10 a 12: La Ruta Real de Migración
La guía definitiva para actualizar Pimcore 10 a 12. PHP 8.3, Symfony 7, DBAL 4, migración de collation, sistema de eventos, ExtJS 7, Redis, OpenSearch, Kubernetes, Flysystem, y cada bug no documentado que encontramos.
Por Qué No Puedes Saltarte Pimcore 11
Lo primero que pregunta cada equipo: ¿podemos saltar directamente de la 10 a la 12? No. Pimcore 11 es el paso intermedio obligatorio. Los cambios del framework son demasiado grandes para absorberlos de un solo golpe, y las herramientas de migración de Pimcore asumen que la versión intermedia está instalada.
Aquí tienes el alcance completo de lo que cambia entre las dos versiones mayores:
| Componente | Pimcore 10.6 | Pimcore 11 | Pimcore 12 |
|---|---|---|---|
| PHP | 8.0+ | 8.1+ | 8.3+ |
| Symfony | 5.4 | 6.2+ | 6.4 / 7.x |
| Doctrine DBAL | 2.x / 3.x | 3.x | 4.x |
| Admin UI | Integrado (ExtJS 6) | Bundle separado (ExtJS 6/7) | Bundle separado (ExtJS 7) + Studio UI (React) |
| Licencia | GPLv3 | GPLv3 | POCL (Pimcore Open Core License) |
| Bundles core | Monolítico | Extraídos | Extraídos |
| Backend de búsqueda | Integrado | Simple Backend Search Bundle | Generic Data Index + OpenSearch |
| WYSIWYG | TinyMCE integrado | TinyMCE bundle | TinyMCE o Quill bundle |
| Registro de producto | Ninguno | Ninguno | Obligatorio (validación offline) |
| Sistema de eventos (JS) | Plugin Broker | Event Listeners | Event Listeners + eventos Studio UI |
| Collation | utf8mb4_general_ci | utf8mb4_general_ci | utf8mb4_unicode_520_ci (obligatorio) |
| Caché | Adaptador Pimcore | Adaptador Symfony | Adaptador Symfony (cambio de config) |
| Full page cache | Integrado | Integrado | Opción como bundle separado |
| Almacenamiento de assets | Local / Flysystem | Local / Flysystem | Local / Flysystem (con bugs en remoto) |
Hemos hecho esta actualización en múltiples instalaciones enterprise de Pimcore, desde PIMs B2B hasta plataformas CMS multi-sitio. Los patrones descritos aquí son genéricos. Para contexto sobre cómo abordamos las implementaciones PIM y la arquitectura de sistemas, esas guías cubren nuestra metodología general. Nuestra página de metodología explica cómo planificamos este tipo de migraciones de alto riesgo.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Pimcore 10.6 │ ──▶ │ Pimcore 11.x │ ──▶ │ Pimcore 12.x │
│ Symfony 5.4 │ │ Symfony 6.2+ │ │ Symfony 6.4/7 │
│ PHP 8.0+ │ │ PHP 8.1+ │ │ PHP 8.3+ │
│ DBAL 2.x/3.x │ │ DBAL 3.x │ │ DBAL 4.x │
│ ExtJS 6 │ │ ExtJS 6/7 │ │ ExtJS 7 + │
│ │ │ │ │ Studio UI │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Fase 1: Preparación (Antes de Tocar Composer)
Esta fase es la que más tiempo lleva y la que más tiempo ahorra. Cada hora invertida aquí te ahorra días de depuración después.
1.1 Checklist Pre-Actualización
# Verificar la versión actual
bin/console --version
# Asegurarse de que todas las migraciones están al día
bin/console doctrine:migrations:up-to-date
bin/console doctrine:migrations:migrate
# Vaciar todas las colas de mensajes antes de actualizar
# Los workers deben terminar de procesar antes de cambiar el código
bin/console messenger:consume --limit=0
Haz backup de todo: base de datos (mysqldump completo), archivos (directorio de assets, directorio var, directorio config), y crea una rama dedicada para la actualización en control de versiones. Prueba tu procedimiento de restauración antes de empezar.
1.2 Métodos Deprecated de Doctrine (DBAL 2/3 a 4)
Este es el paso de preparación más laborioso. Cada clase de repositorio, cada consulta SQL cruda, cada servicio personalizado que toca la base de datos necesita actualización. En una instalación enterprise típica, espera entre 50 y 150 puntos de cambio.
| Deprecated (DBAL 2/3) | Reemplazo (DBAL 4) | Notas |
|---|---|---|
$db->query($sql) | $db->executeQuery($sql) | El tipo de retorno cambia a Result |
$db->executeUpdate($sql) | $db->executeStatement($sql) | Devuelve la cantidad de filas afectadas como int |
$db->fetchRow() | $db->fetchAssociative() | Devuelve un array asociativo o false |
$db->fetchAll() | $db->fetchAllAssociative() | Devuelve un array de arrays asociativos |
$db->fetchCol() | $db->fetchFirstColumn() | Devuelve un array indexado |
$db->fetchColumn() | $db->fetchOne() | Obtiene un valor único |
$db->fetchPairs() | Pimcore\Db\Helper::fetchPairs() | Helper de Pimcore |
$db->insertOrUpdate() | Pimcore\Db\Helper::upsert() | Helper de Pimcore |
$db->quoteInto() | Pimcore\Db\Helper::quoteInto() | Helper de Pimcore |
$db->deleteWhere() | $db->executeStatement() con DELETE | SQL manual |
$db->updateWhere() | $db->executeStatement() con UPDATE | SQL manual |
$db->quote($val, $type) | $db->quote($val) | Solo strings en DBAL 4, sin parámetro de tipo |
Pimcore\Db\ConnectionInterface | Doctrine\DBAL\Connection | Cambio de type hint en DI |
Pimcore\Db\Connection | Doctrine\DBAL\Connection | Reemplazo de clase |
# Buscar todas las llamadas a métodos deprecated en tu código
grep -rn "->query(" src/ --include="*.php"
grep -rn "->fetchRow(" src/ --include="*.php"
grep -rn "->fetchAll(" src/ --include="*.php" | grep -v "fetchAllAssociative"
grep -rn "->fetchCol(" src/ --include="*.php"
grep -rn "->insertOrUpdate(" src/ --include="*.php"
grep -rn "->quoteInto(" src/ --include="*.php"
grep -rn "->deleteWhere(" src/ --include="*.php"
grep -rn "->executeUpdate(" src/ --include="*.php"
grep -rn "Pimcore\\Db\\Connection" src/ --include="*.php"
Ejemplo completo de migración para una clase de repositorio:
// ANTES (Pimcore 10 / DBAL 2-3)
use Pimcore\Db\ConnectionInterface;
class PriceExporter
{
public function __construct(
private readonly ConnectionInterface $connection
) {}
public function export(int $limit, int $offset): array
{
$sql = "SELECT p.o_id, p.price FROM object_store_1 p WHERE p.o_published = 1 LIMIT ? OFFSET ?";
return $this->connection->query($sql)->fetchAll();
}
}
// DESPUÉS (Pimcore 12 / DBAL 4)
use Doctrine\DBAL\Connection;
class PriceExporter
{
public function __construct(
private readonly Connection $connection
) {}
public function export(int $limit, int $offset): array
{
$sql = "SELECT p.id, p.price FROM object_store_1 p WHERE p.published = 1 LIMIT ? OFFSET ?";
return $this->connection->executeQuery($sql, [$limit, $offset])->fetchAllAssociative();
}
}
Actualiza tu services.yaml si usas inyección de dependencias explícita:
services:
App\Export\PriceExporter:
arguments:
$connection: '@doctrine.dbal.default_connection'
1.3 Declaraciones de Tipos de Retorno
Pimcore 11+ impone tipos de retorno estrictos en las clases modelo. Cada clase que extiende clases base de Pimcore necesita actualización:
// Antes (Pimcore 10)
public function save()
{
// lógica personalizada
parent::save();
}
// Después (Pimcore 11+)
public function save(array $parameters = []): static
{
// lógica personalizada
return parent::save($parameters);
}
Esto afecta a cada clase de modelo DataObject, cada listing personalizado y cada servicio que sobreescribe métodos de Pimcore. Revisa todas las clases en tu directorio src/Model/.
1.4 Eliminar el Prefijo o_
Pimcore 11 elimina el prefijo o_ de los nombres de columnas en la tabla objects. Cada consulta SQL cruda y cada condición de listing que haga referencia a estas columnas necesita actualización:
| Antiguo (Pimcore 10) | Nuevo (Pimcore 11+) | Contexto |
|---|---|---|
o_id | id (o oo_id en tablas object store) | Clave primaria |
o_creationDate | creationDate | Timestamp |
o_modificationDate | modificationDate | Timestamp |
o_path | path | Ruta del árbol |
o_key | key | Clave/slug del objeto |
o_published | published | Flag de publicación |
o_parentId | parentId | Referencia al padre |
o_type | type | Tipo de objeto |
o_className | className | Nombre de clase |
o_classId | classId | ID de clase |
o_userOwner | userOwner | ID del usuario creador |
o_userModification | userModification | ID del último modificador |
o_versionCount | versionCount | Contador de versiones |
# Buscar todo el uso del prefijo o_ en PHP y JavaScript
grep -rn "o_id\|o_path\|o_key\|o_published\|o_parentId\|o_type\|o_className\|o_classId" src/ --include="*.php"
grep -rn "o_className\|o_id" public/ --include="*.js"
Ejemplo de migración de condiciones de listing:
// Antes
$listing->setCondition('o_path LIKE ? AND o_published = 1', ['/products/%']);
// Después
$listing->setCondition('path LIKE ? AND published = 1', ['/products/%']);
1.5 Cambios Incompatibles en PHP 8.3 / 8.4
Más allá de lo que requiere Pimcore, PHP 8.3+ introduce sus propios cambios incompatibles:
Parámetros implícitamente nullable (deprecated en 8.4):
// DEPRECATED: nullable implícito
public function findByLocale(string $locale = null): array
// CORREGIDO: nullable explícito
public function findByLocale(?string $locale = null): array
Esto afecta a cada firma de método con = null como valor por defecto. En un codebase enterprise típico, espera más de 50 ocurrencias. Usa Rector para automatizar:
// rector.php
use Rector\Config\RectorConfig;
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src']);
$rectorConfig->rule(ExplicitNullableParamTypeRector::class);
};
# Vista previa de cambios
vendor/bin/rector process --dry-run
# Aplicar cambios
vendor/bin/rector process
Otras funcionalidades de PHP 8.3+ que puedes adoptar:
// Constantes de clase tipadas (PHP 8.3)
public const string DEFAULT_LOCALE = 'en';
public const array SUPPORTED_TYPES = ['product', 'category'];
// Atributo #[\Override] (PHP 8.3)
#[\Override]
public function normalize(mixed $object, ?string $format = null, array $context = []): array
// json_validate() (PHP 8.3)
if (json_validate($input)) { /* ... */ }
1.6 Migración del Sistema de Eventos JavaScript
El antiguo sistema plugin broker se elimina en Pimcore 11. Todo el JavaScript del admin UI debe migrar:
// ANTES (Pimcore 10 - Plugin Broker)
pimcore.plugin.datasheet = Class.create(pimcore.plugin.admin, {
getClassName: function () {
return "pimcore.plugin.datasheet";
},
initialize: function () {
pimcore.plugin.broker.registerPlugin(this);
},
postOpenObject: function (object, type) {
if (object.data.general.o_className === 'Product') {
// Agregar botón a la barra de herramientas
}
}
});
// DESPUÉS (Pimcore 11+ - Event Listeners)
(function() {
'use strict';
document.addEventListener(pimcore.events.postOpenObject, (e) => {
let object = e.detail.object;
if (object.data.general.className === 'Product') { // o_className -> className
object.toolbar.add({
text: t('Generate PDF'),
iconCls: 'pimcore_icon_pdf',
handler: function () {
Ext.Ajax.request({
url: '/api/generate-pdf/' + object.id,
success: function (response) {
pimcore.helpers.showNotification(
t("success"), t("PDF generated"), "success"
);
}
});
}
});
pimcore.layout.refresh();
}
});
})();
Cambios adicionales en JavaScript:
| Antiguo (Pimcore 10) | Nuevo (Pimcore 11+) | Notas |
|---|---|---|
ts() | t() | Función de traducción |
pimcore.helpers.addCsrfTokenToUrl() | Eliminar por completo | CSRF se maneja de otra forma |
o_className en JS | className | Coincide con el cambio en PHP |
Class.create(...) | Sigue soportado, pero mejor usar clases ES6 | Prepararse para el futuro |
/admin/tags/* | Puede cambiar a /pimcore-admin/tags/* | Verifica tus rutas de admin |
1.7 Cambios en ExtJS 7 (Pimcore 12)
Pimcore 12 usa ExtJS 7.x. Cambios incompatibles clave para código personalizado del admin UI:
// ExtJS 6 (Pimcore 10/11)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
root: 'data' // DEPRECATED en ExtJS 7
}
}
});
// ExtJS 7 (Pimcore 12)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
rootProperty: 'data' // Usar rootProperty
}
}
});
Verifica todos los operadores de grid personalizados, plugins de admin y extensiones de data importer para compatibilidad con ExtJS 7. Problemas comunes:
- La propiedad
rootfue renombrada arootPropertyen los readers de store - Algunas configuraciones de columnas de grid cambiaron
- El patching de prototipos (
Ext.override) puede romperse si la estructura interna de clases cambió - Verifica que todas las referencias a
iconClssigan existiendo en el set de iconos de Pimcore 12
1.8 Pimcore Studio UI (React)
Pimcore 12 introduce el nuevo Studio UI construido con React, que se ejecuta junto al admin clásico de ExtJS. No es un reemplazo todavía, sino un complemento:
- Studio UI gestiona algunas funcionalidades nuevas (UI del Generic Data Index, algo de gestión de assets)
- El Admin UI clásico (ExtJS) sigue siendo la interfaz principal para la mayoría de operaciones
- Los plugins personalizados de ExtJS siguen funcionando en el admin clásico
- Los nuevos puntos de extensión de Studio UI usan componentes React
- La dirección a largo plazo es React, pero la migración completa no es obligatoria para P12
Si tienes extensiones personalizadas del admin UI, seguirán funcionando en el admin clásico. No necesitas migrar a React de inmediato. Pero si estás construyendo funcionalidad nueva para el admin, considera construirla para Studio UI. Escribimos un recorrido detallado sobre cómo construir un bundle de producción para Pimcore 12 con integración de Studio UI que cubre todo el proceso, desde el esqueleto del bundle hasta el registro de componentes React.
1.9 Eliminar SensioFrameworkExtraBundle
Este bundle está deprecated y fue eliminado en Symfony 6. Reemplaza todas las anotaciones:
// Antes (Sensio)
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/** @Template("default/default.html.twig") */
public function defaultAction()
{
return [];
}
// Después (Symfony 6+)
public function defaultAction(): Response
{
return $this->render('default/default.html.twig', []);
}
1.10 Anotaciones de Rutas Symfony a Atributos
Todos los 15+ archivos de controladores necesitan este cambio:
// Antes (anotaciones Symfony 5.4)
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/{_locale}/{slug}_p{id}",
* name="product_detail",
* methods={"GET"},
* requirements={"slug"="[\w\s\-\.]+", "id"="[\w\. ]+"}
* )
*/
public function __invoke(Request $request, string $id): Response
// Después (atributos Symfony 6.4+)
use Symfony\Component\Routing\Attribute\Route;
#[Route(
path: '/{_locale}/{slug}_p{id}',
name: 'product_detail',
methods: ['GET'],
requirements: ['slug' => '[\w\s\-\.]+', 'id' => '[\w\. ]+']
)]
public function __invoke(Request $request, string $id): Response
Actualiza config/packages/routing.yaml:
# Cambiar type de 'annotation' a 'attribute'
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
1.11 Cambios en el Serializer de Symfony (Symfony 7.x)
Si tienes Normalizers personalizados (habitual para indexación de búsqueda, serialización de APIs), Symfony 7.x requiere un método nuevo:
// Antes (Symfony 5.4/6.x)
class ProductNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, string $format = null, array $context = []): array
{
// ...
}
public function supportsNormalization(mixed $data, string $format = null): bool
{
return $data instanceof Product;
}
}
// Después (Symfony 7.x)
class ProductNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
// ...
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof Product;
}
// NUEVO: Obligatorio en Symfony 7.x
public function getSupportedTypes(?string $format): array
{
return [
Product::class => true,
];
}
}
Si tienes más de 10 normalizers (típico para indexación con MeiliSearch u OpenSearch), este es un cambio repetitivo pero crítico. Si falta getSupportedTypes(), se produce un error fatal en Symfony 7.x.
1.12 Deprecación de Request::get()
Request::get() está deprecated en Symfony 6.x. Usa los métodos de acceso específicos:
// Deprecated
$value = $request->get('param');
// Usa métodos específicos
$value = $request->query->get('param'); // Parámetros GET
$value = $request->request->get('param'); // Parámetros POST
$value = $request->attributes->get('param'); // Parámetros de ruta
$value = $request->headers->get('param'); // Headers
$value = $request->query->getBoolean('flag'); // Parámetro GET booleano
1.13 Instalar el Puente de Compatibilidad
composer require --no-update pimcore/compatibility-bridge-v10
composer require --no-update symfony/dotenv
Fase 2: Actualizar a Pimcore 11
2.1 Actualización de Composer
{
"require": {
"pimcore/pimcore": "^11.0",
"pimcore/admin-ui-classic-bundle": "^1.0"
}
}
composer remove --no-update sensio/framework-extra-bundle
COMPOSER_MEMORY_LIMIT=-1 composer update -W
Es probable que la resolución de dependencias tenga problemas con bundles de terceros. Contacta a los proveedores de bundles para obtener versiones compatibles con P11 o busca alternativas.
2.2 Registrar el Bundle de Admin
// src/Kernel.php
public function registerBundlesToCollection(BundleCollection $collection): void
{
$collection->addBundle(new \Pimcore\Bundle\AdminBundle\PimcoreAdminBundle(), 60);
}
2.3 Instalar Bundles Extraídos
Muchas funcionalidades core ahora son bundles separados. Instala solo lo que realmente uses:
composer require \
pimcore/static-routes-bundle \
pimcore/custom-reports-bundle \
pimcore/application-logger-bundle \
pimcore/seo-bundle \
pimcore/simple-backend-search-bundle \
pimcore/glossary-bundle \
pimcore/tinymce-bundle \
pimcore/xliff-bundle \
pimcore/word-export-bundle
Registra cada uno en config/bundles.php.
2.4 Actualizar Configuración de Rutas
# config/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing.yaml" # antes: Resources/config/routing.yml
# config/routes/dev/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing_dev.yaml" # antes: Resources/config/routing_dev.yml
2.5 Configuración de Transports de Messenger
Pimcore 11 introduce nuevos transports de mensajes asíncronos:
framework:
messenger:
transports:
# Tus transports existentes...
# NUEVO para Pimcore 11+
pimcore_scheduled_tasks:
dsn: "%messenger.dsn%/pimcore_scheduled_tasks"
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
pimcore_image_optimize:
dsn: "%messenger.dsn%/pimcore_image_optimize"
pimcore_asset_update:
dsn: "%messenger.dsn%/pimcore_asset_update"
pimcore_search_backend_message:
dsn: "%messenger.dsn%/pimcore_search_backend_message"
Si usas RabbitMQ (AMQP), cada transport crea una cola separada. Si usas transporte Doctrine, cada uno crea una partición de tabla separada. Planifica el despliegue de workers en consecuencia.
2.6 Ejecutar Migraciones y Reconstruir
bin/console doctrine:migrations:migrate
bin/console pimcore:cache:clear
bin/console cache:clear
rm -rf var/cache/*
Prueba todo en este punto de control antes de continuar.
Si también estás rediseñando workflows durante esta actualización, lee nuestra guía sobre diseño de workflows enterprise en Pimcore para los patrones de arquitectura.
Fase 3: Actualizar a Pimcore 12
3.1 Licencia y Registro de Producto
Pimcore 12 introduce dos cambios obligatorios:
Licencia: POCL (Pimcore Open Core License) reemplaza a GPLv3. La Community Edition con Admin UI Classic Bundle requiere una licencia de ExtJS (aproximadamente 1.480 EUR).
| Edición | Precio |
|---|---|
| Community (POCL) | Gratuita (ingresos menores a 5M EUR) |
| Professional | 8.400 EUR/año |
| Enterprise Self-Hosted | 25.200 EUR/año |
| Enterprise PaaS | Desde 39.900 USD/año |
Registro de producto: Obligatorio. Se requieren tres variables de entorno:
| Variable | Propósito |
|---|---|
PIMCORE_ENCRYPTION_SECRET | Clave de cifrado Defuse, generada una vez por instalación |
PIMCORE_PRODUCT_KEY | Clave de registro de license.pimcore.com |
PIMCORE_ENTERPRISE_TOKEN | Token de autenticación de Composer para paquetes privados |
Pasos de registro:
# 1. Generar el secreto de cifrado (una vez por instalación, NUNCA cambiar después)
vendor/bin/generate-defuse-key
# 2. Calcular el hash de registro
php -r "echo hash_hmac('sha256', 'your-instance-id', 'your-encryption-secret');"
# 3. Registrar en license.pimcore.com con el hash
# URL: https://license.pimcore.com/register?instance_identifier=ID&instance_hash=HASH
# 4. Recibir la clave de producto, establecer como variable de entorno PIMCORE_PRODUCT_KEY
# config/config.yaml
pimcore:
encryption:
secret: '%env(PIMCORE_ENCRYPTION_SECRET)%'
product_registration:
product_key: '%env(PIMCORE_PRODUCT_KEY)%'
instance_identifier: 'your-instance-id'
La validación es completamente offline. Se ejecuta en cada compilación del contenedor Symfony (cache clear, inicio de la app). Verifica la firma ECDSA de la clave de producto contra una clave pública que viene con Pimcore. No se requiere internet. Si la validación falla, se lanza InvalidConfigurationException y la app se niega a arrancar.
Crítico: Nunca cambies el secreto de cifrado después de la configuración inicial. Todos los datos cifrados en la base de datos dependen de él. Trátalo como la contraseña maestra de la base de datos. Cada instalación separada (staging, producción) puede compartir la misma clave si comparten el mismo identificador de instancia.
3.2 Actualización de Composer
composer require -W pimcore/pimcore:^12.0
3.3 Configuración de Seguridad
security:
# ELIMINAR (es valor por defecto en Symfony 6.4+):
# enable_authenticator_manager: true
firewalls:
pimcore_admin_webdav:
pattern: ^/asset/webdav # antes: ^/admin/asset/webdav
access_control:
- { path: ^/asset/webdav, roles: ROLE_PIMCORE_USER }
3.4 Editor WYSIWYG
# Opción A: TinyMCE (Professional/Enterprise)
composer require pimcore/tinymce-bundle
# Opción B: Quill (gratuito, Community Edition)
composer require pimcore/quill-bundle
3.5 Cambios Finales de Doctrine DBAL 4
Más allá de los reemplazos de métodos de la Fase 1, actualiza la configuración de Doctrine:
# config/packages/doctrine.yaml
doctrine:
dbal:
connections:
default:
driver: "pdo_mysql"
server_version: "mariadb-10.11" # Sé específico
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_520_ci # Valor por defecto en P12
Configuración Detallada de Redis
Redis cumple cuatro funciones críticas en un despliegue en producción de Pimcore 12. Una configuración incorrecta causa fallos silenciosos difíciles de diagnosticar.
1. Caché de Aplicación (Tag-Aware)
# config/packages/cache.yaml
framework:
cache:
# Pimcore 10 (ANTIGUO)
# pools:
# pimcore.cache.pool:
# tags: true # ELIMINAR
# adapter: pimcore.cache.adapter.redis_tag_aware # Nombre antiguo del adaptador
# Pimcore 11+/12 (NUEVO)
pools:
pimcore.cache.pool:
public: true
default_lifetime: 31536000 # 1 año
adapter: cache.adapter.redis_tag_aware # Nombre del adaptador cambiado
provider: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%'
El cambio de nombre del adaptador de pimcore.cache.adapter.redis_tag_aware a cache.adapter.redis_tag_aware es silencioso. El nombre antiguo no lanza un error. El caché simplemente vuelve al sistema de archivos de forma silenciosa, y te preguntas por qué tu despliegue multi-pod tiene datos obsoletos por todas partes.
Sin caché Redis, cada renderizado de página hace consultas a la base de datos para árboles de navegación, configuraciones del sitio, definiciones de clases, traducciones y más. En un despliegue multi-pod en Kubernetes, cada pod mantiene su propio caché de sistema de archivos sin coordinación de invalidación. Redis hace que la invalidación de caché sea consistente en todos los pods.
2. Almacenamiento de Sesiones
# config/packages/framework.yaml
framework:
session:
handler_id: '%env(REDIS_SESSION_DSN)%'
# Ejemplo: redis://redis:6379/1
Para despliegues multi-pod, las sesiones deben ser compartidas. Sin sesiones en Redis, los usuarios pierden su sesión cuando su petición llega a un pod diferente (balanceo de carga round-robin). Este es el síntoma más visible de una configuración de Redis faltante.
3. Full Page Cache
El full page cache de Pimcore almacena la salida HTML renderizada para usuarios anónimos. En Pimcore 12, puede usar Redis como backend:
# config/packages/pimcore.yaml
pimcore:
full_page_cache:
enabled: true
lifetime: 7200 # 2 horas
exclude_patterns:
- '/admin'
- '/api'
exclude_cookie: 'pimcore_admin_sid'
El full page cache reduce drásticamente la carga de base de datos para páginas públicas. Una página que tarda 200ms en renderizarse desde la base de datos se sirve en menos de 5ms desde caché. En sitios de alto tráfico, esta es la diferencia entre necesitar 2 pods web y necesitar 10.
La invalidación del caché ocurre automáticamente cuando los editores publican contenido. Pero ten cuidado con el contenido personalizado: cualquier página que varíe según el contexto del usuario (estado de login, locale, variante de test A/B) debe excluirse del full page cache o usar fragmentos ESI.
4. Transport de Messenger (Opcional)
framework:
messenger:
transports:
pimcore_core:
dsn: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%/messages'
Redis Streams puede reemplazar a RabbitMQ como transport de mensajes. Más simple de operar (un servicio menos), pero carece de dead letter queues, prioridades de mensajes y enrutamiento de exchanges. Para instalaciones con topologías de workers complejas, RabbitMQ sigue siendo mejor. Para configuraciones más simples, Redis Streams funciona bien.
Dimensionamiento de Redis
| Escala de Pimcore | Memoria Redis | Notas |
|---|---|---|
| Pequeña (< 10K objetos) | 256 MB | Instancia única |
| Mediana (10K-100K objetos) | 1-2 GB | Instancia única, persistencia habilitada |
| Grande (100K+ objetos) | 4-8 GB | Considerar Redis Sentinel o Cluster |
Monitorea used_memory y evicted_keys. Si se están eviccionando claves, tu caché es demasiado pequeño y estás perdiendo rendimiento.
La Migración de Collation (La Parte Más Difícil)
Esta es la sección que más tiempo ahorra. La migración de collation es la parte más peligrosa y menos documentada de toda la actualización.
Por Qué Se Rompe
Las migraciones de Pimcore 12 crean tablas nuevas y alteran columnas existentes con collation utf8mb4_unicode_520_ci. Tu base de datos existente de Pimcore 10 usa el valor por defecto que tenía el servidor MySQL. En Azure MySQL 8.0, eso es utf8mb4_0900_ai_ci. En configuraciones más antiguas, podría ser utf8mb4_general_ci.
El resultado: collations mezcladas dentro de la misma tabla y entre tablas. Cuando Pimcore ejecuta consultas UNION entre las tablas objects, assets y documents:
-- Esto es lo que ejecuta getRelationData() de Pimcore
SELECT r.dest_id, r.type, o.className as subtype, concat(o.path, o.`key`) as `path`
FROM objects o, object_relations_X r WHERE ...
UNION
SELECT r.dest_id, r.type, a.type as subtype, concat(a.path, a.filename) as `path`
FROM assets a, object_relations_X r WHERE ...
UNION
SELECT r.dest_id, r.type, d.type as subtype, concat(d.path, d.`key`) as `path`
FROM documents d, object_relations_X r WHERE ...
Si objects.className es utf8mb4_unicode_520_ci (de la migración P12) pero assets.type es utf8mb4_0900_ai_ci (del valor por defecto de Azure), MySQL lanza:
SQLSTATE[HY000]: General error: 1267 Illegal mix of collations for operation 'UNION'
Este error aparece al abrir cualquier DataObject en el admin UI. El sistema queda efectivamente roto.
Distribución Real Que Encontrarás
En una base de datos migrada de P10 a P11 y luego a P12 en Azure MySQL 8.0:
| Collation | Tablas | Columnas | Origen |
|---|---|---|---|
utf8mb4_0900_ai_ci | ~580 | ~5.700 | Valor por defecto de Azure MySQL 8.0 |
utf8mb4_unicode_ci | ~87 | ~155 | Tablas antiguas de bundles |
utf8mb4_unicode_520_ci | ~6 | ~994 | Creadas por migración P12 |
utf8mb3_general_ci | ~4 | ~31 | Tablas legacy muy antiguas |
utf8mb4_general_ci | ~1 | ~3 | Tablas de bundles específicos |
# Verificar tu distribución actual
bin/console doctrine:query:sql "SELECT TABLE_COLLATION, COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION IS NOT NULL GROUP BY TABLE_COLLATION ORDER BY cnt DESC"
# Verificación a nivel de columna (aquí es donde se esconde el verdadero problema)
bin/console doctrine:query:sql "SELECT COLLATION_NAME, COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND COLLATION_NAME IS NOT NULL GROUP BY COLLATION_NAME ORDER BY cnt DESC"
Cinco Casos Especiales Que Rompen el Simple ALTER TABLE
1. Tablas con índices de path (assets, documents, objects, http_error_log):
Los índices compuestos en path + key/filename exceden el límite de 3072 bytes de InnoDB después de la conversión de collation (utf8mb4 = 4 bytes/carácter, 255 caracteres x 4 = 1020 bytes por columna, compuesto = por encima del límite).
-- Eliminar índices, convertir, recrear con longitudes de prefijo
DROP INDEX fullpath ON objects;
DROP INDEX type_path_classId ON objects;
ALTER TABLE objects CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE INDEX fullpath ON objects (path(255), `key`(255));
CREATE INDEX type_path_classId ON objects (type, path(255), classId);
2. Tablas de classification store: Restricción FK entre object_classificationstore_data_* y object_classificationstore_groups_*. El FK debe eliminarse antes de la conversión y recrearse después. El nombre de la columna puede ser id (P12) o o_id (P10/11) dependiendo del estado de migración.
3. Tablas de traducción (translations_admin, translations_messages): El cambio de collation puede exponer duplicados case-insensitive. Si tienes tanto "MyKey" como "mykey" como claves de traducción separadas (válido con collation case-sensitive), convertir a collation case-insensitive causa una violación de restricción unique. Encontramos ~1.600 duplicados en una sola base de datos de staging. Los duplicados deben eliminarse antes de la conversión.
4. Tabla de assets: Hay que recrear el índice fullpath como no-unique. Nombres de archivos con diferente capitalización pueden existir (por ejemplo, EEVA.tif y Eeva.tif son archivos distintos legítimos). Un índice unique rechazaría el segundo después del cambio de collation.
5. Tablas grandes: Tablas con millones de filas (las tablas de consultas localizadas, por ejemplo, pueden tener más de 30 tablas por clase con una por locale) tardan un tiempo significativo en ALTER. En una base de datos con 680 tablas, la conversión completa tarda ~18 minutos.
Ejecutar la Migración
Ejecútala durante una ventana de mantenimiento con los pods web escalados a cero:
# Escalar a cero para evitar contención de locks en tablas de traducción
kubectl scale deployment pimcore pimcore-frontend -n <namespace> --replicas=0
kubectl get pods -n <namespace> -l app=pimcore -w # Esperar la terminación
Después de ejecutar el script de migración automatizado (que gestiona los cinco casos especiales):
# Reconstruir definiciones de clases (restaura restricciones FK)
PIMCORE_CLASS_DEFINITION_WRITABLE=1 bin/console pimcore:deployment:classes-rebuild -c
# Verificar que solo queda una collation
bin/console doctrine:query:sql "SELECT TABLE_COLLATION, COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION IS NOT NULL GROUP BY TABLE_COLLATION"
# Escalar de nuevo
kubectl scale deployment pimcore -n <namespace> --replicas=1
kubectl scale deployment pimcore-frontend -n <namespace> --replicas=3
Referencia Rápida: Errores de Collation
| Error | Causa | Solución |
|---|---|---|
Illegal mix of collations for operation 'UNION' | Collations mezcladas entre tablas en consultas de relaciones | Convertir todas las tablas a utf8mb4_unicode_520_ci |
Specified key was too long; max key length is 3072 bytes | El índice compuesto path+key excede el límite | Eliminar índice, convertir, recrear con longitudes de prefijo |
Referencing column and referenced column are incompatible | FK entre tablas con diferentes collations | Eliminar FK, convertir ambas tablas, reconstruir clases |
Duplicate entry for key 'PRIMARY' | El cambio de collation hace que claves con diferente capitalización sean idénticas | Eliminar duplicados antes de convertir |
Lock wait timeout exceeded | La app mantiene locks en tablas de traducción | Escalar pods a cero antes del ALTER |
Optimización de Base de Datos Después de la Migración
Después de la migración de collation, ejecuta OPTIMIZE en todas las tablas para desfragmentar el almacenamiento y reconstruir índices:
# Tablas core
bin/console doctrine:query:sql "OPTIMIZE TABLE assets, documents, objects, versions, translations_admin, translations_messages, search_backend_data, properties, dependencies"
# Verificar alta fragmentación
bin/console doctrine:query:sql "SELECT TABLE_NAME, TABLE_ROWS, ROUND(DATA_LENGTH/1024/1024,2) AS data_mb, ROUND(DATA_FREE/1024/1024,2) AS free_mb, ROUND(DATA_FREE/(DATA_LENGTH+1)*100,1) AS frag_pct FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND DATA_FREE > DATA_LENGTH * 0.1 AND DATA_LENGTH > 1048576 ORDER BY DATA_FREE DESC"
Configuración SSL de MySQL
Para servicios MySQL gestionados (Azure, AWS RDS) con require_secure_transport=ON:
# config/packages/prod/doctrine.yaml
doctrine:
dbal:
connections:
default:
options:
!php/const PDO::MYSQL_ATTR_SSL_CA: true
!php/const PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false
MYSQL_ATTR_SSL_CA: true habilita SSL sin especificar una ruta de certificado. El servidor proporciona su certificado y el cliente lo acepta. MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false omite la verificación de hostname (seguro para networking interno).
Para ver cómo gestionamos las migraciones de base de datos en nuestra práctica de ingeniería de datos, esa página cubre nuestra metodología.
Integración con OpenSearch
Pimcore 12 introduce el bundle Generic Data Index, que usa OpenSearch (o Elasticsearch) para la búsqueda backend y la indexación de datos.
Configuración de Índices (Debe Hacerse Antes de las Migraciones)
# Crear índices vacíos manualmente (obligatorio antes de ejecutar migraciones)
curl -X PUT "http://opensearch-cluster:9200/pimcore_document" -H 'Content-Type: application/json' -d '{}'
curl -X PUT "http://opensearch-cluster:9200/pimcore_asset" -H 'Content-Type: application/json' -d '{}'
# NO crear manualmente índices de estadísticas del Portal Engine
# Se auto-crean con nombres basados en tiempo: {name}__{year}_{month}
# La creación manual entra en conflicto con el patrón de alias
Esto es crítico porque algunas migraciones P12 ejecutan Document->save(), lo que dispara el evento DocumentIndexUpdateSubscriber. Si el índice no existe: index_not_found_exception seguido de errores de SAVEPOINT en cascada que corrompen el estado de migración.
Después de que las migraciones completen:
# Crear índices para todas las clases DataObject
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
# Comandos disponibles
bin/console list generic-data-index
# generic-data-index:deployment:reindex Crea/actualiza índices para classDefinitions
# generic-data-index:reindex Dispara re-indexación nativa
# generic-data-index:update:index Actualiza índice/mapping
Dimensionamiento del Cluster OpenSearch
| Escala de Instalación | Nodos | Memoria/Nodo | Almacenamiento/Nodo | Java Heap |
|---|---|---|---|---|
| Pequeña (< 10K objetos) | 1 | 4 GB | 50 GB SSD | 2 GB |
| Mediana (10K-100K) | 2 | 8 GB | 100 GB SSD | 4 GB |
| Grande (100K+) | 3 (master-eligible) | 16 GB | 200 GB SSD | 8 GB |
Si estás reemplazando Elasticsearch 7.x, OpenSearch es un reemplazo directo. La API es compatible. Solo cambian la librería del cliente y las rutas de configuración. Para ver cómo diseñamos la arquitectura de búsqueda de forma más amplia, consulta nuestra guía de plataformas ecommerce.
Seguridad de OpenSearch
Para comunicación interna del cluster (pods en el mismo namespace de Kubernetes), puedes desactivar el plugin de seguridad:
# opensearch-cluster.yaml
additionalConfig:
plugins.security.disabled: "true"
Para acceso externo o clusters multi-tenant, configura autenticación adecuada.
Flysystem y Bugs de Almacenamiento Remoto
Si usas almacenamiento remoto de assets (Azure Blob, AWS S3, Google Cloud Storage), Pimcore 12 tiene dos bugs que debes conocer.
Bug 1: getDimensions() Descarga Cada Imagen en Cada Renderizado de Página
Afecta: Pimcore 10.6+, 11.x, 12.x con almacenamiento Flysystem remoto
Cada llamada a thumbnail()->html() dispara getDimensions(), que llama a readDimensionsFromFile() antes de intentar getEstimatedDimensions(). En almacenamiento remoto, cada lectura de archivo cuesta 50-100ms de I/O de red.
Impacto medido: Una página con ~80 referencias a imágenes tardaba 5.735ms (79 llamadas al almacenamiento remoto a ~65ms cada una). Después de corregir el orden de los métodos, la misma página se renderizaba en 170ms con cero llamadas remotas.
Causa raíz en ImageThumbnailTrait::getDimensions():
// Orden actual (INCORRECTO para almacenamiento remoto)
// Paso 1: Verificar caché de thumbnails en DB (rápido, pero frecuentemente falla)
// Paso 2: readDimensionsFromFile() → DESCARGA ARCHIVO DE LA NUBE (~65ms)
// Paso 3: getEstimatedDimensions() → NUNCA SE ALCANZA (paso 2 siempre tiene éxito)
// Orden corregido
if (empty($dimensions) && $config && $asset instanceof Image) {
$dimensions = $config->getEstimatedDimensions($asset); // ~0ms, matemática pura
}
if (empty($dimensions) && $this->exists()) {
$dimensions = $this->readDimensionsFromFile(); // 50-100ms, solo último recurso
}
getEstimatedDimensions() calcula las dimensiones a partir de las dimensiones de la imagen original (almacenadas en la base de datos) y la configuración de transformación del thumbnail. Matemática pura, cero I/O. En producción, el 95%+ de las imágenes tienen dimensiones almacenadas. Esta corrección elimina prácticamente toda la I/O de almacenamiento remoto durante el renderizado de páginas.
Bug 2: Thumbnails de Vista Previa de Carpetas Atascados en Spinner de Carga
Afecta: admin-ui-classic-bundle 2.2.x, 2.3.x
Después de subir imágenes, la pestaña Preview muestra un spinner de carga que nunca se resuelve. El navegador cachea el GIF placeholder permanentemente porque la URL usa modificationDate como cache buster (nunca cambia), y no se despacha generación asíncrona de thumbnails para el origen folderPreview (a diferencia de treeNode que sí lo despacha).
Solución: Agregar Cache-Control: no-store a la respuesta del placeholder y despachar AssetPreviewImageMessage.
Despliegue en Kubernetes
Arquitectura de Pods
| Pod | Propósito | Réplicas |
|---|---|---|
pimcore-web | Nginx + PHP-FPM, admin + frontend | 2-4 |
pimcore-worker | Consumers de Symfony Messenger | 1-3 |
pimcore-ops | Pod CLI de mantenimiento (migraciones, rebuilds) | 1 |
redis | Caché, sesiones, transport opcional | 1+ |
mysql | Base de datos (o servicio gestionado) | 1+ |
opensearch | Índices de búsqueda | 2-3 |
rabbitmq | Broker de mensajes (si no usas Redis) | 1-3 |
Gestión de Secrets
apiVersion: v1
kind: Secret
metadata:
name: pimcore-secrets
type: Opaque
data:
PIMCORE_ENCRYPTION_SECRET: <base64>
PIMCORE_PRODUCT_KEY: <base64>
PIMCORE_ENTERPRISE_TOKEN: <base64>
DATABASE_HOST: <base64>
DATABASE_NAME: <base64>
DATABASE_USER: <base64>
DATABASE_PASSWORD: <base64>
REDIS_HOST: <base64>
REDIS_PORT: <base64>
APP_SECRET: <base64>
Usa Sealed Secrets (Bitnami) u operadores de secrets externos para producción. Nunca hagas commit de secrets en texto plano en Git.
Cada init container (create-assets, create-classes, db-migrations, clear-cache) debe tener TODOS los secretRefs en su lista envFrom. Los secrets faltantes causan errores silenciosos Environment variable not found.
Despliegue de Workers
spec:
replicas: 2
template:
spec:
containers:
- name: worker
command: ["php", "bin/console", "messenger:consume",
"pimcore_core", "pimcore_maintenance", "pimcore_scheduled_tasks",
"pimcore_image_optimize", "pimcore_asset_update",
"pimcore_search_backend_message",
"--time-limit=3600", "--memory-limit=512M"]
--time-limit y --memory-limit aseguran que los workers se reinicien periódicamente, previniendo fugas de memoria de PHP. Kubernetes reinicia el pod cuando el proceso termina de forma controlada.
Orden de Despliegue
1. Desplegar recursos de Kubernetes
2. Abrir shell en el pod de ops
3. Instalar bundles faltantes (PimcoreAdminBundle, GenericExecutionEngine, etc.)
4. Crear índices de OpenSearch manualmente (pimcore_document, pimcore_asset)
5. Ejecutar reindex de OpenSearch para DataObjects
6. Ejecutar migraciones de base de datos (con PIMCORE_CLASS_DEFINITION_WRITABLE=1)
7. Ejecutar migración de collation (ventana de mantenimiento, pods web escalados a cero)
8. Ejecutar optimización de base de datos (OPTIMIZE TABLE)
9. Comandos post-despliegue (classes-rebuild, reindex, assets:install, cache:clear)
10. Verificar: abrir DataObject en admin, comprobar assets, verificar frontend
Nuestros servicios cloud cubren el despliegue y operación de Kubernetes para Pimcore y otras plataformas.
Arquitectura del Sistema de Eventos en Pimcore 12
Comprender el sistema de eventos de Pimcore es fundamental tanto para la actualización como para construir extensiones.
Eventos PHP (Backend)
Pimcore usa el EventDispatcher de Symfony. Eventos clave para DataObjects:
use Pimcore\Event\DataObjectEvents;
// Eventos disponibles
DataObjectEvents::PRE_ADD // Antes del primer guardado
DataObjectEvents::POST_ADD // Después del primer guardado
DataObjectEvents::PRE_UPDATE // Antes de cada guardado
DataObjectEvents::POST_UPDATE // Después de cada guardado
DataObjectEvents::PRE_DELETE // Antes de la eliminación
DataObjectEvents::POST_DELETE // Después de la eliminación
Crear un event subscriber en Pimcore 12:
use Pimcore\Event\Model\DataObjectEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
DataObjectEvents::POST_UPDATE => 'onPostUpdate',
];
}
public function onPostUpdate(DataObjectEvent $event): void
{
$object = $event->getObject();
if (!$object instanceof Product) {
return;
}
// Indexar para búsqueda, generar assets, etc.
}
}
Eventos JavaScript (Admin UI)
Pimcore 12 usa un patrón de suscripción de eventos diferente al de P10:
// Eventos de admin en Pimcore 12
document.addEventListener(pimcore.events.preOpenObject, (e) => {
// Antes de que se abra el editor de objetos
});
document.addEventListener(pimcore.events.postOpenObject, (e) => {
// Después de que se abra el editor, e.detail.object disponible
});
document.addEventListener(pimcore.events.preSaveObject, (e) => {
// Antes de guardar, se puede cancelar con e.preventDefault()
});
document.addEventListener(pimcore.events.postSaveObject, (e) => {
// Después de que se completa el guardado
});
Extender Pimcore 12
Los bundles personalizados en Pimcore 12 siguen las convenciones de bundles de Symfony:
// src/CustomBundle/CustomBundle.php
namespace App\CustomBundle;
use Pimcore\Extension\Bundle\AbstractPimcoreBundle;
class CustomBundle extends AbstractPimcoreBundle
{
public function getJsPaths(): array
{
return [
'/bundles/custom/js/startup.js',
];
}
public function getCssPaths(): array
{
return [
'/bundles/custom/css/admin.css',
];
}
}
Registra el JS y CSS personalizado del admin en la configuración del bundle:
# config/packages/pimcore_admin.yaml
pimcore_admin:
assets:
js:
- '/bundles/custom/js/startup.js'
- '/bundles/custom/js/grid-operators.js'
css:
- '/bundles/custom/css/admin.css'
Verificación Post-Actualización
Secuencia de Comandos
PIMCORE_CLASS_DEFINITION_WRITABLE=1 php -d memory_limit=2G bin/console pimcore:deployment:classes-rebuild -c
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
bin/console assets:install public
php -d memory_limit=2G bin/console pimcore:search-backend-reindex
bin/console messenger:setup-transports
bin/console cache:clear
bin/console cache:warmup
Matriz de Pruebas Funcionales
| Área | Qué Probar | Cómo Verificar |
|---|---|---|
| Login de admin | Login, 2FA, logout | Manual en el navegador |
| DataObjects | Crear, editar, guardar, publicar, eliminar, versiones | Abrir cualquier objeto, editar un campo, guardar |
| Assets | Subir, thumbnails, metadatos, descarga | Subir una imagen, verificar generación de thumbnails |
| Documents | Editar páginas, areabricks, guardar, publicar | Editar una página con areabricks |
| Relaciones | Relaciones de objetos, relaciones de assets | Abrir objeto con relaciones, verificar que la consulta UNION funciona |
| Búsqueda | Búsqueda backend, indexación OpenSearch | Buscar un objeto en la barra de herramientas del admin |
| Messenger | Procesamiento de colas, todos los transports | Verificar bin/console messenger:stats |
| Caché | Conectividad Redis, invalidación | Publicar contenido, verificar que el cambio es visible en otro pod |
| Full page cache | Caché de páginas anónimas, invalidación | Cargar página, verificar headers de respuesta para cache hit |
| Integración ERP | Procesamiento de importaciones, mapeo de campos | Ejecutar una importación de prueba |
| Permisos | RBAC, permisos de workflow | Iniciar sesión como usuario restringido, verificar acceso |
| JS personalizado | Operadores de grid, plugins de admin | Abrir vista de grid, verificar que las columnas personalizadas funcionan |
Benchmarks de Rendimiento
| Métrica | Aceptable | Advertencia | Crítico |
|---|---|---|---|
| Carga de página admin | < 2s | 2-5s | > 5s |
| Apertura de DataObject | < 3s | 3-8s | > 8s |
| Subida de asset (10MB) | < 5s | 5-15s | > 15s |
| Respuesta de búsqueda | < 500ms | 500ms-2s | > 2s |
| Página frontend (cacheada) | < 50ms | 50-200ms | > 200ms |
| Página frontend (sin caché) | < 500ms | 500ms-2s | > 2s |
| Render de thumbnail (almacenamiento remoto) | < 200ms | 200ms-1s | > 1s (verificar bug de getDimensions) |
Errores Comunes
-
Saltarse Pimcore 11. No puedes saltar de la 10 a la 12. La versión intermedia gestiona cambios críticos de schema, extracción de bundles y eliminación de prefijos.
-
Ejecutar migraciones sin índices de OpenSearch. Algunas migraciones disparan event subscribers que consultan OpenSearch.
index_not_found_exceptionse propaga en cascada a un estado de SAVEPOINT corrupto. Crea los índices primero. -
No corregir las collations. Las collations mezcladas causan fallos en consultas UNION al abrir cualquier DataObject. Esto rompe el admin UI de forma silenciosa hasta que un editor abre un producto.
-
Perder el secreto de cifrado. Si se pierde después del despliegue, todos los datos cifrados en la base de datos se vuelven irrecuperables. Esto no tiene solución. Trátalo como la contraseña maestra de la base de datos.
-
Fallback silencioso del caché Redis. El cambio de nombre del adaptador de
pimcore.cache.adapter.redis_tag_awareacache.adapter.redis_tag_awareno lanza errores. El caché vuelve silenciosamente al sistema de archivos. Los despliegues multi-pod se rompen. -
No actualizar los transports de Messenger. Los transports de P11 faltantes hacen que las tareas programadas, el procesamiento de assets y la indexación de búsqueda dejen de funcionar silenciosamente.
-
Ejecutar la migración de collation con pods web activos. La conversión de tablas de traducción se bloquea por deadlock bajo acceso concurrente. Escala a cero primero.
-
Crear manualmente índices de estadísticas del Portal Engine. La auto-creación usa nombres de índice basados en tiempo con alias. La creación manual entra en conflicto con el patrón de alias.
-
Probar en una base de datos vacía. La migración de schema en una base de datos vacía oculta el 90% de los problemas. Usa un clon completo de datos de producción.
-
Ignorar el bug de getDimensions() en almacenamiento cloud. Cada renderizado de página sin caché es 3-7 segundos más lento de lo necesario hasta que se aplica el parche. La corrección es reordenar dos líneas.
-
Falta de
getSupportedTypes()en normalizers. Symfony 7.x requiere este método. Si falta, causa un error fatal, no una advertencia de deprecación. -
No actualizar las llamadas a
Request::get(). Deprecated pero sigue funcionando en Symfony 6.x. Se romperá en 7.x. -
Olvidar
PIMCORE_CLASS_DEFINITION_WRITABLE=1. Las migraciones y rebuilds de clases fallan silenciosamente sin esta variable de entorno. El mensaje de error es claro, pero es fácil de olvidar. -
ExtJS 7
rootvsrootProperty. Los readers de store personalizados que usanroot: 'data'fallan silenciosamente al parsear respuestas en ExtJS 7.
Conclusiones Clave
-
Tres fases, sin atajos. Preparación (corregir todo el código deprecated), Pimcore 11 (actualización del framework + extracción de bundles), Pimcore 12 (registro, DBAL 4, collation, sistema de eventos). Cada fase es un punto de control.
-
La migración de collation es la parte más difícil. Cinco tipos de tablas especiales necesitan manejo personalizado. Reserva una ventana de mantenimiento. Usa un script automatizado. Verifica tanto a nivel de tabla como de columna.
-
El registro de producto es obligatorio y offline. No se necesita internet para la validación. Pero el secreto de cifrado es irrecuperable si se pierde.
-
Redis no es opcional en despliegues multi-pod. La consistencia de caché, el intercambio de sesiones, el full page cache y, opcionalmente, el transport de mensajes dependen de él. El cambio de nombre del adaptador es un cambio incompatible silencioso.
-
Los índices de OpenSearch deben existir antes de las migraciones. Crea los índices de documents y assets manualmente, luego ejecuta el reindex de data objects. El orden importa.
-
La migración del sistema de eventos es mecánica pero crítica. Plugin Broker a Event Listeners,
o_classNameaclassName,@Routea#[Route],Request::get()a accesores específicos. Rector puede automatizar el 80%. -
El almacenamiento remoto expone bugs de Pimcore. El bug del orden en getDimensions() añade segundos a cada renderizado de página. El bug de caché de preview de carpetas deja los thumbnails permanentemente atascados. Ambos tienen correcciones simples.
-
Prueba en un clon de datos de producción. Siempre. Una actualización en base de datos vacía tarda 20 minutos y tiene éxito de forma trivial. Los problemas reales aparecen con 70K assets, 680 tablas con collations mezcladas y más de 50K data objects.
Este es exactamente el tipo de trabajo de ingeniería de software que nuestro equipo maneja regularmente. Si estás planificando una actualización de Pimcore, nuestro equipo de consultoría ha hecho esto para múltiples instalaciones enterprise en la región DACH. También lo ofrecemos como parte de nuestros servicios generales.
¿Listo para empezar tu actualización de Pimcore? Habla con nuestro equipo o solicita un presupuesto.
Temas cubiertos
Guías relacionadas
Guía Empresarial de Sistemas de IA Agéntica
Guia tecnica de sistemas de IA agentica en entornos empresariales. Descubre la arquitectura, capacidades y aplicaciones de agentes IA autonomos.
Leer guíaComercio Agéntico: Cómo Dejar que los Agentes IA Compren de Forma Segura
Cómo diseñar comercio iniciado por agentes IA con gobernanza. Motores de políticas, puertas de aprobación HITL, recibos HMAC, idempotencia, aislamiento de tenants y el Agentic Checkout Protocol completo.
Leer guíaLos 9 Puntos Donde Tu Sistema de IA Filtra Datos (y Cómo Sellar Cada Uno)
Un mapa sistemático de cada lugar donde se filtran datos en sistemas de IA. Prompts, embeddings, logs, llamadas a herramientas, memoria de agentes, mensajes de error, caché, datos de fine-tuning y handoffs entre agentes.
Leer guía¿Listo para construir sistemas de IA listos para producción?
Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.
Iniciar una conversación