This commit is contained in:
Jordan Diaz
2026-04-25 10:27:51 +00:00
parent e84a36c83d
commit 6881d64a08
42 changed files with 3207 additions and 3413 deletions

383
docs/06-hooks-and-cmsapi.md Normal file
View File

@@ -0,0 +1,383 @@
# Hooks y CmsApi (server-side)
Este documento describe cómo crear y consumir hooks PHP en Acai (lógica server-side) y cómo usar `CmsApi` (alias de `CocoDB`) para acceder a la base de datos. Cubre las dos ubicaciones válidas para un hook (global en `hooks/hooks.<id>.php` o propio de módulo en `template/estandar/modulos/<id>/hook.php`), las cuatro formas de invocarlo (filtro Twig, etiqueta `<hook>`, JS `CmsApi.hook`, `c-form`), las reglas obligatorias (devolver array, no `echo`, no `exit`), la API completa de `CmsApi::get/insert/update/delete` con sus opciones (`uploads`, `relations`, `translates`, `groupBy`, `aggregates`), y la tool `set_hook_middleware` para que un hook global se ejecute automáticamente antes de renderizar páginas. Léelo antes de crear cualquier `.php` de hook.
## Hooks — qué son y dónde viven
Hooks son archivos PHP que ejecutan lógica server-side. Hay dos ubicaciones válidas:
### Hook global
- Archivo: `hooks/hooks.<hook-id>.php`
- Endpoint: `/hooks/<hook-id>/`
- Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios.
Ejemplo: `hooks/hooks.calcular_precio.php` → endpoint `/hooks/calcular_precio/`
### Hook propio de módulo
- Archivo: `template/estandar/modulos/<module-id>/hook.php`
- Endpoint: `/hooks/<module-id>/`
- Úsalo cuando la lógica pertenece solo a ese módulo.
Ejemplo: `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` → endpoint `/hooks/buscadorapartados_hjd8s/`
### Regla práctica
- Si la lógica solo sirve a un módulo → `hook.php` dentro del módulo.
- Si varias piezas la consumen → hook global en `hooks/`.
## Reglas obligatorias
- Devuelve datos con `return [...]`. **NUNCA** uses `echo json_encode(...)` ni `exit`.
- Para leer parámetros usa `$_REQUEST[...]` o las variables ya inyectadas (los parámetros pasados al llamar el hook se convierten en variables PHP del mismo nombre).
- En hooks usa `CmsApi::get()` o `CocoDB::get()` como primera opción.
- **NO uses `CocoDB::getInstance()`** salvo necesidad excepcional.
- **NO escribas SQL manual con `prepare()/bind_param()`** salvo que de verdad no haya alternativa con `CmsApi` o `CocoDB`.
### Estructura de un hook
```php
<?php
// Si llamas el hook con { cantidad: 10, tipo: 'mayoreo' },
// recibes $cantidad = 10 y $tipo = 'mayoreo' como variables.
$precioUnitario = 50;
if ($tipo === 'mayoreo' && $cantidad > 10) {
$precioUnitario *= 0.85; // 15% descuento
}
return [
"success" => true,
"precioUnitario" => round($precioUnitario, 2),
"total" => round($precioUnitario * $cantidad, 2),
"descuento" => $tipo === 'mayoreo' ? 15 : 0
];
```
## Cómo invocar un hook
### Desde Twig (filtro `hook`)
```twig
{% set resultado = 'hooks/calcular_precio/' | hook({cantidad: 5, tipo: 'mayoreo'}) %}
<p>Total: {{ resultado.total }}€</p>
```
### Desde HTML (etiqueta `<hook>`)
```html
<hook result="precio" endpoint="/hooks/calcular_precio/" :cantidad="10" :tipo="'mayoreo'"></hook>
<p>{{ precio.message }}</p>
```
### Desde JavaScript
```js
CmsApi.hook('/hooks/calcular_precio/', { cantidad: 10, tipo: 'mayoreo' }, (data) => {
console.log(data.total);
});
```
Si llamas a un hook propio del módulo desde su `script.js`, usa el `module-id` real (`/hooks/buscadorapartados_hjd8s/`). NO construyas el endpoint con Twig dentro de `script.js` — pásalo desde `index-base.tpl` vía `data-hook-endpoint`.
### Desde otro hook PHP
```php
$result = hook("/hooks/calcular_precio/", ["cantidad" => 5, "tipo" => "mayoreo"]);
$mensaje = $result["message"];
```
### Desde `c-form`
Los hooks se ejecutan automáticamente al enviar el formulario si están configurados en los atributos del `c-form`.
### Testing manual
Con Docker corriendo:
```bash
curl {ACAI_WEB_URL}/hooks/calcular_precio/
```
Usa la URL real del proyecto (devuélvela con `get_web_url`), no `localhost:8080`. En desarrollo local no necesitas `X-Hooks-Token`.
## CmsApi (PHP)
API server-side para BD. Disponible en todos los hooks. Alias de `CocoDB`.
### Read — `CmsApi::get()`
```php
// Todos los registros
$products = CmsApi::get("productos");
// Con WHERE string
$active = CmsApi::get("productos", "activo=1");
// Con orden y límite
$latest = CmsApi::get("noticias", "", "fecha DESC", 5);
// Condiciones complejas
$caros = CmsApi::get("productos", "precio > 100");
$resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
// Operadores
$expensive = CmsApi::get("productos", "precio >= 100");
$search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
$inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
// Con opciones
$datos = CmsApi::get("productos", "", "", "", [
'translates' => true,
'uploads' => true,
'relations' => true,
'relationsDepth' => 2
]);
```
#### Opciones
| Option | Tipo | Default | Descripción |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['categoria']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | idioma actual | Código de idioma para `| translate` |
| `groupBy` | string | null | Cláusula GROUP BY |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo ciertos campos |
| `debug` | bool | false | Imprime el SQL generado |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL del cache (segundos) |
### Insert — `CmsApi::insert()`
```php
// Un registro
CmsApi::insert('contacto', [
["nombre" => "John", "email" => "john@example.com", "mensaje" => "Hola"]
]);
// Múltiples registros
CmsApi::insert('productos', [
["nombre" => "Producto A", "precio" => 100],
["nombre" => "Producto B", "precio" => 200]
]);
// Retornar el last id
$result = CmsApi::insert('productos',
[["nombre" => "Nuevo", "precio" => 150]],
[],
['return_last_id' => true]
);
$nuevoNum = $result['lastId'];
```
#### Opciones de insert
| Option | Descripción |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
| `return_last_id` | Devuelve el `num` del último insert |
### Update — `CmsApi::update()`
```php
// Con WHERE string
CmsApi::update('productos', ["precio" => 150], "num=1");
// Con WHERE array
CmsApi::update('productos',
["activo" => 1],
[["column" => "num", "operator" => "=", "value" => 1]]
);
// Múltiples registros
CmsApi::update('productos', ["activo" => 0], "precio < 50");
```
### Delete — `CmsApi::delete()`
```php
CmsApi::delete('productos', "num=5");
CmsApi::delete('productos',
[["column" => "activo", "operator" => "=", "value" => 0]]
);
```
### Reglas CmsApi
- Nombres de tabla **sin prefijo `cms_`**
- Primary key siempre es **`num`**
- Foreign keys: **`<entidad>_num`**
- Upload fields **NO se setean** vía insert/update — usa `upload_record_image` después.
- Operadores soportados: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
## CocoDB (low-level)
Capa de BD que usa internamente `CmsApi`. Misma API que `CmsApi` pero con métodos verbosos:
- `CocoDB::get($table, $where, $order, $limit, $options)` — idéntico a `CmsApi::get`
- `CocoDB::insertRecords($table, $records, $functions, $options)` — idéntico a `CmsApi::insert`
- `CocoDB::updateRecords($table, $records, $where, $functions, $options)` — idéntico a `CmsApi::update`
- `CocoDB::deleteRecords($table, $where, $options)` — idéntico a `CmsApi::delete`
Para uso normal, `CmsApi` es suficiente. Acude a `CocoDB` solo para SQL más bajo nivel cuando `CmsApi` no cubra el caso.
## CmsApi (JavaScript — Client-Side)
```js
// Llamar hook
CmsApi.hook('/hooks/calcular_precio/', { cantidad: 10 }, (response) => {
console.log(response);
});
// Leer registros (si está expuesto vía hooks)
CmsApi.get('productos', { where: 'activo=1' }, (records) => {
console.log(records);
});
```
## Hook middleware — auto-ejecutar antes de páginas
Un hook global puede configurarse como **middleware**: se ejecuta automáticamente antes de renderizar ciertas páginas (o todas). Útil para inyección de variables, redirecciones condicionales, control de acceso, parseo de URLs, etc.
### Tools
- `get_hook_middleware({ hookEndPoint: "/hooks/parse_styles/" })` — leer config actual
- `set_hook_middleware({ hookEndPoint: "/hooks/parse_styles/", middleWare: [...] })` — escribir config
### Valores de `middleWare`
| Valor | Comportamiento |
|-------|----------------|
| `[]` | Hook solo se ejecuta cuando se llama explícitamente (default). |
| `["allurls"]` | Se ejecuta antes de **cada página** del sitio. |
| `["<tableName>-<num>", ...]` | Se ejecuta antes de **registros específicos**. Ejemplo: `["cms_apartados-2"]` para la home. |
> Nota: en el array de `middleWare` los `tableName` van **con** prefijo `cms_` (es la representación interna de `layout.json`).
### Ejemplos
```
// Lógica de redirect que solo afecta a la home (apartados num=2)
set_hook_middleware({
hookEndPoint: "/hooks/redirect_home/",
middleWare: ["cms_apartados-2"]
})
// Inyección global (analytics, vars de sitio)
set_hook_middleware({
hookEndPoint: "/hooks/global_vars/",
middleWare: ["allurls"]
})
// Hook utilitario que se llama desde módulos — sin middleware
set_hook_middleware({
hookEndPoint: "/hooks/calcular_precio/",
middleWare: []
})
```
### Reglas
- El middleware se aplica **solo a hooks globales**, no a hooks de módulo.
- Crear el `.php` con `acai-write` **NO** activa middleware automáticamente — hay que llamar `set_hook_middleware` explícitamente.
- Lee `get_hook_middleware` antes de modificar para no sobrescribir configuraciones existentes.
## Schemas y formato de datos al insertar
Antes de un `CmsApi::insert`/`update` o de un `create_or_update_record` desde MCP, consulta el schema (`get_table_schema`). Tipos de campo y formato esperado:
| Tipo | Formato | Ejemplo |
|------|---------|---------|
| `textfield` | String | `"Texto"` |
| `textbox` | String multilínea | `"Línea 1\nLínea 2"` |
| `date`/datetime | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
| `wysiwyg` | HTML string | `"<p class=\"font-bold\">Texto</p>"` |
| `list` | String o número | `"activo"` o `"1"` |
| `checkbox` | Número 1/0 | `1` o `0` |
| `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` |
| `upload` | NO enviar | Usa `upload_record_image` después |
## Ejemplos prácticos
### Hook con operaciones de BD
```php
<?php
// hook.php del módulo "procesar_compra"
$producto = CmsApi::get("productos", "num=" . intval($producto_id));
if (empty($producto)) {
return ["success" => false, "message" => "Producto no encontrado"];
}
$total = $producto[0]['precio'] * $cantidad;
// Crear venta
$result = CmsApi::insert('ventas', [[
"usuario_num" => $usuario_id,
"producto_num" => $producto_id,
"cantidad" => $cantidad,
"total" => $total,
"fecha" => date('Y-m-d H:i:s')
]], [], ['return_last_id' => true]);
// Actualizar stock
$stock = CmsApi::get("stocks", "producto_num=" . intval($producto_id));
if (!empty($stock)) {
CmsApi::update('stocks',
["cantidad" => $stock[0]['cantidad'] - $cantidad],
"producto_num=$producto_id"
);
}
return ["success" => true, "ventaNum" => $result['lastId'], "total" => $total];
```
### Hook con relaciones cargadas
```php
<?php
$productos = CmsApi::get("productos", "activo=1", "globalOrder ASC", 10, [
'uploads' => true,
'relations' => true,
'relationsDepth' => 1
]);
return [
"success" => true,
"productos" => $productos
];
```
### Hook con búsqueda dinámica
```php
<?php
$where = "1=1";
if (!empty($termino)) {
$termino = addslashes($termino);
$where .= " AND (titulo LIKE '%$termino%' OR descripcion LIKE '%$termino%')";
}
if (!empty($categoria)) {
$where .= " AND categoria_num=" . intval($categoria);
}
$resultados = CmsApi::get("productos", $where, "fecha DESC", 20);
return ["success" => true, "count" => count($resultados), "resultados" => $resultados];
```
## Reglas críticas
1. Hook devuelve `return [...]`**nunca** `echo`/`exit`.
2. Tablas **sin prefijo `cms_`** en `CmsApi`/`CocoDB`. PK siempre `num`.
3. Foreign keys con sufijo `_num` (`categoria_num`, `usuario_num`).
4. Upload fields **no** se setean por insert/update.
5. Sanea variables externas — `intval()`, `addslashes()` o usa parámetros tipados antes de concatenar en SQL.
6. Antes de usar `set_hook_middleware`, lee con `get_hook_middleware` para no sobrescribir.
7. Hooks de módulo: endpoint = `/hooks/<module-id>/` (no `/hooks/hook.<id>/`).