Guia Completa de Arkham Framework en Español

July 28, 2025 · 87 mins

Arkham PHP Framework - Documentación Oficial

Arkham es un framework PHP moderno, ligero y potente, diseñado desde cero para facilitar el desarrollo de aplicaciones web robustas siguiendo el patrón MVC. Con su innovador Database Wizard y herramientas integradas, te permite crear aplicaciones profesionales en minutos.

Tabla de Contenidos

  1. Introducción
  2. Instalación
  3. Primeros Pasos
  4. Database Wizard
  5. Sistema de Rutas
  6. Controladores
  7. Base de Datos
  8. Modelos
  9. Vistas y Plantillas
  10. Autenticación
  11. Middleware
  12. Validación
  13. Manejo de Errores
  14. Testing
  15. Deployment
  16. API Reference

Introducción

¿Por qué Arkham?

En el ecosistema PHP actual, muchos frameworks requieren configuración extensa y conocimiento profundo antes de poder crear algo funcional. Arkham rompe con esta tradición ofreciendo una experiencia de desarrollo que prioriza la productividad sin sacrificar la potencia.

Problema Común: Los desarrolladores pasan horas configurando bases de datos, escribiendo migraciones y configurando archivos antes de poder escribir la primera línea de lógica de negocio.

Solución Arkham: Un asistente visual que configura automáticamente tu aplicación en minutos, permitiéndote enfocarte en lo que realmente importa: tu aplicación.

Características Principales

Database Wizard Único La característica distintiva de Arkham es su asistente visual para configuración de base de datos. No existe nada similar en el ecosistema PHP actual. Te permite:

Arquitectura MVC Moderna Implementación limpia del patrón Modelo-Vista-Controlador con:

Developer Experience de Primera Clase Cada aspecto del framework está diseñado pensando en la experiencia del desarrollador:

Performance y Escalabilidad A pesar de su simplicidad, Arkham está optimizado para:

Filosofía del Framework

Simplicidad sin Sacrificios Arkham cree que la simplicidad no debe venir a costa de la funcionalidad. El framework oculta la complejidad pero no la elimina, permitiendo acceso completo cuando lo necesites.

Convención sobre Configuración Siguiendo este principio, Arkham proporciona defaults sensatos para todo, pero permite personalización completa cuando sea necesario.

Desarrollo Dirigido por la Comunidad Cada decisión de diseño se basa en feedback real de desarrolladores trabajando en proyectos reales.


Instalación

Requisitos del Sistema

PHP y Extensiones

Herramientas de Desarrollo

Servidores Web Soportados

Bases de Datos Soportadas

Instalación Rápida

Método Recomendado: Composer Create-Project

composer create-project arkham-dev/framework mi-aplicacion
cd mi-aplicacion

Este comando:

  1. Descarga la última versión estable
  2. Instala todas las dependencias
  3. Configura la estructura de directorios
  4. Establece permisos apropiados

Método Alternativo: Git Clone

git clone https://github.com/JosueIsOffline/Arkham.git mi-aplicacion
cd mi-aplicacion
composer install

Servidor de Desarrollo

# Usando el servidor integrado de PHP
php -S localhost:8000 -t public

# O usando el comando de Composer
composer serve

Verificación de la Instalación

Verificar Requisitos Arkham incluye un script de verificación que puedes ejecutar:

php bin/check-requirements

Primera Visita Al visitar http://localhost:8000, deberías ser redirigido automáticamente al Database Wizard. Esto confirma que:

Troubleshooting Común

Error 500 Internal Server Error

Database Wizard no aparece


Primeros Pasos

Estructura del Proyecto

Arkham sigue una estructura de directorios intuitiva y bien organizada:

mi-aplicacion/
├── app/                        # Tu aplicación
│   ├── Controllers/            # Lógica de controladores
│   │   ├── HomeController.php
│   │   ├── BookController.php
│   │   └── DatabaseWizardController.php
│   └── Models/                 # Modelos de datos
├── config/                     # Configuración
│   └── database.json          # Config de BD (auto-generada)
├── public/                     # Archivos públicos
│   ├── index.php              # Punto de entrada
│   ├── assets/                # CSS, JS, imágenes
│   └── .htaccess              # Configuración Apache
├── routes/                     # Definición de rutas
│   └── web.php                # Rutas web principales
├── src/                        # Core del framework
│   ├── Controllers/           # Controladores base
│   ├── Database/              # Sistema de base de datos
│   ├── Http/                  # Manejo HTTP
│   ├── Routing/               # Sistema de rutas
│   └── View/                  # Motor de vistas
├── tests/                      # Tests automatizados
│   ├── ConnectionTest.php
│   ├── QueryBuilderTest.php
│   └── ModelTest.php
├── views/                      # Plantillas Twig
│   ├── base.html.twig         # Plantilla base
│   ├── home.html.twig         # Página principal
│   └── book.html.twig         # Vista de libro
├── vendor/                     # Dependencias de Composer
├── composer.json              # Configuración de Composer
└── README.md                   # Documentación del proyecto

Directorios Clave

app/: Contiene el código específico de tu aplicación. Aquí es donde pasarás la mayor parte del tiempo desarrollando.

config/: Almacena archivos de configuración. El Database Wizard genera automáticamente database.json aquí.

public/: Único directorio accesible desde web. Contiene el punto de entrada y assets estáticos.

routes/: Define las rutas de tu aplicación. Separado por tipo (web, api, admin).

views/: Plantillas Twig para renderizar HTML. Soporta herencia y componentes.

Configuración Inicial

Flujo de Configuración Automática

  1. Primera Visita: Al acceder a tu aplicación por primera vez, Arkham detecta automáticamente que no existe configuración y te redirige al Database Wizard.

  2. Database Wizard: Una interfaz paso a paso que te guía through:

    • Selección de driver de base de datos
    • Configuración de credenciales
    • Diseño de esquema de base de datos
    • Creación automática de tablas
  3. Generación de Configuración: El wizard genera automáticamente todos los archivos de configuración necesarios.

  4. Inicialización Completa: Una vez completado, tu aplicación está lista para desarrollo.

Configuración Manual (Opcional)

Si prefieres configurar manualmente, puedes crear config/database.json:

{
  "driver": "mysql",
  "host": "localhost",
  "port": 3306,
  "database": "mi_aplicacion",
  "username": "root",
  "password": "secreto",
  "charset": "utf8mb4",
  "collation": "utf8mb4_unicode_ci"
}

Tu Primera Aplicación

Crear un Controlador

<?php
// app/Controllers/WelcomeController.php

namespace App\Controllers;

use JosueIsOffline\Framework\Controllers\AbstractController;
use JosueIsOffline\Framework\Http\Response;

class WelcomeController extends AbstractController
{
    public function index(): Response
    {
        return $this->render('welcome.html.twig', [
            'title' => 'Bienvenido a Arkham',
            'message' => 'Tu primera aplicación está funcionando!'
        ]);
    }
}

Definir una Ruta

<?php
// routes/web.php

use App\Controllers\WelcomeController;

return [
    ['GET', '/', [WelcomeController::class, 'index']],
];

Crear una Vista

