Introducción y Filosofía

ArtiFrame se basa en el principio de "Convención sobre Configuración". El poder de un framework no proviene de la riqueza de las herramientas que ofrece, sino de la coherencia del orden que establece.

Cero Sobrecarga (Zero Overhead)

Cero dependencias de paquetes de Composer, núcleo del framework o bibliotecas de terceros. Cada línea de código es suya; libre de inflamiento, libre de capas de abstracción innecesarias.

La Seguridad es lo Primero

La protección XSS, validación CSRF, sanitización contra inyección SQL y el control de métodos HTTP están integrados por defecto. La seguridad no es una opción, es un estándar.

Conjunto Estricto de Reglas

Un desarrollador junior recién incorporado al proyecto comprende la arquitectura data-js y la estructura de directorios en minutos. La armonía del equipo está garantizada a nivel del framework.

CLI como Prioridad

No se crean archivos de vista, API o clase manualmente. La CLI genera desde archivos stub, configura los enlaces de assets y mantiene el proyecto coherente.

ℹ️ Licencia AGPLv3: ArtiFrame es de código abierto. Los trabajos derivados que usted produzca pueden usarse libremente siempre que el código fuente permanezca abierto. El aviso de derechos de autor no puede eliminarse.

Instalación

La CLI de ArtiFrame se instala como una herramienta PHP global. Se instala una vez, se usa en todos los proyectos.

1. Instale la Herramienta CLI Globalmente

# Instalación global vía NPM
npm install -g @artilingo/artiframe-cli

# Verifique la instalación
artiframe

2. Shell Interactivo

Simplemente escriba artiframe en la terminal y presione Enter. La CLI no se cierra; se abre un shell interactivo que escucha comandos continuamente:

==================================================
        ArtiFrame CLI Interactive Shell v1.0.0
==================================================
Type 'help' for commands, or 'exit' to quit.

artiframe> 

3. Iniciar un Nuevo Proyecto

artiframe> new mi-proyecto

Este comando crea el directorio mi-proyecto/ y copia todo el esqueleto estructural dentro de él: app/, bin/, config/, public/, src/, .env.example y la primera página index.php.

4. Configuración del Entorno

cp .env.example .env

Abra su archivo .env y rellene la información de la base de datos y la aplicación. Este archivo nunca entra al control de versiones.

⚠️ Configuración del Servidor Web: Apunte la raíz del documento (document root) de Apache/Nginx a la carpeta /public/. Otros directorios nunca deben estar expuestos al exterior.

Estructura de Directorios

La arquitectura generada cuando se inicia el proyecto; asegura una separación clara de responsabilidades (SoC).

nombre-proyecto/ ├── app/ # Capa de infraestructura │ ├── ViewControl.php # Bootstrapper para la Vista (página HTML) │ ├── ApiControl.php # Bootstrapper para los endpoints API │ ├── Database.php # Conexión de base de datos basada en PDO │ ├── DotEnv.php # Lector de .env │ └── R2Manager.php # Administrador de archivos Cloudflare R2 │ ├── bin/ # ⚠️ Sistema central — no lo edite directamente │ ├── SystemMethod.php # Ayudantes globales de API/Backend │ ├── ViewMethod.php # Ayudantes globales de View/Frontend │ └── stubs/ # Archivos de plantilla usados por la CLI │ ├── view.stub │ ├── api-standart.stub │ ├── api-switch-case.stub │ └── class.stub │ ├── config/ # Archivos de configuración │ └── app-version.php # Constantes APP_VERSION y APP_ENV │ ├── public/ # ← El único directorio abierto del servidor web │ ├── assets/ │ │ ├── css/ # Archivos CSS específicos de la Vista │ │ └── js/ # Archivos JS específicos de la Vista │ ├── includes/ # Componentes compartidos │ │ ├── head.php │ │ ├── header.php │ │ └── footer.php │ ├── api/ # Archivos de endpoints API │ └── index.php # Página principal │ ├── src/ # Lógica de negocio y clases ├── .env # Variables de entorno (no entra en git) ├── .env.example # Plantilla — entra en git └── guia.html # Este documento
🚫 No toque el directorio bin/: Los archivos dentro de bin/ son el núcleo del framework. No se añade lógica de negocio específica del proyecto aquí. Las clases y servicios que agregue van debajo de src/, los componentes de infraestructura van debajo de app/.

