/psr7-middlewares

Collection of PSR-7 middlewares

Primary LanguagePHPMIT LicenseMIT

psr7-middlewares

Build Status Scrutinizer Code Quality

Collection of PSR-7 middlewares.

It is installable and autoloadable via Composer as oscarotero/psr7-middlewares.

Requirements

  • PHP >= 5.5
  • A PSR-7 HTTP Message implementation, for example zend-diactoros
  • A PSR-7 middleware dispatcher. For example Relay or any other compatible with the following signature:
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

function (RequestInterface $request, ResponseInterface $response, callable $next) {
    // ...
}

Usage example:

use Psr7Middlewares\Middleware;

use Relay\RelayBuilder;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Stream;

//Set a stream factory used by some middlewares
//(Required only if Zend\Diactoros\Stream is not detected)
Middleware::setStreamFactory(function ($file, $mode) {
    return new Stream($file, $mode);
});

//Create a relay dispatcher and add some middlewares:
$relay = new RelayBuilder();

$dispatcher = $relay->newInstance([

    //Calculate the response time
    Middleware::responseTime(),

    //Add an Uuid to request
    Middleware::uuid(),
    
    //Handle errors
    Middleware::errorHandler('error_handler_function')->catchExceptions(true),

    //Override the method using X-Http-Method-Override header
    Middleware::methodOverride(),

    //Block search engines robots indexing
    Middleware::robots(),

    //Parse the request payload
    Middleware::payload(),

    //Remove the path prefix
    Middleware::basePath('/my-site/web'),

    //Remove the trailing slash
    Middleware::trailingSlash(),

    //Digest authentication
    Middleware::digestAuthentication(['username' => 'password']),

    //Get the client ip
    Middleware::clientIp(),

    //Allow only some ips
    Middleware::firewall(['127.0.0.*']),

    //Detects the user preferred language
    Middleware::languageNegotiator(['gl', 'es', 'en']),

    //Detects the format
    Middleware::formatNegotiator(),

    //Adds the php debug bar
    Middleware::debugBar($app->get('debugbar')),

    //Execute fast route
    Middleware::fastRoute($app->get('dispatcher')),

    //Minify the result
    Middleware::minify()

    //Saves the response in a file
    Middleware::saveResponse('app/public')
]);

$response = $dispatcher(ServerRequestFactory::fromGlobals(), new Response());

Available middlewares

AuraRouter

To use Aura.Router as a middleware. You need the 3.x version, compatible with psr-7:

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\AuraRouter;
use Aura\Router\RouterContainer;

//Create the router
$routerContainer = new RouterContainer();

$map = $routerContainer->getMap();

$map->get('hello', '/hello/{name}', function ($request, $response, $myApp) {

    //The route parameters are stored as attributes
    $name = $request->getAttribute('name');

    //You can get also the route instance
    $route = AuraRouter::getRoute($request);

    //Write directly in the response's body
    $response->getBody()->write('Hello '.$name);

    //or echo the output (it will be captured and writted into body)
    echo 'Hello world';

    //or return a string
    return 'Hello world';

    //or return a new response
    return $response->withStatus(200);
});

//Add to the dispatcher
$dispatcher = $relay->getInstance([

    Middleware::AuraRouter()
        ->router($routerContainer)   //Instance of Aura\Router\RouterContainer
        ->arguments($myApp)          //(optional) append more arguments to the controller
]);

AuraSession

Creates a new Aura.Session instance with the request.

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\AuraSession;

$dispatcher = $relay->getInstance([

    Middleware::AuraSession(),
        ->factory($sessionFactory) //(optional) Intance of Aura\Session\SessionFactory
        ->name('my-session-name'), //(optional) custom session name

    function ($request, $reponse, $next) {
        //Get the session instance
        $session = AuraSession::getSession($request);
    }
]);

BasePath

Strip off the prefix from the uri path of the request. This is useful to combine with routers if the root of the website is in a subdirectory. For example, if the root of your website is /web/public, a request with the uri /web/public/post/34 will be converted to /post/34.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([
    Middleware::BasePath('/web/public'),
]);

BasicAuthentication

Implements the basic http authentication. You have to provide an array with all users and password:

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::BasicAuthentication()
        ->users([
            'username1' => 'password1',
            'username2' => 'password2'
        ])
        ->realm('My realm') //(optional) change the realm value
]);

Cache

To save and reuse responses based in the Cache-Control: max-age directive and Expires header. You need a cache library compatible with psr-6

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::Cache()
        ->cache(new Psr6CachePool()) //the psr-6 cache implementation

    function($request, $response, $next) {
        //Cache the response 1 hour
        return $response->withHeader('Cache-Control', 'max-age=3600');
    }
]);

ClientIp

Detects the client ip(s).

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\ClientIp;

