PSR-15
HTTP Server Request Handlers

 


Fri 04 May 2018
What is PSR?
A long time ago, in a galaxy
far, far away...

Episode I

PHP-FIG


Turmoil has engulfed the PHP Republic with chaotic factions fighting against each other.

An alliance was formed by devs from different frameworks in 2009.

Their name - PHP Framework Interoperability Group (PHP-FIG).

Their weapon - PHP Standards Recommendations (PSR).

A list of some PSRs by PHP-FIG...
  • PSR-0: Autoloading Standard (deprecated)
  • PSR-1: Basic Coding Standard
  • PSR-2: Coding Style Guide
  • PSR-4: Autoloading Standard
  • and many more...


  • PSR-15: HTTP Server Request Handlers

  • Middleware

  • PSR-7: HTTP Message Interfaces
PSR-7: HTTP Message Interfaces

<?php // test.php?city=SG
if (isset($_POST['submit'])) {
    echo 'You typed: ' . $_POST['myText'];
}
?>
City: <?php echo ($_GET['city'] ?? 'None'); ?><br />
<form method="POST">
  Text: <input type="text" name="myText"><br />
  <input type="submit" />
</form>
<?php // localhost/city/SG
class TestController extends AbstractActionController
{
    public function indexAction()
    {
        $request = $this->getRequest();
        $usertext = $request->isPost() ? $request->getPost()['myText'] : '';
        return new ViewModel([
            'city' => $this->params('city', 'None'),
            'form' => new TestForm(),
            'usertext' => $usertext,
        ]);
    }
}
What do frameworks abstract?

  • Form post
  • Access $_SERVER variables
  • Extract request uri for routing purposes
  • Handle file uploads
  • Input and output streams
  • Returning of response
Every HTTP request message has a specific form:
POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat
HTTP response messages have a similar structure:
HTTP/1.1 200 OK
Content-Type: text/plain

This is the response body
MessageInterface
$headers = $message->getHeaders(); // [<headerName> => <array>]

$headerValues = $message->getHeader('Cookie'); // array, may be more than 1

$headerLine = $message->getHeaderLine('Accept'); // string

$body = $message->getBody(); // StreamInterface

$newMessage = $message->withHeader('Content-Type', 'application/json');

$newerMessage = $newMessage->withBody($body->getContents() . 'appended content');
RequestInterface
$method = $request->getMethod(); // GET, POST, etc.

$uri = $request->getUri(); // UriInterface

$body = new Stream();
$body->write('{"id":1}');

$newRequest = $request
              ->withMethod('GET')
              ->withUri('http://example.com/api/v1/users')
              ->withHeader('Accept', 'application/json')
              ->withBody($body);
ResponseInterface
$status = $response->getStatusCode(); // 200, 404, 500

$reason = $response->getReasonPhrase(); // OK, Not Found, Server Error

$data = json_decode((string) $response->getBody(), true);
$data['timestamp'] = gmdate('c');

$newResponse = $response->withBody(json_encode($data));
$newerResponse = $newResponse->withStatus(200);
ServerRequestInterface
$queryParams = $request->getQueryParams();

$parsedBody = $request->getParsedBody(); // deserialized body params

$uploadedFiles = $request->getUploadedFiles(); // array of UploadedFileInterface

// Get `id` passed from route, eg. localhost/user/100.
// Application must parse route and inject into request attributes
$id = $request->getAttribute('id'); // 100
Middleware
What is Middleware?


Credits: http://starwars.wikia.com/wiki/File:Deathstar_blueprint.jpg

Credits: http://2015.phpday.it/talk/pushing-boundaries-zend-framework-3-and-the-future/
In code...
<?php
function middleware(
    ServerRequestInterface $request,
    ResponseInterface $response,
    callable $next
);
<?php // Before: Framework-specific methods, parent classes, naming convention
class TestController extends AbstractActionController
{
    public function indexAction()
    {
        $request = $this->getRequest();
        $usertext = $request->isPost() ? $request->getPost()['myText'] : '';
        return new ViewModel([ // tied to framework
            'usertext' => $usertext,
        ]);
    }
}
<?php // After: Framework-agnostic Middleware
class TestAction
{
    private $renderer;

