Add production rules discovered during real projects
- Correct hooks-and-api.md: file hooks vs module hooks param injection, FK naming not always _num, CmsApi.hook Promise pattern - Add 9 new rules to hooks-and-api.md: reserved `tipo` var, ghost modules, cms_uploads schema, name vs title by menuType, menuOrder, localCache gotcha, slug generation, uploads from hooks, CocoEmail - Add 5 rules to modular-system.md: minified/ dir, Docker workflow, debug tools (?compiletwig/?pruebas), general sections deploy, controlador - Add 2 rules to css-js-conventions.md: Vue inline conflict, Vue mount delay - Add testing section to quick-reference.md - Create docs/deploy-and-sync.md for production deploy and sync rules - Promote 3 critical rules to CLAUDE.md (rules 11-13) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,16 +4,17 @@
|
||||
|
||||
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`.
|
||||
|
||||
### Estructura de un Hook
|
||||
### 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
|
||||
<?php
|
||||
// Los parámetros se reciben como variables directamente
|
||||
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
|
||||
|
||||
// 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,
|
||||
@@ -22,6 +23,22 @@ return [
|
||||
?>
|
||||
```
|
||||
|
||||
#### Hooks de archivo (`hooks/*.php`)
|
||||
Se ejecutan dentro de `function returnData() {}` via `eval()`. **NINGUNA variable del request se inyecta.** Hay que leer manualmente:
|
||||
```php
|
||||
<?php
|
||||
// SIEMPRE hacer esto al inicio de cada hook de archivo:
|
||||
$params = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$nombre = trim(@$params['nombre']);
|
||||
$email = trim(@$params['email']);
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"nombre" => $nombre
|
||||
];
|
||||
?>
|
||||
```
|
||||
|
||||
### Testing Hooks
|
||||
|
||||
El Docker debe estar corriendo. Hacer curl al endpoint del hook:
|
||||
@@ -32,6 +49,18 @@ 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()`.
|
||||
|
||||
**Solución:** Los hooks generales van en `hooks/hooks.{nombre}.php`. Nunca crear hooks como módulos. Si aparecen fantasmas, borrar el directorio.
|
||||
|
||||
### Cómo Llamar Hooks
|
||||
|
||||
**Desde HTML (recomendado para módulos):**
|
||||
@@ -161,7 +190,7 @@ CmsApi::delete('productos',
|
||||
|
||||
- Nombres de tabla **sin** prefijo `cms_`
|
||||
- Primary key siempre es `num`, nunca `id`
|
||||
- Foreign keys: `categoria_num`, no `categoria_id`
|
||||
- Foreign keys: el nombre es **exactamente** el definido en el schema (`.ini.php`). A veces es `categoria_num`, a veces `categoria`. Siempre consultar el schema antes de asumir
|
||||
- Upload fields: no se manejan via insert/update
|
||||
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
||||
|
||||
@@ -170,11 +199,18 @@ CmsApi::delete('productos',
|
||||
## CmsApi (JavaScript — Client-Side)
|
||||
|
||||
```js
|
||||
// Llamar hook
|
||||
// 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
|
||||
@@ -413,3 +449,111 @@ if (!empty($stock)) {
|
||||
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_<tabla>` 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`. Para insertar un upload manualmente:
|
||||
```php
|
||||
global $TABLE_PREFIX;
|
||||
$sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`)
|
||||
VALUES (NOW(), 'productos', $num, 'foto', '$urlPath', $nextOrder)";
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
Reference in New Issue
Block a user