# Hooks & Server-Side API ## Hooks Hooks son archivos PHP en `hooks/` que ejecutan lógica server-side. También pueden estar dentro de un módulo en `template/estandar/modulos//hook.php`. ### Hooks de módulo vs hooks de archivo **IMPORTANTE:** Los hooks de **módulo** (`modulos/*/hook.php`) y los hooks de **archivo** (`hooks/*.php`) reciben parámetros de forma DIFERENTE. #### Hooks de módulo (`modulos/MODULE_ID/hook.php`) Los parámetros se inyectan como variables directamente: ```php true, "message" => "Valor procesado: " . $resultado, "value" => $resultado ]; ?> ``` #### Hooks de archivo (`hooks/*.php`) Se ejecutan dentro de `function returnData() {}` via `eval()`. **NINGUNA variable del request se inyecta.** Hay que leer manualmente: ```php true, "nombre" => $nombre ]; ?> ``` ### 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. ### Gotchas de Hooks #### `tipo` es variable reservada El `.htaccess` de Acai añade `tipo=barra` a cada request. Si envías `tipo` como parámetro en un hook, se sobrescribe → 404 o comportamiento inesperado. **Usar nombres alternativos:** `account_type`, `user_tipo`, `record_tipo`, etc. #### Módulos fantasma interceptan hooks Si `save_module` crea un directorio `modulos/auth_signup/hook.php`, este intercepta la URL `/hooks/auth_signup/` porque `addModulesHooksToLayout()` se ejecuta ANTES que `addFilesHooksToLayout()`. **Regla:** Nunca crear un directorio en `modulos/` solo para tener un hook. Para lógica server-side, usar siempre hooks generales en `hooks/hooks.{nombre}.php`. Si aparecen módulos fantasma por accidente, borrar el directorio. ### Cómo Llamar Hooks **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:** (ver [CmsApi JS](#cmsapi-javascript--client-side) para callback + Promise) ```js CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}).then(res => console.log(res)); ``` **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 $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 como array $caros = CmsApi::get('productos', [ ["column" => "precio", "operator" => ">", "value" => 100] ]); // Múltiples condiciones (AND) $resultados = CmsApi::get('productos', [ ["column" => "activo", "operator" => "=", "value" => 1], ["column" => "stock", "operator" => ">", "value" => 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: consultar siempre el schema (ver [Gotchas de BD](#gotchas-de-base-de-datos)) - Upload fields: no se manejan via insert/update - Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN` - **Traducción en PHP:** Usar `t_var()` para todo texto visible al usuario: ```php return ['error' => t_var('Debes iniciar sesión')]; return ['success' => t_var('Datos guardados correctamente')]; ``` --- ## CmsApi (JavaScript — Client-Side) ```js // Llamar hook (callback) CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) { // response es la salida del hook }); // Llamar hook (Promise) CmsApi.hook('/hooks/module_id/', { param: 'value' }).then(function(res) { // res = lo que devuelve el PHP del hook }).catch(function(err) { // error de red }); // 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. ### `CocoDB::get($table, $where, $order, $limit, $options)` ```php // Básico $records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10); // Where con operadores avanzados $records = CocoDB::get('productos', [ ['column' => 'precio', 'value' => 100, 'operator' => '>='], ['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'], ]); // Condiciones OR $records = CocoDB::get('productos', [ ['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'], ['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true], ]); // NOT $records = CocoDB::get('productos', [ ['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true], ]); // IS NULL $records = CocoDB::get('productos', [ ['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'], ]); // Limit con offset $records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]); ``` #### 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 }}