$dispatcher = $relay->getInstance([

    Middleware::ClientIp()
        ->headers([
            'Client-Ip',
            'X-Forwarded-For',
            'X-Forwarded'
        ]), //(optional) to change the trusted headers

    function ($request, $response, $next) {
        //Get the user ip
        $ip = ClientIp::getIp($request);

        //Get all ips found in the headers
        $all_ips = ClientIp::getIps($request);

        return $next($request, $response);
    }
]);

Cors

To use the neomerx/cors-psr7 library:

use Neomerx\Cors\Strategies\Settings

$relay = new RelayBuilder();

$settings = (new Settings())
    ->setServerOrigin([
        'scheme' => 'http',
        'host'   => 'example.com',
        'port'   => '123',
    ]);

$dispatcher = $relay->getInstance([

    Middleware::Cors()
        ->settings($settings)
]);

DebugBar

Inserts the PHP debug bar in the html body. This middleware requires Middleware::formatNegotiator executed before, to insert the debug bar only in Html responses.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::formatNegotiator(),

    Middleware::DebugBar()
        ->debugBar(new DebugBar\StandardDebugBar())
]);

DigestAuthentication

Implements the digest http authentication. You have to provide an array with the users and password:

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::DigestAuthentication()
        ->users([
            'username1' => 'password1',
            'username2' => 'password2'
        ])
        ->realm('My realm') //(optional) custom realm value
        ->nonce(uniqid())   //(optional) custom nonce value
]);

ErrorHandler

Executes a handler if the response returned by the next middlewares has any error (status code 400-599). You can catch also the exceptions throwed or even use whoops as error handler.

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\ErrorHandler;

function errorHandler($request, $response, $myApp) {
    switch ($response->getStatusCode()) {
        case 404:
            return 'Page not found';

        case 500:
            //you can get the exception catched
            $exception = ErrorHandler::getException($request);

            return 'Server error: '.$exception->getMessage();

        default:
            return 'There was an error'
    }
}

$whoops = new Whoops\Run();

$dispatcher = $relay->getInstance([

    Middleware::ErrorHandler()
        ->handler('errorHandler') //The error handler
        ->arguments($myApp)       //(optional) extra arguments to the handler
        ->whoops($whoops)         //(optional) provide a whoops instance to capture errors and exceptions
        ->catchExceptions()       //(optional) to catch exceptions if you don't use an external library for that
]);

FastRoute

To use FastRoute as a middleware.

use Psr7Middlewares\Middleware;

$router = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) {

    $r->addRoute('GET', '/blog/{id:[0-9]+}', function ($request, $response, $app) {
        return 'This is the post number'.$request->getAttribute('id');
    });
});

$dispatcher = $relay->getInstance([

    Middleware::FastRoute()
        ->router($router)  //Instance of FastRoute\Dispatcher
        ->argument($myApp) //(optional) arguments appended to the controller
]);

Firewall

Uses M6Web/Firewall to provide a IP filtering. This middleware deppends of ClientIp (to extract the ips from the headers).

See the ip formats allowed for trusted/untrusted options:

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    //needed to capture the user ips before
    Middleware::ClientIp(),

    //set the firewall
    Middleware::Firewall()
        ->trusted(['123.0.0.*'])   //(optional) ips allowed
        ->untrusted(['123.0.0.1']) //(optional) ips not allowed
]);

FormatNegotiation

Uses willdurand/Negotiation (2.x) to detect and negotiate the format of the document using the url extension and/or the Accept http header. It also adds the Content-Type header to the response if it's missing.

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\FormatNegotiator;

$dispatcher = $relay->getInstance([

    Middleware::FormatNegotiator()
        ->defaultFormat('html') //(optional) default format if it's unable to detect. (by default is "html")
        ->addFormat('pdf', ['application/pdf', 'application/x-download']) //(optional) add new formats and mimetypes
    },

    function ($request, $response, $next) {
        //get the format (for example: html)
        $format = FormatNegotiator::getFormat($request);

        return $next($request, $response);
    }
]);

LanguageNegotiation

Uses willdurand/Negotiation to detect and negotiate the client language. You must provide an array with all available languages:

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\LanguageNegotiator;

$dispatcher = $relay->getInstance([

    Middleware::LanguageNegotiator()
        ->languages(['gl', 'en', 'es']), //Available languages

    function ($request, $response, $next) {
        //Get the preferred language
        $language = LanguageNegotiator::getLanguage($request);

        return $next($request, $response);
    }
]);

MethodOverride

Overrides the request method using the X-Http-Method-Override header. This is useful for clients unable to send other methods than GET and POST:

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::MethodOverride()
        ->get(['HEAD', 'CONNECT', 'TRACE', 'OPTIONS']), //(optional) to customize the allowed GET overrided methods
        ->post(['PATCH', 'PUT', 'DELETE', 'COPY', 'LOCK', 'UNLOCK']), //(optional) to customize the allowed POST overrided methods
]);

Minify

