Hello, PSR-7. (PHP Benelux 2016)

Hello, PSR-7. (PHP Benelux 2016)

PSR-7 HTTP Message Interface has been kicking around in one form or another since March of 2012. Get a brief history of how PSR-7 evolved from its humble beginnings as a simple HTTP client interface to the game-changing PHP-FIG proposal we have today. Learn how to work with the HTTP Message Interfaces and how they will be used by frameworks in the not to distant future. Find out what it will mean to have a healthy ecosystem of PHP software all speak HTTP the same way. Say hello to your new best friend in PHP, PSR-7.

23d971deeb3975a7d28246192fbbe7b7?s=128

Beau Simensen

January 30, 2016
Tweet

Transcript

  1. 7.

    class halo_HttpRequest { public function __construct( $method, array $queryParams =

    null, array $postParams = null, array $fileData = null, array $env = null, $body = null ) { // ... } }
  2. 8.

    class halo_DispatcherUtil { public static function MAKE_HTTP_REQUEST() { return new

    halo_HttpRequest( $_SERVER['REQUEST_METHOD'], $_GET, $_POST, $_FILES, $_SERVER ); } }
  3. 9.

    namespace Symfony\Component\HttpFoundation; class Request { public function __construct( array $query

    = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null ) { // ... } }
  4. 10.

    namespace Symfony\Component\HttpFoundation; class Request { public static function createFromGlobals() {

    return self::createRequestFromFactory( $_GET, $_POST, array(), $_COOKIE, $_FILES, $server ); } }
  5. 11.

    namespace Zend\Http\PhpEnvironment; class Request extends \Zend\Http\Request { public function __construct()

    { $this->setEnv(new Parameters($_ENV)); if ($_GET) { $this->setQuery(new Parameters($_GET)); } if ($_POST) { $this->setPost(new Parameters($_POST)); } if ($_COOKIE) { $this->setCookies(new Parameters($_COOKIE)); } if ($_FILES) { $files = $this->mapPhpFiles(); $this->setFiles(new Parameters($files)); } $this->setServer(new Parameters($_SERVER)); } }
  6. 12.

    namespace Aura\Web; class WebFactory { public function newRequest() { return

    new Request( $this->newRequestClient(), $this->newRequestContent(), $this->newRequestGlobals(), $this->newRequestHeaders(), $this->newRequestMethod(), $this->newRequestParams(), $this->newRequestUrl() ); } public function newRequestGlobals() { return new Request\Globals( $this->newRequestCookies(), $this->newRequestEnv(), $this->newRequestFiles(), $this->newRequestPost(), $this->newRequestQuery(), $this->newRequestServer() ); } }
  7. 17.
  8. 18.

    What if we could share code between these applications and

    frameworks at the HTTP level? — Igor, Beau, and Christoph
  9. 21.
  10. 22.
  11. 23.
  12. 26.
  13. 27.

    The Problem for both server-side and client-side: HTTP Messages abstraction

    should be a commodity <message line> Header: value Another-Header: value Message body
  14. 28.
  15. 31.
  16. 33.

    $client = create_http_client(); // implementation specific $response = $client->request('GET', 'http://www.php.net');

    if ($response->getStatusCode() == 200) { $content = $response->getContent(); } $response = $client->request('GET', 'http://api/returning.json'); if ($response->getContentType() == 'application/json') { $json = json_decode($response->getContent()); }
  17. 35.

    There was a proposal to create a HTTP client interface

    earlier this year. As was noted at the time, the core HTTP message representation part (ie the request/response objects) is probably of more importance: it's used by both HTTP clients and frameworks. — Chris Wilkinson
  18. 36.

    interface MessageInterface { public function __toString(); public function getStartLine(); public

    function getProtocolVersion(); public function setProtocolVersion($protocolVersion); public function getHeader($header); public function getHeaders(); public function hasHeader($header); public function setHeader($header, $value); public function setHeaders(array $headers); public function addHeaders(array $headers); public function getBody(); public function setBody($body); }
  19. 37.

    interface RequestInterface extends MessageInterface { public function __construct( $method, $url,

    $protocolVersion = '1.1' ); public function getResponse(); public function setResponse(ResponseInterface $response); public function getMethod(); public function setMethod($method); public function getUrl(); public function setUrl($url); }
  20. 38.

    interface ResponseInterface extends MessageInterface { public function __construct( $statusCode, $reasonPhrase

    = null, $protocolVersion = '1.1' ); public function getRequest(); public function setRequest(RequestInterface $request); public function getStatusCode(); public function setStatusCode($statusCode); public function getReasonPhrase(); public function setReasonPhrase($reasonPhrase); }
  21. 46.
  22. 48.
  23. 49.
  24. 50.
  25. 52.

    class Request implements RequestInterface { public function withMethod($method) { $new

    = clone $this; $new->method = $method; return $new; } }
  26. 54.

    use Zend\Diactoros\Request; use Zend\Diactoros\Uri; $request = (new Request()) ->withUri(new Uri('http://example.com'))

    ->withMethod('GET'); // this does nothing to $request. $request->withAddedHeader( 'Content-Type', 'application/json' );
  27. 55.

    use Zend\Diactoros\Request; use Zend\Diactoros\Uri; $request = (new Request()) ->withUri(new Uri('http://example.com'))

    ->withMethod('GET'); // this is probably what you want. $request = $request->withAddedHeader( 'Content-Type', 'application/json' );
  28. 57.

    As somebody who implemented the interfaces, I was able to

    literally rip out huge swathes of defensive code, as state checks became unnecessary. — Matthew Weier O'Phinney
  29. 63.
  30. 65.
  31. 66.

    $message = $message ->withHeader('foo', 'bar') ->withAddedHeader('foo', 'baz'); if ($message->hasHeader('Accept')) {

    /* ... */ } // 'bar, baz' $header = $message->getHeaderLine('foo'); // ['bar', 'baz'] $header = $message->getHeader('foo'); // [ 'foo' => ['bar', 'baz'] ] foreach ($message->getHeaders() as $header => $values) { }
  32. 68.
  33. 69.

    URI // https://api.example.com $uri = $uri->withScheme('https')->withHost('api.example.com'); $usersUri = $uri->withPath('/v2/users'); $booksUri

    = $uri->withPath('/v2/books'); // https://api.example.com/v2/users?limit=10 $getUsersRequest = (new Request()) ->withMethod('GET') ->withUri($usersUri->withQuery('?limit=10')); // https://api.example.com/v2/users $postUsersRequest = (new Request()) ->withMethod('POST') ->withUri($usersUri); // https://api.example.com/v2/books $getBooksRequest = (new Request()) ->withMethod('GET') ->withUri($booksUri);
  34. 73.

    $country = $this->resolveCountryForRequest($request); if ($country) { $request = $request->withAttribute( 'geocode.country',

    $country ); } // ... $country = $request->getAttribute('geocode.country'); if ($country) { // }
  35. 75.

    Given file input names files[0] and files[1] What you might

    expect: [ 'files' => [ 0 => [ 'name' => 'file0.txt', 'type' => 'text/plain', /* etc. */ ], 1 => [ 'name' => 'file1.html', 'type' => 'text/html', /* etc. */ ], ], ];
  36. 76.

    Given file input names files[0] and files[1] What you actually

    get: [ 'files' => [ 'name' => [ 0 => 'file0.txt', 1 => 'file1.html', ], 'type' => [ 0 => 'text/plain', 1 => 'text/html', ], /* etc. */ ], ]
  37. 78.

    $file0 = $request->getUploadedFiles()['files'][0]; $file1 = $request->getUploadedFiles()['files'][1]; printf( "Received the files

    %s and %s", $file0->getClientFilename(), $file1->getClientFilename() ); // "Received the files file0.txt and file1.html"
  38. 79.

    Underlying "filename" is protected // Move a file to an

    upload directory $filename = sprintf( '%s.%s', create_uuid(), pathinfo($file0->getClientFilename(), PATHINFO_EXTENSION) ); $file0->moveTo(DATA_DIR . '/' . $filename); // Stream a file to Amazon S3. // Assume $s3wrapper is a PHP stream that will write to S3, and that // Psr7StreamWrapper is a class that will decorate a StreamInterface as a PHP // StreamWrapper. $stream = new Psr7StreamWrapper($file1->getStream()); stream_copy_to_stream($stream, $s3wrapper);
  39. 82.

    function ($request, $response, $next) { $contentType = $request->getHeaderLine('Content-Type'); if ('application/json'

    === $contentType) { $json = json_decode($request->getBody(), true); $request = $request->withParsedBody($json); } return $next( $request, $response ); }
  40. 85.

    function ($request, $response, $next) { $response = $next( $request, $response

    ); return $response->withAddedHeader( 'X-Clacks-Overhead', 'GNU Terry Pratchett' ); }
  41. 86.

    namespace Acme; class ClacksOverhead { public function __invoke($request, $response, $next)

    { $response = $next( $request, $response ); return $response->withAddedHeader( 'X-Clacks-Overhead', 'GNU Terry Pratchett' ); } }
  42. 88.

    Relay use Relay\RelayBuilder; $relayBuilder = new RelayBuilder(); $relay = $relayBuilder->newInstance([

    // ... new \Acme\ClacksOverhead(), // ... ]); $response = $relay($request, $response);
  43. 89.
  44. 94.

    use Dflydev\FigCookies\FigRequestCookies; // Assume this header is set on $request

    // Cookie: theme=red; foo=bar $cookie = FigRequestCookies::get($request, 'theme'); $cookie->getValue(); // "red" $request = FigRequestCookies::set($request, $cookie->withValue('blue')); $request = FigRequestCookies::set($request, Cookie::create('name', 'beau')); $request = FigRequestCookies::remove($request, 'foo'); // The $request now has the following header: // Cookie: theme=blue; name=beau
  45. 96.

    class EncryptedFigCookiesMiddleware { public function __invoke( RequestInterface $request, ResponseInterface $response,

    callable $next ) { $request = $this->requestCookieDecryptor->decrypt( $request, $this->cookieNames ); $response = $next($request, $response); return $this->responseCookieEncryptor->encrypt( $response, $this->cookieNames ); } }
  46. 97.
  47. 102.

    Nimble monii/nimble-framework A light PSR-7 framework built on top of

    Laravel's Container, nikic/fast-route, and Relay WIP
  48. 106.

    There were at least two other existing nikic/fast-route middleware implementations

    jowy/routing-middleware & oscarotero/psr7-middlewares
  49. 107.

    class NikicFastRoute { public function __invoke($request, $response, $next) { $routeInfo

    = $this->dispatcher->dispatch( $request->getMethod(), $request->getUri()->getPath() ); } }
  50. 108.

    class NikicFastRoute { public function __invoke($request, $response, $next) { $routeInfo

    = $this->dispatcher->dispatch( $request->getMethod(), $request->getUri()->getPath() ); switch ($routeInfo[0]) { case FastRoute\Dispatcher::NOT_FOUND: return $response->withStatus(404); } } }
  51. 109.

    class NikicFastRoute { public function __invoke($request, $response, $next) { $routeInfo

    = $this->dispatcher->dispatch( $request->getMethod(), $request->getUri()->getPath() ); switch ($routeInfo[0]) { case FastRoute\Dispatcher::NOT_FOUND: return $response->withStatus(404); case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: return $response->withStatus(405); } } }
  52. 110.

    class NikicFastRoute { public function __invoke($request, $response, $next) { $routeInfo

    = $this->dispatcher->dispatch( $request->getMethod(), $request->getUri()->getPath() ); switch ($routeInfo[0]) { case FastRoute\Dispatcher::NOT_FOUND: return $response->withStatus(404); case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: return $response->withStatus(405); case FastRoute\Dispatcher::FOUND: $action = $routeInfo[1]; $parameters = $routeInfo[2]; $request = $request ->withAttribute('action', $action) ->withAttribute('parameters', $parameters) ; return $next($request, $response, $next); } } }
  53. 111.

    class NikicFastRoute { public function __invoke($request, $response, $next) { $routeInfo

    = $this->dispatcher->dispatch( $request->getMethod(), $request->getUri()->getPath() ); switch ($routeInfo[0]) { case FastRoute\Dispatcher::NOT_FOUND: return $response->withStatus(404); case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: return $response->withStatus(405); case FastRoute\Dispatcher::FOUND: $action = $routeInfo[1]; $parameters = $routeInfo[2]; $request = $request ->withAttribute('action', $action) ->withAttribute('parameters', $parameters) ; return $next($request, $response, $next); default: return $response->withStatus(500); } } }
  54. 113.

    class ActionHandler { public function __construct(ActionHandlerResolver $actionResolver) { $this->actionResolver =

    $actionResolver; } function __invoke($request, $response, $next) { $action = $request->getAttribute('action'); if (is_null($action)) { throw new \RuntimeException('No action specified for request.'); } $handler = $this->actionResolver->resolve($action); return $handler($request, $response, $next); } }
  55. 114.

    $container->bind(Relay::class, function (Container $container) { $relayBuilder = $container->make(RelayBuilder::class); return $relayBuilder->newInstance([

    $container->make(NikicFastRoute::class), // ... other middleware $container->make(ActionHandler::class), ]); }); $relay = $container->make(Relay::class); $response = $relay( $container->make(ServerRequestInterface::class), $container->make(ResponseInterface::class) ); send_response($response);
  56. 116.
  57. 117.
  58. 118.

    Managed Technical Debt Making a distinction between Managed and Unmanaged

    Technical Debt. http://verraes.net/2013/07/managed-technical-debt/
  59. 119.

    class CorsCompletelyOpenForDevelopment { public function __invoke($request, $response, callable $next) {

    /** @var ResponseInterface $response */ $response = $next($request, $response); if ($response->hasHeader('Access-Control-Allow-Origin')) { // If the response already has an expected header we will // assume that CORS has been handled successfully. return $response; } // This is unsafe and not what people will want in production // unless they really know what they are doing. return $response ->withHeader('Access-Control-Allow-Origin', '*') ->withHeader( 'X-Completely-Open-Cors-For-Development', 'unsafe' ) ; } }
  60. 120.

    $container->bind(Relay::class, function (Container $container) { $relayBuilder = $container->make(RelayBuilder::class); return $relayBuilder->newInstance([

    $container->make(NikicFastRoute::class), // ... other middleware $container->make(ActionHandler::class), ]); });
  61. 121.

    $container->bind(Relay::class, function (Container $container) { $relayBuilder = $container->make(RelayBuilder::class); return $relayBuilder->newInstance([

    $container->make(NikicFastRoute::class), $container->make(CorsCompletelyOpenForDevelopment::class), // ... other middleware $container->make(ActionHandler::class), ]); });
  62. 122.
  63. 124.
  64. 126.
  65. 129.
  66. 130.
  67. 132.