# 02 - Arquitectura del Framework

## Flujo HTTP completo

```
Navegador → public/index.php → core/AutoLoad.php → Resolución de controlador → Controller::process()
                                     │
                                     ├── Session hardening (HttpOnly, SameSite=Strict, Secure)
                                     ├── Composer autoload + fallback classmap
                                     ├── Env::load(.env)
                                     ├── Config + ConfigEnv
                                     ├── TenantContext::bootstrap()
                                     ├── Controller::syncConfigFromEnv()
                                     └── Despacho vía ?c=<controller>&a=<action>
```

### Detalle paso a paso

1. **`public/index.php`** — Define `BASE_PATH` y carga `core/AutoLoad.php`.
2. **`core/AutoLoad.php`** — Bootstrap completo:
   - Configura cookies de sesión endurecidas.
   - Carga Composer autoload.
   - Carga `.env` → `Config` → `ConfigEnv` → `TenantContext`.
   - Registra autoloader fallback con classmap y paths de búsqueda.
   - Resuelve `$_GET['c']` al controlador (default: `Home`).
   - Instancia el controlador y llama `process()`.
   - Emite security headers al final (X-Frame-Options, CSP, HSTS, etc.).
3. **`Controller::process()`** — Ejecuta el pipeline de middleware, luego despacha a `{action}Action()`.

## Componentes del Core

### Controller (`core/Controller.php`)

Clase base abstracta. Todos los controladores heredan de ella.

**Propiedades clave:**
- `$MODULE_NAME` — nombre del módulo (usado para permisos y rutas).
- `$TITLE_NAME` — título visible del módulo.
- `$ViewFolder` — carpeta de vistas (`app/views/{folder}/`).
- `$Model` — instancia del modelo activo.
- `$AccessControl` — mapa de acción → nivel de acceso.
- `$Permission` — permisos del usuario actual para este módulo.
- `$MyLayout` — layout a usar (default: `metronic`).
- `$ActionDefault` — acción por defecto (default: `list`).

**Ciclo de vida del constructor:**

```php
__construct()
├── syncConfigFromEnv()          // sincroniza ConfigEnv → propiedades estáticas
├── configContext                 // carga layouts y application config
├── loadPermission()             // obtiene permisos del usuario para el módulo
├── loadSystemUser()             // carga ID de usuario de sesión
├── loadAccessControl()          // define mapa acción → '*'|'@'
├── Parameters = Request::get()  // captura parámetros GET
└── header Content-Type UTF-8
```

**Pipeline `process()`:**

```php
process()
├── Obtener acción ($_GET['a'] o ActionDefault)
├── MiddlewarePipeline([AuthMiddleware, CsrfMiddleware, RateLimitMiddleware])
│   ├── AuthMiddleware → validateAccess() → verifica sesión + permisos
│   ├── CsrfMiddleware → valida token en POST (marca $_SERVER['KLEE_CSRF_VALIDATED'])
│   └── RateLimitMiddleware → verifica límite de intentos por IP
├── Si validation == 'ACCESS' → ejecuta {action}Action()
└── Si falla → redirect a DIR_INDEX según tipo de error
```

**Acciones CRUD base** (heredadas, pueden sobreescribirse):

| Método | Propósito |
|---|---|
| `listAction()` | Renderiza vista de lista con `ListaAjax` |
| `viewAction()` | Vista de detalle |
| `addAction()` | Formulario de creación (stub) |
| `modifyAction()` | Formulario de edición (stub) |
| `removeAction()` | Eliminación (stub) |
| `dataListAjaxAction()` | Endpoint JSON para DataTables server-side |

### Model (`core/Model.php`)

Clase base abstracta con 4 traits integrados.

**Propiedades clave:**

```php
protected static $TABLE_NAME = '';       // tabla de BD
protected static $VIEW_NAME  = '';       // vista SQL (para lectura)
protected static $CONNECTION_NAME = 'klee';  // conexión nombrada
protected static $LOG   = false;         // habilitar audit log
protected static $CACHE = false;         // habilitar cache de consultas
```

**Métodos principales:**

