kafkiansky/symfony-middleware

PSR-15 Middleware for symfony.

1.0.0 2023-12-13 11:32 UTC

This package is auto-updated.

Last update: 2024-11-13 13:33:46 UTC


README

test Codecov Software License Total Downloads Quality Score

Contents

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

  1. 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'))
    }
}
  1. 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');
    }
}
  1. 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.

  1. 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
    {
    }
}

Also, you can use nested groups:

symiddleware:
  global:
      - App\Controller\SetCorsHeaders
      - web
  groups:
    web:
      middlewares:
        - 'App\Middleware\ModifyRequestMiddleware'
        - debug
    debug:
      if: false
      middlewares:
        - 'App\Middleware\LogSqlQuery'

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.