{# views/welcome.html.twig #}
{% extends "base.html.twig" %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
    <div class="welcome-container">
        <h1>{{ title }}</h1>
        <p>{{ message }}</p>
        <div class="features">
            <h2>Características de Arkham</h2>
            <ul>
                <li>Database Wizard visual</li>
                <li>Arquitectura MVC moderna</li>
                <li>Query Builder fluido</li>
                <li>Sistema de autenticación integrado</li>
            </ul>
        </div>
    </div>
{% endblock %}

Resultado Al visitar tu aplicación, verás una página de bienvenida completamente funcional que demuestra la integración entre controladores, rutas y vistas.


Database Wizard

Introducción al Wizard

El Database Wizard es la característica más innovadora de Arkham Framework. No existe nada comparable en el ecosistema PHP actual. Este asistente visual elimina la necesidad de:

¿Cómo Funciona?

El wizard es una aplicación web integrada que se ejecuta dentro de tu framework. Utiliza una interfaz moderna construida con TailwindCSS y JavaScript vanilla para proporcionar una experiencia fluida y professional.

Configuración de Base de Datos

Paso 1: Selección de Driver

El wizard detecta automáticamente qué drivers de base de datos están disponibles en tu sistema PHP y presenta opciones relevantes:

MySQL: El sistema más popular, ideal para la mayoría de aplicaciones web PostgreSQL: Para aplicaciones que requieren características avanzadas SQLite: Perfecto para desarrollo y aplicaciones pequeñas SQL Server: Para entornos empresariales Microsoft

Para cada driver, el wizard muestra:

Paso 2: Configuración de Credenciales

Para Bases de Datos con Servidor (MySQL, PostgreSQL, SQL Server):

Para SQLite:

Paso 3: Verificación de Conexión

El wizard prueba la conexión antes de proceder:

Diseño de Tablas

Interfaz Visual de Diseño

Esta es la parte más revolucionaria del wizard. Permite diseñar tu esquema de base de datos de forma completamente visual:

Creación de Tablas

Diseño de Campos Para cada tabla, puedes definir campos con:

Nombre del Campo: Identificador único dentro de la tabla Tipo de Dato: VARCHAR, INT, TEXT, DATETIME, TIMESTAMP, DECIMAL Longitud: Especificación de tamaño (ej: VARCHAR(255)) Restricciones:

Validación Inteligente

El wizard incluye validación que previene errores comunes:

Preview en Tiempo Real

Mientras diseñas, el wizard muestra:

Drivers Soportados

MySQL/MariaDB

Características:

Configuración Automática:

PostgreSQL

Características:

Configuración Automática:

SQLite

Características:

Ventajas:

SQL Server

Características:

Configuración Automática:

Migración Entre Drivers

Una característica única del wizard es la capacidad de migrar entre drivers:


Sistema de Rutas

Conceptos Básicos

El sistema de rutas de Arkham está construido sobre FastRoute, proporcionando un enrutamiento rápido y eficiente. Sin embargo, Arkham simplifica la sintaxis mientras mantiene toda la potencia.

Filosofía del Routing

Simplicidad: Las rutas deben ser fáciles de leer y escribir Performance: Enrutamiento ultra-rápido incluso con miles de rutas Flexibilidad: Soporte para patrones complejos cuando sea necesario Consistencia: Sintaxis uniforme para todos los tipos de rutas

Definición de Rutas

Sintaxis Básica

Las rutas se definen en routes/web.php usando una sintaxis de array simple:

return [
    ['{METHOD}', '{PATH}', [{CONTROLLER}, '{ACTION}']],
];

Ejemplos Básicos

<?php
// routes/web.php

use App\Controllers\HomeController;
use App\Controllers\BookController;
use App\Controllers\UserController;

return [
    // Ruta simple
    ['GET', '/', [HomeController::class, 'index']],

    // Múltiples métodos HTTP
    ['GET', '/books', [BookController::class, 'index']],
    ['POST', '/books', [BookController::class, 'store']],
    ['PUT', '/books/{id}', [BookController::class, 'update']],
    ['DELETE', '/books/{id}', [BookController::class, 'destroy']],

    // Rutas API
    ['GET', '/api/users', [UserController::class, 'apiIndex']],
    ['POST', '/api/users', [UserController::class, 'apiStore']],
];

Métodos HTTP Soportados

Parámetros de Ruta

Parámetros Simples

// Captura cualquier valor
['GET', '/users/{id}', [UserController::class, 'show']],
['GET', '/posts/{slug}', [PostController::class, 'show']],

Parámetros con Restricciones

// Solo números
['GET', '/users/{id:\d+}', [UserController::class, 'show']],

// Solo letras
['GET', '/categories/{name:[a-zA-Z]+}', [CategoryController::class, 'show']],

// Patrones personalizados
['GET', '/posts/{year:\d{4}}/{month:\d{2}}', [PostController::class, 'archive']],

Parámetros Opcionales

// Parámetro opcional
['GET', '/posts/{year}/{month?}', [PostController::class, 'archive']],

// Múltiples parámetros opcionales
['GET', '/search/{query?}/{page?}', [SearchController::class, 'results']],

Uso en Controladores

Los parámetros se pasan automáticamente como argumentos al método del controlador:

class UserController extends AbstractController
{
    public function show(int $id): Response
    {
        // $id contiene el valor del parámetro de ruta
        $user = User::find($id);
        return $this->render('users/show.html.twig', ['user' => $user]);
    }

    public function archive(int $year, ?int $month = null): Response
    {
        // Manejo de parámetros opcionales
        $posts = Post::filterByDate($year, $month);
        return $this->render('posts/archive.html.twig', ['posts' => $posts]);
    }
}

Grupos de Rutas

Prefijos Comunes

Aunque Arkham no tiene grupos explícitos como otros frameworks, puedes organizar rutas con prefijos comunes:

return [
    // Admin routes
    ['GET', '/admin', [AdminController::class, 'dashboard']],
    ['GET', '/admin/users', [AdminController::class, 'users']],
    ['GET', '/admin/settings', [AdminController::class, 'settings']],

    // API routes
    ['GET', '/api/v1/users', [ApiController::class, 'users']],
    ['GET', '/api/v1/posts', [ApiController::class, 'posts']],
    ['POST', '/api/v1/auth', [ApiController::class, 'authenticate']],

    // Public routes
    ['GET', '/blog', [BlogController::class, 'index']],
    ['GET', '/blog/{slug}', [BlogController::class, 'show']],
    ['GET', '/blog/category/{category}', [BlogController::class, 'category']],
];

Organización por Archivos

Para aplicaciones grandes, puedes separar rutas en múltiples archivos:

// routes/web.php (rutas principales)
// routes/admin.php (rutas de administración)
// routes/api.php (rutas de API)

El RouteLoader de Arkham carga automáticamente todos los archivos PHP en el directorio routes/.

Protección de Rutas

Middleware de Autenticación

return [
    // Rutas públicas
    ['GET', '/', [HomeController::class, 'index']],
    ['GET', '/login', [AuthController::class, 'loginForm']],
    ['POST', '/login', [AuthController::class, 'login']],

    // Rutas protegidas - requieren login
    ['GET', '/dashboard', [DashboardController::class, 'index'], 'auth'],
    ['GET', '/profile', [ProfileController::class, 'show'], 'auth'],
    ['POST', '/profile', [ProfileController::class, 'update'], 'auth'],
];

Middleware de Roles

return [
    // Requiere rol específico
    ['GET', '/admin', [AdminController::class, 'index'], 'role:admin'],
    ['GET', '/moderator', [ModeratorController::class, 'panel'], 'role:moderator'],
    ['GET', '/vip', [VipController::class, 'content'], 'role:vip'],
];

Múltiples Middlewares

return [
    // Múltiples middlewares en orden
    ['POST', '/admin/users', [UserController::class, 'store'], ['auth', 'role:admin']],
    ['DELETE', '/admin/users/{id}', [UserController::class, 'destroy'], ['auth', 'role:admin', 'csrf']],

    // Middleware personalizado
    ['GET', '/api/data', [ApiController::class, 'data'], [RateLimitMiddleware::class]],
];

Named Routes (Próximamente)

Arkham planea incluir soporte para rutas nombradas:

return [
    ['GET', '/users/{id}', [UserController::class, 'show'], null, 'user.show'],
    ['GET', '/posts/{slug}', [PostController::class, 'show'], null, 'post.show'],
];

Route Caching (Performance)

Para aplicaciones en producción, Arkham puede cachear las rutas compiladas:

php artisan route:cache    # Cachear rutas
php artisan route:clear    # Limpiar cache

Controladores

Introducción a Controladores

Los controladores en Arkham son el corazón de tu aplicación. Manejan la lógica de negocio, procesan requests HTTP y coordinan entre modelos y vistas. Siguiendo el patrón MVC, los controladores actúan como el intermediario entre la interfaz de usuario y los datos.

Responsabilidades de un Controlador

Request Handling: Procesar y validar datos de entrada Business Logic: Coordinar operaciones de negocio Data Manipulation: Interactuar con modelos y base de datos Response Generation: Generar respuestas HTTP apropiadas Authentication: Verificar permisos y autenticación Error Handling: Manejar excepciones y errores

Controlador Base

AbstractController

Todos los controladores en Arkham extienden AbstractController, que proporciona funcionalidad común y métodos de conveniencia:

<?php

namespace App\Controllers;

use JosueIsOffline\Framework\Controllers\AbstractController;
use JosueIsOffline\Framework\Http\Response;

class ExampleController extends AbstractController
{
    public function index(): Response
    {
        // El objeto $this->request está automáticamente disponible
        // ViewResolver está preconfigurado
        // Métodos de autenticación están listos para usar

        return $this->render('example/index.html.twig');
    }
}

Métodos Heredados Principales

Renderizado:

Respuestas:

Request Handling:

Autenticación:

Manejo de Requests

Procesamiento de Formularios

class ContactController extends AbstractController
{
    public function showForm(): Response
    {
        return $this->render('contact/form.html.twig');
    }

    public function processForm(): Response
    {
        // Obtener datos del formulario
        $data = $this->request->getAllPost();

        // Validación básica
        if (empty($data['email'])) {
            return $this->renderWithFlash('contact/form.html.twig', [
                'error' => 'El email es requerido',
                'old' => $data // Mantener datos para repoblar formulario
            ]);
        }

        // Procesar datos
        $this->sendContactEmail($data);

        // Respuesta de éxito
        return $this->success([], 'Mensaje enviado correctamente', 200, '/contact/thanks');
    }
}

Manejo de Archivos

class FileController extends AbstractController
{
    public function upload(): Response
    {
        $files = $_FILES; // Acceso directo a archivos subidos

        if (empty($files['document'])) {
            return $this->error('No se subió ningún archivo', 400);
        }

        $file = $files['document'];

        // Validación
        if ($file['size'] > 5000000) { // 5MB
            return $this->error('Archivo demasiado grande', 400);
        }

        // Procesar archivo
        $filename = $this->processUpload($file);

        return $this->success(['filename' => $filename], 'Archivo subido correctamente');
    }
}

Request Multipropósito

class DataController extends AbstractController
{
    public function store(): Response
    {
        // Detectar tipo de request automáticamente
        if ($this->isJsonRequest()) {
            $data = $this->getJsonInput();
        } else {
            $data = $this->request->getAllPost();
        }

        // Validar datos
        $validation = $this->validateData($data);
        if (!$validation['valid']) {
            return $this->error($validation['message'], 400);
        }

        // Guardar datos
        $result = Model::create($data);

        // Respuesta inteligente (JSON para API, redirect para web)
        return $this->smartResponse(
            ['id' => $result->id],
            '/data/created',
            201
        );
    }
}

Respuestas HTTP

Tipos de Respuesta

HTML Response:

public function show(): Response
{
    $data = ['title' => 'Mi Página'];
    return $this->render('page.html.twig', $data);
}

JSON Response:

public function apiData(): Response
{
    $data = ['users' => User::all()];
    return $this->json($data, 200);
}

Redirect Response:

public function postAction(): Response
{
    // Procesar acción
    return $this->redirect('/success', 302);
}

Error Response:

public function riskyAction(): Response
{
    try {
        // Operación riesgosa
    } catch (Exception $e) {
        return $this->error('Algo salió mal', 500, ['details' => $e->getMessage()]);
    }
}

Respuestas Inteligentes

Arkham incluye lógica para detectar automáticamente el tipo de respuesta apropiada:

class SmartController extends AbstractController
{
    public function handleAction(): Response
    {
        $data = ['success' => true, 'message' => 'Operación completada'];

        // smartResponse detecta automáticamente:
        // - Si es AJAX -> retorna JSON
        // - Si es formulario web -> redirecciona con flash message
        // - Si es API -> retorna JSON
        return $this->smartResponse($data, '/redirect-url');
    }
}

Status Codes Comunes

Controladores de API

Diseño RESTful

class ApiUserController extends AbstractController
{
    // GET /api/users
    public function index(): Response
    {
        $users = User::all();
        return $this->json($users);
    }

    // GET /api/users/{id}
    public function show(int $id): Response
    {
        $user = User::find($id);

        if (!$user) {
            return $this->error('Usuario no encontrado', 404);
        }

        return $this->json($user);
    }

    // POST /api/users
    public function store(): Response
    {
        $data = $this->getJsonInput();

        // Validación
        if (empty($data['email'])) {
            return $this->error('Email requerido', 400);
        }

        $user = User::create($data);

        return $this->json($user, 201);
    }

    // PUT /api/users/{id}
    public function update(int $id): Response
    {
        $user = User::find($id);

        if (!$user) {
            return $this->error('Usuario no encontrado', 404);
        }

        $data = $this->getJsonInput();
        $user->update($data);

        return $this->json($user);
    }

    // DELETE /api/users/{id}
    public function destroy(int $id): Response
    {
        $user = User::find($id);

        if (!$user) {
            return $this->error('Usuario no encontrado', 404);
        }

        $user->delete();

        return $this->json(['message' => 'Usuario eliminado'], 200);
    }
}

Manejo de Errores API

class ApiBaseController extends AbstractController
{
    protected function validateRequired(array $data, array $required): ?Response
    {
        foreach ($required as $field) {
            if (empty($data[$field])) {
                return $this->error("Campo requerido: {$field}", 400);
            }
        }

        return null; // No hay errores
    }

    protected function handleNotFoundException(string $resource): Response
    {
        return $this->error("{$resource} no encontrado", 404);
    }

    protected function handleValidationError(array $errors): Response
    {
        return $this->error('Datos inválidos', 422, $errors);
    }
}

Versionado de API

// routes/api.php
return [
    // API v1
    ['GET', '/api/v1/users', [ApiV1Controller::class, 'users']],
    ['POST', '/api/v1/users', [ApiV1Controller::class, 'createUser']],

    // API v2 (nuevas características)
    ['GET', '/api/v2/users', [ApiV2Controller::class, 'users']],
    ['POST', '/api/v2/users', [ApiV2Controller::class, 'createUser']],
];

Base de Datos

Configuración

Configuración Automática via Wizard

La forma recomendada de configurar la base de datos es usando el Database Wizard, que genera automáticamente la configuración apropiada. Sin embargo, también puedes configurar manualmente.

Configuración Manual

Crea el archivo config/database.json:

{
  "driver": "mysql",
  "host": "localhost",
  "port": 3306,
  "database": "mi_aplicacion",
  "username": "usuario",
  "password": "contraseña",
  "charset": "utf8mb4",
  "collation": "utf8mb4_unicode_ci",
  "options": {
    "timeout": 30,
    "retry_attempts": 3
  }
}

Configuraciones por Entorno

{
  "development": {
    "driver": "sqlite",
    "database": "database/dev.sqlite"
  },
  "testing": {
    "driver": "sqlite",
    "database": ":memory:"
  },
  "production": {
    "driver": "mysql",
    "host": "prod-db-server",
    "database": "prod_db",
    "username": "prod_user",
    "password": "secure_password"
  }
}

Múltiples Conexiones

Arkham soporta múltiples conexiones de base de datos:

{
  "default": "primary",
  "connections": {
    "primary": {
      "driver": "mysql",
      "host": "localhost",
      "database": "main_db"
    },
    "analytics": {
      "driver": "postgresql",
      "host": "analytics-server",
      "database": "analytics_db"
    },
    "cache": {
      "driver": "redis",
      "host": "cache-server"
    }
  }
}

Query Builder

Filosofía del Query Builder

El Query Builder de Arkham está diseñado para ser:

Consultas Básicas

SELECT Queries:

use JosueIsOffline\Framework\Database\DB;

// Seleccionar todos los registros
$users = DB::table('users')->select()->get();

// Seleccionar campos específicos
$users = DB::table('users')->select('name', 'email')->get();

// Obtener un solo registro
$user = DB::table('users')->where('id', 1)->first();

// Obtener con condiciones
$activeUsers = DB::table('users')
    ->where('status', 'active')
    ->where('age', '>', 18)
    ->get();

INSERT Operations:

// Insertar un registro
DB::table('users')->insert([
    'name' => 'Juan Pérez',
    'email' => 'juan@ejemplo.com',
    'password' => password_hash('secreto', PASSWORD_DEFAULT),
    'created_at' => date('Y-m-d H:i:s')
]);

// Insertar múltiples registros
DB::table('users')->insert([
    ['name' => 'Ana', 'email' => 'ana@ejemplo.com'],
    ['name' => 'Luis', 'email' => 'luis@ejemplo.com'],
    ['name' => 'María', 'email' => 'maria@ejemplo.com']
]);

UPDATE Operations:

// Actualizar registros
DB::table('users')
    ->where('id', 1)
    ->update(['name' => 'Juan Carlos Pérez']);

// Actualizar múltiples registros
DB::table('users')
    ->where('status', 'pending')
    ->update(['status' => 'active']);

// Incrementar/decrementar valores
DB::table('posts')->where('id', 1)->increment('views');
DB::table('accounts')->where('user_id', 1)->decrement('balance', 50);

DELETE Operations:

// Eliminar registros específicos
DB::table('users')->where('id', 1)->delete();

// Eliminar con múltiples condiciones
DB::table('logs')
    ->where('level', 'debug')
    ->where('created_at', '<', '2024-01-01')
    ->delete();

Consultas Avanzadas

Joins:

$orders = DB::table('orders')
    ->select('orders.*', 'customers.name', 'customers.email')
    ->join('customers', 'orders.customer_id', '=', 'customers.id')
    ->where('orders.status', 'completed')
    ->get();

Subqueries:

$expensiveProducts = DB::table('products')
    ->where('price', '>', function($query) {
        return $query->select(DB::raw('AVG(price)'))
                    ->from('products');
    })
    ->get();

Aggregations:

$totalSales = DB::table('orders')->sum('total');
$userCount = DB::table('users')->count();
$averageAge = DB::table('users')->avg('age');
$maxPrice = DB::table('products')->max('price');
$minPrice = DB::table('products')->min('price');

Grouping and Having:

$salesByMonth = DB::table('orders')
    ->select(DB::raw('MONTH(created_at) as month'), DB::raw('SUM(total) as total_sales'))
    ->groupBy(DB::raw('MONTH(created_at)'))
    ->having('total_sales', '>', 10000)
    ->get();

Ordering and Limiting:

$recentPosts = DB::table('posts')
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->offset(20)
    ->get();

Raw Queries

Para consultas complejas que requieren SQL específico:

// Query raw completo
$results = DB::raw("
    SELECT u.name, COUNT(p.id) as post_count
    FROM users u
    LEFT JOIN posts p ON u.id = p.user_id
    WHERE u.status = ?
    GROUP BY u.id
    HAVING post_count > ?
", ['active', 5]);

// Raw expressions en query builder
$users = DB::table('users')
    ->select(DB::raw('COUNT(*) as user_count, status'))
    ->groupBy('status')
    ->get();

Transacciones

Transacciones Básicas

DB::transaction(function() {
    // Operaciones que deben ejecutarse todas o ninguna
    DB::table('accounts')
        ->where('id', 1)
        ->decrement('balance', 100);

    DB::table('accounts')
        ->where('id', 2)
        ->increment('balance', 100);

    DB::table('transactions')->insert([
        'from_account' => 1,
        'to_account' => 2,
        'amount' => 100,
        'type' => 'transfer'
    ]);
});

Transacciones Manuales

DB::beginTransaction();

try {
    // Operaciones de base de datos
    $user = DB::table('users')->insertGetId(['name' => 'Test User']);

    DB::table('profiles')->insert([
        'user_id' => $user,
        'bio' => 'Test bio'
    ]);

    DB::commit();
} catch (Exception $e) {
    DB::rollback();
    throw $e;
}

Nested Transactions

DB::transaction(function() {
    // Transacción externa
    DB::table('orders')->insert([...]);

    DB::transaction(function() {
        // Transacción anidada
        DB::table('order_items')->insert([...]);
        DB::table('inventory')->decrement('quantity', 1);
    });

    // Continúa transacción externa
    DB::table('customer_logs')->insert([...]);
});

Migraciones

Concepto de Migraciones

Aunque Arkham incluye el Database Wizard para configuración inicial, también soporta migraciones tradicionales para cambios incrementales:

<?php
// migrations/2024_01_15_create_posts_table.php

use JosueIsOffline\Framework\Database\Migration;
use JosueIsOffline\Framework\Database\Schema\Blueprint;

class CreatePostsTable extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function(Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('slug')->unique();
            $table->enum('status', ['draft', 'published', 'archived']);
            $table->foreignId('user_id')->constrained();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
}

Comandos de Migración

# Crear nueva migración
php artisan make:migration create_posts_table

# Ejecutar migraciones pendientes
php artisan migrate

# Rollback última migración
php artisan migrate:rollback

# Rollback todas las migraciones
php artisan migrate:reset

# Refresh (rollback + migrate)
php artisan migrate:refresh

Modificación de Tablas

class AddEmailVerificationToUsers extends Migration
{
    public function up(): void
    {
        Schema::table('users', function(Blueprint $table) {
            $table->timestamp('email_verified_at')->nullable();
            $table->string('email_verification_token')->nullable();
            $table->index('email_verification_token');
        });
    }

    public function down(): void
    {
        Schema::table('users', function(Blueprint $table) {
            $table->dropColumn(['email_verified_at', 'email_verification_token']);
        });
    }
}

Modelos

Definición de Modelos

Los modelos en Arkham siguen el patrón Active Record, similar a Eloquent de Laravel pero más simple y directo. Cada modelo representa una tabla en la base de datos y proporciona una interfaz orientada a objetos para interactuar con los datos.

Modelo Básico

<?php

namespace App\Models;

use JosueIsOffline\Framework\Model\Model;

class User extends Model
{
    // Nombre de la tabla (opcional - se infiere del nombre de la clase)
    protected string $table = 'users';

    // Clave primaria (por defecto: 'id')
    protected string $primaryKey = 'id';

    // Campos que se pueden asignar masivamente
    protected array $fillable = ['name', 'email', 'password'];

    // Campos que nunca deben ser asignados masivamente
    protected array $guarded = ['id', 'created_at', 'updated_at'];

    // Campos que deben ser ocultados en serialización
    protected array $hidden = ['password', 'remember_token'];

    // Casting automático de tipos
    protected array $casts = [
        'email_verified_at' => 'datetime',
        'is_admin' => 'boolean',
        'settings' => 'json'
    ];
}

Convenciones de Nomenclatura

Tabla: Plural en minúsculas del nombre del modelo

Clave Primaria: ‘id’ por defecto Timestamps: ‘createdat’ y ‘updated_at’ (opcional) _Foreign Keys: {modelo}_id (ej: user_id, category_id)

Eloquent-style ORM

Operaciones CRUD Básicas

Create (Crear):

// Método 1: constructor + save
$user = new User([
    'name' => 'Juan Pérez',
    'email' => 'juan@ejemplo.com',
    'password' => password_hash('secreto', PASSWORD_DEFAULT)
]);
$user->save();

// Método 2: create estático
$user = User::create([
    'name' => 'María González',
    'email' => 'maria@ejemplo.com',
    'password' => password_hash('secreto', PASSWORD_DEFAULT)
]);

// Método 3: firstOrCreate
$user = User::firstOrCreate(
    ['email' => 'admin@ejemplo.com'], // Condición de búsqueda
    ['name' => 'Administrador']       // Datos para crear si no existe
);

Read (Leer):

// Obtener todos los registros
$users = User::all();

// Buscar por clave primaria
$user = User::find(1);

// Buscar con condiciones
$users = User::where('status', 'active');
$user = User::where('email', 'juan@ejemplo.com')->first();

// Buscar o fallar (lanza excepción si no encuentra)
$user = User::findOrFail(1);

// Obtener con ordenamiento
$users = User::orderBy('created_at', 'desc')->get();

// Obtener con límite
$recentUsers = User::latest()->limit(10)->get();

Update (Actualizar):

// Método 1: Encontrar y actualizar
$user = User::find(1);
$user->name = 'Nuevo Nombre';
$user->save();

// Método 2: Update masivo
User::where('status', 'pending')->update(['status' => 'active']);

// Método 3: updateOrCreate
$user = User::updateOrCreate(
    ['email' => 'juan@ejemplo.com'],    // Condición de búsqueda
    ['name' => 'Juan Carlos Pérez']     // Datos para actualizar/crear
);

Delete (Eliminar):

// Eliminar instancia específica
$user = User::find(1);
$user->delete();

// Eliminar por ID directamente
User::destroy(1);

// Eliminar múltiples IDs
User::destroy([1, 2, 3]);

// Eliminar con condiciones
User::where('last_login', '<', '2023-01-01')->delete();

Scopes (Alcances)

Los scopes permiten definir consultas reutilizables:

class User extends Model
{
    // Scope local
    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    public function scopeAdmins($query)
    {
        return $query->where('role', 'admin');
    }

    public function scopeByEmail($query, $email)
    {
        return $query->where('email', $email);
    }
}

// Uso de scopes
$activeUsers = User::active()->get();
$admins = User::admins()->get();
$user = User::byEmail('juan@ejemplo.com')->first();

// Combinar scopes
$activeAdmins = User::active()->admins()->get();

Mutators y Accessors

Accessors (modifican datos al leerlos):

class User extends Model
{
    // Accessor para nombre completo
    public function getFullNameAttribute(): string
    {
        return $this->name . ' (' . $this->email . ')';
    }

    // Accessor para fecha formateada
    public function getFormattedCreatedAtAttribute(): string
    {
        return $this->created_at ? $this->created_at->format('d/m/Y') : '';
    }
}

// Uso
$user = User::find(1);
echo $user->full_name; // "Juan Pérez (juan@ejemplo.com)"
echo $user->formatted_created_at; // "15/01/2024"

Mutators (modifican datos al escribirlos):

class User extends Model
{
    // Mutator para password (hash automático)
    public function setPasswordAttribute($value): void
    {
        $this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
    }

    // Mutator para email (normalización)
    public function setEmailAttribute($value): void
    {
        $this->attributes['email'] = strtolower(trim($value));
    }

    // Mutator para nombre (capitalización)
    public function setNameAttribute($value): void
    {
        $this->attributes['name'] = ucwords(strtolower(trim($value)));
    }
}

// Uso (los mutators se ejecutan automáticamente)
$user = new User([
    'name' => 'juan pérez',           // Se guarda como "Juan Pérez"
    'email' => '  JUAN@EJEMPLO.COM ', // Se guarda como "juan@ejemplo.com"
    'password' => 'secreto'           // Se guarda hasheado
]);

Relaciones

One-to-Many (Uno a Muchos)

// Modelo User
class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

// Modelo Post
class Post extends Model
{
    protected array $fillable = ['title', 'content', 'user_id'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

// Uso
$user = User::find(1);
$posts = $user->posts; // Todos los posts del usuario

$post = Post::find(1);
$author = $post->user; // Usuario que escribió el post

Many-to-Many (Muchos a Muchos)

// Modelo User
class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class, 'user_roles');
    }
}

// Modelo Role
class Role extends Model
{
    public function users()
    {
        return $this->belongsToMany(User::class, 'user_roles');
    }
}

// Uso
$user = User::find(1);
$roles = $user->roles; // Todos los roles del usuario

// Attach/Detach roles
$user->roles()->attach($roleId);
$user->roles()->detach($roleId);
$user->roles()->sync([$role1, $role2]); // Sincronizar roles

One-to-One (Uno a Uno)

// Modelo User
class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }
}

