# 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..php` - Hooks propios de módulo en `template/estandar/modulos//hook.php` ### Tipos de hooks **1. Hook global** - Archivo: `hooks/hooks..php` - Endpoint: `/hooks//` - Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios **2. Hook propio de módulo** - Archivo: `template/estandar/modulos//hook.php` - Endpoint: `/hooks//` - Ú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 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//`: - Para hooks globales, `` es el nombre lógico del hook sin `hooks.` ni `.php` - Para hooks de módulo, `` es el `module-id` de la carpeta del módulo **Desde HTML (recomendado para módulos):** ```html

{{ myVar.message }}

``` **Desde Twig:** ```twig {% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}

{{ resultado.message }}

``` **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 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()` ```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)"); // Con opciones $datos = CmsApi::get("productos", "", "", "", [ 'translates' => true, 'uploads' => true, 'relations' => true, 'relationsDepth' => 2 ]); ``` ### Insert — `CmsApi::insert()` ```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] ); ``` ### Update — `CmsApi::update()` ```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"); ``` ### Delete — `CmsApi::delete()` ```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 ) #### 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) | ### `CocoDB::insertRecords($table, $records, $functions, $options)` ```php // Un registro $count = CocoDB::insertRecords('contacto', [ 'nombre' => 'John', 'email' => 'john@example.com', ]); // Usar mysql_insert_id() para obtener el nuevo num // Múltiples $count = CocoDB::insertRecords('productos', [ ['nombre' => 'Product A', 'precio' => 10], ['nombre' => 'Product B', 'precio' => 20], ]); ``` #### Opciones de insert/update | Option | Description | |--------|-------------| | `forceNum` | Permite setear el campo `num` manualmente | | `ignoreSchema` | Saltar validación de schema | | `ignoreFields` | Array de campos a ignorar | ### `CocoDB::updateRecords($table, $records, $where, $functions, $options)` ```php CocoDB::updateRecords('productos', ['precio' => 29.99, 'activo' => 1], ['num' => 42] ); // Con operador en where CocoDB::updateRecords('productos', ['activo' => 0], [['column' => 'stock', 'value' => 0, 'operator' => '<=']] ); ``` ### `CocoDB::deleteRecords($table, $where, $options)` ```php CocoDB::deleteRecords('productos', ['num' => 42]); CocoDB::deleteRecords('logs', [ ['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<'] ]); ``` ### Parámetro `$functions` Permite aplicar funciones MySQL a valores durante insert/update: ```php CocoDB::insertRecords('logs', [ 'mensaje' => 'Login exitoso', 'fecha' => '', ], [ 'fecha' => 'NOW()', ]); ``` ### Where Clause — Formatos **Simple (key-value):** ```php ['campo' => 'valor'] // campo = 'valor' ``` **Avanzado (array de condiciones):** ```php [ 'column' => 'field_name', 'value' => 'match_value', 'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL 'or' => false, // OR en vez de AND 'not' => false, // Negar la condición 'raw_key' => false, // Saltar check de existencia de columna ] ``` --- ## 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 | `"

Texto

"` | | **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 10) { $precioUnitario *= 0.85; // 15% descuento } return [ "success" => true, "precioUnitario" => round($precioUnitario, 2), "total" => round($precioUnitario * $cantidad, 2), "descuento" => $tipo === 'mayoreo' ? 15 : 0 ]; ?> ``` ```html

Total: ${{ precio.total }}

``` 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 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]; ?> ```