Skip to main content

9. Extendiendo el framework

9.1. Creando middlewares personalizados

Los middlewares son componentes fundamentales en el framework que permiten interceptar y procesar solicitudes HTTP antes de que lleguen a los controladores, o transformar respuestas antes de enviarlas al cliente. Esta sección explica en detalle cómo crear e implementar middlewares personalizados.

9.1.1. Estructura de un middleware

Todos los middlewares en el framework deben implementar la interfaz App\Core\Interfaces\Middleware, que define un único método handle():

namespace App\Core\Interfaces;

use App\Core\RequestFactory as Request;

interface Middleware
{
public function handle(Request $request, callable $next);
}

El método handle() recibe dos parámetros:

  • $request: Una instancia de RequestFactory, que extiende de Symfony\Component\HttpFoundation\Request
  • $next: Un callable que representa el siguiente middleware en la cadena

Plantilla básica de un middleware

Aquí tienes una plantilla básica para crear un middleware personalizado:

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;

class MiMiddlewarePersonalizado implements Middleware
{
public function handle(Request $request, callable $next)
{
// Código que se ejecuta ANTES de pasar al siguiente middleware

// Llamada al siguiente middleware y captura de la respuesta
$response = $next($request);

// Código que se ejecuta DESPUÉS de que se haya generado la respuesta

// Devuelve la respuesta (posiblemente modificada)
return $response;
}
}

9.1.2. Tipos de middlewares

Dependiendo de su comportamiento, los middlewares pueden clasificarse en dos categorías principales:

Middlewares de preprocesamiento

Estos middlewares modifican la solicitud o realizan otras acciones antes de que la solicitud llegue al controlador. Por ejemplo:

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;

class DataSanitizationMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
// Sanitizar datos de entrada
foreach ($request->request->all() as $key => $value) {
if (is_string($value)) {
// Eliminar caracteres potencialmente peligrosos
$sanitized = strip_tags($value);
$request->request->set($key, $sanitized);
}
}

// Continuar con la cadena de middlewares
return $next($request);
}
}

Middlewares de postprocesamiento

Estos middlewares modifican la respuesta después de que ha sido generada por el controlador:

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;

class ResponseHeadersMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
// Obtener la respuesta del controlador
$response = $next($request);

// Agregar o modificar cabeceras
$response->headers->set('X-Application-Version', '1.0.0');
$response->headers->set('X-Response-Time', microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']);

return $response;
}
}

9.1.3. Acceso a datos en middlewares

Acceso a parámetros de la solicitud

Puedes acceder a los parámetros de la solicitud a través del objeto $request:

// Acceso a parámetros GET
$queryParam = $request->query->get('nombre_parametro');

// Acceso a parámetros POST o cuerpo de la solicitud
$bodyParam = $request->request->get('nombre_parametro');

// Acceso a cabeceras
$header = $request->headers->get('Header-Name');

// Acceso a cookies
$cookie = $request->cookies->get('nombre_cookie');

// Acceso a archivos
$file = $request->files->get('nombre_archivo');

Almacenamiento de datos para middlewares posteriores

Puedes almacenar datos para que sean accesibles por middlewares posteriores o controladores usando attributes:

// Guardar un dato
$request->attributes->set('mi_dato', 'valor');

// Recuperar un dato (en un middleware posterior o en el controlador)
$miDato = $request->attributes->get('mi_dato');

9.1.4. Ejemplos prácticos de middlewares

Middleware de autenticación por API Key

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;
use App\Exceptions\UnauthorizedException;

class ApiKeyMiddleware implements Middleware
{
protected $validApiKeys = [
'test-key-123',
'production-key-456'
];

public function handle(Request $request, callable $next)
{
$apiKey = $request->headers->get('X-API-Key');

if (!$apiKey || !in_array($apiKey, $this->validApiKeys)) {
throw new UnauthorizedException('API Key inválida o no proporcionada');
}

// Almacenar información de la API Key para uso posterior
$request->attributes->set('api_key', $apiKey);

return $next($request);
}
}

Middleware de registro de actividad

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;
use App\Helpers\Log;

class ActivityLoggerMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
// Registrar información antes de procesar
$startTime = microtime(true);
$method = $request->getMethod();
$path = $request->getPathInfo();

// Registrar el inicio de la solicitud
Log::info("Inicio de solicitud: $method $path", 'activity');

// Procesar la solicitud
$response = $next($request);

// Registrar información después de procesar
$duration = microtime(true) - $startTime;
$statusCode = $response->getStatusCode();

