Ajustes del scaffold

This commit is contained in:
Jordan
2026-03-23 21:34:03 +00:00
parent 53dde9eb92
commit db90dfaca2
9 changed files with 2400 additions and 319 deletions

View File

@@ -2,155 +2,210 @@
## Hooks
Hooks are PHP files in the `hooks/` directory that execute server-side logic. They can also live inside a module at `template/estandar/modulos/<module-id>/hook.php`.
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`.
### Testing Hooks
To test hooks, the site's Docker container must be running. Make a curl request to the Docker URL with the hook path. For example, if a hook is named `hooks.example_hook.php`:
```bash
curl http://{DOCKER_URL_AND_PORT}/hooks/example_hook/
```
Replace `{DOCKER_URL_AND_PORT}` with your local Docker address (e.g., `localhost:8080`) and parse hook name for url endpoint.
Do not use X-Hooks-Token because its not needed on developer environment.
### How to Call Hooks
**From Twig:**
```twig
{{ 'hooks/module_id/' | hook({param1: 'value1', param2: variable}) }}
```
**From HTML (with result):**
```html
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
<p>{{ myVar }}</p>
```
**From JavaScript:**
```js
CmsApi.hook('/hooks/module_id/', { param1: 'value1' }, function(response) {
console.log(response);
});
```
**From c-form:**
Hooks are automatically triggered on form submission when configured.
### Hook Parameters
Parameters are received as PHP variables:
### Estructura de un Hook
```php
<?php
// Called with: 'hooks/my_hook/' | hook({category: 'electronics', limit: 10})
// Available as:
$category; // 'electronics'
$limit; // 10
// 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
];
?>
```
### Hook Return Values
### Testing Hooks
Hooks can `echo` or `return` values. When called from Twig or `<hook>` tag, the output is captured into the result variable.
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
**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);
});
```
**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)
Server-side API for database operations. Available in all hooks.
API server-side para operaciones de base de datos. Disponible en todos los hooks.
### Read Records
### Read — `CmsApi::get()`
```php
// Get all records
// Todos los registros
$products = CmsApi::get('productos');
// With WHERE condition
// Con condición WHERE
$active = CmsApi::get('productos', ['active' => 1]);
// With order and limit
// Con orden y límite
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5);
// With operators
// 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]]]);
```
### Insert Records
```php
$newRecord = CmsApi::insert('contacto', [
'nombre' => 'John',
'email' => 'john@example.com',
'mensaje' => 'Hello',
// Con opciones
$datos = CmsApi::get('productos', '', '', '', [
'translates' => true,
'uploads' => true,
'relations' => true,
'relationsDepth' => 2
]);
```
### Update Records
### Insert — `CmsApi::insert()`
```php
CmsApi::update('productos',
['precio' => 29.99, 'activo' => 1], // fields to update
['num' => 42] // where condition
// 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]
);
```
### Delete Records
### Update — `CmsApi::update()`
```php
CmsApi::delete('productos', ['num' => 42]);
// 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");
```
### Important Rules
### Delete — `CmsApi::delete()`
- Table names **without** `cms_` prefix
- Primary key is always `num`, never `id`
- Upload fields are handled separately (not via insert/update)
- Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
```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
// Hook call
// Llamar hook
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
// response is the hook output
// response es la salida del hook
});
// Record operations (if exposed via hooks)
// Leer registros (si está expuesto via hooks)
CmsApi.get('tableName', { where: conditions }, function(records) {
// records array
});
```
---
## CocoDB
Low-level database abstraction layer used internally by CmsApi. Use directly from hooks when you need more control.
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)`
Reads records from a table. This is the same method CmsApi::get wraps.
```php
// Basic query
// Básico
$records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10);
// With advanced where (array syntax for operators)
// Where con operadores avanzados
$records = CocoDB::get('productos', [
['column' => 'precio', 'value' => 100, 'operator' => '>='],
['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'],
]);
// OR conditions
// Condiciones OR
$records = CocoDB::get('productos', [
['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'],
['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true],
]);
// NOT condition
// NOT
$records = CocoDB::get('productos', [
['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true],
]);
@@ -160,63 +215,59 @@ $records = CocoDB::get('productos', [
['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'],
]);
// Limit with offset
// Limit con offset
$records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]);
```
#### Options for `get()`
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Include upload field data |
| `relations` | bool/array | `true` | Resolve foreign key relations. Pass array to limit: `['category']` |
| `relationsDepth` | int | 2 | Depth of nested relation resolution |
| `translates` | string | current lang | Language code for translations |
| `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 | `[]` | Aggregate functions |
| `onlyFields` | array | null | Select specific fields only |
| `debug` | bool | false | Output SQL query for debugging |
| `redis` | bool | null | Force Redis cache |
| `redis_expire` | int | 60 | Redis cache TTL in seconds |
| `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)`
Insert one or multiple records.
```php
// Single record
// Un registro
$count = CocoDB::insertRecords('contacto', [
'nombre' => 'John',
'email' => 'john@example.com',
]);
// Returns number of inserted records. Use mysql_insert_id() to get the new num.
// Usar mysql_insert_id() para obtener el nuevo num
// Multiple records
// Múltiples
$count = CocoDB::insertRecords('productos', [
['nombre' => 'Product A', 'precio' => 10],
['nombre' => 'Product B', 'precio' => 20],
]);
```
#### Options for insert/update
#### Opciones de insert/update
| Option | Description |
|--------|-------------|
| `forceNum` | Allow setting the `num` field manually |
| `ignoreSchema` | Skip schema validation |
| `ignoreFields` | Array of field names to skip |
| `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)`
Update records matching a where condition.
```php
CocoDB::updateRecords('productos',
['precio' => 29.99, 'activo' => 1], // fields to update
['num' => 42] // where condition
['precio' => 29.99, 'activo' => 1],
['num' => 42]
);
// With operator in where
// Con operador en where
CocoDB::updateRecords('productos',
['activo' => 0],
[['column' => 'stock', 'value' => 0, 'operator' => '<=']]
@@ -225,68 +276,140 @@ CocoDB::updateRecords('productos',
### `CocoDB::deleteRecords($table, $where, $options)`
Delete records matching a where condition.
```php
CocoDB::deleteRecords('productos', ['num' => 42]);
// With operator
CocoDB::deleteRecords('logs', [
['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<']
]);
```
### Where Clause Syntax
### Parámetro `$functions`
The `$where` parameter supports two formats:
**Simple (key-value):**
```php
['campo' => 'valor'] // campo = 'valor'
```
**Advanced (array of conditions):**
```php
[
'column' => 'field_name', // Required
'value' => 'match_value', // Required
'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL
'or' => false, // Use OR instead of AND
'not' => false, // Negate the condition
'raw_key' => false, // Skip column existence check
]
```
### Functions Parameter
The `$functions` parameter lets you apply MySQL functions to values during insert/update:
Permite aplicar funciones MySQL a valores durante insert/update:
```php
CocoDB::insertRecords('logs', [
'mensaje' => 'Login exitoso',
'fecha' => '',
], [
'fecha' => 'NOW()', // Will use MySQL NOW() instead of the value
'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 | `"<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
Table schemas are stored as JSON in `cms/data/schema/`. Each file defines:
- Field names and types
- Validation rules
- Relationships (foreign keys)
- Display configuration
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
### Field Format Types
---
| Type | PHP Format | Notes |
|------|-----------|-------|
| Text | String | Plain text |
| Date/time | `YYYY-MM-DD HH:mm:ss` | MySQL datetime format |
| Checkbox | `1` or `0` | Boolean as integer |
| WYSIWYG | HTML string | Rich text with Tailwind classes |
| List | String or num | Foreign key if linked to table |
| Multivalores | JSON string | Serialized array |
| Upload | — | Handled separately, never in insert/update |
## 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>
```
### 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];
?>
```