| Método | Propósito |
|---|---|
| `getOptionsAttributes()` | Define columnas/atributos del modelo (array de arrays) |
| `save($attributes)` | Create o Update inteligente (según si `Id` tiene valor). Incluye validación CSRF |
| `saveOnCreate($attributes)` | Fuerza INSERT |
| `saveOnUpdate($attributes)` | Fuerza UPDATE |
| `saveOnly($action)` | Persistencia sin validación de atributos |
| `loadById($id)` | Carga registro por ID |
| `find($criteria)` | Busca registros con criterios |
| `findAll($criteria)` | Todos los registros con criterios |
| `editFromParameters($fields, $criteria)` | Update parcial directo |
| `deleteFromParameters($criteria)` | Delete con criterios |
| `beforeCreate($attributes)` | Hook de validación pre-creación |
| `beforeUpdate($attributes)` | Hook de validación pre-edición |

**Flujo de `save()`:**

```php
save($attributes)
├── Si es POST HTTP:
│   ├── ¿Middleware ya validó CSRF? (KLEE_CSRF_VALIDATED) → skip
│   ├── ¿Modo no-estricto sin token? → permite (legacy)
│   └── ¿Token inválido? → return false + flash error
├── Si Id es null → saveOnCreate()
│   └── beforeCreate() → collectFields → appendTenant → INSERT
└── Si Id tiene valor → saveOnUpdate()
    └── beforeUpdate() → collectFields → UPDATE
```

### View (`core/View.php`)

Renderizado de vistas con protección contra path traversal.

| Método | Propósito |
|---|---|
| `render_view($viewName, $parameters, $layoutName)` | Renderiza vista con layout. Los parámetros se extraen como variables locales |
| `load_view($viewName, $parameters)` | Renderiza vista parcial (sin layout) |
| `render_partial($viewName, $parameters)` | Renderiza parcial y retorna HTML como string |

**Convenciones de archivos de vista:**

```
app/views/{modulo}/
├── list.php          # Vista de listado
├── view.php          # Vista de detalle
├── create.php        # Página de creación
├── edit.php          # Página de edición
├── _form.php         # Partial compartido del formulario (prefijo _)
└── _headboard.php    # Partial de encabezado
```

### Router (`core/Router.php`)

Enrutamiento legacy basado en query string.

```php
// Generar URL (retorna '#' si el usuario no tiene permiso)
$url = Router::create_action_url('modulo', 'accion', ['Id' => 5]);
// Resultado: /dir/?c=modulo&a=accion&Id=5

// Redireccionar
Router::redirect_to_action('modulo', 'list');
```

**Formato de URL:** `{base_url}/{DIR_LLAMADO}/?c={controller}&a={action}&key=val`

> **Regla:** No cambiar el contrato de rutas `?c=&a=`. Todo el sistema depende de este formato.

## Sistema de atributos

Los modelos definen sus columnas mediante `getOptionsAttributes()`, retornando un array de arrays. Cada atributo se convierte en una instancia de clase en `core/attributes/`.

### Tipos de atributo disponibles (26)

| Tipo | Clase | Descripción |
|---|---|---|
| `AutoincrementId` | AutoincrementId | Primary key auto-incrementable |
| `text` | Text | Texto corto (varchar 100) |
| `textarea` | Textarea | Texto largo |
| `email` | Email | Email validado (varchar 250) |
| `password` | Password | Contraseña con confirmación y política |
| `select` | Select | Dropdown (carga datos de tabla) |
| `radio` | Radio | Radio buttons |
| `checkbox` | Checkbox | Boolean checkbox |
| `date` | Date | Fecha (YYYY-MM-DD) |
| `FechaHora` | FechaHora | Fecha y hora |
| `time` | Time | Hora |
| `integer` | Integer | Número entero |
| `decimal` | Decimal | Número decimal |
| `money` | Money | Valor monetario |
| `hidden` | Hidden | Campo oculto |
| `image` | Image | Subida de imagen |
| `document` | Document | Subida de documento |
| `htmlEditor` | HtmlEditor | Editor de texto enriquecido |
| `encrypted` | Encrypted | Valor cifrado |
| `slider` | Slider | Control deslizante |
| `consecutive` | Consecutive | Numeración secuencial |
| `UniqueId` | UniqueId | UUID/identificador único |
| `RegistrationDate` | RegistrationDate | Fecha de creación (automática) |
| `RegistrationUser` | RegistrationUser | Usuario creador (automático) |
| `ModificationDate` | ModificationDate | Fecha de última modificación (automática) |
| `ModificationUser` | ModificationUser | Usuario modificador (automático) |