// Registrar el fin de la solicitud
Log::info("Fin de solicitud: $method $path - Estado: $statusCode - Duración: {$duration}s", 'activity');

return $response;
}
}

Middleware para multi-tenancy

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;
use App\Core\QuApp;
use App\Exceptions\BadRequestException;

class TenantMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
// Obtener el tenant ID de la cabecera
$tenantId = $request->headers->get('X-Tenant-ID');

if (!$tenantId) {
throw new BadRequestException('Se requiere ID de tenant');
}

// Verificar que el tenant existe (simplificado)
// En un caso real, verificarías en base de datos
if (!is_numeric($tenantId)) {
throw new BadRequestException('ID de tenant inválido');
}

// Guardar el tenant ID en el contenedor global para uso posterior
$container = QuApp::getContainer();
$container->set('tenant_id', function() use ($tenantId) {
return $tenantId;
});

// También guardar en atributos de la solicitud
$request->attributes->set('tenant_id', $tenantId);

return $next($request);
}
}

9.1.5. Registrando middlewares en la aplicación

Existen dos formas principales de registrar middlewares en el framework:

1. Middlewares globales

Los middlewares globales se aplican a todas las solicitudes y se registran en el archivo index.php:

// index.php
$kernel = new QuKernel(new QuRouter, $container);

// Agregar middlewares globales
$kernel->addMiddleware(new TransactionStartMiddleware);
$kernel->addMiddleware(new SwitchUtf8Latin1Middleware);
$kernel->addMiddleware(new TransactionEndMiddleware);

2. Middlewares específicos de ruta

Los middlewares específicos de ruta se aplican solo a rutas particulares y se definen en el archivo de rutas:

// src/ApiLayer/Domains/Products/ProductsRoutes.php
namespace ApiLayer\Domains\Products;

use App\Core\Router\Route;
use ApiLayer\Domains\Products\ProductsController;
use App\Middlewares\AuthMiddleware;
use App\Middlewares\MiMiddlewarePersonalizado;

$middlewares = [
'before' => [
AuthMiddleware::class,
MiMiddlewarePersonalizado::class
],
'after' => [
// Middlewares que se ejecutan después del controlador
]
];

Route::group(ProductsController::class, function () use ($middlewares) {
Route::get('products/', 'index', $middlewares);
Route::get('products/:id', 'show', $middlewares);
Route::post('products/', 'store', $middlewares);
Route::put('products/:id', 'update', $middlewares);
Route::delete('products/:id', 'delete', $middlewares);
});

9.1.6. Orden de ejecución de middlewares

Los middlewares se ejecutan en un orden específico:

  1. Primero se ejecutan los middlewares globales en el orden en que fueron registrados
  2. Luego se ejecutan los middlewares before específicos de la ruta
  3. Después se ejecuta el controlador
  4. Finalmente se ejecutan los middlewares after específicos de la ruta en orden inverso

Es importante considerar este orden al diseñar middlewares que dependen unos de otros.

9.1.7. Buenas prácticas para middlewares

  1. Responsabilidad única: Cada middleware debe tener un propósito claro y específico.

  2. Fail fast: Si el middleware detecta un problema, debe fallar lo antes posible para evitar procesamiento innecesario.

  3. No modificar la solicitud innecesariamente: Solo modifica la solicitud si es parte del propósito del middleware.

  4. Documentar dependencias: Si tu middleware depende de datos establecidos por otros middlewares, documéntalo claramente.

  5. Manejar excepciones apropiadamente: Decide si tu middleware debe manejar excepciones o dejarlas propagar.

  6. Usar contenedor para servicios: Si tu middleware necesita servicios, obtenerlos del contenedor en lugar de instanciarlos directamente.

// Ejemplo de middleware con inyección de servicios
class ComplexMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
// Obtener un servicio del contenedor
$container = QuApp::getContainer();
$service = $container->get(MiServicio::class);

// Usar el servicio
$result = $service->process($request->get('data'));
$request->attributes->set('processed_data', $result);

return $next($request);
}
}

9.1.8. Debugging middlewares

Puedes depurar middlewares agregando mensajes de log:

use App\Helpers\Log;

public function handle(Request $request, callable $next)
{
Log::info('Iniciando middleware ' . get_class($this), 'middleware');

// Lógica del middleware

Log::info('Finalizando middleware ' . get_class($this), 'middleware');

return $next($request);
}

También puedes examinar el contenido de la solicitud:

