One of the key characteristics of PSR-7 middleware is the use of Request and Response classes. When applied, this enables different blocks of software to perform together without sharing any specific knowledge between them. In this context, a request class should encompass all aspects of the original user request, including such items as browser settings, the original URL requested, parameters passed, and so forth.
First, be sure to define classes to represent the
Uri
,Stream
, andUploadedFile
value objects, as described in the previous recipe.Now we are ready to define the core
Application\MiddleWare\Message
class. This class consumesStream
andUri
and implementsPsr\Http\Message\MessageInterface
. We first define properties for the key value objects, including those representing the message body (that is, aStreamInterface
instance), version, and HTTP headers:namespace Application\MiddleWare; use Psr\Http\Message\ { MessageInterface, StreamInterface, UriInterface }; class Message implements MessageInterface { protected $body; protected $version; protected $httpHeaders = array();
Next, we have the
getBody()
method that represents aStreamInterface
instance. A companion method,withBody()
, returns the currentMessage
instance and allows us to overwrite the current value ofbody
:public function getBody() { if (!$this->body) { $this->body = new Stream(self::DEFAULT_BODY_STREAM); } return $this->body; } public function withBody(StreamInterface $body) { if (!$body->isReadable()) { throw new InvalidArgumentException(self::ERROR_BODY_UNREADABLE); } $this->body = $body; return $this; }
PSR-7 recommends that headers should be viewed as case-insensitive. Accordingly, we define a
findHeader()
method (not directly defined byMessageInterface
) that locates a header usingstripos()
:protected function findHeader($name) { $found = FALSE; foreach (array_keys($this->getHeaders()) as $header) { if (stripos($header, $name) !== FALSE) { $found = $header; break; } } return $found; }
The next method, not defined by PSR-7, is designed to populate the
$httpHeaders
property. This property is assumed to be an associative array where the key is the header, and the value is the string representing the header value. If there is more than one value, additional values separated by commas are appended to the string. There is an excellentapache_request_headers()
PHP function from the Apache extension that produces headers if they are not already available in$httpHeaders
:protected function getHttpHeaders() { if (!$this->httpHeaders) { if (function_exists('apache_request_headers')) { $this->httpHeaders = apache_request_headers(); } else { $this->httpHeaders = $this->altApacheReqHeaders(); } } return $this->httpHeaders; }
If
apache_request_headers()
is not available (that is, the Apache extension is not enabled), we provide an alternative,altApacheReqHeaders()
:protected function altApacheReqHeaders() { $headers = array(); foreach ($_SERVER as $key => $value) { if (stripos($key, 'HTTP_') !== FALSE) { $headerKey = str_ireplace('HTTP_', '', $key); $headers[$this->explodeHeader($headerKey)] = $value; } elseif (stripos($key, 'CONTENT_') !== FALSE) { $headers[$this->explodeHeader($key)] = $value; } } return $headers; } protected function explodeHeader($header) { $headerParts = explode('_', $header); $headerKey = ucwords(implode(' ', strtolower($headerParts))); return str_replace(' ', '-', $headerKey); }
Implementing
getHeaders()
(required in PSR-7) is now a trivial loop through the$httpHeaders
property produced by thegetHttpHeaders()
method discussed in step 4:public function getHeaders() { foreach ($this->getHttpHeaders() as $key => $value) { header($key . ': ' . $value); } }
Again, we provide a series of
with
methods designed to overwrite or replace headers. Since there can be many headers, we also have a method that adds to the existing set of headers. ThewithoutHeader()
method is used to remove a header instance. Notice the consistent use offindHeader()
, mentioned in the previous step, to allow for case-insensitive handling of headers:public function withHeader($name, $value) { $found = $this->findHeader($name); if ($found) { $this->httpHeaders[$found] = $value; } else { $this->httpHeaders[$name] = $value; } return $this; } public function withAddedHeader($name, $value) { $found = $this->findHeader($name); if ($found) { $this->httpHeaders[$found] .= $value; } else { $this->httpHeaders[$name] = $value; } return $this; } public function withoutHeader($name) { $found = $this->findHeader($name); if ($found) { unset($this->httpHeaders[$found]); } return $this; }
We then provide a series of useful header-related methods to confirm a header exists, retrieve a single header line, and retrieve a header in array form, as per PSR-7:
public function hasHeader($name) { return boolval($this->findHeader($name)); } public function getHeaderLine($name) { $found = $this->findHeader($name); if ($found) { return $this->httpHeaders[$found]; } else { return ''; } } public function getHeader($name) { $line = $this->getHeaderLine($name); if ($line) { return explode(',', $line); } else { return array(); } }
Finally, to round off header handling, we present
getHeadersAsString
that produces a single header string with the headers separated by\r\n
for direct use with PHP stream contexts:public function getHeadersAsString() { $output = ''; $headers = $this->getHeaders(); if ($headers && is_array($headers)) { foreach ($headers as $key => $value) { if ($output) { $output .= "\r\n" . $key . ': ' . $value; } else { $output .= $key . ': ' . $value; } } } return $output; }
Still within the
Message
class, we now turn our attention to version handling. According to PSR-7, the return value for the protocol version (that is, HTTP/1.1) should only be the numerical part. For this reason, we also provideonlyVersion()
that strips off any non-digit character, allowing periods:public function getProtocolVersion() { if (!$this->version) { $this->version = $this->onlyVersion($_SERVER['SERVER_PROTOCOL']); } return $this->version; } public function withProtocolVersion($version) { $this->version = $this->onlyVersion($version); return $this; } protected function onlyVersion($version) { if (!empty($version)) { return preg_replace('/[^0-9\.]/', '', $version); } else { return NULL; } } }
Finally, almost as an anticlimax, we are ready to define our
Request
class. It must be noted here, however, that we need to consider both out-bound as well as in-bound requests. That is to say, we need a class to represent an outgoing request a client will make to a server, as well as a request received from a client by a server. Accordingly, we provideApplication\MiddleWare\Request
(requests a client will make to a server), andApplication\MiddleWare\ServerRequest
(requests received from a client by a server). The good news is that most of our work has already been done: notice that ourRequest
class extendsMessage
. We also provide properties to represent the URI and HTTP method:namespace Application\MiddleWare; use InvalidArgumentException; use Psr\Http\Message\ { RequestInterface, StreamInterface, UriInterface }; class Request extends Message implements RequestInterface { protected $uri; protected $method; // HTTP method protected $uriObj; // Psr\Http\Message\UriInterface instance
All properties in the constructor default to
NULL
, but we leave open the possibility of defining the appropriate arguments right away. We use the inheritedonlyVersion()
method to sanitize the version. We also definecheckMethod()
to make sure any method supplied is on our list of supported HTTP methods, defined as a constant array inConstants
:public function __construct($uri = NULL, $method = NULL, StreamInterface $body = NULL, $headers = NULL, $version = NULL) { $this->uri = $uri; $this->body = $body; $this->method = $this->checkMethod($method); $this->httpHeaders = $headers; $this->version = $this->onlyVersion($version); } protected function checkMethod($method) { if (!$method === NULL) { if (!in_array(strtolower($method), Constants::HTTP_METHODS)) { throw new InvalidArgumentException(Constants::ERROR_HTTP_METHOD); } } return $method; }
We are going to interpret the request target as the originally requested URI in the form of a string. Bear in mind that our
Uri
class has methods that will parse this into its component parts, hence our provision of the$uriObj
property. In the case ofwithRequestTarget()
, notice that we rungetUri()
that performs the aforementioned parsing process:public function getRequestTarget() { return $this->uri ?? Constants::DEFAULT_REQUEST_TARGET; } public function withRequestTarget($requestTarget) { $this->uri = $requestTarget; $this->getUri(); return $this; }
Our
get
andwith
methods, which represent the HTTP method, reveal no surprises. We usecheckMethod()
, used in the constructor as well, to ensure the method matches those we plan to support:public function getMethod() { return $this->method; } public function withMethod($method) { $this->method = $this->checkMethod($method); return $this; }
Finally, we have a
get
andwith
method for the URI. As mentioned in step 14, we retain the original request string in the$uri
property and the newly parsedUri
instance in$uriObj
. Note the extra flag to preserve any existingHost
header:public function getUri() { if (!$this->uriObj) { $this->uriObj = new Uri($this->uri); } return $this->uriObj; } public function withUri(UriInterface $uri, $preserveHost = false) { if ($preserveHost) { $found = $this->findHeader(Constants::HEADER_HOST); if (!$found && $uri->getHost()) { $this->httpHeaders[Constants::HEADER_HOST] = $uri->getHost(); } } elseif ($uri->getHost()) { $this->httpHeaders[Constants::HEADER_HOST] = $uri->getHost(); } $this->uri = $uri->__toString(); return $this; } }
The
ServerRequest
class extendsRequest
and provides additional functionality to retrieve information of interest to a server handling an incoming request. We start by defining properties that will represent incoming data read from the various PHP$_ super-globals
(that is,$_SERVER
,$_POST
, and so on):namespace Application\MiddleWare; use Psr\Http\Message\ { ServerRequestInterface, UploadedFileInterface } ; class ServerRequest extends Request implements ServerRequestInterface { protected $serverParams; protected $cookies; protected $queryParams; protected $contentType; protected $parsedBody; protected $attributes; protected $method; protected $uploadedFileInfo; protected $uploadedFileObjs;
We then define a series of getters to pull super-global information. We do not show everything, to conserve space:
public function getServerParams() { if (!$this->serverParams) { $this->serverParams = $_SERVER; } return $this->serverParams; } // getCookieParams() reads $_COOKIE // getQueryParams() reads $_GET // getUploadedFileInfo() reads $_FILES public function getRequestMethod() { $method = $this->getServerParams()['REQUEST_METHOD'] ?? ''; $this->method = strtolower($method); return $this->method; } public function getContentType() { if (!$this->contentType) { $this->contentType = $this->getServerParams()['CONTENT_TYPE'] ?? ''; $this->contentType = strtolower($this->contentType); } return $this->contentType; }
As uploaded files are supposed to be represented as independent
UploadedFile
objects (presented in the previous recipe), we also define a method that takes$uploadedFileInfo
and createsUploadedFile
objects:public function getUploadedFiles() { if (!$this->uploadedFileObjs) { foreach ($this->getUploadedFileInfo() as $field => $value) { $this->uploadedFileObjs[$field] = new UploadedFile($field, $value); } } return $this->uploadedFileObjs; }
As with the other classes defined previously, we provide
with
methods that add or overwrite properties and return the new instance:public function withCookieParams(array $cookies) { array_merge($this->getCookieParams(), $cookies); return $this; } public function withQueryParams(array $query) { array_merge($this->getQueryParams(), $query); return $this; } public function withUploadedFiles(array $uploadedFiles) { if (!count($uploadedFiles)) { throw new InvalidArgumentException(Constant::ERROR_NO_UPLOADED_FILES); } foreach ($uploadedFiles as $fileObj) { if (!$fileObj instanceof UploadedFileInterface) { throw new InvalidArgumentException(Constant::ERROR_INVALID_UPLOADED); } } $this->uploadedFileObjs = $uploadedFiles; }
One important aspect of PSR-7 messages is that the body should also be available in a parsed manner, that is to say, a sort of structured representation rather than just a raw stream. Accordingly, we define
getParsedBody()
and its accompanyingwith
method. The PSR-7 recommendations are quite specific when it comes to form posting. Note the series ofif
statements that check theContent-Type
header as well as the method:public function getParsedBody() { if (!$this->parsedBody) { if (($this->getContentType() == Constants::CONTENT_TYPE_FORM_ENCODED || $this->getContentType() == Constants::CONTENT_TYPE_MULTI_FORM) && $this->getRequestMethod() == Constants::METHOD_POST) { $this->parsedBody = $_POST; } elseif ($this->getContentType() == Constants::CONTENT_TYPE_JSON || $this->getContentType() == Constants::CONTENT_TYPE_HAL_JSON) { ini_set("allow_url_fopen", true); $this->parsedBody = json_decode(file_get_contents('php://input')); } elseif (!empty($_REQUEST)) { $this->parsedBody = $_REQUEST; } else { ini_set("allow_url_fopen", true); $this->parsedBody = file_get_contents('php://input'); } } return $this->parsedBody; } public function withParsedBody($data) { $this->parsedBody = $data; return $this; }
We also allow for attributes that are not precisely defined in PSR-7. Rather, we leave this open so that the developer can provide whatever is appropriate for the application. Notice the use of
withoutAttributes()
that allows you to remove attributes at will:public function getAttributes() { return $this->attributes; } public function getAttribute($name, $default = NULL) { return $this->attributes[$name] ?? $default; } public function withAttribute($name, $value) { $this->attributes[$name] = $value; return $this; } public function withoutAttribute($name) { if (isset($this->attributes[$name])) { unset($this->attributes[$name]); } return $this; } }
Finally, in order to load the different properties from an in-bound request, we define
initialize()
, which is not in PSR-7, but is extremely convenient:public function initialize() { $this->getServerParams(); $this->getCookieParams(); $this->getQueryParams(); $this->getUploadedFiles; $this->getRequestMethod(); $this->getContentType(); $this->getParsedBody(); return $this; }
First, be sure to complete the preceding recipe, as the Message
and Request
classes consume Uri
, Stream
, and UploadedFile
value objects. After that, go ahead and define the classes summarized in the following table:
Class |
Steps they are discussed in |
---|---|
|
2 to 9 |
|
10 to 14 |
|
15 to 20 |
After that, you can define a server program, chap_09_middleware_server.php
, which sets up autoloading and uses the appropriate classes. This script will pull the incoming request into a ServerRequest
instance, initialize it, and then use var_dump()
to show what information was received:
<?php require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\MiddleWare\ServerRequest; $request = new ServerRequest(); $request->initialize(); echo '<pre>', var_dump($request), '</pre>';
To run the server program, first change to the /path/to/source/for/this/chapter folder
. You can then run the following command:
php -S localhost:8080 chap_09_middleware_server.php'
As for the client, first create a calling program, chap_09_middleware_request.php
, that sets up autoloading, uses the appropriate classes, and defines the target server and a local text file:
<?php define('READ_FILE', __DIR__ . '/gettysburg.txt'); define('TEST_SERVER', 'http://localhost:8080'); require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\MiddleWare\ { Request, Stream, Constants };
Next, you can create a Stream
instance using the text as a source. This will become the body of a new Request, which, in this case, mirrors what might be expected for a form posting:
$body = new Stream(READ_FILE);
You can then directly build a Request
instance, supplying parameters as appropriate:
$request = new Request( TEST_SERVER, Constants::METHOD_POST, $body, [Constants::HEADER_CONTENT_TYPE => Constants::CONTENT_TYPE_FORM_ENCODED,Constants::HEADER_CONTENT_LENGTH => $body->getSize()] );
Alternatively, you can use the fluent interface syntax to produce exactly the same results:
$uriObj = new Uri(TEST_SERVER); $request = new Request(); $request->withRequestTarget(TEST_SERVER) ->withMethod(Constants::METHOD_POST) ->withBody($body) ->withHeader(Constants::HEADER_CONTENT_TYPE, Constants::CONTENT_TYPE_FORM_ENCODED) ->withAddedHeader(Constants::HEADER_CONTENT_LENGTH, $body->getSize());
You can then set up a cURL resource to simulate a form posting, where the data parameter is the contents of the text file. You can follow that with curl_init()
, curl_exec()
, and so on, echoing the results:
$data = http_build_query(['data' => $request->getBody()->getContents()]); $defaults = array( CURLOPT_URL => $request->getUri()->getUriString(), CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data, ); $ch = curl_init(); curl_setopt_array($ch, $defaults); $response = curl_exec($ch); curl_close($ch);
An excellent article that shows example usage written by Matthew Weir O'Phinney, the editor of PSR-7 (also the lead architect for Zend Framework 1, 2, and 3), is available here: https://mwop.net/blog/2015-01-26-psr-7-by-example.html