PSR-17 Meta Document

HTTP Factories Meta

1. Summary

The purpose of this PSR is to provide factory interfaces that define methods to create PSR-7 objects.

2. Why Bother?

The current specification for PSR-7 allows for most objects to be modified by creating immutable copies. However, there are two notable exceptions:

  • StreamInterface is a mutable object based on a resource that only allows the resource to be written to when the resource is writable.
  • UploadedFileInterface is a read-only object based on a resource that offers no modification capabilities.

The former is a significant pain point for PSR-7 middleware, as it can leave the response in an incomplete state. If the stream attached to the response body is not seekable or not writable, there is no way to recover from an error condition in which the body has already been written to.

This scenario can be avoided by providing a factory to create new streams. Due to the lack of a formal standard for HTTP object factories, a developer must rely on a specific vendor implementation in order to create these objects.

Another pain point is when writing re-usable middleware or request handlers. In such cases, package authors may need to create and return a response. However, creating discrete instances then ties the package to a specific PSR-7 implementation. If these packages rely on the request factory interface instead, they can remain agnostic of the PSR-7 implementation.

Creating a formal standard for factories will allow developers to avoid dependencies on specific implementations while having the ability to create new objects when necessary.

3. Scope

3.1 Goals

  • Provide a set of interfaces that define methods to create PSR-7 compatible objects.

3.2 Non-Goals

  • Provide a specific implementation of PSR-7 factories.

4. Approaches

4.1 Chosen Approach

The factory method definition has been chosen based on whether or not the object can be modified after instantiation. For interfaces that cannot be modified, all of the object properties must be defined at the time of instantiation.

In the case of UriInterface a complete URI may be passed for convenience.

The method names used will not conflict. This allows for a single class to implement multiple interfaces when appropriate.

4.2 Existing Implementations

All of the current implementations of PSR-7 have defined their own requirements. In most cases, the required parameters are the same or less strict than the proposed factory methods.

4.2.1 Diactoros

Diactoros was one of the first HTTP Messages implementations for server usage, and was developed parallel to the PSR-7 specification.

  • Request No required parameters, method and URI default to null.
  • Response No required parameters, status code defaults to 200.
  • ServerRequest No required parameters. Contains a separate ServerRequestFactory for creating requests from globals.
  • Stream Requires string|resource $stream for the body.
  • UploadedFile Requires string|resource $streamOrFile, int $size, int $errorStatus. Error status must be a PHP upload constant.
  • Uri No required parameters, string $uri is empty by default.

Overall this approach is quite similar to the proposed factories. In some cases, more options are given by Diactoros which are not required for a valid object. The proposed uploaded file factory allows for size and error status to be optional.

4.2.2 Guzzle

Guzzle is an HTTP Messages implementation that focuses on client usage.

  • Request Requires both string $method and string|UriInterface $uri.
  • Response No required parameters, status code defaults to 200.
  • Stream Requires resource $stream for the body.
  • Uri No required parameters, string $uri is empty by default.

Being geared towards client usage, Guzzle does not contain a ServerRequest or UploadedFile implementation.

Overall this approach is also quite similar to the proposed factories. One notable difference is that Guzzle requires streams to be constructed with a resource and does not allow a string. However, it does contain a helper function stream_for that will create a stream from a string of content and a function try_fopen that will create a resource from a file path.

4.2.3 Slim

Slim is a micro-framework that makes use of HTTP Messages from version 3.0 forward.

  • Request Requires string $method, UriInterface $uri, HeadersInterface $headers, array $cookies, array $serverParams, and StreamInterface $body. Contains a factory method createFromEnvironment(Environment $environment) that is framework specific but analogous to the proposed createServerRequestFromArray.
  • Response No required parameters, status code defaults to 200.
  • Stream Requires resource $stream for the body.
  • UploadedFile Requires string $file for the source file. Contains a factory method parseUploadedFiles(array $uploadedFiles) for creating an array of UploadedFile instances from $_FILES or similar format. Also contains a factory method createFromEnvironment(Environment $env) that is framework specific and makes use of parseUploadedFiles.
  • Uri Requires string $scheme and string $host. Contains a factory method createFromString($uri) that can be used to create a Uri from a string.

Being geared towards server usage only, Slim does not contain an implementation of Request. The implementation listed above is an implementation of ServerRequest.

Of the compared approaches, Slim is most different from the proposed factories. Most notably, the Request implementation contains requirements specific to the framework that are not defined in HTTP Messages specification. The factory methods that are included are generally similar with the proposed factories.