Log::info([
'method' => $request->getMethod(),
'path' => $request->getPathInfo(),
'query' => $request->query->all(),
'body' => $request->request->all(),
'headers' => $request->headers->all(),
], 'request');

9.1.9. Ejemplos del framework

El framework incluye varios middlewares que puedes usar como referencia:

TransactionStartMiddleware

Este middleware se ejecuta al principio de cada solicitud y configura la información de transacción:

// src/App/Middlewares/TransactionStartMiddleware.php
namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;
use function App\Helpers\generateUUID;

class TransactionStartMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
$transaction = [
'method' => $request->getMethod(),
'url' => $request->getUri(),
'timestamp' => new \DateTime(),
'host' => $request->getHost(),
'origin' => $request->headers->get('origin'),
'referer' => $request->headers->get('referer'),
'user_agent' => $request->headers->get('user-agent'),
'ip' => $request->getClientIp(),
'ips' => $request->getClientIps(),
'nombre_usuario' => null,
'cod_usuario_ext' => null,
'num_usuario' => null,
'actions' => [],
'payload' => [
'body' => $request->request->all(),
'query' => $request->query->all(),
],
'request_id' => generateUUID()
];

$request->attributes->set('transaction', $transaction);
$request->attributes->set('actions', []);

return $next($request);
}
}

AuthMiddleware

Este middleware maneja la autenticación verificando tokens JWT o sesiones:

// src/App/Middlewares/AuthMiddleware.php (simplificado)
namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;
use App\Core\CoreQuartup as Q;
use App\Exceptions\UnauthorizedException;
use function App\Helpers\decodeToken;

class AuthMiddleware implements Middleware
{
public function handle(Request $request, callable $next)
{
$authorizationHeader = $request->headers->get('Authorization');
$traitRefresh = $request->headers->get('TraitRefresh');

if ($authorizationHeader || $traitRefresh) {
// Manejo de autenticación API con token
$token = trim(str_replace('Bearer', '', $authorizationHeader));
$decode = decodeToken($token);
$sessionId = $decode["data"]["session_id"];

// Verificar sesión
if (!$sessionId) {
throw new UnauthorizedException();
}

session_id($sessionId);
Q::getQuBasic($sessionId);

if (!Q::isValidUserSession()) {
throw new UnauthorizedException();
}

// Establecer datos de usuario en la solicitud
$request->attributes->set("num_usuario", $decode["data"]["num_usuario"]);
// ... más atributos
} else {
// Manejo de autenticación por cookie de sesión
// ... código omitido
}

return $next($request);
}
}

9.1.10. Creación avanzada: Middleware con parámetros

A veces necesitarás crear middlewares que acepten parámetros de configuración. Aquí tienes un ejemplo:

<?php

namespace App\Middlewares;

use App\Core\Interfaces\Middleware;
use App\Core\RequestFactory as Request;
use App\Exceptions\BadRequestException;

class RequiredParametersMiddleware implements Middleware
{
protected $requiredParameters;

public function __construct(array $requiredParameters)
{
$this->requiredParameters = $requiredParameters;
}

public function handle(Request $request, callable $next)
{
foreach ($this->requiredParameters as $param) {
if (!$request->request->has($param) && !$request->query->has($param)) {
throw new BadRequestException("Parámetro requerido no encontrado: $param");
}
}

return $next($request);
}
}

Para usar este middleware, primero necesitas instanciarlo y luego registrarlo:

// Registrarlo como middleware global (en index.php)
$kernel->addMiddleware(new RequiredParametersMiddleware(['api_version', 'client_id']));

// O para rutas específicas
$middlewareWithParams = new RequiredParametersMiddleware(['product_id', 'store_id']);

$middlewares = [
'before' => [
AuthMiddleware::class,
$middlewareWithParams // Instancia del middleware con parámetros
]
];

Route::group(ProductsController::class, function () use ($middlewares) {
Route::get('products/featured', 'featured', $middlewares);
});

Con estos conocimientos y ejemplos, ahora deberías poder crear middlewares personalizados adaptados a las necesidades específicas de tu aplicación, mejorando la modularidad, seguridad y mantenibilidad de tu código.

9.2. Servicios transversales a nivel de aplicación

9.2.1. Qué son los servicios transversales

Es importante distinguir entre dos conceptos que comparten la palabra "servicio" ("service") pero tienen funciones distintas en el framework:

  1. Servicios de dominio (DomainService): Son clases que implementan la lógica de negocio específica para un dominio particular dentro de la arquitectura controlador-servicio. Por ejemplo, ProductsService maneja la lógica relacionada con productos.

  2. Servicios transversales: Son componentes que proporcionan funcionalidades generales utilizadas por múltiples partes de la aplicación, independientes de la lógica de dominio específica. Por ejemplo, servicios de correo electrónico, almacenamiento, logging, etc.