// Modelo Profile
class Profile extends Model
{
    protected array $fillable = ['bio', 'avatar', 'user_id'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

// Uso
$user = User::find(1);
$profile = $user->profile; // Perfil del usuario

$profile = Profile::find(1);
$user = $profile->user; // Usuario del perfil

Eager Loading (Carga Anticipada)

Para evitar el problema N+1, usa eager loading:

// Problema N+1 (malo)
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->user->name; // 1 query por post
}

// Solución con eager loading (bueno)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
    echo $post->user->name; // Solo 2 queries total
}

// Múltiples relaciones
$posts = Post::with(['user', 'comments', 'tags'])->get();

// Relaciones anidadas
$posts = Post::with(['user.profile', 'comments.user'])->get();

Mutadores y Accessors

Casting Automático

class User extends Model
{
    protected array $casts = [
        'is_admin' => 'boolean',
        'settings' => 'json',
        'birth_date' => 'date',
        'last_login' => 'datetime',
        'salary' => 'decimal:2'
    ];
}

// Los casts se aplican automáticamente
$user = User::find(1);
$isAdmin = $user->is_admin;        // boolean, no string
$settings = $user->settings;       // array, no JSON string
$birthDate = $user->birth_date;    // Carbon date instance

Accessors Avanzados

class Product extends Model
{
    // Precio con formato
    public function getFormattedPriceAttribute(): string
    {
        return '$' . number_format($this->price, 2);
    }

