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
- Introducción
- Instalación
- Primeros Pasos
- Database Wizard
- Sistema de Rutas
- Controladores
- Base de Datos
- Modelos
- Vistas y Plantillas
- Autenticación
- Middleware
- Validación
- Manejo de Errores
- Testing
- Deployment
- 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:
- Configurar conexiones de base de datos visualmente
- Diseñar tablas sin escribir SQL
- Crear esquemas complejos con interfaz drag-and-drop
- Soporte para múltiples drivers de base de datos
Arquitectura MVC Moderna Implementación limpia del patrón Modelo-Vista-Controlador con:
- Separación clara de responsabilidades
- Inyección de dependencias automática
- Sistema de eventos integrado
- Extensibilidad total
Developer Experience de Primera Clase Cada aspecto del framework está diseñado pensando en la experiencia del desarrollador:
- Mensajes de error descriptivos
- Hot reload en desarrollo
- Debugging integrado
- Documentación contextual
Performance y Escalabilidad A pesar de su simplicidad, Arkham está optimizado para:
- Alto rendimiento en producción
- Escalabilidad horizontal
- Uso eficiente de memoria
- Caching inteligente
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
- PHP 8.1 o superior
- ext-pdo (para base de datos)
- ext-json (para configuración)
- ext-mbstring (para manejo de strings)
Herramientas de Desarrollo
- Composer 2.0+
- Git (para instalación desde repositorio)
Servidores Web Soportados
- Apache 2.4+ con mod_rewrite
- Nginx 1.18+
- PHP Built-in Server (para desarrollo)
- IIS con URL Rewrite (experimental)
Bases de Datos Soportadas
- MySQL 5.7+ / MariaDB 10.3+
- PostgreSQL 12+
- SQLite 3.35+
- SQL Server 2019+
Instalación Rápida
Método Recomendado: Composer Create-Project
composer create-project arkham-dev/framework mi-aplicacion
cd mi-aplicacion
Este comando:
- Descarga la última versión estable
- Instala todas las dependencias
- Configura la estructura de directorios
- 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:
- El servidor web está configurado correctamente
- PHP está funcionando
- Las rutas están configuradas apropiadamente
- El framework está listo para usar
Troubleshooting Común
Error 500 Internal Server Error
- Verificar permisos de archivos
- Revisar logs de error del servidor
- Comprobar que mod_rewrite esté habilitado (Apache)
Database Wizard no aparece
- Verificar que no existe archivo
config/database.json - Comprobar permisos de escritura en directorio
config/ - Revisar configuración de rutas
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
-
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.
-
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
-
Generación de Configuración: El wizard genera automáticamente todos los archivos de configuración necesarios.
-
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:
- Escribir configuraciones de base de datos manualmente
- Crear migraciones para el esquema inicial
- Lidiar con diferencias entre sistemas de base de datos
- Configurar conexiones y credenciales
¿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:
- Descripción del sistema de base de datos
- Requisitos de extensiones PHP
- Puerto por defecto
- Casos de uso recomendados
Paso 2: Configuración de Credenciales
Para Bases de Datos con Servidor (MySQL, PostgreSQL, SQL Server):
- Host: Dirección del servidor (localhost por defecto)
- Puerto: Puerto de conexión (detectado automáticamente)
- Usuario: Credenciales de acceso
- Contraseña: Autenticación
- Nombre de base de datos: Se crea automáticamente si no existe
Para SQLite:
- Ruta del archivo: Especifica donde almacenar la base de datos
- Creación automática: El wizard crea el archivo si no existe
Paso 3: Verificación de Conexión
El wizard prueba la conexión antes de proceder:
- Verifica credenciales
- Comprueba permisos
- Valida conectividad de red
- Detecta problemas comunes
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
- Agregar nuevas tablas con un click
- Nombrar tablas intuitivamente
- Duplicar estructuras similares
- Eliminar tablas no deseadas
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:
- NULL/NOT NULL
- PRIMARY KEY
- AUTO_INCREMENT
- UNIQUE (próximamente)
- FOREIGN KEY (próximamente)
Validación Inteligente
El wizard incluye validación que previene errores comunes:
- Campos AUTO_INCREMENT automáticamente se configuran como PRIMARY KEY
- PRIMARY KEYs no pueden ser NULL
- Tipos de datos incompatibles se detectan
- Nombres duplicados se previenen
Preview en Tiempo Real
Mientras diseñas, el wizard muestra:
- SQL generado en tiempo real
- Estructura de tabla resultante
- Advertencias sobre decisiones de diseño
- Estimación de espacio de almacenamiento
Drivers Soportados
MySQL/MariaDB
Características:
- Soporte completo para MySQL 5.7+ y MariaDB 10.3+
- Manejo automático de character sets (utf8mb4 por defecto)
- Soporte para storage engines (InnoDB por defecto)
- Optimizaciones específicas para MySQL
Configuración Automática:
- Puerto 3306 por defecto
- Charset utf8mb4_unicode_ci para soporte Unicode completo
- InnoDB engine para transacciones ACID
PostgreSQL
Características:
- Soporte para PostgreSQL 12+
- Manejo de tipos de datos avanzados
- Soporte para schemas
- Extensiones PostgreSQL disponibles
Configuración Automática:
- Puerto 5432 por defecto
- Encoding UTF8
- Timezone UTC
SQLite
Características:
- Base de datos sin servidor
- Perfecto para desarrollo
- Archivo único portable
- Sin configuración adicional requerida
Ventajas:
- Configuración instantánea
- No requiere servidor separado
- Ideal para testing
- Deployment simplificado
SQL Server
Características:
- Soporte para SQL Server 2019+
- Integración con ecosistema Microsoft
- Características empresariales
- Herramientas de management incluidas
Configuración Automática:
- Puerto 1433 por defecto
- Autenticación SQL y Windows
- Collation Latin1_General_CI_AS
Migración Entre Drivers
Una característica única del wizard es la capacidad de migrar entre drivers:
- Exportar esquema de un driver
- Importar en otro driver diferente
- Conversión automática de tipos de datos
- Mantenimiento de relaciones
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
- GET: Recuperar datos
- POST: Crear nuevos recursos
- PUT: Actualizar recursos completos
- PATCH: Actualizar recursos parcialmente
- DELETE: Eliminar recursos
- OPTIONS: Metadata de recursos
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:
render(): Renderiza una plantilla TwigrenderWithFlash(): Incluye mensajes flash automáticamenteviewExists(): Verifica si una vista existe
Respuestas:
json(): Retorna respuesta JSONsuccess(): Respuesta de éxito con redirección inteligenteerror(): Respuesta de error estructuradaredirect(): Redirección HTTP
Request Handling:
getAllPost(): Todos los datos POSTgetJsonInput(): Datos JSON del request bodyisJsonRequest(): Detecta requests JSONisAjaxOrApiRequest(): Detecta requests AJAX/API
Autenticación:
auth(): Servicio de autenticaciónuser(): Usuario autenticado actualhasRole(): Verificar rol específicohasPermission(): Verificar permiso específico
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
- 200: OK - Solicitud exitosa
- 201: Created - Recurso creado exitosamente
- 302: Found - Redirección temporal
- 400: Bad Request - Datos inválidos
- 401: Unauthorized - No autenticado
- 403: Forbidden - Sin permisos
- 404: Not Found - Recurso no encontrado
- 500: Internal Server Error - Error del servidor
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:
- Intuitivo: Sintaxis que se lee como inglés natural
- Potente: Soporte para consultas complejas
- Seguro: Protección automática contra SQL injection
- Portable: Funciona con todos los drivers soportados
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
- User → users
- BlogPost → blog_posts
- Category → categories
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>© {{ "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
- Seguridad: Hashing de contraseñas con algoritmos modernos
- Sesiones: Manejo automático de sesiones PHP
- Remember Me: Persistencia de login opcional
- Roles: Sistema jerárquico de roles
- Permisos: Permisos granulares por acción
- Middleware: Protección automática de rutas
- Events: Hooks para login/logout/registro
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
- Autenticación: Verificar si el usuario está logueado
- Autorización: Comprobar permisos y roles
- Rate Limiting: Limitar peticiones por IP/usuario
- CORS: Configurar headers para Cross-Origin requests
- Logging: Registrar peticiones y respuestas
- Security: Aplicar headers de seguridad
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?
- Ayuda a que más desarrolladores descubran Arkham Framework
- Motiva el desarrollo continuo y nuevas características
- Contribuye al crecimiento de la comunidad PHP hispana
- Es gratis y solo toma un segundo de tu tiempo
- Apoya el trabajo de código abierto
Contribuir al Proyecto
Formas de Contribuir
- Código: Envía pull requests con mejoras o nuevas características
- Documentación: Ayuda a mejorar esta documentación
- Reportar Bugs: Informa sobre problemas que encuentres
- Traducciones: Ayuda a traducir la documentación a otros idiomas
- Ejemplos: Crea proyectos de ejemplo usando Arkham
Proceso de Contribución
- Fork el repositorio
- Crea una rama para tu característica (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abre un Pull Request
Comunidad y Soporte
Canales de Comunicación
- GitHub Issues: Para reportar bugs y solicitar características
Obtener Ayuda
Si necesitas ayuda con Arkham Framework:
- Revisa esta documentación completa
- Busca en GitHub Issues si tu problema ya fue reportado
- Consulta los ejemplos en el repositorio
- Pregunta en GitHub Discussions
- Contacta al equipo de desarrollo
Licencia
Arkham Framework es software de código abierto licenciado bajo la Licencia MIT.
Esto significa que puedes:
- Usar el framework comercialmente
- Modificar el código fuente
- Distribuir el framework
- Incluirlo en proyectos privados
Con las únicas condiciones de:
- Incluir el aviso de copyright
- Incluir la licencia MIT
🔗 GitHub Repository
¿Encontraste un error en la documentación? Ayúdanos a mejorarla