    public function __construct(TemplateRendererInterface $renderer)
    {
        $this->renderer = $renderer; // dependency injection
    }

    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        $usertext = $request->getParsedBody['myText'] ?? '';
        $response->getBody()->write( // renderer not tied to framework
            $this->renderer->render('app::test', ['usertext' => $usertext])
        );
        return $next($request, $response);
    }
}
PSR-15: HTTP Server Request Handlers
Why Bother?

Credits: https://xkcd.com/927

Previously in Middleware...
<?php
public function __invoke(
    ServerRequestInterface $request,
    ResponseInterface $response,
    callable $next
) {
    // do something
    return $next($request, $response);
}
Psr\Http\Server\RequestHandlerInterface
<?php
interface RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request): ResponseInterface;
}
Previously in Middleware...
<?php
public function __invoke(
    ServerRequestInterface $request,
    ResponseInterface $response,
    callable $next
) {
    // do something
    return $next($request, $response);
}
Psr\Http\Server\MiddlewareInterface
<?php
interface MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface;
}
Double Pass
fn(request, response, next): response
Single Pass
fn(request, next): response
fn(request, response, next): response // Problems with Double Pass
  • Passing an empty response does not guarantee it is in a usable state
  • Incorrect headers from previous middleware
  • Corrupted body content
  • Dependency inversion with response prototype can be solved better with factories
  • callable type hint for $next


Chosen approach: Single Pass
fn(request, next): response
Example: Queue-based request handler
class QueueRequestHandler implements RequestHandlerInterface {
    public function __construct(RequestHandlerInterface $fallbackHandler) {
        $this->fallbackHandler = $fallbackHandler;
    }

    public function add(MiddlewareInterface $middleware) {
        $this->middleware[] = $middleware;
    }

    public function handle(ServerRequestInterface $request): ResponseInterface {
        // Last middleware in the queue has called on the request handler.
        if (0 === count($this->middleware)) {
            return $this->fallbackHandler->handle($request);
        }
        $middleware = array_shift($this->middleware);
        return $middleware->process($request, $this);
    }
}
Example: Queue-based request handler
// An application bootstrap may look like this:

// Fallback handler:
$fallbackHandler = new NotFoundHandler();

// Create request handler instance:
$app = new QueueRequestHandler($fallbackHandler);

// Add one or more middleware:
$app->add(new AuthorizationMiddleware());
$app->add(new RoutingMiddleware());

// execute it:
$response = $app->handle(ServerRequestFactory::fromGlobals());
https://www.php-fig.org/psr/psr-15/meta/#queue-based-request-handler
Reusable MW: AuthorizationMiddleware
class AuthorizationMiddleware implements MiddlewareInterface {
    public function __construct(AuthorizationMap $authorizationMap) {
        $this->authorizationMap = $authorizationMap;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
        if (! $authorizationMap->needsAuthorization($request)) {
            return $handler->handle($request);
        }

        if (! $authorizationMap->isAuthorized($request)) {
            return $authorizationMap->prepareUnauthorizedResponse();
        }

        $response = $handler->handle($request);
        return $authorizationMap->signResponse($response, $request);
    }
}
Reusable MW: RoutingMiddleware
class RoutingMiddleware implements MiddlewareInterface
{
    public function __construct(Router $router)
    {
        $this->router = $router;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $result = $this->router->match($request);

        if ($result->isSuccess()) {
            return $result->getHandler()->handle($request);
        }

        return $handler->handle($request);
    }
}
Zend Expressive 3

https://docs.zendframework.com/zend-expressive/

May the Fourth be with you...
Happy Star Wars Day!