prestashop-admin-columns · git:20260930.8a68a16 · 2026-09-30 · sha256 91678ffeadd7a537
prestashop-admin-columns git:20260930.8a68a16A
Immutable. This exact content never changes and is served at /api/v1/blob/91678ffeadd7a537.
---
name: prestashop-admin-columns
description: Skill para la gestion y adicion de columnas personalizadas en listados de administracion de PrestaShop 8/9 usando Symfony Grid.
---
# PRESTASHOP ADMIN COLUMNS SKILL (PS 8 / PS 9)
Este skill proporciona los patrones para extender los listados de administracion (Grids) de PrestaShop usando el sistema Symfony Grid.
> ⛔ **AVISO CRITICO (corregido 2026-07-22, verificado en produccion PS 8.1.1)**
>
> La afirmacion anterior de este skill —"en PS 8+ TODOS los listados usan Symfony Grid y los hooks legacy NO aplican"— es **FALSA** y ha costado horas de depuracion.
>
> **En PrestaShop 8.1.1 el listado de PRODUCTOS usa el HelperList LEGACY.** Evidencia real: con el modulo registrado en ambos hooks, `actionProductGridDefinitionModifier` **NUNCA se ejecuta** (0 invocaciones registradas) mientras que `actionAdminProductsListingFieldsModifier` **si se ejecuta en cada carga**.
>
> **Regla:** NO asumas cual de los dos sistemas usa el listado. **Registra los DOS e instrumenta ambos con un log en la PRIMERA linea** del hook (ver §6). El que escriba en el log es el que manda en esa instalacion.
>
> | Version | Productos | Pedidos / Clientes |
> | :--- | :--- | :--- |
> | PS 9.x | Symfony Grid ✔ (verificado) | Symfony Grid |
> | PS 8.2+ | Symfony Grid (probable, sin verificar) | Symfony Grid |
> | **PS 8.1.x** | **LEGACY HelperList ✔ (verificado)** | Symfony Grid (sin verificar) |
> | PS 1.7.x | Legacy HelperList | Legacy HelperList |
## 1. HOOKS POR ENTIDAD
| Entidad | Hook Query Builder | Hook Definition |
| :--- | :--- | :--- |
| **Orders** | `actionOrderGridQueryBuilderModifier` | `actionOrderGridDefinitionModifier` |
| **Customers** | `actionCustomerGridQueryBuilderModifier` | `actionCustomerGridDefinitionModifier` |
| **Products** | `actionProductGridQueryBuilderModifier` | `actionProductGridDefinitionModifier` |
| **Carts** | `actionCartGridQueryBuilderModifier` | `actionCartGridDefinitionModifier` |
Patron general: `action{GridId}GridQueryBuilderModifier` y `action{GridId}GridDefinitionModifier`.
---
## 2. IMPLEMENTACION (SYMFONY GRID)
Requiere 2 o 3 hooks coordinados + registro en `install()`.
### 2.0 Registro de Hooks
```php
public function install()
{
return parent::install()
&& $this->registerHook('actionOrderGridQueryBuilderModifier')
&& $this->registerHook('actionOrderGridDefinitionModifier');
}
```
### 2.1 Modificar la Query (QueryBuilder)
```php
public function hookActionOrderGridQueryBuilderModifier(array $params): void
{
/** @var \PrestaShop\PrestaShop\Core\Grid\Query\DoctrineSearchCriteria $searchCriteria */
$searchCriteria = $params['search_criteria'];
/** @var \Doctrine\DBAL\Query\QueryBuilder $searchQueryBuilder */
$searchQueryBuilder = $params['search_query_builder'];
$searchQueryBuilder->addSelect('m.my_value');
$searchQueryBuilder->leftJoin(
'o', // Alias principal (o=Orders, c=Customers, p=Products)
_DB_PREFIX_ . 'my_table',
'm',
'm.id_order = o.id_order'
);
// Tambien para el count
$countQueryBuilder = $params['count_query_builder'];
$countQueryBuilder->leftJoin('o', _DB_PREFIX_ . 'my_table', 'm', 'm.id_order = o.id_order');
}
```
### 2.2 Modificar la Definicion (Columns)
Se añade la columna visual al objeto GridDefinition.
```php
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
public function hookActionOrderGridDefinitionModifier(array $params): void
{
/** @var \PrestaShop\PrestaShop\Core\Grid\Definition\GridDefinitionInterface $definition */
$definition = $params['definition'];
$definition->getColumns()->addAfter(
'reference', // ID de la columna despues de la cual se inserta
(new DataColumn('my_value'))
->setName($this->trans('Mi Valor', [], 'Modules.Mymodule.Admin'))
->setOptions([
'field' => 'my_value',
])
);
}
```
> ⛔ **FQCN de `DataColumn` — CORREGIDO (verificado en PS 8.1.1 y PS 9)**
>
> Este skill decia que el namespace "correcto" era `...\Grid\Column\Type\Common\DataColumn`. **Es FALSO para PS 8.1 y PS 9.** El real es **sin `Common\`**:
>
> ```
> PrestaShop\PrestaShop\Core\Grid\Column\Type\DataColumn <-- CORRECTO (8.1.1 y 9 verificados)
> PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn <-- NO existe en 8.1/9
> ```
>
> **NUNCA fijes un solo FQCN.** Prueba candidatos y usa el que exista, y **deja rastro** de cual se uso:
>
> ```php
> $candidatos = array(
> 'PrestaShop\\PrestaShop\\Core\\Grid\\Column\\Type\\DataColumn',
> 'PrestaShop\\PrestaShop\\Core\\Grid\\Column\\Type\\Common\\DataColumn',
> 'PrestaShopBundle\\Core\\Grid\\Column\\Type\\DataColumn',
> );
> $fqcn = '';
> foreach ($candidatos as $c) {
> if (class_exists($c)) { $fqcn = $c; break; }
> }
> if ($fqcn === '') {
> // LOGUEARLO. Un class_exists que falla en silencio = columnas que no salen sin motivo aparente.
> return;
> }
> ```
### 2.3 Filtros y Ordenacion
```php
// Dentro de hookActionOrderGridQueryBuilderModifier
foreach ($searchCriteria->getFilters() as $filterName => $filterValue) {
if ($filterName === 'my_value') {
$searchQueryBuilder->andHaving('m.my_value LIKE :my_value');
$searchQueryBuilder->setParameter('my_value', '%' . pSQL($filterValue) . '%');
}
}
if ($searchCriteria->getOrderBy() === 'my_value') {
$searchQueryBuilder->orderBy('m.my_value', $searchCriteria->getOrderWay());
}
```
### 2.4 Columnas con HTML personalizado
Para mostrar HTML, usar `HtmlColumn` o una plantilla Twig:
```php
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\HtmlColumn;
$definition->getColumns()->addAfter(
'reference',
(new HtmlColumn('my_html_field'))
->setName($this->trans('Mi Campo', [], 'Modules.Mymodule.Admin'))
->setOptions([
'field' => 'my_html_field',
])
);
```
Para mayor control con plantilla Twig:
```php
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
// Crear columna con template Twig personalizado
(new DataColumn('my_custom_field'))
->setOptions([
'field' => 'my_custom_field',
])
```
---
## 3. ENTIDADES ESPECIFICAS (CHEAT SHEET)
### PRODUCTOS (AdminProducts)
```php
// Query Builder
$searchQueryBuilder->leftJoin(
'p',
_DB_PREFIX_ . 'product_lang',
'pl',
'pl.id_product = p.id_product AND pl.id_lang = ' . (int) \Context::getContext()->language->id
);
// Definition
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
$definition->getColumns()->addAfter(
'name',
(new DataColumn('my_field'))
->setName($this->trans('Label', [], 'Modules.Mymodule.Admin'))
->setOptions(['field' => 'my_field'])
);
```
---
## 4. REGLAS Y BUENAS PRACTICAS
1. **Strict Types**: Siempre usar `declare(strict_types=1);` al principio del archivo.
2. **Namespace Global**: El archivo principal `.php` del modulo **NO** debe tener namespace.
3. **`ClassName::class`**: Usar libremente (valido desde PHP 5.5). Solo evitar `$obj::class` si se necesita compatibilidad con PHP < 8.0 (PS 8.0).
4. **Proteccion SQL**: Usar siempre `pSQL()` para strings y cast `(int)` para IDs.
5. **Multitienda**: Siempre añadir `Shop::addSqlRestriction()` o filtrar por `id_shop`.
6. **Escapado HTML**: En columnas con contenido del usuario, usar `HtmlColumn` con datos previamente escapados o `DataColumn` (escapa automaticamente).
7. **Traducciones**: Usar `$this->trans('text', [], 'Modules.Mymodule.Admin')`, NUNCA `$this->l()`.
---
## 4-BIS. VIA LEGACY (HelperList) — OBLIGATORIA PARA PS 8.1.x Y 1.7.x
Cuando el listado es legacy, el Symfony Grid NO se dispara. Hooks:
| Entidad | Hook campos + query | Hook resultados |
| :--- | :--- | :--- |
| Products | `actionAdminProductsListingFieldsModifier` | `actionAdminProductsListingResultsModifier` |
| Orders | `actionAdminOrdersListingFieldsModifier` | `actionAdminOrdersListingResultsModifier` |
| Customers | `actionAdminCustomersListingFieldsModifier` | `actionAdminCustomersListingResultsModifier` |
Patron: `action{ControllerName}ListingFieldsModifier`, lanzado desde `AdminController::getList()`.
⛔ **ATENCION — PRODUCTOS es la EXCEPCION (verificado PS 8.1.1)**
`AdminProductsController` **NO usa `AdminController::getList()`**: tiene su propia implementacion y lanza el hook con claves **`sql_*` (SQL crudo), SIN `fields`**. Log real de produccion:
```
[LEGACY] PS=8.1.1 | claves=[_ps_version,sql_select,sql_table,sql_where,
sql_group_by,sql_order,sql_limit,cookie,cart,altern]
| n_fields=-1 | columna_anadida=NO
```
Consecuencia: **con `actionAdminProductsListingFieldsModifier` NO se pueden anadir columnas** en el listado de productos; solo modificar la consulta (`sql_select`, `sql_table` para el JOIN). Para las COLUMNAS hay que usar **`actionAdminProductsListingResultsModifier`**, que si recibe `fields_display` (= `$this->fields_list`) y `list` (las filas).
```php
public function hookActionAdminProductsListingResultsModifier($params)
{
// 'fields_display' = columnas ; 'list' = filas (ambos por referencia)
$params['fields_display']['my_value'] = array('title' => 'Mi columna', 'search' => false, 'orderby' => false);
foreach ($params['list'] as $k => $row) {
$params['list'][$k]['my_value'] = /* valor calculado */;
}
}
```
> **Consejo**: rellenar los valores en el Results hook (una consulta propia por los `id_product` visibles) en vez de tocar `sql_select`/`sql_table`. Si te equivocas con el alias del SQL **revientas la pagina de productos entera**; rellenando valores, lo peor que pasa es que salgan a 0.
Para el resto de entidades (Orders, Customers…), que si usan `AdminController::getList()`, `$params` llega con estas claves, **y sus valores son REFERENCIAS** al controlador (por eso `$params` se declara por VALOR y aun asi las modificaciones surten efecto):
```php
'select' => &$this->_select, 'join' => &$this->_join,
'where' => &$this->_where, 'group_by' => &$this->_group,
'having' => &$this->_having, 'fields' => &$this->fields_list,
'order_by' => &$this->_orderBy, 'order_way' => &$this->_orderWay,
```
```php
public function hookActionAdminProductsListingFieldsModifier($params)
{
// Alias de la tabla principal en HelperList clasico = 'a' (NO 'p', que es del Symfony Grid).
$params['select'] .= ', COALESCE(m.my_value, 0) AS my_value';
$params['join'] .= ' LEFT JOIN `' . _DB_PREFIX_ . 'my_table` m ON (m.id_product = a.id_product)';
$params['fields']['my_value'] = array(
'title' => $this->l('Mi columna'),
'align' => 'text-center',
'search' => false, // true exige indice + havingFilter
'orderby' => false,
);
}
```
⚠ **Trampa mortal**: si envuelves cada modificacion en `if (isset($params['x']))` y las claves no llegan, **no hace nada y no te enteras**. Registra SIEMPRE `array_keys($params)` y si tu columna quedo puesta (ver §6).
---
## 6. VERIFICACION OBLIGATORIA — "registrado" NO es "ejecutado"
El fallo mas caro de este skill: dar por bueno que el hook funciona porque `isRegisteredInHook()` devuelve true. **Registrado ≠ invocado.**
**Antes de dar por hecho que un hook de listado funciona, instrumentalo:**
1. **Log en la PRIMERA linea del hook**, antes de cualquier `return`/guard:
```php
MiLog::write('grid.log', date('c') . ' [DEF] hook disparado. PS=' . _PS_VERSION_);
```
2. **Log del RESULTADO real**, nunca un "aplicado" incondicional:
```php
MiLog::write('grid.log', ' claves=[' . implode(',', array_keys($params)) . ']'
. ' | columna_anadida=' . (isset($params['fields']['my_value']) ? 'SI' : 'NO'));
```
Un log que dice "aplicado" cuando no aplico nada es peor que no tener log.
3. **Registra los DOS sistemas** (Grid + legacy) y deja que el log diga cual corre.
4. **Coherencia definicion ↔ query**: si el hook de query no puede hacer el JOIN (tabla ausente, etc.), el de definicion **tampoco debe anadir la columna**; si no, el grid intenta pintar un campo que no viene en el SELECT.
5. **`try/catch` que no traga**: si envuelves el hook en try/catch, el catch DEBE escribir a log. Un catch mudo convierte un fatal en "no salen las columnas y no se sabe por que".
---
## 5. TIPS DE RENDIMIENTO
- **GROUP BY**: Evitar duplicados en listados al usar JOINs 1:N.
- **Indexacion**: Asegurarse de que las columnas usadas en el JOIN de tablas personalizadas tengan indices.
- **Cache**: Si la obtencion del dato es costosa, considerar guardarlo en una tabla de soporte o usar `Configuration` si es global.