12 KiB
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.phpdentro del módulo. - Si varias piezas la consumen → hook global en
hooks/.
Reglas obligatorias
- Devuelve datos con
return [...]. NUNCA usesecho json_encode(...)niexit. - 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()oCocoDB::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 conCmsApioCocoDB.
Estructura de un hook
<?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)
{% set resultado = 'hooks/calcular_precio/' | hook({cantidad: 5, tipo: 'mayoreo'}) %}
<p>Total: {{ resultado.total }}€</p>
Desde HTML (etiqueta <hook>)
<hook result="precio" endpoint="/hooks/calcular_precio/" :cantidad="10" :tipo="'mayoreo'"></hook>
<p>{{ precio.message }}</p>
Desde JavaScript
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
$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:
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()
// 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 ` |
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()
// 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()
// 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()
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_imagedespué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 aCmsApi::getCocoDB::insertRecords($table, $records, $functions, $options)— idéntico aCmsApi::insertCocoDB::updateRecords($table, $records, $where, $functions, $options)— idéntico aCmsApi::updateCocoDB::deleteRecords($table, $where, $options)— idéntico aCmsApi::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)
// 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 actualset_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
middleWarelostableNamevan con prefijocms_(es la representación interna delayout.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
.phpconacai-writeNO activa middleware automáticamente — hay que llamarset_hook_middlewareexplícitamente. - Lee
get_hook_middlewareantes 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
// 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
$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
$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
- Hook devuelve
return [...]— nuncaecho/exit. - Tablas sin prefijo
cms_enCmsApi/CocoDB. PK siemprenum. - Foreign keys con sufijo
_num(categoria_num,usuario_num). - Upload fields no se setean por insert/update.
- Sanea variables externas —
intval(),addslashes()o usa parámetros tipados antes de concatenar en SQL. - Antes de usar
set_hook_middleware, lee conget_hook_middlewarepara no sobrescribir. - Hooks de módulo: endpoint =
/hooks/<module-id>/(no/hooks/hook.<id>/).