Uses mrclay/minify to minify the html, css and js code from the responses.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([
    
    //needed to get the format of the response
    Middleware::formatNegotiator(),

    Middleware::Minify()
        ->forCache(true)   //(optional) only minify cacheable responses
        ->inlineCss(false) //(optional) enable/disable inline css minification
        ->inlineJs(false)  //(optional) enable/disable inline js minification
]);

Payload

Parses the body of the request if it's not parsed and the method is POST, PUT or DELETE. It has support for json, csv and url encoded format.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([
    
    Middleware::Payload()
        ->associative(true) //(optional) To generate associative arrays with json objects

    function ($request, $response, $next) {
        //Get the parsed body
        $content = $request->getParsedBody();

        return $next($request, $response);
    }
]);

ReadResponse

Read the response content from a file. It's the opposite of SaveResponse

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::ReadResponse()
        ->storage('path/to/document/root') //Path where the files are stored
        ->basePath('public')               //(optional) basepath ignored from the request uri
]);

ResponseTime

Calculates the response time (in miliseconds) and saves it into X-Response-Time header:

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::ResponseTime()
]);

Robots

Disables the robots of the search engines for non-production environment. Adds automatically the header X-Robots-Tag: noindex, nofollow, noarchive in all responses and returns a default body for /robots.txt request.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::Robots()
]);

SaveResponse

Saves the response content into a file if all of the following conditions are met:

  • The method is GET
  • The status code is 200
  • The Cache-Control header does not contain no-cache value
  • The request has not query parameters.

This is useful for cache purposes

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::SaveResponse()
        ->storage('path/to/document/root') //Path directory where save the responses
        ->basePath('public')               //(optional) basepath ignored from the request uri
]);

Shutdown

Useful to display a 503 maintenance page. You need to specify a handler.

use Psr7Middlewares\Middleware;

function shutdownHandler ($request, $response, $app) {
    $response->getBody()->write('Service unavailable');
}

$dispatcher = $relay->getInstance([

    Middleware::Shutdown()
        ->handler('shutdownHandler') //Callable that generate the response
        ->arguments($app)            //(optional) to add extra arguments to the handler
]);

TrailingSlash

Removes (or adds) the trailing slash of the path. For example, /post/23/ will be converted to /post/23. If the path is / it won't be converted. Useful if you have problems with the router.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::TrailingSlash()
        ->addSlash(true)     //(optional) to add the trailing slash instead remove
        ->redirect(301)      //(optional) to return a 301 (seo friendly) or 302 response to the new path
        ->basePath('public') //(optional) basepath
]);

Uuid

Uses ramsey/uuid to generate an Uuid (Universally Unique Identifiers) for each request (compatible with RFC 4122 versions 1, 3, 4 and 5). It's usefull for debugging purposes.

use Psr7Middlewares\Middleware;
use Psr7Middlewares\Middleware\Uuid;

$dispatcher = $relay->getInstance([

    Middleware::Uuid()
        ->version(4)     //(optional) version of the identifier (1 by default). Versions 3 and 5 need more arguments (see https://github.com/ramsey/uuid#examples)
        ->header(false), //(optional) Name of the header to store the identifier (X-Uuid by default). Set false to don't save header

    function ($request, $response, $next) {
        //Get the X-Uuid header
        $id = $request->getHeaderLine('X-Uuid');

        //Get the Uuid instance
        $uuid = Uuid::getUuid($request);

        echo $uuid->toString();

        return $next($request, $response);
    }
]);

When

Execute a middleware only if a condition is evaluated as true. This is useful to add middleware only under some circunstances or environments.

use Psr7Middlewares\Middleware;

$dispatcher = $relay->getInstance([

    Middleware::When()
        ->condition(getenv('ENV') === 'production') //The condition to be evaluated
        ->middleware(Middleware::minify()),         //The middleware to be executed when the condition is true

    //You can use also callables:
    Middleware::when()
        ->condition(function ($request, $response) {
            return $request->hasHeader('X-Foo')
        })
        ->middleware(function ($request, $response, $next) {
            //your code
            return $next($request, $response);
        })
]);

Combine with container interop

If you're using any container compatible with the Container Interoperability Project you can use it to provide instances to some middlewares (routers, debugbar, psr6 cache, etc). To do that, there's the method ->from($container, $id) available in the following middlewares:

$container = new MyContainerInterop();

$dispatcher = $relay->getInstance([
    Middleware::Cache()->from($container, 'cache'),
    Middleware::Cors()->from($container, 'cors-settings'),
    Middleware::DebugBar()->from($container, 'debug-bar'),
    Middleware::AuraSession()->from($container, 'session-factory'),
    Middleware::FastRouter()->from($container, 'fast-router'),
]);

By using containers instead creating and passing the instances directly, these instances will be created (for example $container->get('fast-router')) only if the middleware is executed.

Contribution

New middlewares are appreciated. Just create a pull request.