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.
|
Interface |
Extends |
Notes |
What the methods handle |
|---|---|---|---|
|
|
Defines methods common to HTTP messages |
Headers, message body (that is, content), and protocol | |
|
|
|
Represents requests generated by a client |
The URI, HTTP method, and the request target |
|
|
|
Represents a request coming to a server from a client |
Server and query parameters, cookies, uploaded files, and the parsed body |
|
|
|
Represents a response from the server to client |
HTTP status code and reason |
|
|
Represents the data stream |
Streaming behavior such as seek, tell, read, write, and so on | |
|
|
Represents the URI |
Scheme (that is, HTTP, HTTPS), host, port, username, password (that is, for FTP), query parameters, path, and fragment | |
|
|
Deals with uploaded files |
File size, media type, moving the file, and filename |
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',
];
}A complete list of HTTP status codes can be found here: https://tools.ietf.org/html/rfc7231#section-6.1.
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;
}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.
public function getScheme()
{
return strtolower($this->uriParts['scheme']) ?? '';
}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;
}ftp://username:password@website.com:/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;
}public function getHost()
{
if (empty($this->uriParts['host'])) {
return '';
}
return strtolower($this->uriParts['host']);
}STANDARD_PORTS constant, the return value is NULL, 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'];
}
}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 using explode(), 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'])));
}query string (that is, from $_GET). These too must be URL-encoded. First, we define getQueryParams(), 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 define getQuery(), 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);
}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']);
}withXXX() methods, which match the getXXX() 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 with withScheme():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;
}withQuery() method resets the query parameters array. withHost(), withPort(), withPath(), and withFragment() 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;
}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() . '://' : '';authority URI part is present, we add it. authority includes the user information, host, and port. Otherwise, we just append host and port:if ($this->getAuthority()) {
$uri .= $this->getAuthority();
} else {
$uri .= ($this->getHost()) ? $this->getHost() : '';
$uri .= ($this->getPort())
? ':' . $this->getPort() : '';
}path, we first check whether the first character is /. If not, we need to add this separator. We then add query and fragment, 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();
}
}Streams sub-system, so this is a natural fit. PSR-7 formalizes this by way of Psr\Http\Message\StreamInterface that defines such methods as read(), write(), seek(), and so on. We now present Application\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;fopen() command. We then use stream_get_meta_data() to get information on the stream. For other details, we create an SplFileInfo 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);
}SplFileInfo instance:public function getStream()
{
return $this->stream;
}
public function getInfo()
{
return $this->info;
}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();
}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();
}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();
}
}Stream class shown previously is TextStream 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;$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!
}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;
}read() and write() 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;
}
}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 = '';$_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;
}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;
}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;
}$_FILES from the $info property. Please note that the return values from getClientFilename() and getClientMediaType() 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); ?>Here is how the output might appear:
