Book Image

PHP 7 Programming Cookbook

By : Doug Bierer
Book Image

PHP 7 Programming Cookbook

By: Doug Bierer

Overview of this book

PHP 7 comes with a myriad of new features and great tools to optimize your code and make your code perform faster than in previous versions. Most importantly, it allows you to maintain high traffic on your websites with low-cost hardware and servers through a multithreading web server. This book demonstrates intermediate to advanced PHP techniques with a focus on PHP 7. Each recipe is designed to solve practical, real-world problems faced by PHP developers like yourself every day. We also cover new ways of writing PHP code made possible only in version 7. In addition, we discuss backward-compatibility breaks and give you plenty of guidance on when and where PHP 5 code needs to be changed to produce the correct results when running under PHP 7. This book also incorporates the latest PHP 7.x features. By the end of the book, you will be equipped with the tools and skills required to deliver efficient applications for your websites and enterprises.
Table of Contents (22 chapters)
PHP 7 Programming Cookbook
Credits
Foreword
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Developing a PSR-7 Request class


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.

How to do it...

  1. First, be sure to define classes to represent the Uri, Stream, and UploadedFile value objects, as described in the previous recipe.

  2. Now we are ready to define the core 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();
  3. Next, we have the 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;
    }
  4. PSR-7 recommends that headers should be viewed as case-insensitive. Accordingly, we define a 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;
    }
  5. 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 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;
    }
  6. 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);
    }
  7. Implementing 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);
      }
    }
  8. 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. 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;
    }
  9. 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();
      }
    }
  10. 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;
    }
  11. 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 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;
      }
    }
    
    }
  12. 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 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 instance
  13. All properties in the constructor default to NULL, 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;
    }
  14. 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 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;
    }
  15. Our 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;
    }
  16. Finally, we have a 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;
      }
    }
  17. The 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;
  18. 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;
    }
  19. 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 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;
    }
  20. 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;
    }
  21. 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 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;
    }
  22. 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;
    }
    
    }
  23. 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;
    }

How it works...

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

Application\MiddleWare\Message

2 to 9

Application\MiddleWare\Request

10 to 14

Application\MiddleWare\ServerRequest

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:

See also