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.
Uri, Stream, and UploadedFile value objects, as described in the previous recipe.Application\MiddleWare\Message class. This class consumes Stream and Uri and implements Psr\Http\Message\MessageInterface. We first define properties for the key value objects, including those representing the message body (that is, a StreamInterface 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();getBody() method that represents a StreamInterface instance. A companion method, withBody(), returns the current Message instance and allows us to overwrite the current value of body: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;
}findHeader() method (not directly defined by MessageInterface) that locates a header using stripos():protected function findHeader($name)
{
$found = FALSE;
foreach (array_keys($this->getHeaders()) as $header) {
if (stripos($header, $name) !== FALSE) {
$found = $header;
break;
}
}
return $found;
}$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 excellent apache_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;
}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);
}getHeaders() (required in PSR-7) is now a trivial loop through the $httpHeaders property produced by the getHttpHeaders() method discussed in step 4:public function getHeaders()
{
foreach ($this->getHttpHeaders() as $key => $value) {
header($key . ': ' . $value);
}
}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. The withoutHeader() method is used to remove a header instance. Notice the consistent use of findHeader(), 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;
}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();
}
}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;
}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 provide onlyVersion() 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;
}
}
}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 provide Application\MiddleWare\Request (requests a client will make to a server), and Application\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 our Request class extends Message. 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 instanceNULL, but we leave open the possibility of defining the appropriate arguments right away. We use the inherited onlyVersion() method to sanitize the version. We also define checkMethod() to make sure any method supplied is on our list of supported HTTP methods, defined as a constant array in Constants: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;
}Uri class has methods that will parse this into its component parts, hence our provision of the $uriObj property. In the case of withRequestTarget(), notice that we run getUri() 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;
}get and with methods, which represent the HTTP method, reveal no surprises. We use checkMethod(), 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;
}get and with method for the URI. As mentioned in step 14, we retain the original request string in the $uri property and the newly parsed Uri instance in $uriObj. Note the extra flag to preserve any existing Host 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;
}
}ServerRequest class extends Request 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;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;
}UploadedFile objects (presented in the previous recipe), we also define a method that takes $uploadedFileInfo and creates UploadedFile objects:public function getUploadedFiles()
{
if (!$this->uploadedFileObjs) {
foreach ($this->getUploadedFileInfo() as $field => $value) {
$this->uploadedFileObjs[$field] = new UploadedFile($field, $value);
}
}
return $this->uploadedFileObjs;
}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;
}getParsedBody() and its accompanying with method. The PSR-7 recommendations are quite specific when it comes to form posting. Note the series of if statements that check the Content-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;
}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;
}
}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);Here is how the direct output might appear:
