El planner generaba 3+ steps para tareas simples causando que el coder repitiera acciones en cada step (creaba el módulo varias veces). Ahora el engine fusiona los steps en 1 coder con descripción combinada. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
389 lines
11 KiB
Markdown
389 lines
11 KiB
Markdown
# Hooks & Server-Side API
|
|
|
|
## Hooks
|
|
|
|
Hooks son archivos PHP que ejecutan lógica server-side. Pueden existir en dos sitios:
|
|
- Hooks globales en `hooks/hooks.<hook-id>.php`
|
|
- Hooks propios de módulo en `template/estandar/modulos/<module-id>/hook.php`
|
|
|
|
### Tipos de hooks
|
|
|
|
**1. 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
|
|
|
|
**2. 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
|
|
|
|
Ejemplos:
|
|
- `hooks/hooks.buscar_barcos.php` -> `/hooks/buscar_barcos/`
|
|
- `template/estandar/modulos/hero_banner/hook.php` -> `/hooks/hero_banner/`
|
|
|
|
Regla práctica:
|
|
- Si el hook solo sirve a un módulo, créalo dentro del módulo
|
|
- Si varias piezas del proyecto lo van a consumir, créalo como hook global
|
|
|
|
## Reglas obligatorias para hooks
|
|
|
|
- Un hook debe devolver datos con `return [...]`
|
|
- No uses `echo json_encode(...)`
|
|
- No uses `exit`
|
|
- Para leer parámetros, usa `$_REQUEST[...]` o las variables ya inyectadas por el sistema
|
|
- En hooks, usa `CmsApi::get()` o `CocoDB::get()` como primera opción
|
|
- No uses `CocoDB::getInstance()` salvo necesidad real muy excepcional
|
|
- No escribas SQL manual con `prepare()/bind_param()` salvo que no exista forma razonable de resolverlo con `CmsApi` o `CocoDB`
|
|
|
|
### Estructura de un Hook
|
|
|
|
```php
|
|
<?php
|
|
// Los parámetros se reciben como variables directamente
|
|
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
|
|
|
|
$resultado = $param1 * 2;
|
|
|
|
// Retornar un array (se convierte a JSON)
|
|
return [
|
|
"success" => true,
|
|
"message" => "Valor procesado: " . $resultado,
|
|
"value" => $resultado
|
|
];
|
|
?>
|
|
```
|
|
|
|
### Testing Hooks
|
|
|
|
El Docker debe estar corriendo. Hacer curl al endpoint del hook:
|
|
|
|
```bash
|
|
curl http://localhost:8080/hooks/example_hook/
|
|
```
|
|
|
|
No usar X-Hooks-Token en desarrollo local.
|
|
|
|
### Cómo Llamar Hooks
|
|
|
|
La referencia siempre se hace con el endpoint `/hooks/<id>/`:
|
|
- Para hooks globales, `<id>` es el nombre lógico del hook sin `hooks.` ni `.php`
|
|
- Para hooks de módulo, `<id>` es el `module-id` de la carpeta del módulo
|
|
|
|
**Desde HTML (recomendado para módulos):**
|
|
```html
|
|
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
|
<p>{{ myVar.message }}</p>
|
|
```
|
|
|
|
**Desde Twig:**
|
|
```twig
|
|
{% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}
|
|
<p>{{ resultado.message }}</p>
|
|
```
|
|
|
|
**Desde JavaScript:**
|
|
```js
|
|
CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
|
|
console.log(data.message);
|
|
});
|
|
```
|
|
|
|
**Ejemplo real para hook de módulo:**
|
|
```js
|
|
// Módulo: template/estandar/modulos/buscadorapartados_hjd8s/
|
|
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (data) => {
|
|
console.log(data);
|
|
});
|
|
```
|
|
|
|
**Desde otro Hook PHP:**
|
|
```php
|
|
<?php
|
|
$result = hook("/hooks/mimodulo/", ["param1" => 100, "param2" => "texto"]);
|
|
$mensaje = $result["message"];
|
|
?>
|
|
```
|
|
|
|
**Desde c-form:** Los hooks se ejecutan automáticamente al enviar el formulario si están configurados.
|
|
|
|
---
|
|
|
|
## CmsApi (PHP)
|
|
|
|
API server-side para operaciones de base de datos. Disponible en todos los hooks.
|
|
|
|
### Read — `CmsApi::get()`
|
|
|
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
|
|
```php
|
|
// Todos los registros
|
|
$products = CmsApi::get("productos");
|
|
|
|
// Con condición WHERE en string
|
|
$active = CmsApi::get("productos", "active=1");
|
|
|
|
// Con orden y límite
|
|
$latest = CmsApi::get("noticias", "", "fecha DESC", 5);
|
|
|
|
// Con condición string
|
|
$activos = CmsApi::get("productos", "activo=1");
|
|
|
|
// Condición compleja
|
|
$caros = CmsApi::get("productos", "precio > 100");
|
|
|
|
// Múltiples condiciones (AND)
|
|
$resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
|
|
|
|
// Con operadores
|
|
$expensive = CmsApi::get("productos", "precio >= 100");
|
|
$search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
|
|
$inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
|
|
|
|
#### Opciones de `get()`
|
|
|
|
| Option | Type | Default | Description |
|
|
|--------|------|---------|-------------|
|
|
| `uploads` | bool | `true` | Incluir datos de upload fields |
|
|
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
|
|
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
|
|
| `translates` | string | current lang | Código de idioma |
|
|
| `groupBy` | string | null | GROUP BY clause |
|
|
| `aggregates` | array | `[]` | Funciones de agregación |
|
|
| `onlyFields` | array | null | Seleccionar solo campos específicos |
|
|
| `debug` | bool | false | Mostrar SQL query |
|
|
| `redis` | bool | null | Forzar cache Redis |
|
|
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
|
|
|
|
// Con opciones
|
|
$datos = CmsApi::get("productos", "", "", "", [
|
|
'translates' => true,
|
|
'uploads' => true,
|
|
'relations' => true,
|
|
'relationsDepth' => 2
|
|
]);
|
|
```
|
|
|
|
### Insert — `CmsApi::insert()`
|
|
|
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
|
|
```php
|
|
// Un registro
|
|
CmsApi::insert('contacto', [
|
|
["nombre" => "John", "email" => "john@example.com", "mensaje" => "Hello"]
|
|
]);
|
|
|
|
// Múltiples registros
|
|
CmsApi::insert('productos', [
|
|
["nombre" => "Producto A", "precio" => 100],
|
|
["nombre" => "Producto B", "precio" => 200]
|
|
]);
|
|
|
|
// Con retorno del último ID
|
|
CmsApi::insert('productos',
|
|
[["nombre" => "Nuevo", "precio" => 150]],
|
|
[],
|
|
['return_last_id' => true]
|
|
);
|
|
```
|
|
|
|
#### Opciones de insert
|
|
|
|
| Option | Description |
|
|
|--------|-------------|
|
|
| `forceNum` | Permite setear el campo `num` manualmente |
|
|
| `ignoreSchema` | Saltar validación de schema |
|
|
| `ignoreFields` | Array de campos a ignorar |
|
|
|
|
|
|
### Update — `CmsApi::update()`
|
|
|
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
|
|
```php
|
|
// Con condición string
|
|
CmsApi::update('productos', ["precio" => 150], "num=1");
|
|
|
|
// Con condición array
|
|
CmsApi::update('productos',
|
|
["activo" => 1],
|
|
[["column" => "num", "operator" => "=", "value" => 1]]
|
|
);
|
|
|
|
// Múltiples registros
|
|
CmsApi::update('productos', ["activo" => 0], "precio < 50");
|
|
```
|
|
|
|
#### Opciones de update
|
|
|
|
| Option | Description |
|
|
|--------|-------------|
|
|
| `forceNum` | Permite setear el campo `num` manualmente |
|
|
| `ignoreSchema` | Saltar validación de schema |
|
|
| `ignoreFields` | Array de campos a ignorar |
|
|
|
|
|
|
### Delete — `CmsApi::delete()`
|
|
|
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
|
|
```php
|
|
CmsApi::delete('productos', "num=5");
|
|
|
|
CmsApi::delete('productos',
|
|
[["column" => "activo", "operator" => "=", "value" => 0]]
|
|
);
|
|
```
|
|
|
|
### Reglas importantes
|
|
|
|
- Nombres de tabla **sin** prefijo `cms_`
|
|
- Primary key siempre es `num`, nunca `id`
|
|
- Foreign keys: `categoria_num`, no `categoria_id`
|
|
- Upload fields: no se manejan via insert/update
|
|
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
|
|
|
---
|
|
|
|
## CmsApi (JavaScript — Client-Side)
|
|
|
|
```js
|
|
// Llamar hook
|
|
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
|
|
// response es la salida del hook
|
|
});
|
|
|
|
// Leer registros (si está expuesto via hooks)
|
|
CmsApi.get('tableName', { where: conditions }, function(records) {
|
|
// records array
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## CocoDB
|
|
|
|
Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control.
|
|
|
|
Para búsquedas y lecturas habituales, prioriza:
|
|
1. `CmsApi::get()`
|
|
2. `CocoDB::get()`
|
|
3. SQL manual solo si de verdad no hay alternativa razonable
|
|
|
|
### `CocoDB::get($table, $where, $order, $limit, $options)`
|
|
## Funcionalidad exactamente igual a CmsApi::get ( ver referencia CmsApi::get )
|
|
|
|
|
|
### `CocoDB::insertRecords($table, $records, $functions, $options)`
|
|
## Funcionalidad exactamente igual a CmsApi::insert ( ver referencia CmsApi::insert )
|
|
|
|
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
|
|
## Funcionalidad exactamente igual a CmsApi::update ( ver referencia CmsApi::update )
|
|
|
|
|
|
### `CocoDB::deleteRecords($table, $where, $options)`
|
|
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
|
|
|
|
---
|
|
|
|
## Creación y Actualización de Registros
|
|
|
|
### Flujo correcto
|
|
|
|
1. Consultar el esquema de la tabla (leer `cms/data/schema/{tabla}.ini.php`)
|
|
2. Revisar los tipos de campo
|
|
3. Rellenar según el tipo de dato
|
|
4. Enviar con la estructura correcta
|
|
|
|
### Tipos de campo y formato
|
|
|
|
| Tipo | Formato | Ejemplo |
|
|
|------|---------|---------|
|
|
| **Text field** | String | `"Texto"` |
|
|
| **Text box** | String multilínea | `"Línea 1\nLínea 2"` |
|
|
| **Date/time** | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
|
|
| **Wysiwyg** | String HTML | `"<p class=\"font-bold\">Texto</p>"` |
|
|
| **List** | String o número | `"activo"` o `"1"` (num si es foreign key) |
|
|
| **Checkbox** | Número 1/0 | `1` o `0` |
|
|
| **Multivalores** | String JSON | `"[{\"producto\":\"1\"}]"` |
|
|
| **Upload** | **NO enviar** — usar `upload_record_image` después de crear el registro |
|
|
|
|
---
|
|
|
|
## Table Schemas
|
|
|
|
Los schemas están en `cms/data/schema/` como archivos `.ini.php`. Definen:
|
|
- Nombres y tipos de campo
|
|
- Reglas de validación
|
|
- Relaciones (foreign keys)
|
|
- Configuración de display
|
|
|
|
---
|
|
|
|
## Ejemplos Prácticos
|
|
|
|
### Hook de Cálculo de Precio
|
|
|
|
```php
|
|
<?php
|
|
// hook.php del módulo "calcular_precio"
|
|
$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
|
|
];
|
|
?>
|
|
```
|
|
|
|
```html
|
|
<hook result="precio" endpoint="/hooks/calcular_precio/" :cantidad="10" :tipo="'mayoreo'"></hook>
|
|
<p>Total: ${{ precio.total }}</p>
|
|
```
|
|
|
|
En este ejemplo, el endpoint usa `calcular_precio` porque el archivo vive en:
|
|
`template/estandar/modulos/calcular_precio/hook.php`
|
|
|
|
### Hook con Operaciones de BD
|
|
|
|
```php
|
|
<?php
|
|
// hook.php del módulo "procesar_compra"
|
|
$producto = CmsApi::get("productos", "num=" . $producto_id);
|
|
|
|
if (empty($producto)) {
|
|
return ["success" => false, "message" => "Producto no encontrado"];
|
|
}
|
|
|
|
$total = $producto[0]['precio'] * $cantidad;
|
|
|
|
// Crear venta
|
|
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=" . $producto_id);
|
|
if (!empty($stock)) {
|
|
CmsApi::update('stocks',
|
|
["cantidad" => $stock[0]['cantidad'] - $cantidad],
|
|
"producto_num=$producto_id"
|
|
);
|
|
}
|
|
|
|
return ["success" => true, "total" => $total];
|
|
?>
|
|
```
|