En esta sección nos centramos en los servicios transversales, que tienen las siguientes características:

  • Infraestructurales: Suelen gestionar recursos externos como bases de datos, sistemas de archivos, APIs de terceros o servicios del sistema.
  • Agnósticos de dominio: No están vinculados a ningún dominio específico de la aplicación.
  • Reutilizables: Se utilizan por varios controladores, servicios de dominio y/o middlewares.
  • Intercambiables: A menudo implementan interfaces que permiten cambiar su implementación sin afectar al resto del sistema.

9.2.2. Cuándo crear un servicio transversal

Los servicios transversales son apropiados cuando:

  1. La funcionalidad trasciende los límites de dominio: Es utilizada por múltiples dominios de negocio.

  2. Se integra con sistemas externos: Email, SMS, almacenamiento en la nube, pasarelas de pago, etc.

  3. Necesitas encapsular la complejidad técnica: Aislar detalles de implementación complejos del resto del código.

  4. Buscas una estrategia de cacheo o manejo de recursos: Para centralizar la gestión de recursos críticos.

  5. Requieres abstracción para facilitar testing: Permitir mocks o stubs durante las pruebas.

  6. Hay lógica común con múltiples implementaciones: Cuando diferentes entornos o configuraciones requieren comportamientos distintos.

9.2.3. Creando servicios transversales

Para crear un servicio transversal efectivo:

1. Define una interfaz clara

Primero, crea una interfaz que defina el contrato del servicio:

<?php
// src/Services/Interfaces/MailServiceInterface.php
namespace Services\Interfaces;

interface MailServiceInterface
{
/**
* Envía un correo electrónico
*
* @param array $email Datos del correo (destinatario, asunto, cuerpo, etc.)
* @return bool Éxito del envío
*/
public function send(array $email);
}

2. Implementa la interfaz

A continuación, crea una o más implementaciones concretas:

<?php
// src/Services/MailLog.php
namespace Services;

use Services\Interfaces\MailServiceInterface;
use App\Helpers\Log;

class MailLog implements MailServiceInterface
{
public function send(array $email)
{
// Lógica de envío (en este caso, solo registra el intento)
Log::info($email, 'email');
return true;
}
}
<?php
// src/Services/SmtpMailService.php
namespace Services;

use Services\Interfaces\MailServiceInterface;
use App\Helpers\Log;

class SmtpMailService implements MailServiceInterface
{
protected $smtpConfig;

public function __construct(array $smtpConfig)
{
$this->smtpConfig = $smtpConfig;
}

public function send(array $email)
{
// Configuración SMTP usando los parámetros proporcionados
$transport = new \Swift_SmtpTransport(
$this->smtpConfig['host'],
$this->smtpConfig['port'],
$this->smtpConfig['encryption']
);

$transport->setUsername($this->smtpConfig['username']);
$transport->setPassword($this->smtpConfig['password']);

// Crear el mailer y enviar
$mailer = new \Swift_Mailer($transport);
$message = new \Swift_Message($email['subject']);
$message->setFrom([$this->smtpConfig['from_email'] => $this->smtpConfig['from_name']]);
$message->setTo($email['to']);
$message->setBody($email['body'], 'text/html');

$result = $mailer->send($message);

// Registro de la acción
if ($result) {
Log::info("Email enviado a: {$email['to']}", 'email');
} else {
Log::error("Error al enviar email a: {$email['to']}", 'email');
}

return $result > 0;
}
}

3. Servicios para almacenamiento

Otro ejemplo común de servicio transversal es para gestionar el almacenamiento de archivos:

<?php
// src/Services/Interfaces/StorageServiceInterface.php
namespace Services\Interfaces;

interface StorageServiceInterface
{
/**
* Almacena un archivo
*
* @param string $path Ruta de destino
* @param string|resource $content Contenido a almacenar
* @param array $options Opciones adicionales (permisos, metadata, etc)
* @return bool Éxito de la operación
*/
public function put(string $path, $content, array $options = []): bool;

/**
* Recupera el contenido de un archivo
*
* @param string $path Ruta del archivo
* @return string|null Contenido del archivo o null si no existe
*/
public function get(string $path): ?string;

/**
* Verifica si un archivo existe
*
* @param string $path Ruta del archivo
* @return bool
*/
public function exists(string $path): bool;

/**
* Elimina un archivo
*
* @param string $path Ruta del archivo
* @return bool Éxito de la operación
*/
public function delete(string $path): bool;
}

