# 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..php` o propio de módulo en `template/estandar/modulos//hook.php`), las cuatro formas de invocarlo (filtro Twig, etiqueta ``, 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..php` - Endpoint: `/hooks//` - Ú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//hook.php` - Endpoint: `/hooks//` - Ú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 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'}) %}

Total: {{ resultado.total }}€

``` ### Desde HTML (etiqueta ``) ```html

{{ precio.message }}

``` ### 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: **`_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. | | `["-", ...]` | 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 | `"

Texto

"` | | `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 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 true, 'relations' => true, 'relationsDepth' => 1 ]); return [ "success" => true, "productos" => $productos ]; ``` ### Hook con búsqueda dinámica ```php 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//` (no `/hooks/hook./`).