### Ejemplo de definición de atributos

```php
public static function getOptionsAttributes()
{
    return array(
        array('Type' => 'AutoincrementId', 'Name' => 'Id'),
        array('Type' => 'text', 'Name' => 'Nombre', 'Required' => true, 'Title' => 'Nombre completo'),
        array('Type' => 'email', 'Name' => 'Email', 'Required' => true),
        array('Type' => 'select', 'Name' => 'Estado', 'Tabla' => 'estados', 'Required' => true),
        array('Type' => 'textarea', 'Name' => 'Descripcion', 'Title' => 'Descripción'),
        array('Type' => 'RegistrationDate', 'Name' => 'FechaRegistro'),
        array('Type' => 'RegistrationUser', 'Name' => 'UsuarioRegistro'),
    );
}
```

**Opciones comunes por atributo:**

| Opción | Tipo | Descripción |
|---|---|---|
| `Name` | string | Nombre de la columna (obligatorio) |
| `Type` | string | Tipo de atributo (obligatorio) |
| `Title` | string | Etiqueta visible en formularios |
| `Required` | bool | Campo obligatorio |
| `Tabla` | string | Tabla origen para selects |
| `Exportar` | bool | Incluir en exportaciones |
| `textHelp` | string | Texto de ayuda en formularios |
| `MaxLength` | int | Longitud máxima |
| `Multiple` | bool | Selección múltiple (select) |

## Traits del modelo

### HasValidation

Maneja la recepción y validación de datos desde formularios.

```php
// Internamente en save():
$this->beforeCreate($attributes);  // llama receiveData('create', $attributes)
$this->beforeUpdate($attributes);  // llama receiveData('update', $attributes)
```

- `receiveData()` itera los atributos, llama `receiveData()` en cada `Atributo` que permita edición.
- `$attributes = ['*']` valida todos; `['Nombre', 'Email']` valida solo esos campos.

### HasTenant

Multi-tenancy transparente.

```php
// Automático en saveOnCreate(): agrega TenantId si la tabla tiene esa columna
$this->appendTenantOnCreateIfNeeded($fields, $values);

// Automático en consultas: filtra por tenant
$criteria = $this->appendTenantCriteria($criteria);
```

- Detecta si la tabla tiene columna `TenantId` via `DESCRIBE`.
- Agrega filtros WHERE automáticamente.

### HasCache

Cache de consultas por modelo.

```php
protected static $CACHE = true;  // activar en el modelo

// Internamente el modelo usa:
$this->cacheRead($key);
$this->cacheWrite($key, $value, $ttl);
$this->cacheInvalidateModel(static::class);
```

### HasAuditLog

Auditoría de cambios.

```php
protected static $LOG = true;  // activar en el modelo

// En save(), después de persistir:
$this->log('Creación');   // registra datos nuevos
$this->log('Edición');    // registra diff old→new
$this->log('Eliminación');
```

- Guarda en `LogModulesModel`: usuario, URL, datos JSON antes/después, tabla, fecha/hora.

## Middleware

### Pipeline

```php
// En Controller::getMiddlewareStack()
return array(
    new AuthMiddleware(),
    new CsrfMiddleware(),
    new RateLimitMiddleware(),
);
```

Cada middleware implementa `MiddlewareInterface::handle(array $context, callable $next)`.

Si un middleware necesita abortar: `return ['abort' => true, 'validation' => 'REASON']`.
Para continuar: `return $next($context)`.

### AuthMiddleware

Llama `$controller->validateAccess($action)`. Verifica:
1. ¿Módulo habilitado en `Modules.php`?
2. ¿Acción registrada en `AccessControl`?
3. Si `'*'` → acceso libre. Si `'@'` → requiere sesión + permisos.

### CsrfMiddleware