Arquitectura Bootstrapper Crítico

ArtiFrame usa dos bootstrappers completamente independientes. Esta arquitectura previene inherentemente problemas de encabezados HTML y vulnerabilidades de seguridad.

View ViewControl.php

Se usa para páginas HTML (archivos view). Inicia sesión, carga ViewMethod.

// public/perfil.php — HASTA ARRIBA, antes de imprimir HTML
<?php
require_once $_SERVER['DOCUMENT_ROOT']
    . '/../app/ViewControl.php';
use Bin\ViewMethod;
?>
<!DOCTYPE html>
...

API ApiControl.php

Se usa para archivos de endpoints API. Establece el encabezado JSON, comprueba el método HTTP, carga SystemMethod.

// public/api/usuario/obtener.php
<?php
// $allowedMethods DEBE definirse antes del require
$allowedMethods = ['GET'];

require_once $_SERVER['DOCUMENT_ROOT']
    . '/../app/ApiControl.php';

use Bin\SystemMethod;
🚫 ViewControl y ApiControl nunca se mezclan: Si ViewControl se requiere en un archivo de API, puede devolver un encabezado HTML en lugar de un encabezado JSON y corromper toda la respuesta de la API. Si ApiControl se requiere en una página HTML, la sesión no se inicia y la página se rompe.

Conjunto de Reglas Estándar

Regla 1: Arquitectura data-js Crítico

Los eventos de JavaScript nunca deben escucharse a través de un class o id. Estos son identificadores visuales / de estilo. Todas las interacciones de JS se manejan mediante el atributo data-js. Cuando el CSS elimina una clase, JavaScript nunca se rompe.

<!-- ❌ Anti-Patrón — no soportado -->
<button id="submitBtn" class="btn">Enviar</button>
// JS: document.getElementById('submitBtn').addEventListener(...)

<!-- ✅ Estándar ArtiFrame -->
<button class="btn btn-primary" data-js="login-submit">Enviar</button>
// JS: document.querySelector('[data-js="login-submit"]').addEventListener(...)

Regla 2: Arquitectura de Temas

El modo Oscuro/Claro y los temas se administran a través de los atributos data-theme y data-mode de la etiqueta <html>. No se utilizan clases en el body.

<!-- Etiqueta de apertura HTML de la plantilla view.stub -->
<html lang="es" data-theme="default" data-mode="light">

/* Definición del tema en app.css */
html[data-theme="default"][data-mode="dark"] {
    --bg-color: #0b0c0e;
    --text-main: #ffffff;
}
html[data-theme="default"][data-mode="light"] {
    --bg-color: #ffffff;
    --text-main: #111111;
}

Regla 3: Flujo de Datos Seguro

Cada dato que proviene de la base de datos se envuelve con display() antes de imprimirse en el DOM. Cada dato que llega a la API se limpia con sanitizeString() o sanitizeInt() antes de ser procesado.

Regla 4: Manejo de Errores con APP_ENV

El valor de APP_ENV en el archivo .env determina la visibilidad del error. En Producción (Production), no se muestran mensajes de error al usuario.

// config/app-version.php
define('APP_ENV', (int)$_ENV['APP_ENV']); // 1=Debug, 0=Production

if (APP_ENV === 1) {
    ini_set('display_errors', 1);
    error_reporting(E_ALL);
} else {
    ini_set('display_errors', 0);
}

Ecosistema CLI

La CLI de ArtiFrame abre un shell interactivo cuando escribe artiframe en la terminal. Todos los comandos se ejecutan dentro de este shell. Los comandos también se pueden ejecutar de forma puntual.

# Modo interactivo (recomendado)
artiframe
artiframe> make:view admin/usuarios.php

# Modo puntual (una sola vez)
artiframe make:view admin/usuarios.php

new Comando CLI

Crea un nuevo proyecto ArtiFrame. Crea todo el esqueleto de directorios, los archivos bootstrapper y la página index inicial.

artiframe> new nombre-proyecto

Estructura creada:

nombre-proyecto/ ├── app/ (ViewControl.php, ApiControl.php, Database.php, DotEnv.php) ├── bin/ (SystemMethod.php, ViewMethod.php, stubs/) ├── config/ (app-version.php) ├── public/ (index.php, assets/, includes/, api/) ├── src/ ├── .env.example └── guia.html

make:view Comando CLI

