PSR-15 middleware now in Symfony
Contents
- Installation
- Configuration
- Usage
- Examples
- Customization
- Caching
- Real World Example
- Middlewares
- Testing
- License
Installation
composer require kafkiansky/symfony-middleware
Configuration
Ensure you have added bundle in config/bundles.php
:
Kafkiansky\SymfonyMiddleware\SymiddlewareBundle::class => ['all' => true],
Create the configuration file in packages/symiddleware.yaml
:
symiddleware:
global:
##
Usage
Each middleware must implement the Psr\Http\Server\MiddlewareInterface
interface. Thanks for symfony autoconfiguration now the middleware registry knows your middleware.
So that middlewares can start execution, they must be defined on controller class and/or on controller method.
use Kafkiansky\SymfonyMiddleware\Attribute\Middleware;
#[Middleware([ValidatesQueryParams::class])]
final class SomeController
{
#[Middleware([ConvertStringsToNull::class])]
public function index(): void
{
}
}
If controller is invokable, middleware can be defined just on controller class:
use Kafkiansky\SymfonyMiddleware\Attribute\Middleware;
#[Middleware([ValidatesQueryParams::class, ConvertStringsToNull::class])]
final class SomeController
{
public function __invoke(): void
{
}
}
groups
If you want to use the list of middlewares, you can define middleware group inside symfony_middleware.yaml
configuration file:
symiddleware:
groups:
debug:
if: '%env(RUN_DEBUG_MIDDLEWARE)%'
middlewares:
- 'App\Middleware\TrackRequestTime'
- 'App\Middleware\EnableSqlLogger'
Now define this middleware on controller class or method:
use Kafkiansky\SymfonyMiddleware\Attribute\Middleware;
#[Middleware(['debug'])]
final class SomeController
{
public function __invoke(): void
{
}
}
Pay attention to the if
parameter in configuration file. This parameter tells the middleware runner when the middleware group can be run. If false, this middleware will not be executed.
global
If you want to run the list of middleware every request, you need the global
middleware section. This keyword is reserved and if
parameter is not supported.
symiddleware:
global:
- App\Controller\SetCorsHeaders
groups:
web:
middlewares:
- 'App\Middleware\ModifyRequestMiddleware'
Now the App\Controller\SetCorsHeaders
middleware will execute on every request.
Examples
- Simple middleware that modifies request:
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
final class ModifyRequestMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $handler->handle($request->withAttribute(__CLASS__, 'handled'))
}
}
- Middleware that modifies response:
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
final class ModifyResponseMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request)
return $response->withHeader('x-developer', 'kafkiansky');
}
}
- Middleware that stop execution:
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Nyholm\Psr7\Response;
final class StopExecution implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = new Response(200, [], json_encode(['success' => false]));
return $response;
}
}
In this example controller will not be executed.
- Stop execution with symfony response:
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Nyholm\Psr7\Response;
use Kafkiansky\SymfonyMiddleware\Psr\PsrResponseTransformer;
use Symfony\Component\HttpFoundation\JsonResponse;
final class StopExecution implements MiddlewareInterface
{
private PsrResponseTransformer $psrResponseTransformer;
public function __construct(PsrResponseTransformer $psrResponseTransformer)
{
$this->psrResponseTransformer = $psrResponseTransformer;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $this->psrResponseTransformer->toPsrResponse(new JsonResponse(['success' => false]));
}
}
You can compose middleware group with single middleware, use list of Middleware
attributes and so on. All the following examples will work:
use Kafkiansky\SymfonyMiddleware\Attribute\Middleware;
#[Middleware(['debug', 'api', SomeMiddleware::class])]
#[Middleware([SomeAnotherMiddleware::class])]
final class SomeController
{
public function __invoke(): void
{
}
}
use Kafkiansky\SymfonyMiddleware\Attribute\Middleware;
#[Middleware(['debug', 'api', SomeMiddleware::class])]
final class SomeController
{
#[Middleware([SomeAnotherMiddleware::class, 'web'])]
#[Middleware(['tracking'])]
public function index(): void
{
}
}
Duplicated middlewares will be removed.
Customization
PSR middlewares and Symfony has different incompatible Request objects. If your middleware going to change the request object, only attributes
, query params
, headers
and parsed body
will be copied from psr request to symfony request. If you wish to change this behaviour, you may change the Kafkiansky\SymfonyMiddleware\Psr\PsrRequestCloner
interface binding it to your realization.
Caching
Package use caching on production environment to prevent reflection usage. First of all, package will search of the app.cache_middleware
parameter. If package doesn't find it, it's going to use the kernel.environment
definition and will cache attributes when it set to prod
.
Package will cache all controllers even if it doesn't found the attributes for it. This approach will allow to remember all the controllers and not use reflection further.
Real World Example
Imagine that you have some endpoints which requires authorization access via basic. Write middleware:
# services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$basicUser: 'root'
$basicPassword: 'secret'
// Authorization Middleware
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Nyholm\Psr7\Response;
final class AuthorizeRequests implements MiddlewareInterface
{
private string $basicUser;
private string $basicPassword;
public function __construct(string $basicUser, string $basicPassword)
{
$this->basicUser = $basicUser;
$this->basicPassword = $basicPassword;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$user = $request->getServerParams()['PHP_AUTH_USER'] ?? null;
$passwd = $request->getServerParams()['PHP_AUTH_PW'] ?? null;
if ($user === $this->basicUser && $passwd === $this->basicPassword) {
return $handler->handle($request);
}
return new Response(401, [
'WWW-Authenticate' => 'Basic realm="Backend"'
]);
}
}
# example configuration
symiddleware:
groups:
basic:
middlewares:
- App\Middleware\AuthorizeRequests
// Some controller
use Symfony\Component\HttpFoundation\JsonResponse;
use Kafkiansky\SymfonyMiddleware\Attribute\Middleware;
final class SomeController
{
#[Middleware(['basic'])] // via middleware group
public function writeArticle(): JsonResponse
{
}
#[Middleware([App\Middleware\AuthorizeRequests::class])] // via concrete class
public function deleteArticle(): JsonResponse
{
}
}
Middlewares
Handle HTTP Basic Auth PSR-15 middleware for Symfony
Testing
$ composer test
License
The MIT License (MIT). See License File for more information.