2

Since a PSR-7 Response is supposed to be immutable, why can I write this disturbingly "mutating" piece of code?

public function controller(Response $response): Response
{
    $response->getBody()->write("Hey.");

    return $response;
}

It seems to me that while the Response in itself is immutable, meaning that we get a new object when we call $response->withHeader(…) for instance, we still can (and usually do) mutate its Body object (not the least important part of the response).

Isn’t that inconsistent? Or is it perfectly sensible? It just seems quite weird to me.

sylbru
  • 1,461
  • 1
  • 11
  • 19

1 Answers1

4

Your question is directly answered in the meta for PSR-7:

Why are streams mutable?
The StreamInterface API includes methods such as write() which can change the message content – which directly contradicts having immutable messages.

The problem that arises is due to the fact that the interface is intended to wrap a PHP stream or similar. A write operation therefore will proxy to writing to the stream. Even if we made StreamInterface immutable, once the stream has been updated, any instance that wraps that stream will also be updated – making immutability impossible to enforce.

Our recommendation is that implementations use read-only streams for server-side requests and client-side responses.

Chris Haas
  • 53,986
  • 12
  • 141
  • 274
  • Thanks, that’s what I was looking for! – sylbru Sep 11 '20 at 09:35
  • Now I’m not sure what they mean exactly in the last sentence, or how to do that, but it’s not really bothering me that much in the end. – sylbru Sep 11 '20 at 09:36
  • 1
    @Niavlys, if you look at the [stream interface from the PSR](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) you'll see that they explicitly built in `isWritable()` however it is up to actual implementations to actually do that. [Guzzle](https://github.com/guzzle/psr7/blob/ad1de77a65b751d598ced37747bf4c17d457fbc9/src/Stream.php#L50), for instance, allows you to specify read-only for their streams and [will throw](https://github.com/guzzle/psr7/blob/ad1de77a65b751d598ced37747bf4c17d457fbc9/src/Stream.php#L245) if you try to write to that. – Chris Haas Sep 11 '20 at 13:28
  • @Niavlys Each `StreamInterface` implementation depends/relies on a _PHP stream_, in order to write/read the content of the HTTP message body to/from it. There are _read-only_, _write-only_ and _read-write_ streams. The list of streams to be used in the context of HTTP messages are presented in the [documentation](https://www.php.net/manual/en/wrappers.php.php) of the _"php://"_ wrapper. The first part of the last sentence advices the use of a _read-only_ PHP stream (a right choice would be `php://temp`) as the underlying saving space for the message body content. – PajuranCodes Sep 11 '20 at 13:56
  • @Niavlys Though the second part of the sentence is a bit abstract. It refers, probably, to how PHP handles the HTTP messages in an application context. Either is PHP used as a _HTTP client_ (for ex. [Guzzle](https://docs.guzzlephp.org/en/stable/)), or as a _server-side application_ (for ex. [laminas-diactoros](https://docs.laminas.dev/laminas-diactoros/)). For more details read [PSR-7 Meta Document](https://www.php-fig.org/psr/psr-7/meta/) and laminas-diactoros' [Usage](https://docs.laminas.dev/laminas-diactoros/v2/usage/) chapter. – PajuranCodes Sep 11 '20 at 13:57
  • @Niavlys For an MVC _server-side application_ you could use the stream _"php://temp"_ as follows: a) As a stream with _read-only_ access when creating a `StreamInterface` instance to pass to the `ServerRequestInterface` implementation: `$body = $streamFactory->createStreamFromFile('php://temp', 'rb');`; b) As a stream with _read-write_ access when creating a `StreamInterface` instance to pass to the `ResponseInterface` implementation: `$body = $streamFactory->createStreamFromFile('php://temp', 'r+b');`. – PajuranCodes Sep 14 '20 at 04:01
  • @dakis Thanks a lot! I get now that it’s up to you what kind of stream you use in your `StreamInterface` instance. You are still suggesting using `php://temp` in a writable/mutable way for building the HTTP response, right? I guess at this level it’s not about immutability since you have to write stuff somewhere… I looked up in the Slim codebase and they also use `php://temp`, `w+` for the request body and `r+` for the response body. – sylbru Sep 15 '20 at 07:48
  • 1
    @Niavlys Yes, the stream type is your choice, based on the resources that your project needs/consumes. Yes, I'm saying that using _"php://temp"_ in a writable/mutable way for building the HTTP response is also right. Though, I can't say anymore, why exactly I choose those access modes, e.g. _read-only_ for the request and _read-write_ for the response, since my researches on the matter were done some time ago. You will probably need [fopen](https://www.php.net/manual/en/function.fopen.php) to create a stream from a given filename – PajuranCodes Sep 15 '20 at 12:08
  • 1
    @Niavlys (e.g. to create a resource of type _"stream"_ from a given filename, e.g. to open the file specified by a given filename), using a given _access mode_ - where filename is, for ex., `php://temp` and access mode is, for ex., `rb`. The allowed _access modes_ are listed/described in the table _"A list of possible modes for fopen() using mode"_ and the following notes at [fopen](https://www.php.net/manual/en/function.fopen.php). There is also detailed the use of the letter _"b"_ (as in "r+b") in order to force the binary mode. Good luck. – PajuranCodes Sep 15 '20 at 12:12