Con implementaciones para sistemas de archivos locales o en la nube:

<?php
// src/Services/LocalStorageService.php
namespace Services;

use Services\Interfaces\StorageServiceInterface;

class LocalStorageService implements StorageServiceInterface
{
protected $basePath;

public function __construct(string $basePath)
{
$this->basePath = rtrim($basePath, '/') . '/';
}

public function put(string $path, $content, array $options = []): bool
{
$fullPath = $this->getFullPath($path);
$directory = dirname($fullPath);

// Asegurar que el directorio existe
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}

return file_put_contents($fullPath, $content) !== false;
}

public function get(string $path): ?string
{
$fullPath = $this->getFullPath($path);

if (!file_exists($fullPath)) {
return null;
}

return file_get_contents($fullPath);
}

public function exists(string $path): bool
{
return file_exists($this->getFullPath($path));
}

public function delete(string $path): bool
{
$fullPath = $this->getFullPath($path);

if (!file_exists($fullPath)) {
return false;
}

return unlink($fullPath);
}

protected function getFullPath(string $path): string
{
return $this->basePath . ltrim($path, '/');
}
}

9.2.4. Registrando servicios en el contenedor

Los servicios transversales se registran en el contenedor de dependencias para hacerlos disponibles en toda la aplicación:

1. Registro en el archivo services.php

El archivo principal para registrar servicios transversales es src/Services/services.php:

<?php
// src/Services/services.php
return [
// Mapeo de interfaces a implementaciones
Services\Interfaces\MailServiceInterface::class => Services\MailLog::class,
Services\Interfaces\StorageServiceInterface::class => function($container) {
// Implementación condicional basada en configuración
$environment = getenv('APP_ENV') ?: 'dev';

if ($environment === 'production') {
// Usar almacenamiento real en producción
return new Services\S3StorageService([
'key' => getenv('AWS_KEY'),
'secret' => getenv('AWS_SECRET'),
'region' => getenv('AWS_REGION'),
'bucket' => getenv('AWS_BUCKET')
]);
} else {
// Usar almacenamiento local en desarrollo
return new Services\LocalStorageService(STORAGE_DIR);
}
},
Services\Interfaces\StoreTransactionServiceInterface::class => Services\StoreTransactionLog::class
];

2. Uso de servicios en el código

Una vez registrados, estos servicios pueden inyectarse en controladores, servicios de dominio u otros componentes:

<?php
// src/ApiLayer/Domains/Products/ProductsService.php
namespace ApiLayer\Domains\Products;

use App\Base\DomainService;
use Services\Interfaces\StorageServiceInterface;
use Infrastructures\Legacy\PMaes\MaesartiProductos\MaesartiProductosRepository;

class ProductsService extends DomainService
{
protected $storageService;
protected $maesartiRepository;

public function __construct(
MaesartiProductosRepository $maesartiRepository,
StorageServiceInterface $storageService
) {
$this->maesartiRepository = $maesartiRepository;
$this->storageService = $storageService;
}

public function storeProductImage($productId, $imageData)
{
// Validar que el producto existe
$product = $this->maesartiRepository->selectById($productId);
if (!$product) {
throw new \App\Exceptions\NotFoundException('Producto no encontrado');
}

// Generar nombre de archivo
$filename = "product_{$productId}_" . uniqid() . ".jpg";
$path = "products/{$productId}/images/{$filename}";

// Almacenar la imagen usando el servicio
$success = $this->storageService->put($path, $imageData);

if (!$success) {
throw new \Exception('No se pudo guardar la imagen');
}

return [
'success' => true,
'path' => $path,
'url' => "/storage/{$path}" // URL relativa para acceso web
];
}
}

9.2.5. Beneficios de usar servicios transversales

Los servicios transversales ofrecen múltiples ventajas:

  1. Separación de responsabilidades: Los servicios de dominio se centran en la lógica de negocio mientras que los servicios transversales se encargan de las preocupaciones técnicas.

  2. Testabilidad mejorada: Los servicios se pueden reemplazar fácilmente por mocks durante las pruebas.

  3. Mantenibilidad: Centralizar la lógica de infraestructura en servicios dedicados facilita los cambios y mejoras.

  4. Coherencia: La interfaz uniforme garantiza que todos los componentes interactúan con sistemas externos de manera consistente.

  5. Configurabilidad: Permite cambiar el comportamiento de la aplicación sin modificar el código de negocio.

