Forzar máximo 2 steps en plan: 1 coder + 1 reviewer opcional
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>
This commit is contained in:
@@ -2,7 +2,39 @@
|
||||
|
||||
## 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/<module-id>/hook.php`.
|
||||
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
|
||||
|
||||
@@ -34,6 +66,10 @@ 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>
|
||||
@@ -53,6 +89,14 @@ CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
|
||||
});
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -71,37 +115,49 @@ 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');
|
||||
$products = CmsApi::get("productos");
|
||||
|
||||
// Con condición WHERE
|
||||
$active = CmsApi::get('productos', ['active' => 1]);
|
||||
// Con condición WHERE en string
|
||||
$active = CmsApi::get("productos", "active=1");
|
||||
|
||||
// Con orden y límite
|
||||
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5);
|
||||
$latest = CmsApi::get("noticias", "", "fecha DESC", 5);
|
||||
|
||||
// Con condición string
|
||||
$activos = CmsApi::get('productos', 'activo=1');
|
||||
$activos = CmsApi::get("productos", "activo=1");
|
||||
|
||||
// Condición compleja como array
|
||||
$caros = CmsApi::get('productos', [
|
||||
["column" => "precio", "operator" => ">", "value" => 100]
|
||||
]);
|
||||
// Condición compleja
|
||||
$caros = CmsApi::get("productos", "precio > 100");
|
||||
|
||||
// Múltiples condiciones (AND)
|
||||
$resultados = CmsApi::get('productos', [
|
||||
["column" => "activo", "operator" => "=", "value" => 1],
|
||||
["column" => "stock", "operator" => ">", "value" => 0]
|
||||
]);
|
||||
$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]]]);
|
||||
$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', '', '', '', [
|
||||
$datos = CmsApi::get("productos", "", "", "", [
|
||||
'translates' => true,
|
||||
'uploads' => true,
|
||||
'relations' => true,
|
||||
@@ -111,6 +167,8 @@ $datos = CmsApi::get('productos', '', '', '', [
|
||||
|
||||
### 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', [
|
||||
@@ -131,8 +189,19 @@ CmsApi::insert('productos',
|
||||
);
|
||||
```
|
||||
|
||||
#### 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");
|
||||
@@ -147,8 +216,19 @@ CmsApi::update('productos',
|
||||
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");
|
||||
|
||||
@@ -187,134 +267,24 @@ CmsApi.get('tableName', { where: conditions }, function(records) {
|
||||
|
||||
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 )
|
||||
|
||||
```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 |
|
||||
## 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 )
|
||||
|
||||
```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
|
||||
]
|
||||
```
|
||||
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
|
||||
|
||||
---
|
||||
|
||||
@@ -379,12 +349,15 @@ return [
|
||||
<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");
|
||||
$producto = CmsApi::get("productos", "num=" . $producto_id);
|
||||
|
||||
if (empty($producto)) {
|
||||
return ["success" => false, "message" => "Producto no encontrado"];
|
||||
@@ -402,7 +375,7 @@ CmsApi::insert('ventas', [[
|
||||
]], [], ['return_last_id' => true]);
|
||||
|
||||
// Actualizar stock
|
||||
$stock = CmsApi::get('stocks', "producto_num=$producto_id");
|
||||
$stock = CmsApi::get("stocks", "producto_num=" . $producto_id);
|
||||
if (!empty($stock)) {
|
||||
CmsApi::update('stocks',
|
||||
["cantidad" => $stock[0]['cantidad'] - $cantidad],
|
||||
|
||||
Reference in New Issue
Block a user