Crea un nuevo archivo de página (view) y sus recursos CSS/JS específicos. Los recursos se vinculan automáticamente a la página y se aplica la invalidación de caché (cache-busting) con ?v=APP_VERSION.

artiframe> make:view admin/usuarios.php

Archivos creados:

✔  public/admin/usuarios.php
✔  public/assets/css/admin/usuarios.css
✔  public/assets/js/admin/usuarios.js

El archivo de vista creado viene con ViewControl requerido al principio, los includes head/header/footer añadidos, y los enlaces CSS/JS conectados con cache-busting.

make:api Comando CLI

Crea un archivo de endpoint API eligiendo una de las dos plantillas diferentes. En cada nuevo archivo de API, la variable $allowedMethods y el requerimiento de ApiControl.php ya vienen preparados.

standart — API de Acción Única

Para endpoints que realizan un solo trabajo (iniciar sesión, enviar, borrar). La lógica de negocio se escribe directamente.

artiframe> make:api standart api/auth/entrar.php
<?php
$allowedMethods = ['POST']; // Aceptar solo POST
require_once $_SERVER['DOCUMENT_ROOT'] . '/../app/ApiControl.php';

use Bin\SystemMethod;

// Lógica de negocio aquí...
jsonResponse(['status' => 'success'], 200);

switch-case — API de Múltiples Acciones

Una estructura que maneja operaciones CRUD para un módulo en un único endpoint. Qué acción se debe realizar se determina por el parámetro action.

artiframe> make:api switch-case api/usuario/administrar.php
<?php
$allowedMethods = ['POST'];
require_once $_SERVER['DOCUMENT_ROOT'] . '/../app/ApiControl.php';

use Bin\SystemMethod;

$action = sanitizeString($_POST['action'] ?? '');

switch ($action) {
    case 'create':
        jsonResponse(['status' => 'success', 'message' => 'Creado.'], 200);
        break;
    case 'update':
        jsonResponse(['status' => 'success', 'message' => 'Actualizado.'], 200);
        break;
    case 'delete':
        jsonResponse(['status' => 'success', 'message' => 'Eliminado.'], 200);
        break;
    default:
        jsonResponse(['status' => 'error', 'message' => 'Acción no válida.'], 400);
}

make:class Comando CLI

Crea un nuevo archivo de clase PHP con el namespace y la estructura básica (boilerplate) preparados.

artiframe> make:class src/Service/EmailService.php

Si no se especifica el directorio de la clase, el comando devolverá un error:

artiframe> make:class
❌ Error: ¡Se debe especificar el directorio para la clase! (ej. /app o /src)
Example: /src/Service/PaymentService

version Comando CLI

Actualiza el número de versión en config/app-version.php de acuerdo a las reglas de versionamiento semántico (SemVer). Formato de versión: MAYOR.MENOR.PARCHE

ComandoDescripciónEjemplo
version upgrade patchCorrección de errores, mejora menor1.2.3 → 1.2.4
version upgrade minorNueva característica compatible con versiones anteriores1.2.3 → 1.3.0
version upgrade majorCambio importante que rompe compatibilidad1.2.3 → 2.0.0
version downgrade patchDeshacer el último parche1.2.4 → 1.2.3
version downgrade minorDeshacer el último menor1.3.0 → 1.2.0
version downgrade majorDeshacer el último mayor2.0.0 → 1.0.0
artiframe> version upgrade minor
✔  Versión actualizada a 1.2.0 → 1.3.0.

Ayudantes de Vista (View Helpers) — display() ViewMethod

Aplica una protección XSS obligatoria mientras imprime datos provenientes de la base de datos o entradas del usuario al DOM. En lugar de lanzar un error en valores nulos o vacíos, devuelve un valor predeterminado.

string display(mixed $data, string $default = '')
💡 Consejo: Uso de $default
El segundo parámetro ($default) es el texto de rescate que se mostrará en pantalla cuando los datos que provienen de la base de datos estén vacíos (nulo, falso, cadena vacía). Por ejemplo, si utiliza display($nombre, 'Anónimo') para un usuario cuyo nombre no se ha introducido, su página presentará contenido significativo en lugar de horribles espacios en blanco.
<!-- ❌ Inseguro — crea una vulnerabilidad XSS -->
<h1><?= $user['nombre'] ?></h1>

<!-- ✅ Uso Seguro de ArtiFrame -->
<h1><?= display($user['nombre'], 'Usuario Anónimo') ?></h1>