    // URL de imagen con fallback
    public function getImageUrlAttribute(): string
    {
        return $this->image
            ? asset('images/products/' . $this->image)
            : asset('images/no-image.png');
    }

    // Status legible
    public function getStatusLabelAttribute(): string
    {
        return match($this->status) {
            'active' => 'Activo',
            'inactive' => 'Inactivo',
            'pending' => 'Pendiente',
            default => 'Desconocido'
        };
    }
}

Mutators Avanzados

class Article extends Model
{
    // Generar slug automáticamente
    public function setTitleAttribute($value): void
    {
        $this->attributes['title'] = $value;
        $this->attributes['slug'] = Str::slug($value);
    }

    // Sanitizar HTML
    public function setContentAttribute($value): void
    {
        $this->attributes['content'] = strip_tags($value, '<p><a><strong><em>');
    }

    // Normalizar tags
    public function setTagsAttribute($value): void
    {
        if (is_array($value)) {
            $this->attributes['tags'] = json_encode($value);
        } else {
            // Convertir string separado por comas a array
            $tags = array_map('trim', explode(',', $value));
            $this->attributes['tags'] = json_encode($tags);
        }
    }
}

Vistas y Plantillas

Motor Twig

Arkham utiliza Twig como su motor de plantillas principal. Twig proporciona una sintaxis limpia, segura y potente para generar HTML dinámico, con características como herencia de plantillas, filtros, y extensiones personalizadas.

