This package provides interoperable interfaces to encapsulate, buffer, and send server-side response values in PHP 8.4 or later, in order to reduce the global mutable state and inspection problems that exist with the PHP response-sending functions. It reflects, resolves, and refines the common practices of over a dozen different userland projects.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174).
This package defines the following interfaces:
-
ResponseStruct encapsulates the server response status line, headers, and body.
-
ResponseStatusLineStruct encapsulates status line for the response, including the HTTP version and the status code.
-
ResponseHeadersCollection encapsulates the headers for the response, including affordances for cookie management.
-
ResponseBodyContent affords management of non-string, resource-intensive, or response-modifying content.
-
ResponseCookieHelperService affords conversion of cookie representations to and from strings and arrays.
Response-Interop also defines a marker interface, ResponseThrowable, for marking an Exception as response-related.
Finally, Response-Interop defines a ResponseTypeAliases interface with PHPStan types to aid static analysis.
Notes:
-
Response-Interop interfaces are mutable. None of the researched projects afforded readonly or immutable response objects.
-
Whereas PHP sends-as-it-goes, Response-Interop collects-then-sends. For example, the PHP sends a header at the moment the
header()function is called. In contrast, Response-Interop buffers the header specifications, and sends them only when thesendResponse()method is called.
The ResponseStruct interface encapsulates the server response.
-
Properties:
-
public ResponseStatusLineStruct $statusLine { get; set; }
- The status line for the response.
-
public ResponseHeadersCollection $headers { get; set; }
- The headers for the response, including affordances for cookie management.
-
public Stringable|ResponseBodyContent|string $body { get; set; }
-
The body for the response.
-
Notes:
- The
$bodymay be a string, a Stringable object, or some other content source. The single most common kind of body content is an in-memory string. However, there are other common kinds of content, such as when sending a large file for download, at which point a ResponseBodyContent instance affords improved resource management and response modification.
- The
-
-
-
Methods:
-
public function sendResponse() : void;
-
Sends the response.
-
Directives:
- If the
$bodyis an instance of ResponseBodyContent, implementations MUST call itsprepareResponse()method before sending anything. - Implementations MAY check to see if the response can be sent; when doing so, implementations MUST throw a ResponseThrowable if the response cannot be sent.
- If the
-
-
The ResponseStatusLineStruct interface encapsulates the status line for the server response.
-
Properties:
-
public response_http_version_string $httpVersion { get; set; }
-
The HTTP version string for the response; e.g.
'1.1'. -
Directives:
- Implementations MAY validate this value; implementations doing so MUST throw a ResponseThrowable on invalidity.
-
-
public response_status_code_int $statusCode { get; set; }
-
The status code for the response; e.g.
200. -
Directives:
- Implementations MAY validate this value; implementations doing so MUST throw a ResponseThrowable on invalidity.
-
-
-
Methods:
-
public function sendResponseStatusLine() : void;
- Sends the response status line.
-
The ResponseHeadersCollection interface encapsulates the headers for the response, including affordances for cookie management.
-
Directives:
-
Implementations MUST normalize each
response_header_field_stringargument to lower case. -
Implementations MUST validate each
response_header_field_stringargument, and MUST throw a ResponseThrowable on invalidity. -
Implementations MUST throw a ResponseThrowable if a
response_header_value_stringargument is blank. -
Implementations MAY validate other method arguments; when doing so, implementations MUST throw a ResponseThrowable on invalidity.
-
-
Notes:
-
Header fields are retained in lower case. This standardizes expectations around header field lookups.
-
Header fields must be valid. In general, this means the header field must match the regular expression
/^:?[a-z][a-z0-9-]+$/. -
Header values cannot be blank. If
trim($value) === ''then the$valueis blank.
-
-
Methods:
-
public function setHeader( response_header_field_string $field, response_header_value_string $value, ) : void;
-
Sets the
$valuefor a header, replacing all existing$values for that header. -
Directives:
- If the normalized
$fieldisset-cookie, implementations MUST retain the$valuesuch that the cookie can be retrieved by name (e.g. viagetCookieAsArray()orgetCookieAsString()); if the cookie cannot be retained in such a way, implementations MUST throw a ResponseThrowable.
- If the normalized
-
-
public function addHeader( response_header_field_string $field, response_header_value_string $value, ) : void;
-
Adds a
$valuefor a header, keeping all previous$values for that header. -
Directives:
-
If there are no existing
$values for the header, implementations MUST behave as ifsetHeader()was called with the same$fieldand$value. -
Implementations MUST retain each added
$valueseparately from all previous$values. -
If the normalized
$fieldisset-cookie, implementations MUST retain the$valuesuch that can be retrieved by name (e.g. viagetCookieAsArray()orgetCookieAsString()); if the cookie cannot be retained in such a way, implementations MUST throw a ResponseThrowable.
-
-
-
public function hasHeader(response_header_field_string $field) : bool;
- Reports if a header exists.
-
public function getHeader( string $field, ) : null|response_header_value_string|response_header_value_string[];
-
Returns the
$value(s) for a header. -
Directives:
-
Implementations MUST return
nullif there is no$valuefor the header. -
Implementations MUST use a string if there is only one
$valuefor the header. -
Implementations MUST use an array of strings if there is more than one
$valuefor the header.
-
-
Notes:
-
This method returns a string if there is only one value. This is to support the most common case for most response headers; i.e., a single value. This reduces the occurrence of the idiom
getHeader('field-name')[0]. If consumers require the return to be an array regardless of the number of values, they may cast the return to(array). -
Cookies are always returned as strings. This is to keep the return types consistent for all headers, such that the returned values can be used directly in
header()calls if needed. In practical terms, the implementation should usegetCookiesAsStrings()as the source forset-cookievalues.
-
-
-
public function unsetHeader(response_header_field_string $field) : void;
- Removes a header entirely.
-
public function hasHeaders() : bool;
- Reports if any headers exist.
-
public function getHeaders() : array<response_header_field_string,response_header_value_string|response_header_value_string[]>;
-
Returns an array of all
$values of all headers, keyed by the header field. -
Directives:
-
Implementations MUST use a string if there is only one
$valuefor a header. -
Implementations MUST use an array of strings if there is more than one
$valuefor a header.
-
-
Notes:
- Cookies are always returned as strings. This is to keep the
return types consistent for all headers, such that the returned
values can be used directly in
header()calls if needed. In practical terms, the implementation should usegetCookiesAsStrings()as the source forset-cookievalues.
- Cookies are always returned as strings. This is to keep the
return types consistent for all headers, such that the returned
values can be used directly in
-
-
public function unsetHeaders() : void;
- Removes all headers.
-
public function setCookie( response_cookie_name_string $name, response_cookie_value_string $value, response_cookie_attributes_array $attributes, ) : void;
-
Sets a named cookie as a
response_cookie_array, replacing any existing cookie of the same name. -
Directives:
-
Implementations MUST retain the cookie such that it can be retrieved by name (e.g. via
getCookieAsArray()orgetCookieAsString()). -
Implementations MUST NOT encode the arguments.
-
-
-
public function hasCookie(response_cookie_name_string $name) : bool;
- Reports if a named cookie exists.
-
public function getCookieAsArray( response_cookie_name_string $name, ) : ?response_cookie_array;
-
Returns a named cookie as a
response_cookie_array, ornullif it does not exist. -
Directives:
- Implementations retaining the cookie as a
response_header_value_stringMUST convert it to aresponse_cookie_arrayvia the ResponseCookieHelperService methodparseResponseCookieString().
- Implementations retaining the cookie as a
-
-
public function getCookieAsString( response_cookie_name_string $name, ) : ?response_header_value_string;
-
Returns a named cookie as a string suitable for use as a header value, or or
nullif it does not exist. -
Directives:
- Implementations retaining the cookie as a
response_cookie_arrayMUST convert it to aresponse_header_value_stringvia the ResponseCookieHelperService methodcomposeResponseCookieString().
- Implementations retaining the cookie as a
-
-
public function unsetCookie(response_cookie_name_string $name) : void;
-
Removes a named cookie.
-
Notes:
- This is not the same as deleting a cookie from the browser. To do that, consumers need to send a named cookie with an expiration date in the past.
-
-
public function hasCookies() : bool;
- Reports if any cookies exist.
-
public function getCookiesAsArrays() : array<response_cookie_name_string,response_cookie_array>;
-
Returns all cookies as an array where each key is the cookie name and each value is its
response_cookie_array. -
Directives:
- Implementations retaining a cookie as a
response_header_value_stringMUST represent that cookie as if it had been retrieved via thegetCookieAsArray()method.
- Implementations retaining a cookie as a
-
-
public function getCookiesAsStrings() : array<response_cookie_name_string,response_header_value_string>;
-
Returns all cookies as an array where each key is the cookie name and each value is its
response_header_value_string. -
Directives:
- Implementations retaining a cookie as a
response_cookie_arrayMUST represent that cookie as if it had been retrieved via thegetCookieAsString()method.
- Implementations retaining a cookie as a
-
-
public function unsetCookies() : void;
-
Removes the
set-cookieheader entirely. -
Notes:
- This is not the same as deleting all cookies from the browser. To do that, consumers need to send named cookies with expiration dates in the past.
-
-
public function sendResponseHeaders() : void;
-
Sends all headers.
-
Directives:
- Implementations SHOULD send header fields in lower case, but MAY send header fields in some other RFC-approved case.
-
-
The ResponseBodyContent interface affords management and sending of non-string, resource-intensive, or response-modifying content.
-
Notes:
-
Not all content is easily managed as an in-memory string. Although an in-memory string is the single most common kind of body content, there are many other kinds of content that a response may represent. The body may be generated from an array, object, file, stream, or some other source. Many of these sources might best be converted only as the response is being sent; for example, when sending a file to download, it may be wise to send the file in chunks instead ofreading the whole file into memory.
-
Setting and getting content is implementation-specific. Because of the varied, domain-specific, and sometimes proprietary requirements of non-string content, there can be no generic setter or getter interface here. Implementors are encouraged to publish their implementations for shared use.
-
-
Methods:
-
public function prepareResponse(ResponseStruct $response) : void;
-
Modifies the
$responseas appropriate for the body content. -
Notes:
-
The content source or implementation may carry information relevant to the rest of the response. These may include values related to:
- the
content-typeheader and itscharsetparameter - the
content-encodingheader - an
etagstring - a
last-modifiedtime - the status code
- and so on.
Such information might best be recorded in the response only at the time of sending. This method affords the opportunity to do so in a content-specific fashion.
- the
-
-
-
public function sendResponseBody() : void;
-
Sends the body content.
-
Notes:
- Sending logic is content- and implementation-specific. Different
kinds of content require different sending mechanisms. Some kinds may
be amenable to a simple
echo, others may require specific encoding, and yet others may require more involved resource or stream handling.
- Sending logic is content- and implementation-specific. Different
kinds of content require different sending mechanisms. Some kinds may
be amenable to a simple
-
-
The ResponseThrowable interface extends Throwable to mark an Exception as response-related. It adds no class members.
- Methods:
The ResponseTypeAliases interface defines these PHPStan type aliases to aid static analysis.
-
response_cookie_array array{ name: response_cookie_name_string, value: response_cookie_value_string, attributes: response_cookie_attributes_array }- An
arrayof cookie components.
- An
-
response_cookie_attributes_array array{ expires?:string, max-age?:numeric-string, path?:string, domain?:string, secure?:true, httponly?:true, samesite?:string, partitioned?:true, }- An
arrayintended to specify cookie attributes.
- An
-
response_cookie_name_string- A
stringintended as a cookie name.
- A
-
response_cookie_value_string- A
stringintended as a cookie value.
- A
-
response_header_field_string- A
stringintended to be a header field name, typically as part of the first argument toheader().
- A
-
response_header_value_string- A
stringintended to be header value, typically as part of the first argument toheader().
- A
-
response_http_version_string- A
stringused for specifying an HTTP version.
- A
-
response_status_code_int- An
intspecifying an HTTP response code.
- An
Implementations MAY define additional class members not defined in these interfaces.
Notes:
- Reference implementations may be found at https://github.com/response-interop/impl.
None of the researched projects model their response objects as immutable or readonly.
None of the researched projects model their response objects that way.
A more general answer is from Fowler in Patterns of Enterprise Application Architecture (2003, p 21):
... I think there is a good distinction to be made between an interface that you provide as a service to others and your use of someone else's service. ... I find it beneficial to think about these differently because the difference in clients alters the way you think about the service.
Response-Interop attempts to model an interface that provides a response for presentation, not one that uses a response from an external source.
Of the 13 researched projects:
- 2 provide a constant or Enum for reason-phrase mappings to status codes; and,
- 2 others provide a static array of status codes mappings to reason phrases.
The relative rarity, and inconsistency, of such constants and mappings makes it difficult to discern a standard here.
Implementors desiring status codes constants or Enums are not prevented from providing them with their implementations.
6 of the 13 projects allow for a reason phrase in one way or another. Thus, while not the majority design choice, allowing for a reason phrase warrants consideration.
On further inspection, the projects that allow for a reason phrase sometimes set it with the status code, and sometimes set it separately. This makes it difficult to resolve the differences between the projects. Given that the HTTP specifications indicate reason phrases are optional, Response-Interop does not attempt to resolve those differences.
Implementors desiring a reason phrase are encouraged to add one approriate for
the status code, perhaps in their sendResponse() logic.
Research revealed that separate header and/or cookie collections are used in 6 of the 13 projects. Thus, while not the majority design choice, delegating these methods to a separate object is common enough to warrant consideration.
With that in mind, Response-Interop finds that the segregation of status line, headers, and body into their own properties appropriately separates the concerns around building a response.
None of the researched projects do so. Further, doing so adds complexity to the implemetation directives on how to retain such values, as well to the return typehints on the various getter methods. Avoiding Stringable therefore reduces the implementation burden.
If consumers need to pass a Stringable, they may cast it to (string) at
call-time.
Although some response headers may hold multiple values, the main use-case for most response headers is to hold a single value.
Consider an example case of the location header. If one wants to check that
the location is /foo, the always-array idiom looks like the following:
$location = $response->headers->getHeader('location') ?? [];
if (
count($location) === 1
&& reset($location) === '/foo'
) {
// location is /foo
};That is, the consumer cannot be guaranteed that the location exists at all
as an array, nor that is has only one value if it does, nor that the only array
key is 0.
What about an always-string idiom? If there were multiple values, they would have to be concatenated in a comma-separated string (which might not even be a valid format for the header in question). In turn, that makes it difficult to iterate over the values without first parsing the returned string into an array, which is burdensome for consumers.
In contrast, the string-or-array idiom looks like this for single-valued headers:
$location = $response->headers->getHeader('location');
if ($location === '/foo') {
// location is /foo
}With this idiom, if location has been specified multiple times, the
identicality check is guaranteed to fail (as it should).
Multiple-valued headers are returned as an array of strings, making it trivial to iterate over them.
Finally, if consumers must allow for multiple values, even when only a single
value might be present, it is easy to cast the getHeader() return to an
(array) as needed:
$xFooValues = $response->headers->getHeader('x-foo') ?? [];
foreach ((array) $xFooValues as $xFooValue) {
// ...
}With all that in mind, Response-Interop favors of the string-or-array approach.
PHP provides a header_register_callback() function to execute callbacks
when headers are sent. None the researched projects provided any equivalent
affordances.
Implementors desiring something similar are encouraged to add such logic as
necessary, perhaps in the sendResponseHeaders() logic.
All of the researched projects provide some sort of affordance for cookie
management. Indeed, PHP itself has a setcookie() function separate from
the more general header() function.
In terms of interface design, the set-cookie values are more complex than most
response headers. That difference makes appropriate typehinting more difficult
on methods designed for both general headers and set-cookie headers.
Further, it is useful to be able to find or replace a cookie by its name, and general-purpose header methods cannot accomplish that.
The researched projects included other affordances around specific headers and
behaviors. For example, many of them afford a redirect() mechanism to set
the status code and location header at the same time. Others provide
affordances around caching-related headers such as etag, vary, the
cache-control directives, and so on.
However, the choice of affordances and their different implementations varied too widely to discern any common approaches. As such, Response-Interop does not specify affordances for other behaviors.
-
Status line
-
Headers
-
Cookies