Ayudantes de Vista (View Helpers) — escapeUrl() ViewMethod

Se utiliza al insertar enlaces recibidos de los usuarios (como sitios web de perfil) dentro de un <a href="..."> o <img src="...">. Neutraliza cargas útiles (payloads) maliciosas como javascript:alert(1) (XSS Almacenado) y evita la ejecución de código a través de enlaces.

string escapeUrl(string $url)
<!-- ❌ Inseguro — se puede filtrar código JS a través de la URL -->
<a href="<?= $user['website'] ?>">Visitar Sitio</a>

<!-- ✅ Uso Seguro de ArtiFrame -->
<a href="<?= escapeUrl($user['website']) ?>">Visitar Sitio</a>

Ayudantes de Vista (View Helpers) — csrfField() ViewMethod

Añade un campo de token oculto a los formularios HTML contra ataques CSRF. Es obligatorio su uso en todas las páginas que contengan formularios POST.

string csrfField()
<form action="/api/guardar.php" method="POST">
    <?= csrfField() ?>   <!-- Es obligatorio por seguridad -->
    <input type="text" name="nombre">
    <button type="submit" data-js="btn-guardar">Guardar</button>
</form>

Ayudantes de Vista (View Helpers) — Funciones de Fecha ViewMethod

Procesa cadenas de fecha en formato Y-m-d H:i:s de la base de datos o valores de marca de tiempo (timestamp) de UNIX. Todas las salidas textuales están localizadas según el idioma (tr, en, de, fr, es).

FunciónEjemplo de SalidaDescripción
day($date)24Solo día
month($date)07Solo mes (número)
year($date)2026Año
timeOnly($date)14:30Hora:Minuto
fulldate($date)24.07.2026Fecha completa
formatDate($date, $format)24.07.2026 14:30Formato personalizado
monthName($date, $lang)Julio / July / JuliNombre del mes (según el idioma)
fulldateName($date, $lang)24 de julio de 2026 / July 24, 2026Fecha completa (con el nombre del mes, según el idioma)
timeAgo($date, $lang)Hace 5 minutos / 5 minutes agoEstilo de redes sociales (según el idioma)
💡 Entendiendo los Parámetros: $format y $lang
  • $format (Formato): Se usa únicamente con la función formatDate(). Acepta letras de formato de fecha PHP estándar. (Ejemplo: 'd/m/Y' ➔ 24/07/2026, o 'H:i' ➔ 15:30). Permite crear su propio patrón de fecha cuando otras funciones preparadas (day, year, etc.) no son suficientes.
  • $lang (Idioma): Se usa en funciones que contienen texto en su salida (nombre del mes, la palabra "hace"). Si deja este parámetro vacío, el sistema opera por defecto en 'tr' (Turco). Si está haciendo un proyecto multilingüe, simplemente ingrese el código del idioma (tr, en, de, fr, es) en el segundo parámetro. (Ej: timeAgo($fecha, 'es') ➔ hace 5 minutos)
<!-- Supongamos que es una marca de tiempo UNIX obtenida con la función time() -->
<?php $fecha = time(); ?>

<!-- Fecha completa según el idioma -->
<span><?= fulldateName($fecha, 'es') ?></span>
<!-- Salida: 24 de julio de 2026 -->

<!-- Representación de tiempo estilo redes sociales -->
<span><?= timeAgo($fecha, 'en') ?></span>
<!-- Salida: 5 minutes ago -->

<span><?= timeAgo($fecha, 'de') ?></span>
<!-- Salida: vor 5 Minuten -->

Ayudantes de Vista (View Helpers) — Formateo de Texto ViewMethod

truncate($text, $length, $append)

string truncate(string $text, int $length = 100, string $append = '...')

Recorta textos largos (por ejemplo, resúmenes de blog) en el límite de caracteres deseado sin cortar la última palabra y agrega el sufijo especificado al final.

<p><?= truncate($post['contenido'], 160, '...') ?></p>

Ayudantes de Vista (View Helpers) — money() ViewMethod

Formatea la cantidad con el símbolo de moneda. El código de la moneda (ISO) determina automáticamente la ubicación del símbolo. La moneda por defecto es usd.