4.3 Potential Issues

The most difficult task in establishing this standard will be defining the method signatures for the interfaces. As there is no clear declaration in PSR-7 as to what values are explicitly required, the properties that are read-only must be inferred based on whether the interfaces have methods to copy-and-modify the object.

5. Design Decisions

5.1 Why PHP 7?

While PSR-7 does not target PHP 7, the authors of this specification note that, at the time of writing (April 2018), PHP 5.6 stopped receiving bugfixes 15 months ago, and will no longer receive security patches in 8 months; PHP 7.0 itself will stop receiving security fixes in 7 months (see the PHP supported versions document for current support details). Since specifications are meant to be long-term, the authors feel the specification should target versions that will be supported for the foreseeable future; PHP 5 will not. As such, from a security standpoint, targeting anything under PHP 7 is a disservice to users, as doing so would be tacit approval of usage of unsupported PHP versions.

Additionally, and equally importantly, PHP 7 gives us the ability to provide return type hints to interfaces we define. This guarantees a strong, predicatable contract for end users, as they can assume that the values returned by implementations will be exactly what they expect.

5.2 Why multiple interfaces?

Each proposed interface is (primarily) responsible for producing one PSR-7 type. This allows consumers to typehint on exactly what they need: if they need a response, they typehint on ResponseFactoryInterface; if they need a URI, they typehint on UriFactoryInterface. In this way, users can be granular about what they need.

Doing so also allows application developers to provide anonymous implementations based on the PSR-7 implementation they are using, producing only the instances they need for the specific context. This reduces boilerplate; developers do not need to write stubs for unused methods.

5.3 Why does the $reasonPhrase argument to the ResponseFactoryInterface exist?

ResponseFactoryInterface::createResponse() includes an optional string argument, $reasonPhrase. In the PSR-7 specification, you can only provide a reason phrase at the same time you provide a status code, as the two are related pieces of data. The authors of this specification have chosen to mimic the PSR-7 ResponseInterface::withStatus() signature to ensure both sets of data may be present in the response created.

5.4 Why does the $serverParams argument to the ServerRequestFactoryInterface exist?

ServerRequestFactoryInterface::createServerRequest() includes an optional $serverParams array argument. The reason this is provided is to ensure that an instance can be created with the server params populated. Of the data accessible via the ServerRequestInterface, the only data that does not have a mutator method is the one corresponding to the server params. As such, this data MUST be provided at initial creation. For this reason, it exists as an argument to the factory method.

5.5 Why is there no factory for creating a ServerRequestInterface from superglobals?

The primary use case of ServerRequestFactoryInterface is for creating a new ServerRequestInterface instance from known data. Any solution around marshaling data from superglobals assumes that:

  • superglobals are present
  • superglobals follow a specific structure

These two assumptions are not always true. When using asynchronous systems such as Swoole, ReactPHP, and others:

  • will not populate standard superglobals such as $_GET, $_POST, $_COOKIE, and $_FILES
  • will not populate $_SERVER with the same elements as a standard SAPI (such as mod_php, mod-cgi, and mod-fpm)

Moreover, different standard SAPIs provide different information to $_SERVER and access to request headers, requiring different approaches for initial population of the request.

As such, designing an interface for population of an instance from superglobals is out of scope of this specification, and should largely be implementation-specfic.

5.6 Why does RequestFactoryInterface::createRequest allow a string URI?

The primary use case of RequestFactoryInterface is to create a request, and the only required values for any request are the request method and a URI. While RequestFactoryInterface::createRequest() can accept a UriInterface instance, it also allows a string.

The rationale is two-fold. First, the majority use case is to create a request instance; creation of the URI instance is secondary. Requiring a UriInterface means users would either need to also have access to a UriFactoryInterface, or the RequestFactoryInterface would have a hard requirement on a UriFactoryInterface. The first complicates usage for consumers of the factory, the second complicates usage for either developers of the factory, or those creating the factory instance.

Second, UriFactoryInterface provides exactly one way to create a UriInterface instance, and that is from a string URI. If creation of the URI is based on a string, there's no reason for the RequestFactoryInterface not to allow the same semantics. Additionally, every PSR-7 implementation surveyed at the time this proposal was developed allowed a string URI when creating a RequestInterface instance, as the value was then passed to whatever UriInterface implementation they provided. As such, accepting a string is expedient and follows existing semantics.

6. People

This PSR was produced by a FIG Working Group with the following members:

The working group would also like to acknowledge the contributions of:

7. Votes

8. Relevant Links

Note: Order descending chronologically.