Files
acai-scaffold/docs/hooks-and-api.md
2026-04-01 13:31:36 +00:00

11 KiB

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

Estructura de un Hook

<?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:

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):

<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
<p>{{ myVar.message }}</p>

Desde Twig:

{% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}
<p>{{ resultado.message }}</p>

Desde JavaScript:

CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
  console.log(data.message);
});

Desde otro Hook 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()

// 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()

// 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()

// 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()

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)

// 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.

CocoDB::get($table, $where, $order, $limit, $options)

// 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)

// 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)

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)

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:

CocoDB::insertRecords('logs', [
    'mensaje' => 'Login exitoso',
    'fecha' => '',
], [
    'fecha' => 'NOW()',
]);

Where Clause — Formatos

Simple (key-value):

['campo' => 'valor']  // campo = 'valor'

Avanzado (array de condiciones):

[
    '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 "<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
// 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
];
?>
<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
// 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];
?>