In order to work with PSR-7 requests and responses, we first need to define a series of value objects. These are classes that represent logical objects used in web-based activities such as URIs, file uploads, and streaming request or response bodies.
The source code for the PSR-7 interfaces is available as a Composer
package. It is considered a best practice to use Composer
to manage external software, including PSR-7 interfaces.
First of all, go to the following URL to obtain the latest versions of the PSR-7 interface definitions: https://github.com/php-fig/http-message. The source code is also available. At the time of writing, the following definitions are available:
Interface
Extends
Notes
What the methods handle
MessageInterface
Defines methods common to HTTP messages
Headers, message body (that is, content), and protocol
RequestInterface
MessageInterface
Represents requests generated by a client
The URI, HTTP method, and the request target
ServerRequestInterface
RequestInterface
Represents a request coming to a server from a client
Server and query parameters, cookies, uploaded files, and the parsed body
ResponseInterface
MessageInterface
Represents a response from the server to client
HTTP status code and reason
StreamInterface
Represents the data stream
Streaming behavior such as seek, tell, read, write, and so on
UriInterface
Represents the URI
Scheme (that is, HTTP, HTTPS), host, port, username, password (that is, for FTP), query parameters, path, and fragment
UploadedFileInterface
Deals with uploaded files
File size, media type, moving the file, and filename
Unfortunately, we will need to create concrete classes that implement these interfaces in order to utilize PSR-7. Fortunately, the interface classes are extensively documented internally through a series of comments. We will start with a separate class that contains useful constants:
Tip
Note that we take advantage of a new feature introduced in PHP 7 that allows us to define a constant as an array.
namespace Application\MiddleWare; class Constants { const HEADER_HOST = 'Host'; // host header const HEADER_CONTENT_TYPE = 'Content-Type'; const HEADER_CONTENT_LENGTH = 'Content-Length'; const METHOD_GET = 'get'; const METHOD_POST = 'post'; const METHOD_PUT = 'put'; const METHOD_DELETE = 'delete'; const HTTP_METHODS = ['get','put','post','delete']; const STANDARD_PORTS = [ 'ftp' => 21, 'ssh' => 22, 'http' => 80, 'https' => 443 ]; const CONTENT_TYPE_FORM_ENCODED = 'application/x-www-form-urlencoded'; const CONTENT_TYPE_MULTI_FORM = 'multipart/form-data'; const CONTENT_TYPE_JSON = 'application/json'; const CONTENT_TYPE_HAL_JSON = 'application/hal+json'; const DEFAULT_STATUS_CODE = 200; const DEFAULT_BODY_STREAM = 'php://input'; const DEFAULT_REQUEST_TARGET = '/'; const MODE_READ = 'r'; const MODE_WRITE = 'w'; // NOTE: not all error constants are shown to conserve space const ERROR_BAD = 'ERROR: '; const ERROR_UNKNOWN = 'ERROR: unknown'; // NOTE: not all status codes are shown here! const STATUS_CODES = [ 200 => 'OK', 301 => 'Moved Permanently', 302 => 'Found', 401 => 'Unauthorized', 404 => 'Not Found', 405 => 'Method Not Allowed', 418 => 'I_m A Teapot', 500 => 'Internal Server Error', ]; }
Note
A complete list of HTTP status codes can be found here: https://tools.ietf.org/html/rfc7231#section-6.1.
Next, we will tackle classes that represent value objects used by other PSR-7 classes. For a start, here is the class that represents a URI. In the constructor, we accept a URI string as an argument, and break it down into its component parts using the
parse_url()
function:namespace Application\MiddleWare; use InvalidArgumentException; use Psr\Http\Message\UriInterface; class Uri implements UriInterface { protected $uriString; protected $uriParts = array(); public function __construct($uriString) { $this->uriParts = parse_url($uriString); if (!$this->uriParts) { throw new InvalidArgumentException( Constants::ERROR_INVALID_URI); } $this->uriString = $uriString; }
Note
URI stands for Uniform Resource Indicator. This is what you would see at the top of your browser when making a request. For more information on what comprises a URI, have a look at http://tools.ietf.org/html/rfc3986.
Following the constructor, we define methods to access the component parts of the URI. The scheme represents a PHP wrapper (that is, HTTP, FTP, and so on):
public function getScheme() { return strtolower($this->uriParts['scheme']) ?? ''; }
The authority represents the username (if present), the host, and optionally the port number:
public function getAuthority() { $val = ''; if (!empty($this->getUserInfo())) $val .= $this->getUserInfo() . '@'; $val .= $this->uriParts['host'] ?? ''; if (!empty($this->uriParts['port'])) $val .= ':' . $this->uriParts['port']; return $val; }
User info represents the username (if present) and optionally the password. An example of when a password is used is when accessing an FTP website such as
ftp://username:[email protected]:/path
:public function getUserInfo() { if (empty($this->uriParts['user'])) { return ''; } $val = $this->uriParts['user']; if (!empty($this->uriParts['pass'])) $val .= ':' . $this->uriParts['pass']; return $val; }
Host is the DNS address included in the URI:
public function getHost() { if (empty($this->uriParts['host'])) { return ''; } return strtolower($this->uriParts['host']); }
Port is the HTTP port, if present. You will note if a port is listed in our
STANDARD_PORTS
constant, the return value isNULL
, according to the requirements of PSR-7:public function getPort() { if (empty($this->uriParts['port'])) { return NULL; } else { if ($this->getScheme()) { if ($this->uriParts['port'] == Constants::STANDARD_PORTS[$this->getScheme()]) { return NULL; } } return (int) $this->uriParts['port']; } }
Path is the part of the URI that follows the DNS address. According to PSR-7, this must be encoded. We use the
rawurlencode()
PHP function as it is compliant with RFC 3986. We cannot just encode the entire path, however, as the path separator (that is,/
) would also get encoded! Accordingly, we need to first break it up usingexplode()
, encode the parts, and then reassemble it:public function getPath() { if (empty($this->urlParts['path'])) { return ''; } return implode('/', array_map("rawurlencode", explode('/', $this->urlParts['path']))); }
Next, we define a method to retrieve the
query
string (that is, from$_GET
). These too must be URL-encoded. First, we definegetQueryParams()
, which breaks the query string into an associative array. You will note the reset option in case we wish to refresh the query parameters. We then definegetQuery()
, which takes the array and produces a proper URL-encoded string:public function getQueryParams($reset = FALSE) { if ($this->queryParams && !$reset) { return $this->queryParams; } $this->queryParams = []; if (!empty($this->uriParts['query'])) { foreach (explode('&', $this->uriParts['query']) as $keyPair) { list($param,$value) = explode('=',$keyPair); $this->queryParams[$param] = $value; } } return $this->queryParams; } public function getQuery() { if (!$this->getQueryParams()) { return ''; } $output = ''; foreach ($this->getQueryParams() as $key => $value) { $output .= rawurlencode($key) . '=' . rawurlencode($value) . '&'; } return substr($output, 0, -1); }
After that, we provide a method to return the
fragment
(that is, a#
in the URI), and any part following it:public function getFragment() { if (empty($this->urlParts['fragment'])) { return ''; } return rawurlencode($this->urlParts['fragment']); }
Next, we define a series of
withXXX()
methods, which match thegetXXX()
methods described above. These methods are designed to add, replace, or remove properties associated with the request class (scheme, authority, user info, and so on). In addition, these methods return the current instance that allows us to use these methods in a series of successive calls (often referred to as the fluent interface). We start withwithScheme()
:Note
You will note that an empty argument, according to PSR-7, signals the removal of that property. You will also note that we do not allow a scheme that does not match what is defined in our
Constants::STANDARD_PORTS
array.public function withScheme($scheme) { if (empty($scheme) && $this->getScheme()) { unset($this->uriParts['scheme']); } else { if (isset(STANDARD_PORTS[strtolower($scheme)])) { $this->uriParts['scheme'] = $scheme; } else { throw new InvalidArgumentException(Constants::ERROR_BAD . __METHOD__); } } return $this; }
We then apply similar logic to methods that overwrite, add, or replace the user info, host, port, path, query, and fragment. Note that the
withQuery()
method resets the query parameters array.withHost()
,withPort()
,withPath()
, andwithFragment()
use the same logic, but are not shown to conserve space:public function withUserInfo($user, $password = null) { if (empty($user) && $this->getUserInfo()) { unset($this->uriParts['user']); } else { $this->urlParts['user'] = $user; if ($password) { $this->urlParts['pass'] = $password; } } return $this; } // Not shown: withHost(),withPort(),withPath(),withFragment() public function withQuery($query) { if (empty($query) && $this->getQuery()) { unset($this->uriParts['query']); } else { $this->uriParts['query'] = $query; } // reset query params array $this->getQueryParams(TRUE); return $this; }
Finally, we wrap up the
Application\MiddleWare\Uri
class with__toString()
, which, when the object is used in a string context, returns a proper URI, assembled from$uriParts
. We also define a convenience method,getUriString()
, that simply calls__toString()
:public function __toString() { $uri = ($this->getScheme()) ? $this->getScheme() . '://' : '';
If the
authority
URI part is present, we add it.authority
includes the user information, host, and port. Otherwise, we just appendhost
andport
:if ($this->getAuthority()) { $uri .= $this->getAuthority(); } else { $uri .= ($this->getHost()) ? $this->getHost() : ''; $uri .= ($this->getPort()) ? ':' . $this->getPort() : ''; }
Before adding
path
, we first check whether the first character is/
. If not, we need to add this separator. We then addquery
andfragment
, if present:$path = $this->getPath(); if ($path) { if ($path[0] != '/') { $uri .= '/' . $path; } else { $uri .= $path; } } $uri .= ($this->getQuery()) ? '?' . $this->getQuery() : ''; $uri .= ($this->getFragment()) ? '#' . $this->getFragment() : ''; return $uri; } public function getUriString() { return $this->__toString(); } }
Next, we turn our attention to a class that represents the body of the message. As it is not known how large the body might be, PSR-7 recommends that the body should be treated as a stream. A stream is a resource that allows access to input and output sources in a linear fashion. In PHP, all file commands operate on top of the
Streams
sub-system, so this is a natural fit. PSR-7 formalizes this by way ofPsr\Http\Message\StreamInterface
that defines such methods asread()
,write()
,seek()
, and so on. We now presentApplication\MiddleWare\Stream
that we can use to represent the body of incoming or outgoing requests and/or responses:namespace Application\MiddleWare; use SplFileInfo; use Throwable; use RuntimeException; use Psr\Http\Message\StreamInterface; class Stream implements StreamInterface { protected $stream; protected $metadata; protected $info;
In the constructor, we open the stream using a simple
fopen()
command. We then usestream_get_meta_data()
to get information on the stream. For other details, we create anSplFileInfo
instance:public function __construct($input, $mode = self::MODE_READ) { $this->stream = fopen($input, $mode); $this->metadata = stream_get_meta_data($this->stream); $this->info = new SplFileInfo($input); }
We include two convenience methods that provide access to the resource, as well as access to the
SplFileInfo
instance:public function getStream() { return $this->stream; } public function getInfo() { return $this->info; }
Next, we define low-level core streaming methods:
public function read($length) { if (!fread($this->stream, $length)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function write($string) { if (!fwrite($this->stream, $string)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function rewind() { if (!rewind($this->stream)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function eof() { return eof($this->stream); } public function tell() { try { return ftell($this->stream); } catch (Throwable $e) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function seek($offset, $whence = SEEK_SET) { try { fseek($this->stream, $offset, $whence); } catch (Throwable $e) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function close() { if ($this->stream) { fclose($this->stream); } } public function detach() { return $this->close(); }
We also need to define informational methods that tell us about the stream:
public function getMetadata($key = null) { if ($key) { return $this->metadata[$key] ?? NULL; } else { return $this->metadata; } } public function getSize() { return $this->info->getSize(); } public function isSeekable() { return boolval($this->metadata['seekable']); } public function isWritable() { return $this->stream->isWritable(); } public function isReadable() { return $this->info->isReadable(); }
Following PSR-7 guidelines, we then define
getContents()
and__toString()
in order to dump the contents of the stream:public function __toString() { $this->rewind(); return $this->getContents(); } public function getContents() { ob_start(); if (!fpassthru($this->stream)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } return ob_get_clean(); } }
An important variation of the
Stream
class shown previously isTextStream
that is designed for situations where the body is a string (that is, an array encoded as JSON) rather than a file. As we need to make absolutely certain that the incoming$input
value is of the string data type, we invoke PHP 7 strict types just after the opening tag. We also identify a$pos
property (that is, position) that will emulate a file pointer, but instead point to a position within the string:<?php declare(strict_types=1); namespace Application\MiddleWare; use Throwable; use RuntimeException; use SplFileInfo; use Psr\Http\Message\StreamInterface; class TextStream implements StreamInterface { protected $stream; protected $pos = 0;
Most of the methods are quite simple and self-explanatory. The
$stream
property is the input string:public function __construct(string $input) { $this->stream = $input; } public function getStream() { return $this->stream; } public function getInfo() { return NULL; } public function getContents() { return $this->stream; } public function __toString() { return $this->getContents(); } public function getSize() { return strlen($this->stream); } public function close() { // do nothing: how can you "close" string??? } public function detach() { return $this->close(); // that is, do nothing! }
To emulate streaming behavior,
tell()
,eof()
,seek()
, and so on, work with$pos
:public function tell() { return $this->pos; } public function eof() { return ($this->pos == strlen($this->stream)); } public function isSeekable() { return TRUE; } public function seek($offset, $whence = NULL) { if ($offset < $this->getSize()) { $this->pos = $offset; } else { throw new RuntimeException( Constants::ERROR_BAD . __METHOD__); } } public function rewind() { $this->pos = 0; } public function isWritable() { return TRUE; }
The
read()
andwrite()
methods work with$pos
and substrings:public function write($string) { $temp = substr($this->stream, 0, $this->pos); $this->stream = $temp . $string; $this->pos = strlen($this->stream); } public function isReadable() { return TRUE; } public function read($length) { return substr($this->stream, $this->pos, $length); } public function getMetadata($key = null) { return NULL; } }
The last of the value objects to be presented is
Application\MiddleWare\UploadedFile
. As with the other classes, we first define properties that represent aspects of a file upload:namespace Application\MiddleWare; use RuntimeException; use InvalidArgumentException; use Psr\Http\Message\UploadedFileInterface; class UploadedFile implements UploadedFileInterface { protected $field; // original name of file upload field protected $info; // $_FILES[$field] protected $randomize; protected $movedName = '';
In the constructor, we allow the definition of the name attribute of the file upload form field, as well as the corresponding array in
$_FILES
. We add the last parameter to signal whether or not we want the class to generate a new random filename once the uploaded file is confirmed:public function __construct($field, array $info, $randomize = FALSE) { $this->field = $field; $this->info = $info; $this->randomize = $randomize; }
Next, we create a
Stream
class instance for the temporary or moved file:public function getStream() { if (!$this->stream) { if ($this->movedName) { $this->stream = new Stream($this->movedName); } else { $this->stream = new Stream($info['tmp_name']); } } return $this->stream; }
The
moveTo()
method performs the actual file movement. Note the extensive series of safety checks to help prevent an injection attack. If randomize is not enabled, we use the original user-supplied filename:public function moveTo($targetPath) { if ($this->moved) { throw new Exception(Constants::ERROR_MOVE_DONE); } if (!file_exists($targetPath)) { throw new InvalidArgumentException(Constants::ERROR_BAD_DIR); } $tempFile = $this->info['tmp_name'] ?? FALSE; if (!$tempFile || !file_exists($tempFile)) { throw new Exception(Constants::ERROR_BAD_FILE); } if (!is_uploaded_file($tempFile)) { throw new Exception(Constants::ERROR_FILE_NOT); } if ($this->randomize) { $final = bin2hex(random_bytes(8)) . '.txt'; } else { $final = $this->info['name']; } $final = $targetPath . '/' . $final; $final = str_replace('//', '/', $final); if (!move_uploaded_file($tempFile, $final)) { throw new RuntimeException(Constants::ERROR_MOVE_UNABLE); } $this->movedName = $final; return TRUE; }
We then provide access to the other parameters returned in
$_FILES
from the$info
property. Please note that the return values fromgetClientFilename()
andgetClientMediaType()
should be considered untrusted, as they originate from the outside. We also add a method to return the moved filename:public function getMovedName() { return $this->movedName ?? NULL; } public function getSize() { return $this->info['size'] ?? NULL; } public function getError() { if (!$this->moved) { return UPLOAD_ERR_OK; } return $this->info['error']; } public function getClientFilename() { return $this->info['name'] ?? NULL; } public function getClientMediaType() { return $this->info['type'] ?? NULL; } }
First of all, go to https://github.com/php-fig/http-message/tree/master/src, the GitHub repository for the PSR-7 interfaces, and download them. Create a directory called Psr/Http/Message
in /path/to/source
and places the files there. Alternatively, you can visit https://packagist.org/packages/psr/http-message and install the source code using Composer
. (For instructions on how to obtain and use
Composer
, you can visit https://getcomposer.org/.)
Then, go ahead and define the classes discussed previously, summarized in this table:
Class |
Steps discussed in |
---|---|
|
2 |
|
3 to 16 |
|
17 to 22 |
|
23 to 26 |
|
27 to 31 |
Next, define a chap_09_middleware_value_objects_uri.php
calling program that implements autoloading and uses the appropriate classes. Please note that if you use Composer
, unless otherwise instructed, it will create a folder called vendor
. Composer
also adds its own autoloader, which you are free to use here:
<?php require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\MiddleWare\Uri;
You can then create a Uri
instance and use the with
methods to add parameters. You can then echo the Uri
instance directly as __toString()
is defined:
$uri = new Uri(); $uri->withScheme('https') ->withHost('localhost') ->withPort('8080') ->withPath('chap_09_middleware_value_objects_uri.php') ->withQuery('param=TEST'); echo $uri;
Here is the expected result:
Next, create a directory called uploads
from /path/to/source/for/this/chapter
. Go ahead and define another calling program, chap_09_middleware_value_objects_file_upload.php
, that sets up autoloading and uses the appropriate classes:
<?php define('TARGET_DIR', __DIR__ . '/uploads'); require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\MiddleWare\UploadedFile;
Inside a try...catch
block, check to see whether any files were uploaded. If so, loop through $_FILES
and create UploadedFile
instances where tmp_name
is set. You can then use the moveTo()
method to move the files to TARGET_DIR
:
try { $message = ''; $uploadedFiles = array(); if (isset($_FILES)) { foreach ($_FILES as $key => $info) { if ($info['tmp_name']) { $uploadedFiles[$key] = new UploadedFile($key, $info, TRUE); $uploadedFiles[$key]->moveTo(TARGET_DIR); } } } } catch (Throwable $e) { $message = $e->getMessage(); } ?>
In the view logic, display a simple file upload form. You could also use phpinfo(
) to display information about what was uploaded:
<form name="search" method="post" enctype="<?= Constants::CONTENT_TYPE_MULTI_FORM ?>"> <table class="display" cellspacing="0" width="100%"> <tr><th>Upload 1</th><td><input type="file" name="upload_1" /></td></tr> <tr><th>Upload 2</th><td><input type="file" name="upload_2" /></td></tr> <tr><th>Upload 3</th><td><input type="file" name="upload_3" /></td></tr> <tr><th> </th><td><input type="submit" /></td></tr> </table> </form> <?= ($message) ? '<h1>' . $message . '</h1>' : ''; ?>
Next, if there were any uploaded files, you can display information on each one. You can also use getStream()
followed by getContents()
to display each file (assuming you're using short text files):
<?php if ($uploadedFiles) : ?> <table class="display" cellspacing="0" width="100%"> <tr> <th>Filename</th><th>Size</th> <th>Moved Filename</th><th>Text</th> </tr> <?php foreach ($uploadedFiles as $obj) : ?> <?php if ($obj->getMovedName()) : ?> <tr> <td><?= htmlspecialchars($obj->getClientFilename()) ?></td> <td><?= $obj->getSize() ?></td> <td><?= $obj->getMovedName() ?></td> <td><?= $obj->getStream()->getContents() ?></td> </tr> <?php endif; ?> <?php endforeach; ?> </table> <?php endif; ?> <?php phpinfo(INFO_VARIABLES); ?>
For more information on PSR, please have a look at https://en.wikipedia.org/wiki/PHP_Standard_Recommendation
For information on PSR-7 specifically, here is the official description: http://www.php-fig.org/psr/psr-7/
For information on PHP streams, take a look at http://php.net/manual/en/book.stream.php