Table of Contents for
PHP 7: Real World Application Development

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition PHP 7: Real World Application Development by Branko Ajzele Published by Packt Publishing, 2016
  1. Cover
  2. Table of Contents
  3. PHP 7: Real World Application Development
  4. PHP 7: Real World Application Development
  5. PHP 7: Real World Application Development
  6. Credits
  7. Preface
  8. What you need for this learning path
  9. Who this learning path is for
  10. Reader feedback
  11. Customer support
  12. 1. Module 1
  13. 1. Building a Foundation
  14. PHP 7 installation considerations
  15. Using the built-in PHP web server
  16. Defining a test MySQL database
  17. Installing PHPUnit
  18. Implementing class autoloading
  19. Hoovering a website
  20. Building a deep web scanner
  21. Creating a PHP 5 to PHP 7 code converter
  22. 2. Using PHP 7 High Performance Features
  23. Understanding the abstract syntax tree
  24. Understanding differences in parsing
  25. Understanding differences in foreach() handling
  26. Improving performance using PHP 7 enhancements
  27. Iterating through a massive file
  28. Uploading a spreadsheet into a database
  29. Recursive directory iterator
  30. 3. Working with PHP Functions
  31. Developing functions
  32. Hinting at data types
  33. Using return value data typing
  34. Using iterators
  35. Writing your own iterator using generators
  36. 4. Working with PHP Object-Oriented Programming
  37. Developing classes
  38. Extending classes
  39. Using static properties and methods
  40. Using namespaces
  41. Defining visibility
  42. Using interfaces
  43. Using traits
  44. Implementing anonymous classes
  45. 5. Interacting with a Database
  46. Using PDO to connect to a database
  47. Building an OOP SQL query builder
  48. Handling pagination
  49. Defining entities to match database tables
  50. Tying entity classes to RDBMS queries
  51. Embedding secondary lookups into query results
  52. Implementing jQuery DataTables PHP lookups
  53. 6. Building Scalable Websites
  54. Creating a generic form element generator
  55. Creating an HTML radio element generator
  56. Creating an HTML select element generator
  57. Implementing a form factory
  58. Chaining $_POST filters
  59. Chaining $_POST validators
  60. Tying validation to a form
  61. 7. Accessing Web Services
  62. Converting between PHP and XML
  63. Creating a simple REST client
  64. Creating a simple REST server
  65. Creating a simple SOAP client
  66. Creating a simple SOAP server
  67. 8. Working with Date/Time and International Aspects
  68. Using emoticons or emoji in a view script
  69. Converting complex characters
  70. Getting the locale from browser data
  71. Formatting numbers by locale
  72. Handling currency by locale
  73. Formatting date/time by locale
  74. Creating an HTML international calendar generator
  75. Building a recurring events generator
  76. Handling translation without gettext
  77. 9. Developing Middleware
  78. Authenticating with middleware
  79. Using middleware to implement access control
  80. Improving performance using the cache
  81. Implementing routing
  82. Making inter-framework system calls
  83. Using middleware to cross languages
  84. 10. Looking at Advanced Algorithms
  85. Using getters and setters
  86. Implementing a linked list
  87. Building a bubble sort
  88. Implementing a stack
  89. Building a binary search class
  90. Implementing a search engine
  91. Displaying a multi-dimensional array and accumulating totals
  92. 11. Implementing Software Design Patterns
  93. Creating an array to object hydrator
  94. Building an object to array hydrator
  95. Implementing a strategy pattern
  96. Defining a mapper
  97. Implementing object-relational mapping
  98. Implementing the Pub/Sub design pattern
  99. 12. Improving Web Security
  100. Filtering $_POST data
  101. Validating $_POST data
  102. Safeguarding the PHP session
  103. Securing forms with a token
  104. Building a secure password generator
  105. Safeguarding forms with a CAPTCHA
  106. Encrypting/decrypting without mcrypt
  107. 13. Best Practices, Testing, and Debugging
  108. Using Traits and Interfaces
  109. Universal exception handler
  110. Universal error handler
  111. Writing a simple test
  112. Writing a test suite
  113. Generating fake test data
  114. Customizing sessions using session_start parameters
  115. A. Defining PSR-7 Classes
  116. Implementing PSR-7 value object classes
  117. Developing a PSR-7 Request class
  118. Defining a PSR-7 Response class
  119. 2. Module 2
  120. 1. Setting Up the Environment
  121. Setting up Debian or Ubuntu
  122. Setting up CentOS
  123. Setting up Vagrant
  124. Summary
  125. 2. New Features in PHP 7
  126. New operators
  127. Uniform variable syntax
  128. Miscellaneous features and changes
  129. Summary
  130. 3. Improving PHP 7 Application Performance
  131. HTTP server optimization
  132. HTTP persistent connection
  133. Content Delivery Network (CDN)
  134. CSS and JavaScript optimization
  135. Full page caching
  136. Varnish
  137. The infrastructure
  138. Summary
  139. 4. Improving Database Performance
  140. Storage engines
  141. The Percona Server - a fork of MySQL
  142. MySQL performance monitoring tools
  143. Percona XtraDB Cluster (PXC)
  144. Redis – the key-value cache store
  145. Memcached key-value cache store
  146. Summary
  147. 5. Debugging and Profiling
  148. Profiling with Xdebug
  149. PHP DebugBar
  150. Summary
  151. 6. Stress/Load Testing PHP Applications
  152. ApacheBench (ab)
  153. Siege
  154. Load testing real-world applications
  155. Summary
  156. 7. Best Practices in PHP Programming
  157. Test-driven development (TDD)
  158. Design patterns
  159. Service-oriented architecture (SOA)
  160. Being object-oriented and reusable always
  161. PHP frameworks
  162. Version control system (VCS) and Git
  163. Deployment and Continuous Integration (CI)
  164. Summary
  165. A. Tools to Make Life Easy
  166. Git – A version control system
  167. Grunt watch
  168. Summary
  169. B. MVC and Frameworks
  170. Laravel
  171. Lumen
  172. Apigility
  173. Summary
  174. 3. Module 3
  175. 1. Ecosystem Overview
  176. Summary
  177. 2. GoF Design Patterns
  178. Structural patterns
  179. Behavioral patterns
  180. Summary
  181. 3. SOLID Design Principles
  182. Open/closed principle
  183. Liskov substitution principle
  184. Interface Segregation Principle
  185. Dependency inversion principle
  186. Summary
  187. 4. Requirement Specification for a Modular Web Shop App
  188. Wireframing
  189. Defining a technology stack
  190. Summary
  191. 5. Symfony at a Glance
  192. Creating a blank project
  193. Using Symfony console
  194. Controller
  195. Routing
  196. Templates
  197. Forms
  198. Configuring Symfony
  199. The bundle system
  200. Databases and Doctrine
  201. Testing
  202. Validation
  203. Summary
  204. 6. Building the Core Module
  205. Dependencies
  206. Implementation
  207. Unit testing
  208. Functional testing
  209. Summary
  210. 7. Building the Catalog Module
  211. Dependencies
  212. Implementation
  213. Unit testing
  214. Functional testing
  215. Summary
  216. 8. Building the Customer Module
  217. Dependencies
  218. Implementation
  219. Unit testing
  220. Functional testing
  221. Summary
  222. 9. Building the Payment Module
  223. Dependencies
  224. Implementation
  225. Unit testing
  226. Functional testing
  227. Summary
  228. 10. Building the Shipment Module
  229. Dependencies
  230. Implementation
  231. Unit testing
  232. Functional testing
  233. Summary
  234. 11. Building the Sales Module
  235. Dependencies
  236. Implementation
  237. Unit testing
  238. Functional testing
  239. Summary
  240. 12. Integrating and Distributing Modules
  241. Understanding GitHub
  242. Understanding Composer
  243. Understanding Packagist
  244. Summary
  245. Bibliography
  246. Index