9.2.6. Mejores prácticas para servicios transversales

Para aprovechar al máximo los servicios transversales:

  1. Diseñar por interfaces: Siempre define una interfaz clara antes de implementar un servicio.

  2. Mantener las interfaces minimalistas: Define solo los métodos necesarios en la interfaz.

  3. Evitar dependencias de frameworks: Los servicios deben ser lo más independientes posible de frameworks específicos.

  4. Priorizar la inmutabilidad: Los servicios no deberían cambiar su estado interno después de la inicialización.

  5. Documentar claramente: Proporciona una documentación clara sobre cómo usar cada servicio.

  6. Centralizar la configuración: Mantén la configuración de servicios en un lugar centralizado.

  7. Implementar logging consistente: Asegúrate de que todos los servicios registren sus operaciones de manera coherente.

Con esta comprensión clara de los servicios transversales, ahora puedes integrarlos eficazmente en tu aplicación sin confundirlos con los servicios de dominio, mejorando así la modularidad y mantenibilidad de tu código.

9.3. Extendiendo el sistema de validación

9.3.1. Creando reglas de validación personalizadas

El framework permite extender el sistema de validación creando reglas personalizadas para cubrir necesidades específicas. Estas reglas se integran perfectamente con la infraestructura de validación existente.

Estructura de una regla de validación

Todas las reglas de validación deben implementar la interfaz App\Validations\Contracts\RuleInterface, que define un método validate():

<?php

namespace App\Validations\Contracts;

interface RuleInterface
{
public function validate(string $field, $value, array $params): bool;
}

El método validate() recibe tres parámetros:

  • $field: El nombre del campo que se está validando
  • $value: El valor a validar
  • $params: Parámetros adicionales para la regla (p.ej., mínimo/máximo para un rango)

Pasos para crear una nueva regla

  1. Crear una nueva clase de regla en el directorio App\Validations\Rules

  2. Implementar la interfaz RuleInterface con la lógica de validación necesaria

  3. Asegurarse de que el nombre siga la convención: La clase debe nombrarse como [NombreRegla]Rule.php

Ejemplo: Crear una regla para validar códigos postales españoles

Vamos a crear una regla personalizada que valide códigos postales españoles (5 dígitos que deben empezar con un número entre 01-52):

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;

class SpanishPostalCodeRule implements RuleInterface
{
/**
* Valida que el valor sea un código postal español válido
*
* @param string $field Nombre del campo
* @param mixed $value Valor a validar
* @param array $params Parámetros adicionales (no utilizados en este caso)
* @return bool
*/
public function validate(string $field, $value, array $params): bool
{
// Verificar si es string y tiene 5 caracteres
if (!is_string($value) || strlen($value) !== 5) {
return false;
}

// Verificar si contiene solo dígitos
if (!ctype_digit($value)) {
return false;
}

// Extraer los primeros dos dígitos para la provincia
$provinceCode = (int) substr($value, 0, 2);

// Verificar si la provincia está en el rango válido (01-52)
return $provinceCode >= 1 && $provinceCode <= 52;
}
}

Ejemplo: Crear una regla para validar fechas específicas

Vamos a crear una regla que valide que una fecha caiga en un día laborable (de lunes a viernes):

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;

class BusinessDayRule implements RuleInterface
{
/**
* Valida que el valor sea una fecha en día laborable (L-V)
*
* @param string $field Nombre del campo
* @param mixed $value Valor a validar (fecha en formato Y-m-d)
* @param array $params Parámetros adicionales (no utilizados en este caso)
* @return bool
*/
public function validate(string $field, $value, array $params): bool
{
// Verificar si el valor es una fecha válida
if (!is_string($value) || !strtotime($value)) {
return false;
}

// Crear objeto DateTime
$date = new \DateTime($value);

// Obtener el día de la semana (1=lunes, 7=domingo)
$dayOfWeek = (int) $date->format('N');

// Verificar si es día laborable (1-5 = lunes a viernes)
return $dayOfWeek >= 1 && $dayOfWeek <= 5;
}
}

Reglas con parámetros

Las reglas también pueden aceptar parámetros para hacerlas más flexibles. Estos parámetros se pasan después del nombre de la regla, separados por dos puntos:

'field_name' => 'custom_rule:param1,param2'

Veamos un ejemplo de una regla que valida un valor dentro de un conjunto específico:

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;

