Problem/Motivation
To send a streamed response the HtmlResponse::send() method would need to be overwritten.
However cloning the response is not a good option as any subclasses of MyOwnHtmlResponse with added properties would break that.
Also cloning into a new class is quite error prone.
Proposed resolution
Introduce Drupal\Component\HttpFoundation\ResponseEmitterInterface and Drupal\Component\HttpFoundation\ResponseEmitterTrait based on https://github.com/zendframework/zend-diactoros/blob/master/src/Response....
doing basically:
use Symfony\Component\HttpFoundation\Response;
interface ResponseEmitterInterface
{
/**
* Emit a response.
*
* @param ResponseInterface $response
*/
public function emit(ResponseInterface $response);
}
and for the trait
trait ResponseEmitterTrait
{
/**
* Send a response.
*/
public function send() {
if ($this->responseEmitter) {
$this->responseEmitter->emit($this);
return $this;
}
parent::send();
return $this;
}
public function getResponseEmitter() {
return $this->responseEmitter;
}
public function setResponseEmitter(ResponseEmitterInterface $response_emitter) {
$this->responseEmitter = $response_emitter;
}
}
and
class HtmlResponse implements ..., ResponseEmitterInterface {
use ResponseEmitterTrait;
// ...
}
and thats it.
Remaining tasks
- Do it
- Write (unit) tests
API changes
- API addition for the Interface
Why this should be an RC target
Soft-Blocker for #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts
Comment | File | Size | Author |
---|---|---|---|
#32 | interdiff.txt | 11.42 KB | Fabianx |
#31 | allow_htmlresponse_to-2577631-31.patch | 11.62 KB | Fabianx |
#6 | 2577631-emitters.patch | 5.44 KB | Crell |
Comments
Comment #2
Wim LeersMoving to the right component, and assigning to @Crell.
Comment #3
Crell CreditAttribution: Crell as a volunteer commentedThis is essentially what Fabian and I discussed at DrupalCon BCN. However, we'd need an EmittableResponseInterface, which is what the trait is for. Don't confuse the emitter with the emitted.
Wim, are you assigning to me for validation or asking me to write it? :-)
Comment #4
Wim LeersValidation :)
Comment #5
Wim LeersPer #2469431-114: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts and #2469431-115: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts:
+
Comment #6
Crell CreditAttribution: Crell as a volunteer commentedWell, here's some code anyway. :-) It doesn't wire up any emitters yet, as I'm not certain where Fabian intended to do so, but this provides the basic code we discussed.
That said, while working on it, it occurred to me that perhaps we could just leverage Diactoros' emitters directly. We can convert a Symfony Response to PSR-7, can we not? And there's already a model in place for PSR-7 response emitters, in Diactoros. It's already in vendor. Could we just use that directly?
Comment #7
Fabianx CreditAttribution: Fabianx for Acquia commentedI like #6 - except for The DefaultResponseEmitter, which cannot call send() for obvious reasons, but should copy the default response send() code and calling sendHeaders() / sendBody() this is exactly what we need.
I don't think it would be good to convert the HtmlResponse to PSR-7 now, because we then need to duplicate all Interfaces of HtmlResponse on PSR-7 and most middlewares are at this moment still in Symfony land.
However as we use kinda the same interface, once we switch to PSR-7 for good, it should continue to work with almost no changes needed.
Comment #8
yched CreditAttribution: yched commentedI still have to learn how to resist a naming nitpick, it seems, but :
It feels confusing to formulate this in such a way that "some Responses are emittable, and some are not" - and then what, we keep those and never send them to the client ? ;-). The distinction between "send" and "emit" is fairly subtle, especially since we are still in an HttpFoundation stack , and *not* in PSR-7/Diactoros land (AFAICT, Emitters seem to be a Diactoros construct rather than a PSR-7 one, right ?)
Sticking with layman's terms, this is not really about being "a response that can be emitted", but rather "a response to which you can assign a specific emitter" ?
--> Proposal : ResponseWithEmitterInterface, EmitterAwareResponse ?
That remains understandable even for people that don't have the background of the "we'd like to do some Diactoros stuff but have to stay in HttpFoundation at the moment so we port a concept over" discussion that happened in this issue ?
Comment #9
Crell CreditAttribution: Crell as a volunteer commentedFabianX: I'm not sure I follow on DefaultResponseEmitter. What's wrong with send()? The net result is the same: The response object gets sent to the client. In practice I don't know that we'll use the Default emitter, but it's there to have something to implement that interface. :-) The interesting ones would be the ones you have to write yet. (nudge nudge... :-) )
yched: Yeah, the terminology is a bit mixed as we are, in essence, working around a design limitation of HttpFoundation and using Diactoros as a model. (Emitters are Diactoros specific, but the existence of something like them is implied by PSR-7 being data-only and having no send() of its own. So any PSR-7 server implementation would need something along those lines one way or another.)
I would be OK with EmitterAwareResponseInterface instead of EmittableResponseInterface, although I generally prefer adjective interface names to awareness. (There's something deeply philosophical in that sentence, I'm sure...)
Comment #10
yched CreditAttribution: yched commented@Crell :
- Emitters / Diactoros / PSR-7 : thanks for the clarification, totally makes sense.
- "I generally prefer adjective interface names to awareness" - Sure, in the cases where a good adjective exists, ++ on the adjective. "Being able to delegate sending to an external independant emitter implementation" seems hard though (in english at least - I'm sure it's easy peasy in german :-p)
+ lol at the philosophical implications. I guess in Europe we like "aware" because of Jean-Claude Van Damme.
- DefaultResponseEmitter : I think @Fabianx means that the current results in an infinte "you do it, no you do it, no you..." loop between Response::send() -> DefaultResponseEmitter::emit() -> Response::send() -> ... ?
Comment #11
Fabianx CreditAttribution: Fabianx for Acquia commented#10: Yes, that is what I meant.
I like the new naming.
Comment #12
Wim Leers… You just pulled in Jean-Claude Van Damme into a sentence with "philosophical"? HERESY! He's a Belgian actor doing silly American movies, and he's being brought up by a French actor, home to Descarte and Sartre. I really don't know what the hell is happening here :D
Comment #13
Crell CreditAttribution: Crell as a volunteer commentedI feel like a Hercule Poirot reference would be appropriate...
I see what you mean on the infinite loop. (Maybe that's how Jean-Claude Van Damme got in here?) I was thinking that the Emitter could work with any Response, and for most responses (ie, not the ones we're using it for) send() would still work. So we probably need a guard to not send() for an EmitterAwareResponse, or something.
Whatever, at this point I turn it over to FabianX to figure out how to leverage. :-)
Comment #14
Crell CreditAttribution: Crell as a volunteer commentedComment #15
Wim Leers#13:
<offtopic>
Hercule Poirot is most welcome at https://twitter.com/wimleers/status/653510681820557313</offtopic>
Comment #16
Wim LeersSorry for the distractions on this issue so far.
It seems that "emittable" kinda means "streamable" here. Which got me thinking… why do we even need all this? Can't we just change
HtmlResponse
to extendStreamedResponse
instead ofResponse
?Then we could have a
RESPONSE
event listener that doessetCallback('BigPipeStreamer::send')
, which then results inindex.php-> $response->send() -> StreamedResponse::send() -> BigPipeStreamer::send()
. The only thing you can't do with that AFAICT is control how headers are sent.Just playing the devil's advocate here, just trying to minimize change.
Comment #17
Wim LeersI think the answer to #16 belongs in the IS, because I strongly suspect a committer will ask the same question.
Comment #18
Fabianx CreditAttribution: Fabianx for Acquia commented#16: Nope, streamed responses don't have getContent() methods, so that won't work.
It is really only the emitting we want to control - not set a callback and make the assumptions that StreamedResponse makes.
Comment #19
Wim LeersOh, wow, what a useless piece of ****** is
StreamedResponse
!? Indeed, that doesn't work at all.Ok, so let's continue with the current approach. I'm happy to review.
Comment #20
Wim LeersSo, from #5:
So actually this is NOT clear why this makes it a must-have. If it's just for the sake of making it nicer, that doesn't make it a must-have.
The question is simple: is BigPipe possible without this? If no, then this is critical. If yes, then this isn't critical.
I'm still confused why this could be a must-have/critical if we've had a working BigPipe implementation without emitters.
Comment #21
Crell CreditAttribution: Crell as a volunteer commentedWim: The issue is the Response. Symfony Response objects are by default self-sending. That is, they have a single hard-coded emitter, send(). That can only be changed by extension, not by composition. That is the problem.
All responses get $response->send() called at the end. Period. That's where the actual printing happens.
In concept, suppose we did write a BigPipe routine, and stuck it into send(). How do you turn on bigpipe, then? You would have to change the response class you're using, at runtime, based on configuration. That's much more involved and complicated, and means you have 2 classes to maintain. If you have an HtmlResponse returned from somewhere (which does *not* have to come from the Renderer service, necessarily, so we can't rely on that), how do you "swap" from "normal page" to "BigPipe page"? You would have to add a Response listener that detects HtmlResponse (or similar), dissects it, and builds a BigPipeHtmlResponse out of it. That's extra cost, and very hard-coded. If you want another output mechanism you have to do that all over again.
If, instead, you allow a Response to opt-in to externalizing its send() (ie, composition), then a listener need only toss in an externalized send(), ie, an emitter. Very lightweight, very flexible.
So I guess it depends on your definition of "possible". Technically we could *probably* implement bigpipe without this approach, but the result would be so brittle that it would barely be usable, or worth using. Fabian?
(Side note: I think I will now use this example as my go-to for the danger of complecting a value object, Response, with behavior, emitting.)
Comment #22
xjmI gather that the reason this is proposed as an RC target is that it's a soft/niceness blocker for BigPipe? I updated the summary to that effect. Would be good to answer #20 there as well.
Comment #23
Fabianx CreditAttribution: Fabianx for Acquia commented#21: Yes, the only thing a big_pipe contrib atm. can do is to hard-code assumptions to HtmlResponse and probably should check the exact class for that, then clone it as good as possible.
But as there is a known and working work-around, Wim and catch are of the opinion this is not RC material.
Which is unfortunate, but I guess it should be fine to add in 8.1.0, which is just 6 months away after release anyway.
And then present BigPipe as a complete and clean solution / product, rather than rush parts of this in - even though the disruption of this change here is zero.
Personally I actually like having more time overall :).
Comment #24
Crell CreditAttribution: Crell as a volunteer commentedPer Fabian, then.
Comment #25
Fabianx CreditAttribution: Fabianx for Acquia commentedThanks, Crell :).
Comment #26
Wim LeersEmphasis mine. This is my biggest concern also.
Until 8.1.0, the current solution, which indeed involves a response subscriber, will work just fine. #21 uses the adjective "brittle", which I think is a vast exaggeration. 99% of
text/html
responses in Drupal 8 will beHtmlResponse
objects. We can't magically apply BigPipe to plainResponse
objects anyway.This is new territory. I'm glad we're not rushing it in. Especially considering there's no API change anyway.
Comment #27
Wim LeersComment #29
xjmComment #30
Fabianx CreditAttribution: Fabianx at Tag1 Consulting commented.
Comment #31
Fabianx CreditAttribution: Fabianx at Tag1 Consulting commentedAnd here is Crells start with the renames and BigPipe converted.
As it is easily seeable, changes to BigPipe are minimal - though more could be deleted - but as BigPipeInterface holds a lot of documentation, that would be not a good idea to delete it ...
I do think that except from some unit tests, this should be good to go.
All it does is, that it allows someone to specify the emitter to use for the send() method within a response - nothing more and nothing less.
Comment #32
Fabianx CreditAttribution: Fabianx at Tag1 Consulting commentedInterdiff for previous comment.
Comment #33
Wim LeersLooks nice. But still needs tests. And I'm not sure about the
EmitterAwareResponseInterface
name. I thinkResponseStreamerInterface
andStreamedResponseInterface
would make more sense. Especially because #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits will likely addCacheableStreamedResponseInterface
— see comments 44 and 45 there.@file
docblocks are not necessary anymore :)This is not Symfony, but Drupal. I guess the intent is to get this into upstream Symfony, and that's why it is named like this?
This must to be typehinted to
EmitterAwareResponseInterface
, notResponse
.s/to a response with BigPipe emitter/to use the BigPipe emitter/
Well this can then just become
$response->setResponseEmitter($this->bigPipe)
Comment #48
steinmb CreditAttribution: steinmb at University Of Bergen commentedDigging into a some performance issues on a site. Noticed this issue. Still needed or? The IS point to https://github.com/zendframework/zend-diactoros/blob/master/src/Response... that is archived and moved to https://github.com/laminas/laminas-diactoros/tree/3.4.x/src/Response that longer have this interface, could be renamed/moved though.
Comment #49
catchiirc this was mainly for https://www.drupal.org/project/big_pipe_sessionless but didn't end up either blocking that or the core bigpipe module.