response-interop / interface
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 3
Open Issues: 0
pkg:composer/response-interop/interface
Requires
- php: ^8.4
Requires (Dev)
- pds/composer-script-names: ^1.0
- pds/skeleton: ^1.0
- phpstan/phpstan: ^2.0
This package is auto-updated.
Last update: 2025-12-09 18:06:57 UTC
README
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, refines, and reconciles the common practices identified within several pre-existing 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).
Interfaces
This package defines the following interfaces:
-
ResponseStruct encapsulates the server response status line, headers, and body.
-
ResponseHeadersCollection encapsulates the headers for the response, including affordances for cookie management.
-
ResponseBodyHandler affords management of non-string, resource-intensive, or header-modifying content.
-
ResponseCookieHelperService affords conversion of cookie representations to and from strings and arrays.
-
ResponseSenderService affords sending the server response.
-
ResponseThrowable extends Throwable to mark an Exception as response-related.
-
ResponseTypeAliases provides PHPStan type aliases 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.
ResponseStruct
The ResponseStruct interface encapsulates the server response.
ResponseStruct 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.
-
-
public ResponseHeadersCollection $headers { get; set; }
- The headers for the response, including affordances for cookie management.
-
public Stringable|ResponseBodyHandler|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 ResponseBodyHandler instance affords improved resource management and response modification.
- The
-
ResponseHeadersCollection
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 header fields should consist only of letters, digits, hyphens (
-), and underscores (_); the first character should be a letter or an underscore. Cf. https://datatracker.ietf.org/doc/html/rfc3864#section-4.1. -
Header values cannot be blank. If
trim($value) === ''then the$valueis blank.
-
ResponseHeadersCollection 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 it 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 return a string if there is only one
$valuefor the header. -
Implementations MUST return 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() : response_headers_array;
-
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
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() : response_named_cookie_arrays;
-
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() : response_named_cookie_strings;
-
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.
-
ResponseBodyHandler
The ResponseBodyHandler interface affords management and sending of non-string, resource-intensive, or header-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 of reading 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.
-
ResponseBodyHandler 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(ResponseSenderService $sender) : void;
-
Sends the body content of the response.
-
Directives:
- Implementations MUST send the body content using the
$sender.
- Implementations MUST send the body content using the
-
Notes:
- Send the body via the
$sender, not by usingechoor some other means. This allows the sending logic to specify the output destination. The$senderprovides affordances for sending strings and resources (whether in whole or in part).
- Send the body via the
-
ResponseCookieHelperService
Response-Interop affords representing set-cookie header values in two
ways:
-
as a
response_header_value_string, for working with completeset-cookieheader strings; and, -
as a
response_cookie_array, for working withset-cookiecomponents more conveniently.
The ResponseCookieHelperService affords conversion between the two representations.
ResponseCookieHelperService Methods
-
public function parseResponseCookieString( response_header_value_string $setCookieString, ) : ?response_cookie_array;
-
Parses a
response_header_value_stringinto aresponse_cookie_array. -
Directives:
-
Implementations MUST use a parsing algorithm equivalent to the one in RFC 6265 section 5.2.
-
Implementations MAY ignore the attribute-specific parsing and validating algorithms in RFC 6265 sections 5.2.1 et al.
-
Implementations MAY validate parsed attributes; implementations doing so MUST treat invalid attributes as missing.
-
Implementations MUST return
nullwhen the parsed<name-value-pair>lacks a%x3D(=) character, or when the parsed cookie name is empty. -
Implementations MUST decode the parsed cookie name and value appropriately.
-
Implementations MUST normalize parsed attribute names to lower case.
-
Implementations MUST represent the value of attributes specified without
=<attribute-value>as booleantrue.
-
-
Notes:
-
These directives are specific but non-restrictive. For example, cookie attributes other than the ones found in RFC 6265 may be parsed and captured into the
response_cookie_array, such asSameSiteandPartitioned. -
The parsed cookie name and value are to be decoded. Typically this means using
urldecode(). -
Some attributes do not have values. For example, the
HttpOnlyattribute is defined as having no accompanying value (i.e., it has no=<attribute-value>portion). Thus, ifHttpOnlyis present in theresponse_header_value_stringas an attribute, its correspondingresponse_cookie_arrayelement must be represented as['httponly' => true]. If it is not present as an attribute, it is missing, and thus should not be present in theresponse_cookie_array.Note that this is different from an attribute having an empty value. For example,
expires=;has an empty value, and so should be represented as an empty string:['expires' => '']. (This is an invalid value forexpiresand so implementations may treat it as missing.)
-
-
-
public function composeResponseCookieString( response_cookie_array $cookie, ) : response_header_value_string;
-
Composes a
response_cookie_arrayinto aresponse_header_value_string. -
Directives:
-
Implementations MUST encode the cookie name and value appropriately.
-
Implementations SHOULD use lower case for attribute names but MAY use any other case approved in the relevant RFCs.
-
Implementations MUST omit
=<attribute-value>when the attribute value is booleantrue.
-
-
Notes:
-
These directives are specific but non-restrictive. For example, cookie attributes other than the ones found in RFC 6265 may be composed into the
response_header_value_string, such asSameSiteandPartitioned. -
The cookie name and value are to be encoded. Typically this means using
urlencode().
-
-
ResponseSenderService
The ResponseSenderService affords sending the server response.
ResponseSenderService Methods
-
public function sendResponse(ResponseStruct $response) : void;
-
Sends the entire response, including the status line, headers, and body.
-
Directives:
-
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 ResponseStruct
$bodyis an instance of ResponseBodyHandler, implementations MUST call itsprepareResponse()method before sending anything. -
Implementations SHOULD use
header()to send headers, but MAY use some other mechanism. -
Implementations SHOULD send header fields in lower case, but MAY send header fields in some other RFC-approved case.
-
-
-
public function sendResponseBodyString(Stringable|string $content) : void;
-
Sends body content from a string.
-
Directives:
- Implementations SHOULD write the string to the
php://outputstream, but MAY use some other mechanism or destination.
- Implementations SHOULD write the string to the
-
Notes:
- Prefer writing to a resource over calling
echo,print, etc. Although echoing a body string is the single most common use case, callingfwrite()with aphp://outputresource does exactly the same thing. This also allows specifying the output destination at call-time, such as when testing.
- Prefer writing to a resource over calling
-
-
public function sendResponseBodyResource( resource $content, ?int $length = null, ?int $offset = null, ) : int;
-
Sends body content from a resource.
-
Directives:
-
Implementations SHOULD send the
$contentto thephp://outputstream, but MAY use some other mechanism or destination. -
If the
$offsetis null, implementations MUST begin reading from the current$contentpointer position. -
If the
$offsetis zero or positive, implementations MUST begin reading from the$contentstarting at that byte; implementations MAY move the pointer as needed, e.g. viafseek(). -
If the
$lengthis null, implementations MUST send all remaining bytes from the$content. -
If the
$lengthis not null, implementations MUST send that many bytes from the$content(or all remaining bytes from the$content, whichever comes first). -
Implementations MUST return the number of bytes sent.
-
Implementations MUST throw a ResponseThrowable on failure.
-
-
Notes:
-
The method signature is subtly different from related streaming functions in PHP. Whereas
stream_copy_to_stream()defaults to$offset = 0, andstream_get_contents()defaults to-1, the default here isnull. -
By default, do not move the starting pointer position. Some implementations attempt to
rewind()the resource before sending. When sending a complete file, that may be fine; however, it may be necessary to start at exactly where the resource pointer already is. Therefore, do not change the pointer starting position when the$offsetis null. -
An
$offsetof0is the equivalent of rewind-before-send. To indicate arewind()or its equivalent is needed before sending, consumers should specify an$offsetof0. Alternatively, consumers mightrewind()the resource themselves before sending.
-
-
-
public function flushResponse() : void;
-
Flushes the system output buffer.
-
Notes:
- This is an equivalent to
flush(). It may be useful when sending content withTransfer-Encoding: chunked.
- This is an equivalent to
-
ResponseThrowable
The ResponseThrowable interface extends Throwable to mark an Exception as response-related. It adds no class members.
ResponseTypeAliases
The ResponseTypeAliases interface provides 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_headers_array array< response_header_field_string, response_header_value_string|response_header_value_string[] >- An
arrayof header values keyed on the header fields.
- An
-
response_http_version_string- A
stringused for specifying an HTTP version.
- A
-
response_named_cookie_arrays array< response_cookie_name_string, response_cookie_array >- An
arrayof cookie component arrays keyed on the cookie name.
- An
-
response_named_cookie_strings array< response_cookie_name_string, response_header_value_string >- An
arrayof cookie header strings keyed on the cookie name.
- An
-
response_status_code_int- An
intspecifying an HTTP response code.
- An
Implementations
-
Directives:
- Implementations MAY define additional class members not defined in these interfaces.
-
Notes:
- Reference implementations may be found at https://github.com/response-interop/impl.
Q & A
Why are there only mutable (as vs. immutable or readonly) interfaces?
None of the researched projects model their response objects as immutable or readonly.
Why is it a ResponseStruct and not just a Response ?
Response-Interop wants to avoid Interface suffixes, and wants to avoid making implementors use import aliases. Calling it a Response would mean any implementation also called Response would have to alias the interop interface. It is the difference between this less-preferable alternative ...
use ResponseInterop\Interface\Response as ResponseInteropInterface; class Response implements ResponseInteropInterface { // ... }
... and this more-preferable one:
use ResponseInterop\Interface\ResponseStruct; class Response implements ResponseStruct { // ... }
Further, the Response definition is struct-like in that it is composed almost entirely of properties.
It is true that none of the researched implementations use Struct in their naming; but then, the interop is for the interface, so existing implementation names can remain as they are.
Why is ResponseStruct not identical to a client-side response interface?
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.
Why does ResponseStruct not provide constants for status codes?
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.
Why does ResponseStruct not have a property for the reason phrase?
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, some of the projects that allow for a reason phrase set it with the status code, while others 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.
Why is ResponseStruct not self-sending?
The majority of researched projects (9/13) have a send() method, or its
equivalent, directly on their response objects. The remainder place the sending
logic somewhere outside the response object itself.
Earlier versions of Response-Interop defined a ResponseStruct method called
sendResponse() for sending the response. However, private review suggested
that a struct-like object ought not to have methods on it.
Further, private review indicated that separating the sending logic to its own interface would make it easier to provide alternative mechanisms for sending, such as during testing, or when integrating with pre-existing packages and libraries.
With all that in mind, Response-Interop opts to separate the sending logic to its own interface, ResponseSenderService. This keeps the ResponseStruct more struct-like, and makes the sending logic independent of any particular ResponseStruct implementation.
Why not put the ResponseHeadersCollection methods directly on the ResponseStruct?
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 HTTP version, status code, headers, and body into their own properties appropriately separates the concerns around building a response.
Why does ResponseHeadersCollection allow string but not Stringable $value types?
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.
Why does ResponseHeadersCollection return individual headers either as string or as array?
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.
Why does ResponseHeadersCollection provide no methods for managing header callbacks?
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.
Why does ResponseHeadersCollection provide cookie affordance methods?
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.
Why does ResponseHeadersCollection not provide other affordance methods?
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.