``` ### 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]; ?> ``` --- ## Gotchas de Base de Datos ### Nombres de columnas = nombres en el schema Los campos foreign key NO añaden `_num` automáticamente. El nombre es exactamente el definido en el `.ini.php`. Siempre consultar `DESCRIBE cms_` o el schema antes de escribir INSERT/UPDATE. ### Campos `name` vs `title` según `menuType` El campo principal depende del `menuType` de la tabla: | menuType | Campo principal | Ejemplos | |----------|----------------|----------| | `category` | `name` | apartados, categorias_productos, productos | | `multi` | `title` | users, localizaciones, correos, favoritos | | `single` | no aplica | configuracion, configuracion_tienda | Las tablas `category` generan `name` automáticamente como campo de navegación. Las `multi` generan `title` como campo scaffold por defecto. ### Orden de tablas y campos Al crear tablas o campos, usar `menuOrder` / `order` alto (99+) para que aparezcan al final del listado admin sin alterar el orden existente. ### `cms_uploads` tiene esquema diferente La tabla usa `createdTime` (no `createdDate`). No tiene `createdByUserNum`, `updatedDate`, ni `updatedByUserNum`. Ver [Uploads desde Hooks](#uploads-desde-hooks) para el ejemplo completo de INSERT. ### Upload fields en PHP Incluso con una sola imagen, los uploads son arrays: ```php $image = @$p['foto'][0]['urlPath'] ?: ''; ``` --- ## CocoDB Cache en Memoria `CocoDB::localCache()` se llama en cada request. Todos los `CmsApi::get()` se cachean en memoria por hash de SQL+options. Si insertas un registro y luego haces get con la misma query exacta, devuelve el resultado cacheado (sin el nuevo registro). **Solución:** Usar opciones diferentes en la segunda query (ej: `ignoreSchema: true` en una, sin en la otra) para que el hash sea distinto. --- ## Generación de Slug (enlace) `CmsApi::insert()` desde hooks NO genera el slug automáticamente (el panel admin sí lo hace). Generar manualmente: ```php $slug = strtolower(trim($name)); $slug = preg_replace('/[áàâäã]/u', 'a', $slug); $slug = preg_replace('/[éèêë]/u', 'e', $slug); $slug = preg_replace('/[íìîï]/u', 'i', $slug); $slug = preg_replace('/[óòôöõ]/u', 'o', $slug); $slug = preg_replace('/[úùûü]/u', 'u', $slug); $slug = preg_replace('/[ñ]/u', 'n', $slug); $slug = preg_replace('/[^a-z0-9\-]/', '-', $slug); $slug = preg_replace('/-+/', '-', $slug); $slug = trim($slug, '-'); $enlace = '/' . $slug . '-' . substr(uniqid(), -6) . '/'; ``` --- ## Uploads desde Hooks CmsApi no gestiona uploads directamente. Para subir archivos desde un hook: 1. Recibir base64 via `php://input` 2. Decodificar y guardar en `$_SERVER['DOCUMENT_ROOT'] . '/cms/uploads/'` 3. Insertar manualmente en `cms_uploads` ```php $params = json_decode(file_get_contents('php://input'), true) ?: []; $data = base64_decode($params['file_b64']); $filename = $num . '_' . time() . '.' . $extension; $uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/cms/uploads'; file_put_contents($uploadsDir . '/' . $filename, $data); global $TABLE_PREFIX; $sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`) VALUES (NOW(), 'productos', $num, 'foto', '/cms/uploads/$filename', 1)"; mysql_query($sql); ``` --- ## Email (CocoEmail) ### Requiere template en BD `CocoEmail::send('IDENTIFICADOR', $params, $to)` busca el template en `cms_correos` por `identificador`. Si no existe → excepción "Correo no encontrado". Si `$to` tiene emails inválidos/vacíos → "No hay destinatarios válidos". **Siempre envolver en try/catch:** ```php try { hook("/hooks/customEmailHeader/", ["remote" => @$_SESSION["REMOTE_URL"]]); CocoEmail::send("ACTIVAR_CUENTA", $user, [$user["email"]]); } catch (Exception $e) { // Email falló pero la operación principal sigue } ``` ### Variables en templates de email Las variables se reemplazan con `{nombre_campo}`: ``` Hola {nombre}, tu código es {codigo} ``` El segundo parámetro de `send()` es el array de datos con las variables.