Files
agenticSystem/docs/06-hooks-and-cmsapi.md
Jordan Diaz 6881d64a08 ajustes
2026-04-25 10:27:51 +00:00

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.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
// 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_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)

// 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
// 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

  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>/).