class AllowedValuesRule implements RuleInterface
{
/**
* Valida que el valor esté dentro de un conjunto específico
*
* @param string $field Nombre del campo
* @param mixed $value Valor a validar
* @param array $params Lista de valores permitidos
* @return bool
*/
public function validate(string $field, $value, array $params): bool
{
if (empty($params)) {
// Si no hay valores permitidos especificados, la validación falla
return false;
}

return in_array($value, $params, true);
}
}

Uso en la validación:

$this->validations = [
'body' => [
'status' => 'required|allowed_values:active,pending,cancelled',
'type' => 'allowed_values:simple,complex'
]
];

9.3.2. Implementando validadores con Factory

El framework utiliza el patrón Factory para crear instancias de las reglas de validación. Esto se implementa a través de la clase App\Validations\Rules\RuleFactory.

Cómo funciona el patrón Factory

El patrón Factory permite crear objetos sin especificar la clase exacta del objeto que se creará. En el caso del sistema de validación:

  1. La clase ValidaRequest recibe reglas como cadenas ('required', 'email', etc.)
  2. El RuleFactory convierte estas cadenas en instancias de clases de reglas
  3. Estas instancias se utilizan para validar los datos de entrada

La clase RuleFactory

La clase RuleFactory es la encargada de instanciar las reglas de validación:

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;
use Exception;

class RuleFactory
{
public static function make(string $rule): RuleInterface
{
$ruleClass = __NAMESPACE__ . '\\' . ucfirst($rule) . 'Rule';

if (!class_exists($ruleClass)) {
throw new Exception("Regla de validación no encontrada: $rule");
}

return new $ruleClass();
}
}

Proceso de validación

Cuando se valida un campo, ocurre el siguiente proceso:

  1. Se divide la cadena de regla para separar el nombre de la regla y sus parámetros
  2. RuleFactory::make() crea una instancia de la clase de regla correspondiente
  3. Se invoca el método validate() de la regla con el campo, valor y parámetros
  4. Si validate() devuelve false, se registra un error para ese campo

Extendiendo el RuleFactory

En algunos casos, puede ser necesario extender el RuleFactory para manejar reglas más complejas o reglas que requieren dependencias adicionales.

Aquí hay un ejemplo de cómo podríamos extender RuleFactory para soportar inyección de dependencias:

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;
use App\Core\QuApp;
use Exception;

class ExtendedRuleFactory extends RuleFactory
{
/**
* Crea una nueva instancia de regla, con soporte para inyección de dependencias
*
* @param string $rule Nombre de la regla
* @return RuleInterface
* @throws Exception
*/
public static function make(string $rule): RuleInterface
{
// Reglas que requieren dependencias
$specialRules = [
'database_exists' => DatabaseExistsRule::class,
'unique' => UniqueRule::class
];

// Si es una regla especial que requiere dependencias
if (isset($specialRules[$rule])) {
$ruleClass = $specialRules[$rule];

// Usar el contenedor para resolver las dependencias
$container = QuApp::getContainer();
return $container->build($ruleClass);
}

// Para las reglas comunes, usar el factory estándar
return parent::make($rule);
}
}

Implementando una regla con dependencias

Veamos un ejemplo de una regla que requiere dependencias para funcionar:

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;
use App\Core\CoreQuartup as Q;

class UniqueRule implements RuleInterface
{
/**
* Valida que el valor sea único en una tabla específica
*
* @param string $field Nombre del campo
* @param mixed $value Valor a validar
* @param array $params [0] = tabla, [1] = columna (opcional si es igual a $field)
* @return bool
*/
public function validate(string $field, $value, array $params): bool
{
if (empty($params) || !isset($params[0])) {
throw new \InvalidArgumentException("La regla 'unique' requiere al menos un parámetro (tabla)");
}

$table = $params[0];
$column = $params[1] ?? $field;

// Prevenir inyección SQL
$table = addslashes($table);
$column = addslashes($column);
$safeValue = addslashes($value);

// Consulta para verificar si el valor ya existe
$sql = "SELECT COUNT(*) as count FROM {$table} WHERE {$column} = '{$safeValue}'";
$result = Q::select($sql);

// Si el conteo es 0, el valor es único
return (int)$result[0]['count'] === 0;
}
}

Uso de esta regla:

$this->validations = [
'body' => [
'email' => 'required|email|unique:users,email',
'username' => 'required|unique:users,username'
]
];

Reglas con callbacks para validación avanzada