Implementing PSR-7 value object classes

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.

Getting ready

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.

How to do it...

  1. 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

  2. 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.

  3. 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.

  4. 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']) ?? '';
    }
  5. 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;
    }
  6. 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: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;
    }
  7. Host is the DNS address included in the URI:
    public function getHost()
    {
      if (empty($this->uriParts['host'])) {
        return '';
      }
      return strtolower($this->uriParts['host']);
    }
  8. Port is the HTTP port, if present. You will note if a port is listed in our 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'];
      }
    }
  9. 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 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'])));
    }
  10. Next, we define a method to retrieve the 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);
    }
  11. 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']);
    }
  12. Next, we define a series of 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():

    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;
    }
  13. 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(), 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;
    }
  14. 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() . '://' : '';
  15. If the 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() : '';
    }
  16. Before adding 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();
    }
    
    }

    Note

    Note the use of string dereferencing (that is, $path[0]), now part of PHP 7.

  17. 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 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;
  18. In the constructor, we open the stream using a simple 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);
    }

    Note

    The reason why we chose fopen() over the more modern SplFileObject is that the latter does not allow direct access to the inner file resource object, and is therefore useless for this application.

  19. 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;
    }
  20. 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();
    }
  21. 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();
    }
  22. 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();
    }
    }
  23. An important variation of the 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;
  24. 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!
    }
  25. 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;
    }
  26. The 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;
    }
    
    }
  27. 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 = '';
  28. 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;
    }
  29. 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;
    }
  30. 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;
    }
  31. We then provide access to the other parameters returned in $_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;
    }
    
    }

How it works...

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

Application\MiddleWare\Constants

2

Application\MiddleWare\Uri

3 to 16

Application\MiddleWare\Stream

17 to 22

Application\MiddleWare\TextStream

23 to 26

Application\MiddleWare\UploadedFile

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:

How it works...

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>&nbsp;</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:

How it works...

See also