string money(float $amount, string $currency = 'usd')
CódigoSalidaMoneda
usd$1.250,00Dólar estadounidense
eur1.250,00 €Euro
try / tl1.250,00 ₺Lira Turca
gbp£1.250,00Libra Esterlina Británica
jpy1.250,00 ¥Yen Japonés
inr1.250,00 ₹Rupia India
rub1.250,00 ₽Rublo Ruso
krw1.250,00 ₩Won Surcoreano
brlR$1.250,00Real Brasileño
aed1.250,00 د.إDírham de EAU
<span><?= money($producto['precio'], 'try') ?></span>
<!-- Salida: 1.250,00 ₺ -->

<span><?= money($producto['precio'], 'usd') ?></span>
<!-- Salida: $1.250,00 -->

Ayudantes del Sistema (System Helpers) — jsonResponse() SystemMethod

Configura el encabezado JSON, proporciona el código de estado HTTP y emite la salida de manera segura con json_encode, luego termina el script. Todas las respuestas de API deben darse a través de esta función.

void jsonResponse(array $data, int $statusCode = 200)
// Respuesta exitosa
jsonResponse(['status' => 'success', 'data' => $usuario], 200);

// Respuesta de error
jsonResponse(['status' => 'error', 'message' => 'Acceso no autorizado.'], 401);

Ayudantes del Sistema (System Helpers) — verifyCsrf() SystemMethod

Verifica que la solicitud POST entrante proviene de un formulario legítimo. Si no se agregó csrfField() dentro del formulario o si el token no es válido, devuelve false.

bool verifyCsrf(string $token)
if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
    jsonResponse(['status' => 'error', 'message' => 'Token CSRF no válido.'], 403);
}

Ayudantes del Sistema (System Helpers) — Funciones de Sanitización SystemMethod

FunciónDescripción
sanitizeInt($value)Elimina todas las letras, símbolos y comas que contiene, dejando solo números enteros. Utilizado para ID o límites.
sanitizeFloat($value)Limpia todo excepto números decimales (fraccionarios). Utilizado para montos de dinero o métricas.
sanitizeString($value)Destruye todas las etiquetas HTML y PHP (<script>, <iframe>, etc.) para evitar ataques XSS y derivados. Deja un texto sin formato y seguro.
sanitizeEmail($email)Filtra todos los caracteres inválidos y peligrosos que no se ajustan al formato de correo electrónico.
$id    = sanitizeInt($_POST['id'] ?? 0);
$nombre= sanitizeString($_POST['nombre'] ?? '');
$email = sanitizeEmail($_POST['email'] ?? '');

Ayudantes del Sistema (System Helpers) — HTTP y Petición SystemMethod

FunciónDescripción
isAjax()Comprueba el encabezado HTTP_X_REQUESTED_WITH para verificar si la solicitud vino de Fetch API / XMLHttpRequest.
getClientIp()Captura de forma segura la dirección IP real del usuario (incluso si está detrás de proxies o Cloudflare).
redirect($url)Crea un encabezado de Location y finaliza de inmediato la ejecución.
// Bloquear acceso directo desde el navegador al endpoint API
if (!isAjax()) {
    jsonResponse(['error' => 'El acceso directo está restringido.'], 403);
}

// Redirigir si no hay sesión
if (!isset($_SESSION['user'])) {
    redirect('/login.php');
}

$ip = getClientIp(); // IP real incluso detrás de Cloudflare

Ayudantes del Sistema (System Helpers) — Seguridad SystemMethod

FunciónDescripción
generateCsrf()Genera un nuevo token CSRF y lo guarda en la sesión
generateToken($length)Cadena hex aleatoria segura criptográficamente (restablecimiento de contraseña, clave API, etc.)
hashPassword($password)Hashea la contraseña con bcrypt
verifyPassword($password, $hash)Verifica la contraseña con el hash
// Generación de token seguro (clave API, enlace de verificación de correo electrónico, etc.)
$token = generateToken(32); 
// ⚠️ Nota: Se generan 32 bytes de datos, pero debido a que se convierte al formato
// hexadecimal (base 16), la salida es exactamente el doble, es decir, una cadena de 64 caracteres de largo.

// Registro de contraseña
$hash = hashPassword($_POST['clave']);

// Verificación de contraseña
if (!verifyPassword($_POST['clave'], $usuario['clave_hash'])) {
    jsonResponse(['error' => 'Contraseña incorrecta.'], 401);
}

API — Control de Métodos HTTP ApiControl