Solo actúa en POST. Flujo:
1. Si `CSRF_STRICT=false` y no viene token → permite (compatibilidad legacy).
2. Si viene token → valida con `Controller::validateCsrfToken()`.
3. Si válido → marca `$_SERVER['KLEE_CSRF_VALIDATED'] = '1'` (evita doble validación en Model::save()).
4. Si inválido → `['abort' => true, 'validation' => 'NO_CSRF']`.

### RateLimitMiddleware

Limita intentos por IP usando `RateLimiter`. Configurable por ventana de tiempo.

## Servicios utilitarios

### Request (`core/Request.php`)

```php
Request::get('id');           // $_GET['id'] o null
Request::get();               // todos los GET
Request::post('nombre');      // $_POST['nombre'] o null
Request::post();              // todos los POST
Request::input('key');        // $_REQUEST['key']
Request::all();               // merge GET + POST
Request::files('foto');       // $_FILES['foto']
Request::session('user');     // $_SESSION['user']
Request::server('HTTP_HOST'); // $_SERVER['HTTP_HOST']
Request::method();            // 'GET', 'POST', etc.
Request::isAjax();            // true si X-Requested-With = XMLHttpRequest
```

### Response (`core/Response.php`)

```php
Response::json(['data' => $items]);           // JSON 200
Response::json(['data' => $items], 201);      // JSON con status custom
Response::error('No encontrado', 404);        // JSON error
Response::error('Fallo', 500, ['debug' => $e->getMessage()]);
```

### UserFlash (`core/UserFlash.php`)

Mensajes flash por sesión:

```php
UserFlash::setFlash('Success', 'Usuario creado correctamente');
UserFlash::setFlash('Error', 'No se pudo guardar');
UserFlash::setFlash('Warning', 'Datos incompletos');

// En la vista (el layout los renderiza automáticamente):
$flashes = UserFlash::getFlash();  // obtiene y limpia
UserFlash::hasFlash();             // ¿hay mensajes pendientes?
```

### EventDispatcher (`core/EventDispatcher.php`)

Pub/sub estático:

```php
// Registrar listener
EventDispatcher::listen('usuario.creado', function ($payload) {
    // $payload = datos del evento
    LoggerManager::logAction('Nuevo usuario: ' . $payload['nombre']);
});

// Disparar evento
EventDispatcher::dispatch('usuario.creado', ['nombre' => 'Juan', 'id' => 42]);

// Limpiar listeners
EventDispatcher::clear('usuario.creado');
EventDispatcher::clear();  // todos
```

### MailService (`core/MailService.php`)

Wrapper PHPMailer con cola de archivos:

```php
// Envío inmediato
$result = MailService::send(
    'destino@email.com',
    'Asunto del correo',
    '<p>Contenido HTML</p>',
    Controller::getEmailSendConfig()
);
// $result = ['success' => true, 'message' => 'Enviado']

// Encolar para envío posterior
MailService::queue(
    'destino@email.com',
    'Asunto',
    '<p>Contenido</p>',
    Controller::getEmailSendConfig(),
    ['referencia' => 'pedido-123']
);
// Se guarda en storage/pendientes/mail_queue.jsonl
```

### Lista y ListaAjax (`core/Lista.php`, `core/ListaAjax.php`)

Sistema de tablas DataTables. `ListaAjax` extiende `Lista` con carga server-side.

```php
// En el controlador:
protected function getListAjaxObject()
{
    $table = new ListaAjax($this->Module, $this->CurrentAction);
    $model = new MiModelo();
    $table->setModel($model);

    // Columnas a mostrar
    $fieldsShow = array('Id', 'Nombre', 'Email', 'Estado');
    $titles     = array('ID', 'Nombre', 'Correo', 'Estado');

    $table->setData(null, null, $titles, null, $fieldsShow);
    $table->setPermission($this->Permission);
    $table->setActions(array(
        array('name' => 'Ver', 'controller' => $this->Module, 'action' => 'view', 'icon' => 'eye', 'color' => 'primary'),
        array('name' => 'Editar', 'controller' => $this->Module, 'action' => 'edit', 'icon' => 'pencil', 'color' => 'warning'),
    ));

    return $table;
}
```

En la vista `list.php`:

```php
<?php echo $listaHtml; ?>  <!-- Renderiza tabla + JS DataTables -->
```

## Permisos y acceso

### Definición en el controlador

