ajustes
This commit is contained in:
383
docs/06-hooks-and-cmsapi.md
Normal file
383
docs/06-hooks-and-cmsapi.md
Normal 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>/`).
|
||||
Reference in New Issue
Block a user