El arreglo $allowedMethods debe definirse antes del require de ApiControl. Al leer este arreglo, ApiControl rechaza automáticamente las solicitudes de los métodos no permitidos con un 405 Method Not Allowed.

// Un endpoint que solo acepta GET y POST
$allowedMethods = ['GET', 'POST'];
require_once ... . '/../app/ApiControl.php';

// Un endpoint que solo acepta DELETE
$allowedMethods = ['DELETE'];
require_once ... . '/../app/ApiControl.php';

Las solicitudes de preflight OPTIONS devuelven automáticamente 200 para CORS y el script termina — no es necesaria ninguna intervención manual.

API — CORS ApiControl (Comentado)

Si desea permitir el acceso a la API desde otros dominios o aplicaciones móviles, active el bloque CORS en ApiControl.php.

// Dentro de ApiControl.php — se activa eliminando el comentario
header("Access-Control-Allow-Origin: https://su-dominio.com");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
⚠️ No utilice * (todos los dominios) en un entorno de Producción. Especifique explícitamente la dirección del dominio.

API — Limitación de Tasa (Rate Limiting) ApiControl (Comentado)

Para evitar que usuarios malintencionados o bots inunden (flood) la API, puede activar el bloque de Rate Limiting dentro de ApiControl.php. La regla por defecto es: 60 peticiones en 60 segundos por IP.

⚠️ Servidor Redis Requerido: La función de Limitación de Tasa (Rate Limiting) opera usando Redis a través de la clase \Src\Service\RedisService::getInstance(). Antes de descomentar este bloque de código, asegúrese de haber instalado un servicio Redis en su proyecto y haber creado una clase de conexión bajo src/Service/.
// Si hay más de 60 peticiones en 60 segundos de la misma IP:
http_response_code(429); // Demasiadas Peticiones
echo json_encode(['error' => 'Demasiadas peticiones. Por favor, espere.']);

Flujo de Trabajo Completo: Formulario de Contacto (De Extremo a Extremo)

Examinemos paso a paso cómo desarrollar una función de Formulario de Contacto con los estándares de ArtiFrame de principio a fin.

  1. Cree el Archivo de Vista (View)
    artiframe> make:view contacto.php
    ✔  public/contacto.php
    ✔  public/assets/css/contacto.css
    ✔  public/assets/js/contacto.js
  2. Cree un Endpoint API
    artiframe> make:api standart api/contacto/enviar.php
  3. Codifique el Formulario HTML
    Añada el siguiente formulario al archivo public/contacto.php:
    <form action="/api/contacto/enviar.php" method="POST">
        <?= csrfField() ?>
        <input type="text"  name="nombre"    placeholder="Su Nombre">
        <input type="email" name="email"     placeholder="Correo Electrónico">
        <textarea name="mensaje" placeholder="Su Mensaje"></textarea>
        <button type="submit" data-js="contacto-enviar">Enviar</button>
    </form>
  4. Conecte la API de Fetch con JavaScript
    En el archivo public/assets/js/contacto.js:
    document.querySelector('[data-js="contacto-enviar"]').addEventListener('click', async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target.closest('form'));
        const res = await fetch('/api/contacto/enviar.php', {
            method: 'POST',
            body: formData
        });
        const data = await res.json();
        console.log(data);
    });
  5. Complete la Lógica del Servidor (Backend)
    En el archivo public/api/contacto/enviar.php:
    <?php
    $allowedMethods = ['POST'];
    require_once $_SERVER['DOCUMENT_ROOT'] . '/../app/ApiControl.php';
    use Bin\SystemMethod;
    
    // 1. Validación CSRF
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        jsonResponse(['error' => 'Token inválido.'], 403);
    }
    
    // 2. Sanitización de Datos
    $nombre  = sanitizeString($_POST['nombre'] ?? '');
    $email   = sanitizeEmail($_POST['email'] ?? '');
    $mensaje = sanitizeString($_POST['mensaje'] ?? '');
    
    // 3. Lógica de Negocio (Enviar correo, guardar en DB, etc.)
    // ...
    
    // 4. Respuesta
    jsonResponse(['status' => 'success', 'message' => 'Su mensaje ha sido recibido.'], 200);
✅ ¡Completado! Ha creado un flujo de formulario totalmente seguro y que cumple con los estándares, con protección XSS, validación CSRF, restricción de métodos HTTP y arquitectura data-js.