```php
protected function loadAccessControl()
{
    $this->AccessControl = array(
        'create'       => '@',  // requiere sesión + permiso
        'edit'         => '@',
        'remove'       => '@',
        'view'         => '@',
        'list'         => '@',
        'dataListAjax' => '@',
        'exportar'     => '@',
        'reporte'      => '*',  // público (sin sesión)
    );
}
```

- `'*'` → abierto para todos.
- `'@'` → requiere sesión activa + permiso registrado en BD.
- Acción no listada → denegada para todos.

### Registro de permisos en BD

Los permisos se crean con seeders. El `PermissionsSeeder` generado por `bin/make` registra las acciones CRUD:

```php
$permisos = array(
    array('Modulo' => 'MiModulo', 'Accion' => 'list'),
    array('Modulo' => 'MiModulo', 'Accion' => 'view'),
    array('Modulo' => 'MiModulo', 'Accion' => 'create'),
    array('Modulo' => 'MiModulo', 'Accion' => 'edit'),
    array('Modulo' => 'MiModulo', 'Accion' => 'remove'),
    array('Modulo' => 'MiModulo', 'Accion' => 'dataListAjax'),
);
```

### Verificación de permisos en URLs

`Router::create_action_url()` verifica permisos internamente. Si el usuario no tiene acceso, retorna `'#'` en lugar de la URL.

## Registro de módulos (`app/config/Modules.php`)

Cada módulo se registra en el array estático `$registry`:

```php
'mi_modulo' => array(
    'enabled'    => true,
    'controller' => 'mimodulo',
    'metadata'   => array(
        'icon'     => '<i class="bi bi-box fs-2"></i>',
        'category' => 'Herramientas',
        'order'    => 50,
    ),
    'menu' => array(
        'name'   => 'herramientas_mimodulo',
        'title'  => 'Mi Módulo',
        'action' => 'list',
        'group'  => 'herramientas',  // submenú del grupo padre
    ),
    'quick_actions' => array(
        array(
            'id'          => 'qa-mimodulo-nuevo',
            'label'       => 'Nuevo registro',
            'keywords'    => array('crear', 'nuevo', 'registro'),
            'path'        => array('controller' => 'mimodulo', 'action' => 'create'),
            'module'      => 'Herramientas',
            'permissions' => array(array('controller' => 'Mimodulo', 'action' => 'create')),
        ),
    ),
),
```

## CSRF — Flujo completo

1. **Generación:** En la vista del formulario, incluir el token:
   ```php
   Controller::generateCsrfToken();
   ```
   ```html
   <input type="hidden" name="_csrf_token" value="<?php echo Controller::generateCsrfToken(); ?>">
   ```

2. **Validación en middleware:** `CsrfMiddleware` valida en POST y marca `$_SERVER['KLEE_CSRF_VALIDATED']`.

3. **Validación en Model::save():** Verifica si el middleware ya validó. Si no, valida por sí mismo.

4. **Regeneración:** Después de validar exitosamente, el token se regenera automáticamente.

> **Importante:** No validar CSRF manualmente si el middleware ya está activo. La doble validación está protegida, pero es innecesaria.

## Seguridad transversal

| Capa | Mecanismo |
|---|---|
| Sesión | Cookie endurecida: HttpOnly, SameSite=Strict, Secure (si HTTPS) |
| CSRF | Token por sesión + middleware + regeneración automática |
| Rate limiting | Middleware por IP con ventana configurable |
| Permisos | Por acción en controlador, verificados en `validateAccess()` |
| Headers | X-Frame-Options: DENY, CSP, X-Content-Type-Options, Referrer-Policy, XSS-Protection, HSTS |
| Path traversal | `View::sanitizeRelativePath()` y `resolveSafePhpPath()` validan rutas de vistas |

## App y ConfigContext

```php
// Acceder a cualquier propiedad de ConfigEnv:
App::config('appId');
App::config('DB_CONNECTIONS');
App::config();  // retorna instancia completa de ConfigEnv
```

`TenantContext::current()` retorna el tenant resuelto:
```php
$tenant = TenantContext::current();
// ['key' => 'default', 'tenant_id' => 1, 'app_id' => 'KleePlanner2', 'branding' => [...], 'modules_override' => [...]]
```
