Osumi Framework
es en eu v9.9.1 GitHub

Middlewares

Los Middlewares de Osumi Framework son clases reutilizables que participan en el ciclo de vida de una petición HTTP.

Pueden ejecutarse en tres fases:

Se usan habitualmente para autenticación, autorización, validación de tokens, carga de contexto, modificación de respuestas, cabeceras, códigos de estado, auditoría y logging.

1. Estructura de un Middleware

Los Middlewares de aplicación se almacenan normalmente en src/Middleware/.

<?php

declare(strict_types=1);

namespace Osumi\OsumiFramework\App\Middleware;

final class ExampleMiddleware {
    /**
     * Handle a middleware execution phase.
     *
     * @param string $phase Current middleware phase.
     * @param array<string, mixed> $data Current middleware pipeline data.
     *
     * @return array<string, mixed> Middleware result.
     */
    public static function handle(
        string $phase,
        array $data
    ): array {
        return [];
    }
}

Una misma clase puede registrarse en una o varias fases. $phase identifica la fase actual y $data contiene la información de la petición y el estado acumulado del pipeline.

2. Fases

Osumi Framework define:

OMiddleware::PHASE_BEFORE
OMiddleware::PHASE_AFTER_RENDER
OMiddleware::PHASE_AFTER_RESPONSE

Orden de ejecución:

Routing
↓
before
↓
Componente
↓
afterRender
↓
Layout
↓
afterResponse
↓
Respuesta HTTP

before

Se ejecuta antes de instanciar el componente. Es la fase adecuada para autenticación, autorización, validación, bloqueo de peticiones y carga de contexto.

Puede detener el pipeline antes de ejecutar el componente.

afterRender

Se ejecuta después de renderizar el template del componente y antes de aplicar el layout. Puede inspeccionar o sustituir el cuerpo del componente, añadir cabeceras o cambiar el código de estado.

Si detiene el pipeline, el layout no se renderiza.

afterResponse

Se ejecuta después de generar el cuerpo final. Es adecuada para auditoría, logging, cambios finales de cabeceras y transformaciones finales de la respuesta.

También se ejecuta cuando before o afterRender han detenido el pipeline, de modo que puede inspeccionar el estado de error.

Si un Middleware de afterResponse detiene la ejecución, se omiten los Middlewares restantes de esa fase y su respuesta de error se envía directamente.

3. Resultado de un Middleware

handle() siempre debe devolver un array. Un array vacío significa que no hay cambios:

return [];

Claves admitidas:

context

Publica datos para Middlewares posteriores, ORequest y DTOs:

return [
    'context' => [
        'id' => 42,
        'role' => 'admin'
    ]
];

El contexto se almacena con el nombre público del Middleware. LoginMiddleware se expone como Login.

body

Sustituye un cuerpo de respuesta:

return [
    'body' => 'Modified response'
];

En afterRender sustituye el cuerpo del componente. En afterResponse sustituye el cuerpo final.

headers

Añade o sustituye cabeceras HTTP:

return [
    'headers' => [
        'X-Request-Id' => 'abc123'
    ]
];

status_code

Cambia el estado HTTP:

return [
    'status_code' => 201
];

Los valores válidos están entre 100 y 599.

stop

Detiene la fase actual:

return [
    'stop' => true,
    'status_code' => 403,
    'message' => 'Forbidden'
];

Cuando stop es true, no se ejecutan los Middlewares posteriores de esa fase y la petición entra en estado de error de Middleware. Si se omiten, status_code usa 500 y message usa Middleware stopped execution.

4. Ejemplo de autenticación

<?php

declare(strict_types=1);

namespace Osumi\OsumiFramework\App\Middleware;

use Osumi\OsumiFramework\Core\OMiddleware;

final class LoginMiddleware {
    /**
     * Validate the request and publish authenticated user context.
     *
     * @param string $phase Current middleware phase.
     * @param array<string, mixed> $data Current middleware pipeline data.
     *
     * @return array<string, mixed> Middleware result.
     */
    public static function handle(
        string $phase,
        array $data
    ): array {
        if ($phase !== OMiddleware::PHASE_BEFORE) {
            return [];
        }

        $headers = $data['headers'];

        if (
            !is_array($headers) ||
            !array_key_exists('Authorization', $headers)
        ) {
            return [
                'stop' => true,
                'status_code' => 401,
                'message' => 'Unauthorized'
            ];
        }

        return [
            'context' => [
                'id' => 42,
                'role' => 'admin'
            ]
        ];
    }
}

5. Middlewares globales

Se configuran en src/Middleware/Middlewares.php:

<?php

declare(strict_types=1);

namespace Osumi\OsumiFramework\App\Middleware;

use Osumi\OsumiFramework\Core\OMiddleware;

OMiddleware::setGlobal([
    OMiddleware::PHASE_BEFORE => [
        RequestMiddleware::class
    ],
    OMiddleware::PHASE_AFTER_RENDER => [],
    OMiddleware::PHASE_AFTER_RESPONSE => [
        AuditMiddleware::class
    ]
]);

6. Middlewares de ruta

ORoute::get(
    '/profile',
    ProfileComponent::class,
    [
        OMiddleware::PHASE_BEFORE => [
            LoginMiddleware::class
        ],
        OMiddleware::PHASE_AFTER_RESPONSE => [
            AuditMiddleware::class
        ]
    ]
);

7. Middlewares de grupo

prefix(), layout() y group() aceptan definiciones de Middlewares.

ORoute::prefix(
    '/api',
    static function (): void {
        ORoute::get(
            '/profile',
            ProfileComponent::class
        );
    },
    [
        OMiddleware::PHASE_BEFORE => [
            ApiMiddleware::class
        ]
    ]
);

Los grupos anidados acumulan sus Middlewares. En cada fase el orden es:

global
↓
grupo exterior
↓
grupo interior
↓
ruta

8. Acceso al contexto desde ORequest

$login = $req->getMiddleware(
    'Login'
);

$id = $req->getMiddlewareValue(
    'Login',
    'id'
);

Un contexto inexistente devuelve un array vacío. Una propiedad inexistente devuelve null.

9. Contexto de Middleware desde DTOs

#[ODTOField(
    required: true,
    middleware: 'Login',
    middlewareProperty: 'id'
)]
public ?int $idUser = null;

middleware y middlewareProperty deben definirse juntos. El contexto de Middleware es un origen explícito y no usa como alternativa los datos enviados por el cliente.

10. Estado de error en afterResponse

Cuando before o afterRender detienen el pipeline, afterResponse continúa ejecutándose. Su $data contiene:

$data['is_error']
$data['error_phase']
$data['error_status_code']
$data['error_message']

11. Buenas prácticas

12. Flujo completo

Petición del cliente
↓
Routing
↓
Middlewares before globales
↓
Middlewares before de grupos
↓
Middlewares before de ruta
↓
Componente / DTO / ORequest
↓
Renderizado del componente
↓
Middlewares afterRender
↓
Renderizado del layout
↓
Middlewares afterResponse
↓
Respuesta HTTP

Un stop en before o afterRender omite el procesamiento normal restante pero continúa hasta afterResponse.

Un stop dentro de afterResponse termina esa fase final y envía directamente su respuesta de error.