Para casos especiales donde la validación requiere lógica específica, podemos implementar una regla que acepte un callback como parámetro:

<?php

namespace App\Validations\Rules;

use App\Validations\Contracts\RuleInterface;

class CallbackRule implements RuleInterface
{
protected $callbacks = [];

/**
* Constructor que registra callbacks predefinidos
*/
public function __construct()
{
// Registrar callbacks comunes
$this->callbacks = [
'is_even' => function($value) {
return is_numeric($value) && $value % 2 === 0;
},
'is_future_date' => function($value) {
$date = strtotime($value);
return $date && $date > time();
},
'has_uppercase' => function($value) {
return is_string($value) && preg_match('/[A-Z]/', $value);
}
];
}

/**
* Valida usando el callback especificado
*
* @param string $field Nombre del campo
* @param mixed $value Valor a validar
* @param array $params [0] = nombre del callback
* @return bool
*/
public function validate(string $field, $value, array $params): bool
{
if (empty($params) || !isset($params[0])) {
throw new \InvalidArgumentException("La regla 'callback' requiere un nombre de callback");
}

$callbackName = $params[0];

if (!isset($this->callbacks[$callbackName])) {
throw new \InvalidArgumentException("Callback '{$callbackName}' no encontrado");
}

return call_user_func($this->callbacks[$callbackName], $value);
}

/**
* Registra un nuevo callback
*
* @param string $name Nombre del callback
* @param callable $callback Función de validación
*/
public function registerCallback(string $name, callable $callback)
{
$this->callbacks[$name] = $callback;
}
}

Esta implementación permitiría usar la regla así:

$this->validations = [
'body' => [
'number' => 'required|callback:is_even',
'expiration_date' => 'required|date|callback:is_future_date',
'password' => 'required|min:8|callback:has_uppercase'
]
];

Integración con el sistema existente

Una vez que has creado tus reglas personalizadas, se integran automáticamente con el sistema de validación existente. No se requiere ninguna configuración adicional, ya que el RuleFactory las buscará por nombre.

Ejemplo completo de uso

Veamos un ejemplo completo de cómo se usarían las reglas personalizadas en un controlador:

<?php

namespace ApiLayer\Domains\Shipping;

use App\Base\DomainController;
use App\Core\RequestFactory as Request;
use ApiLayer\Domains\Shipping\ShippingService;

class ShippingController extends DomainController
{
protected $shippingService;

public function __construct(ShippingService $service)
{
parent::__construct($service);

// Uso de reglas estándar y personalizadas
$this->validations = [
'headers' => [
'x-tienda' => 'required|numeric'
],
'body' => [
'recipient_name' => 'required|string',
'postal_code' => 'required|spanish_postal_code', // Regla personalizada
'shipping_date' => 'required|date|business_day', // Regla personalizada
'shipping_method' => 'required|allowed_values:standard,express,priority', // Regla con parámetros
'tracking_number' => 'string|unique:shipments,tracking_number' // Regla con dependencias
]
];
}

// Método personalizado con validación ad-hoc
public function reschedule($id, Request $request)
{
// Validación para este método específico
ValidaRequest::make($request)
->validateHeaders(['x-tienda' => 'required|numeric'])
->validateBody([
'new_date' => 'required|date|business_day',
'reason' => 'required|string'
])
->handle();

return response()->json($this->shippingService->reschedule($id, $request->all()), 200);
}
}

9.3.4. Buenas prácticas para reglas de validación

  1. Sigue el principio de responsabilidad única: Cada regla debe hacer una sola cosa y hacerla bien.

  2. Haz que las reglas sean reutilizables: Diseña tus reglas para que puedan utilizarse en diferentes contextos.

  3. Documenta tus reglas: Incluye comentarios PHPDoc detallados que expliquen qué hace la regla y qué parámetros acepta.

  4. Maneja los valores nulos o indefinidos: Asegúrate de que tu regla maneje correctamente los valores nulos o indefinidos.

  5. Considera la internacionalización: Si es relevante, diseña tus reglas para que funcionen con diferentes formatos según el idioma o región.

  6. Prueba exhaustivamente: Escribe pruebas unitarias para tus reglas, incluyendo casos límite y entradas inesperadas.

  7. Cuida el rendimiento: Asegúrate de que tus reglas sean eficientes, especialmente si realizan operaciones pesadas como consultas a bases de datos.

Con estas herramientas y ejemplos, ahora puedes extender el sistema de validación del framework según tus necesidades específicas, creando reglas personalizadas que se integren perfectamente con el sistema existente.