¿Por qué Twig?

Seguridad: Escape automático de variables para prevenir XSS Performance: Compilación de plantillas para máxima velocidad Flexibilidad: Sistema extensible con filtros y funciones personalizadas Legibilidad: Sintaxis clara que separa lógica de presentación Herencia: Sistema robusto de plantillas base y extensión

Sintaxis Básica

{# Comentarios - no aparecen en HTML final #}

{# Variables #}
{{ nombre }}
{{ usuario.email }}
{{ productos[0].titulo }}

{# Filtros #}
{{ nombre|upper }}
{{ precio|number_format(2) }}
{{ contenido|raw }}

{# Estructuras de control #}
{% if usuario %}
    Bienvenido {{ usuario.nombre }}
{% endif %}

{% for producto in productos %}
    <div>{{ producto.nombre }}</div>
{% endfor %}

{# Bloques para herencia #}
{% block contenido %}
    Contenido por defecto
{% endblock %}

Herencia de Plantillas

Plantilla Base

{# views/base.html.twig #}
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Mi Aplicación{% endblock %}</title>

    <!-- CSS Base -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    {% block styles %}{% endblock %}

    <style>
        .navbar-brand { font-weight: bold; }
        .footer { margin-top: 50px; background: #f8f9fa; }
    </style>
</head>
<body>
    <!-- Navegación Principal -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" href="/">{{ config.app_name|default('Arkham App') }}</a>

            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    {% block navigation %}
                        <li class="nav-item">
                            <a class="nav-link" href="/">Inicio</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/about">Acerca de</a>
                        </li>
                    {% endblock %}
                </ul>

                <ul class="navbar-nav">
                    {% if auth_check() %}
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
                                {{ auth_user().name }}
                            </a>
                            <ul class="dropdown-menu">
                                <li><a class="dropdown-item" href="/profile">Perfil</a></li>
                                {% if has_role('admin') %}
                                    <li><a class="dropdown-item" href="/admin">Administración</a></li>
                                {% endif %}
                                <li><hr class="dropdown-divider"></li>
                                <li><a class="dropdown-item" href="/logout">Cerrar Sesión</a></li>
                            </ul>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="/login">Iniciar Sesión</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/register">Registrarse</a>
                        </li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>

    <!-- Mensajes Flash -->
    {% if success %}
        <div class="container mt-3">
            <div class="alert alert-success alert-dismissible fade show">
                {{ success }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        </div>
    {% endif %}

    {% if error %}
        <div class="container mt-3">
            <div class="alert alert-danger alert-dismissible fade show">
                {{ error }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        </div>
    {% endif %}

    <!-- Contenido Principal -->
    <main class="container mt-4">
        {% block content %}
            <h1>Página por defecto</h1>
            <p>Este contenido aparece si no se define un bloque content.</p>
        {% endblock %}
    </main>

    <!-- Footer -->
    <footer class="footer mt-5 py-4">
        <div class="container">
            <div class="row">
                <div class="col-md-6">
                    {% block footer_left %}
                        <p>&copy; {{ "now"|date("Y") }} Mi Aplicación. Todos los derechos reservados.</p>
                    {% endblock %}
                </div>
                <div class="col-md-6 text-end">
                    {% block footer_right %}
                        <p>Creado con <a href="https://github.com/JosueIsOffline/Arkham">Arkham Framework</a></p>
                    {% endblock %}
                </div>
            </div>
        </div>
    </footer>

    <!-- JavaScript Base -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    {% block scripts %}{% endblock %}
</body>
</html>

Plantillas Específicas

{# views/productos/index.html.twig #}
{% extends "base.html.twig" %}

{% block title %}Catálogo de Productos - {{ parent() }}{% endblock %}

{% block navigation %}
    {{ parent() }}
    <li class="nav-item">
        <a class="nav-link active" href="/productos">Productos</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="/categorias">Categorías</a>
    </li>
{% endblock %}

{% block content %}
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1>Catálogo de Productos</h1>
        {% if has_role('admin') %}
            <a href="/productos/crear" class="btn btn-primary">Agregar Producto</a>
        {% endif %}
    </div>

    <!-- Filtros -->
    <div class="row mb-4">
        <div class="col-md-4">
            <select class="form-select" onchange="filtrarPorCategoria(this.value)">
                <option value="">Todas las categorías</option>
                {% for categoria in categorias %}
                    <option value="{{ categoria.id }}">{{ categoria.nombre }}</option>
                {% endfor %}
            </select>
        </div>
        <div class="col-md-4">
            <input type="text" class="form-control" placeholder="Buscar productos..."
                   onkeyup="buscarProductos(this.value)">
        </div>
    </div>

    <!-- Grid de Productos -->
    {% if productos|length > 0 %}
        <div class="row" id="productos-grid">
            {% for producto in productos %}
                <div class="col-md-4 mb-4 producto-item" data-categoria="{{ producto.categoria_id }}">
                    <div class="card h-100">
                        {% if producto.imagen %}
                            <img src="{{ producto.imagen_url }}" class="card-img-top" alt="{{ producto.nombre }}"
                                 style="height: 200px; object-fit: cover;">
                        {% else %}
                            <div class="card-img-top bg-light d-flex align-items-center justify-content-center"
                                 style="height: 200px;">
                                <span class="text-muted">Sin imagen</span>
                            </div>
                        {% endif %}

                        <div class="card-body d-flex flex-column">
                            <h5 class="card-title">{{ producto.nombre }}</h5>
                            <p class="card-text flex-grow-1">{{ producto.descripcion|slice(0, 100) }}...</p>
                            <div class="mt-auto">
                                <div class="d-flex justify-content-between align-items-center">
                                    <strong class="text-primary">{{ producto.precio_formateado }}</strong>
                                    {% if producto.stock > 0 %}
                                        <span class="badge bg-success">En stock</span>
                                    {% else %}
                                        <span class="badge bg-danger">Agotado</span>
                                    {% endif %}
                                </div>
                                <div class="mt-2">
                                    <a href="/productos/{{ producto.id }}" class="btn btn-outline-primary btn-sm">Ver detalles</a>
                                    {% if producto.stock > 0 %}
                                        <button class="btn btn-primary btn-sm" onclick="agregarAlCarrito({{ producto.id }})">
                                            Agregar al carrito
                                        </button>
                                    {% endif %}
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>

        <!-- Paginación -->
        {% if paginacion %}
            <nav aria-label="Paginación de productos">
                <ul class="pagination justify-content-center">
                    {% if paginacion.pagina_anterior %}
                        <li class="page-item">
                            <a class="page-link" href="?pagina={{ paginacion.pagina_anterior }}">Anterior</a>
                        </li>
                    {% endif %}

                    {% for pagina in paginacion.paginas %}
                        <li class="page-item {{ pagina == paginacion.pagina_actual ? 'active' : '' }}">
                            <a class="page-link" href="?pagina={{ pagina }}">{{ pagina }}</a>
                        </li>
                    {% endfor %}

                    {% if paginacion.pagina_siguiente %}
                        <li class="page-item">
                            <a class="page-link" href="?pagina={{ paginacion.pagina_siguiente }}">Siguiente</a>
                        </li>
                    {% endif %}
                </ul>
            </nav>
        {% endif %}
    {% else %}
        <div class="text-center py-5">
            <h3>No hay productos disponibles</h3>
            <p class="text-muted">Vuelve pronto para ver nuestros nuevos productos.</p>
            {% if has_role('admin') %}
                <a href="/productos/crear" class="btn btn-primary">Agregar primer producto</a>
            {% endif %}
        </div>
    {% endif %}
{% endblock %}

{% block scripts %}
    <script>
        function filtrarPorCategoria(categoriaId) {
            const productos = document.querySelectorAll('.producto-item');
            productos.forEach(producto => {
                if (!categoriaId || producto.dataset.categoria === categoriaId) {
                    producto.style.display = 'block';
                } else {
                    producto.style.display = 'none';
                }
            });
        }

        function buscarProductos(query) {
            const productos = document.querySelectorAll('.producto-item');
            productos.forEach(producto => {
                const titulo = producto.querySelector('.card-title').textContent.toLowerCase();
                if (titulo.includes(query.toLowerCase())) {
                    producto.style.display = 'block';
                } else {
                    producto.style.display = 'none';
                }
            });
        }

        function agregarAlCarrito(productoId) {
            // Implementar lógica de carrito
            console.log('Agregando producto', productoId, 'al carrito');
        }
    </script>
{% endblock %}

Componentes

Componentes Reutilizables

{# views/components/card.html.twig #}
<div class="card {{ clase|default('') }}">
    {% if titulo %}
        <div class="card-header">
            <h5 class="mb-0">{{ titulo }}</h5>
        </div>
    {% endif %}

    <div class="card-body">
        {{ contenido|raw }}
    </div>

    {% if acciones %}
        <div class="card-footer">
            {{ acciones|raw }}
        </div>
    {% endif %}
</div>

Uso de Componentes

{# Incluir componente simple #}
{% include 'components/card.html.twig' with {
    'titulo': 'Mi Tarjeta',
    'contenido': '<p>Contenido de la tarjeta</p>',
    'clase': 'border-primary'
} %}

{# Usar embed para contenido complejo #}
{% embed 'components/card.html.twig' with {'titulo': 'Estadísticas'} %}
    {% block contenido %}
        <div class="row">
            <div class="col-6">
                <strong>Usuarios:</strong> {{ stats.usuarios }}
            </div>
            <div class="col-6">
                <strong>Ventas:</strong> {{ stats.ventas }}
            </div>
        </div>
    {% endblock %}
{% endembed %}

Funciones Personalizadas

Funciones Integradas de Arkham

{# Autenticación #}
{% if auth_check() %}
    Usuario conectado: {{ auth_user().name }}
{% endif %}

{# Verificación de roles #}
{% if has_role('admin') %}
    <a href="/admin" class="btn btn-danger">Panel de Administración</a>
{% endif %}

{# Assets helpers #}
{{ asset('css/app.css') }}
{{ asset('js/app.js') }}

{# URL helpers #}
{{ url('/productos') }}
{{ route('user.show', {'id': 123}) }}

{# Configuración #}
{{ config('app.name') }}
{{ config('app.version') }}

Filtros Personalizados

// En el ViewResolver
$this->twig->addFilter(new \Twig\TwigFilter('money', function ($amount) {
    return '$' . number_format($amount, 2);
}));

$this->twig->addFilter(new \Twig\TwigFilter('excerpt', function ($text, $length = 100) {
    return strlen($text) > $length ? substr($text, 0, $length) . '...' : $text;
}));
{# Uso de filtros personalizados #}
Precio: {{ producto.precio|money }}
Resumen: {{ articulo.contenido|excerpt(150) }}

Funciones Globales Personalizadas

// Función para generar breadcrumbs
$this->twig->addFunction(new \Twig\TwigFunction('breadcrumbs', function ($items) {
    $html = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
    foreach ($items as $item) {
        if (isset($item['url'])) {
            $html .= '<li class="breadcrumb-item"><a href="' . $item['url'] . '">' . $item['title'] . '</a></li>';
        } else {
            $html .= '<li class="breadcrumb-item active">' . $item['title'] . '</li>';
        }
    }
    $html .= '</ol></nav>';
    return $html;
}, ['is_safe' => ['html']]));
{# Uso de funciones personalizadas #}
{{ breadcrumbs([
    {'title': 'Inicio', 'url': '/'},
    {'title': 'Productos', 'url': '/productos'},
    {'title': 'Categoría Tech'}
]) }}

Macros para Reutilización

{# views/macros/forms.html.twig #}
{% macro input(name, type, label, value, attributes) %}
    <div class="mb-3">
        <label for="{{ name }}" class="form-label">{{ label }}</label>
        <input type="{{ type|default('text') }}"
               class="form-control"
               id="{{ name }}"
               name="{{ name }}"
               value="{{ value|default('') }}"
               {% for attr, val in attributes|default({}) %}
                   {{ attr }}="{{ val }}"
               {% endfor %}>
    </div>
{% endmacro %}

{% macro select(name, label, options, selected, attributes) %}
    <div class="mb-3">
        <label for="{{ name }}" class="form-label">{{ label }}</label>
        <select class="form-select" id="{{ name }}" name="{{ name }}"
                {% for attr, val in attributes|default({}) %}
                    {{ attr }}="{{ val }}"
                {% endfor %}>
            {% for value, text in options %}
                <option value="{{ value }}" {{ value == selected ? 'selected' : '' }}>
                    {{ text }}
                </option>
            {% endfor %}
        </select>
    </div>
{% endmacro %}
{# Uso de macros #}
{% import 'macros/forms.html.twig' as forms %}

{{ forms.input('email', 'email', 'Correo Electrónico', user.email, {'required': 'required'}) }}

{{ forms.select('categoria', 'Categoría', categorias, producto.categoria_id, {'class': 'form-select'}) }}

Autenticación

Sistema de Auth

Arkham incluye un sistema de autenticación completo y fácil de usar que maneja sesiones, cookies, roles y permisos. El sistema está diseñado para ser seguro por defecto mientras permanece flexible para diferentes necesidades.

Características del Sistema de Auth

Configuración de Tablas

El Database Wizard puede crear automáticamente las tablas necesarias para autenticación:

-- Tabla de usuarios
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    email_verified_at TIMESTAMP NULL,
    remember_token VARCHAR(100) NULL,
    role_id INT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Tabla de roles
CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) UNIQUE NOT NULL,
    display_name VARCHAR(255) NOT NULL,
    description TEXT NULL,
    permissions JSON NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Login y Registro

Controlador de Autenticación

<?php

namespace App\Controllers;

use JosueIsOffline\Framework\Controllers\AbstractController;
use JosueIsOffline\Framework\Http\Response;
use JosueIsOffline\Framework\Auth\AuthService;
use App\Models\User;

class AuthController extends AbstractController
{
    private AuthService $authService;

    public function __construct()
    {
        parent::__construct();
        $this->authService = new AuthService();
    }

    public function loginForm(): Response
    {
        if ($this->authService->check()) {
            return $this->redirect('/dashboard');
        }

        return $this->render('auth/login.html.twig');
    }

    public function login(): Response
    {
        $data = $this->request->getAllPost();

        if (empty($data['email']) || empty($data['password'])) {
            return $this->renderWithFlash('auth/login.html.twig', [
                'error' => 'Email y contraseña son requeridos',
                'old' => $data
            ]);
        }

        if ($this->authService->attempt($data['email'], $data['password'])) {
            if (!empty($data['remember'])) {
                $this->authService->setRememberToken();
            }

            $redirectTo = $_SESSION['intended_url'] ?? '/dashboard';
            unset($_SESSION['intended_url']);

            return $this->success([], 'Bienvenido de vuelta!', 200, $redirectTo);
        }

        return $this->renderWithFlash('auth/login.html.twig', [
            'error' => 'Credenciales incorrectas',
            'old' => ['email' => $data['email']]
        ]);
    }

    public function logout(): Response
    {
        $this->authService->logout();
        return $this->success([], 'Sesión cerrada correctamente', 200, '/');
    }
}

Roles y Permisos

Modelo de Roles

<?php

namespace App\Models;

use JosueIsOffline\Framework\Model\Model;

class Role extends Model
{
    protected string $table = 'roles';
    protected array $fillable = ['name', 'display_name', 'description', 'permissions'];

    protected array $casts = [
        'permissions' => 'json'
    ];

    public function users()
    {
        return $this->hasMany(User::class, 'role_id');
    }

    public function hasPermission(string $permission): bool
    {
        return in_array($permission, $this->permissions ?? []);
    }

    public static function createDefaultRoles(): void
    {
        $roles = [
            [
                'name' => 'admin',
                'display_name' => 'Administrador',
                'description' => 'Acceso completo al sistema',
                'permissions' => [
                    'manage_users', 'manage_roles', 'manage_content',
                    'view_admin_panel', 'manage_settings', 'view_reports'
                ]
            ],
            [
                'name' => 'user',
                'display_name' => 'Usuario',
                'description' => 'Usuario estándar del sistema',
                'permissions' => [
                    'read_own_profile', 'update_own_profile', 'create_content'
                ]
            ]
        ];

        foreach ($roles as $roleData) {
            Role::firstOrCreate(['name' => $roleData['name']], $roleData);
        }
    }
}

Middleware de Auth

AuthMiddleware

<?php

namespace JosueIsOffline\Framework\Middleware;

use JosueIsOffline\Framework\Http\Request;
use JosueIsOffline\Framework\Http\Response;
use JosueIsOffline\Framework\Auth\AuthService;

class AuthMiddleware
{
    public function handle(Request $request, callable $next): Response
    {
        $auth = new AuthService();

        if (!$auth->check()) {
            $_SESSION['intended_url'] = $request->getUri();

            if ($this->isApiRequest($request)) {
                return new JsonResponse(['error' => 'No autenticado'], 401);
            }

            return new Response('', 302, ['Location' => '/login']);
        }

        return $next($request);
    }

    private function isApiRequest(Request $request): bool
    {
        return strpos($request->getUri(), '/api/') === 0 ||
               strpos($request->getServer('HTTP_ACCEPT'), 'application/json') !== false;
    }
}

Middleware

Concepto de Middleware

Los middlewares en Arkham actúan como filtros HTTP que procesan las peticiones antes de que lleguen a los controladores. Proporcionan una forma elegante de filtrar, modificar y validar requests HTTP.

Casos de Uso Comunes

Middleware Incluidos

RoleMiddleware

<?php

namespace JosueIsOffline\Framework\Middleware;

class RoleMiddleware
{
    private string $requiredRole;

    public function __construct(string $role)
    {
        $this->requiredRole = $role;
    }

    public function handle(Request $request, callable $next): Response
    {
        $auth = new AuthService();

        if (!$auth->check() || !$auth->hasRole($this->requiredRole)) {
            return new Response('Forbidden', 403);
        }

        return $next($request);
    }
}

Middleware Personalizado

RateLimitMiddleware

<?php

namespace App\Middleware;

class RateLimitMiddleware
{
    private int $maxAttempts;
    private int $decayMinutes;

    public function __construct(int $maxAttempts = 60, int $decayMinutes = 1)
    {
        $this->maxAttempts = $maxAttempts;
        $this->decayMinutes = $decayMinutes;
    }

    public function handle(Request $request, callable $next): Response
    {
        $key = $this->resolveRequestSignature($request);
        $attempts = $this->getAttempts($key);

        if ($attempts >= $this->maxAttempts) {
            return $this->buildRateLimitResponse();
        }

        $this->incrementAttempts($key);
        $response = $next($request);

        $response->setHeader('X-RateLimit-Limit', (string)$this->maxAttempts);
        $response->setHeader('X-RateLimit-Remaining', (string)($this->maxAttempts - $attempts - 1));

        return $response;
    }
}

Orden de Ejecución

Configuración de Middleware en Rutas

// routes/web.php
return [
    // Sin middleware
    ['GET', '/', [HomeController::class, 'index']],

    // Middleware único
    ['GET', '/profile', [ProfileController::class, 'show'], 'auth'],

    // Múltiples middlewares (se ejecutan en orden)
    ['GET', '/admin', [AdminController::class, 'index'], [
        'auth',
        'role:admin',
        SecurityHeadersMiddleware::class
    ]],
];

Validación

Validación de Formularios

Arkham proporciona un sistema de validación flexible que puede manejar tanto formularios web como APIs JSON.

Validator Básico

<?php

namespace JosueIsOffline\Framework\Validation;

class Validator
{
    private array $data;
    private array $rules;
    private array $errors = [];

    public function __construct(array $data, array $rules)
    {
        $this->data = $data;
        $this->rules = $rules;
    }

    public static function make(array $data, array $rules): self
    {
        return new self($data, $rules);
    }

    public function validate(): bool
    {
        $this->errors = [];

        foreach ($this->rules as $field => $ruleString) {
            $this->validateField($field, $ruleString);
        }

        return empty($this->errors);
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    private function validateField(string $field, string $ruleString): void
    {
        $rules = explode('|', $ruleString);
        $value = $this->data[$field] ?? null;

        foreach ($rules as $rule) {
            $this->applyRule($field, $value, $rule);
        }
    }
}

Reglas de Validación

Reglas Disponibles

// Uso básico
$validator = Validator::make($data, [
    'name' => 'required|min:2|max:50',
    'email' => 'required|email|unique:users,email',
    'password' => 'required|min:8|confirmed',
    'age' => 'required|integer|between:18,120'
]);

if ($validator->fails()) {
    $errors = $validator->getErrors();
}

Mensajes de Error

Personalización de Mensajes

$validator = Validator::make($data, [
    'email' => 'required|email'
], [
    'email.required' => 'Por favor ingresa tu email',
    'email.email' => 'El formato del email no es válido'
]);

Validación de API

En Controladores

class ApiUserController extends AbstractController
{
    public function store(): Response
    {
        $data = $this->getJsonInput();

        $validator = Validator::make($data, [
            'name' => 'required|min:2',
            'email' => 'required|email|unique:users'
        ]);

        if ($validator->fails()) {
            return $this->json([
                'error' => 'Validation failed',
                'errors' => $validator->getErrors()
            ], 422);
        }

        $user = User::create($data);
        return $this->json($user, 201);
    }
}

Manejo de Errores

Páginas de Error

Arkham proporciona un sistema robusto para manejar errores tanto en desarrollo como en producción.

Configuración de Errores

<?php

namespace JosueIsOffline\Framework\Exceptions;

class ExceptionHandler
{
    private bool $debug;

    public function __construct(bool $debug = false)
    {
        $this->debug = $debug;
    }

    public function handleException(\Throwable $exception): void
    {
        $this->logException($exception);

        if ($this->debug) {
            $this->renderDebugPage($exception);
        } else {
            $this->renderProductionErrorPage($exception);
        }
    }

    private function logException(\Throwable $exception): void
    {
        $logEntry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'type' => get_class($exception),
            'message' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'trace' => $exception->getTraceAsString()
        ];

        file_put_contents('logs/errors.log', json_encode($logEntry) . PHP_EOL, FILE_APPEND);
    }
}

Logging

Logger Personalizado

<?php

namespace JosueIsOffline\Framework\Logging;

class Logger
{
    public const DEBUG = 'debug';
    public const INFO = 'info';
    public const ERROR = 'error';

    public function error(string $message, array $context = []): void
    {
        $this->log(self::ERROR, $message, $context);
    }

    public function log(string $level, string $message, array $context = []): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $contextString = empty($context) ? '' : ' ' . json_encode($context);

        $logEntry = "[{$timestamp}] {$level}: {$message}{$contextString}" . PHP_EOL;
        file_put_contents('logs/app.log', $logEntry, FILE_APPEND);
    }
}

Debug Mode

Páginas de Error Personalizadas

{# views/errors/404.html.twig #}
{% extends "base.html.twig" %}

{% block title %}Página no encontrada{% endblock %}

{% block content %}
<div class="text-center py-5">
    <h1 class="display-1 fw-bold text-primary">404</h1>
    <h2 class="mb-3">Página no encontrada</h2>
    <p class="lead mb-4">Lo sentimos, la página que buscas no existe.</p>
    <a href="/" class="btn btn-primary">Ir al inicio</a>
</div>
{% endblock %}

Manejo de Excepciones

Excepciones Personalizadas

<?php

namespace JosueIsOffline\Framework\Exceptions;

class NotFoundException extends \Exception
{
    public function __construct(string $message = 'Resource not found')
    {
        parent::__construct($message, 404);
    }
}

class ValidationException extends \Exception
{
    private array $errors;

    public function __construct(array $errors)
    {
        parent::__construct('Validation failed');
        $this->errors = $errors;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }
}

Testing

Configuración de Tests

Arkham utiliza PHPUnit como framework de testing principal.

TestCase Base

<?php

namespace Tests;

use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use JosueIsOffline\Framework\Database\DB;

abstract class TestCase extends PHPUnitTestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        $this->setupDatabase();
    }

    private function setupDatabase(): void
    {
        DB::configure([
            'driver' => 'sqlite',
            'database' => ':memory:'
        ]);

        $this->createTables();
    }

    private function createTables(): void
    {
        DB::raw('CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL,
            password TEXT NOT NULL
        )');
    }

    protected function createUser(array $attributes = []): array
    {
        $defaults = [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => password_hash('password', PASSWORD_DEFAULT)
        ];

        $userData = array_merge($defaults, $attributes);

        DB::table('users')->insert($userData);

        return $userData;
    }
}

Tests Unitarios

Test de Modelos

<?php

namespace Tests\Unit;

use Tests\TestCase;
use App\Models\User;

class UserTest extends TestCase
{
    public function testUserCreation(): void
    {
        $user = User::create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123'
        ]);

        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals('John Doe', $user->name);
    }

    public function testPasswordIsHashed(): void
    {
        $user = User::create([
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'plaintext'
        ]);

        $this->assertNotEquals('plaintext', $user->password);
        $this->assertTrue(password_verify('plaintext', $user->password));
    }
}

Tests de Integración

Test de API

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ApiTest extends TestCase
{
    public function testCreateUserEndpoint(): void
    {
        $userData = [
            'name' => 'New User',
            'email' => 'newuser@example.com',
            'password' => 'password123'
        ];

        $response = $this->postJson('/api/users', $userData);

        $this->assertEquals(201, $response->getStatus());

        $responseData = json_decode($response->getContent(), true);
        $this->assertEquals($userData['name'], $responseData['name']);
    }
}

Mocking

Test con Mocks

<?php

namespace Tests\Unit;

use Tests\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

class EmailServiceTest extends TestCase
{
    public function testSendWelcomeEmail(): void
    {
        $mailerMock = $this->createMock(MailerInterface::class);

        $mailerMock
            ->expects($this->once())
            ->method('send')
            ->willReturn(true);

        $emailService = new EmailService($mailerMock);
        $result = $emailService->sendWelcomeEmail($user);

        $this->assertTrue($result);
    }
}

Deployment

Preparación para Producción

Checklist de Pre-deployment

# 1. Optimizar dependencias
composer install --no-dev --optimize-autoloader

# 2. Limpiar cache de desarrollo
rm -rf storage/cache/*

# 3. Configurar variables de entorno
cp .env.example .env.production

# 4. Ejecutar tests
composer test

Configuración de Producción

# .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://tu-dominio.com

DB_CONNECTION=mysql
DB_HOST=db-server.example.com
DB_DATABASE=produccion_db
DB_USERNAME=app_user
DB_PASSWORD=super_secure_password

Configuración del Servidor

Apache Virtual Host

<VirtualHost *:443>
    ServerName tu-dominio.com
    DocumentRoot /var/www/tu-app/public

    SSLEngine on
    SSLCertificateFile /path/to/certificate.crt
    SSLCertificateKeyFile /path/to/private.key

    <Directory /var/www/tu-app/public>
        AllowOverride All
        Require all granted

        RewriteEngine On
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule ^(.*)$ index.php [QSA,L]
    </Directory>
</VirtualHost>

Nginx Configuration

server {
    listen 443 ssl http2;
    server_name tu-dominio.com;
    root /var/www/tu-app/public;
    index index.php;

    ssl_certificate /path/to/certificate.crt;
    ssl_certificate_key /path/to/private.key;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

API Reference

Core Classes

Framework Classes

// AbstractController - Clase base para controladores
JosueIsOffline\Framework\Controllers\AbstractController

// Métodos principales:
render(string $template, array $data = []): Response
json(array $data, int $status = 200): JsonResponse
success(array $data = [], ?string $message = null): Response
error(string $message, int $status = 400): JsonResponse
redirect(string $url, int $status = 302): Response

// Database Query Builder
JosueIsOffline\Framework\Database\DB

// Métodos estáticos:
DB::table(string $table): QueryBuilder
DB::raw(string $sql, array $params = []): mixed
DB::configure(array $config): void

// QueryBuilder methods:
select(...$columns): self
where(string $column, $operator, $value = null): self
get(): array
first(): array
insert(array $data): bool
update(array $data): bool
delete(): bool

// HTTP Classes
JosueIsOffline\Framework\Http\Request
JosueIsOffline\Framework\Http\Response
JosueIsOffline\Framework\Http\JsonResponse

Helper Functions

Funciones Globales


// Database
db(): Connection
table(string $name): QueryBuilder

// Authentication
auth(): AuthService
user(): ?array

Funciones Twig

{# Autenticación #}
{{ auth_check() }}
{{ auth_user() }}
{{ has_role('admin') }}

Configuration

Archivos de Configuración

// config/database.json
{
    "driver": "mysql|pgsql|sqlite|sqlsrv",
    "host": "localhost",
    "port": 3306,
    "database": "nombre_bd",
    "username": "usuario",
    "password": "contraseña"
}

¿Te gustó Arkham Framework?

Si esta documentación y el framework te han sido útiles para tu desarrollo, no olvides darle una estrella al repositorio:

⭐ Repositorio en GitHub

¿Por qué dar una estrella?

Contribuir al Proyecto

Formas de Contribuir

Proceso de Contribución

  1. Fork el repositorio
  2. Crea una rama para tu característica (git checkout -b feature/amazing-feature)
  3. Commit tus cambios (git commit -m 'Add amazing feature')
  4. Push a la rama (git push origin feature/amazing-feature)
  5. Abre un Pull Request

Comunidad y Soporte

Canales de Comunicación

Obtener Ayuda

Si necesitas ayuda con Arkham Framework:

  1. Revisa esta documentación completa
  2. Busca en GitHub Issues si tu problema ya fue reportado
  3. Consulta los ejemplos en el repositorio
  4. Pregunta en GitHub Discussions
  5. Contacta al equipo de desarrollo

Licencia

Arkham Framework es software de código abierto licenciado bajo la Licencia MIT.

Esto significa que puedes:

Con las únicas condiciones de:


🔗 GitHub Repository

¿Encontraste un error en la documentación? Ayúdanos a mejorarla