Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Message: support body to be a callback #65

Merged
merged 6 commits into from
Oct 7, 2016

Conversation

petrkotek
Copy link
Contributor

@petrkotek petrkotek commented Sep 28, 2016

What?

In Sabre\HTTP\MessageInterface (and Sabre\HTTP\Message), support $body to be a callback outputting the body into php://output

Why?

Some responses may be big, so it's not scalable to hold the whole response in a variable (in memory), see https://github.com/fruux/sabre-dav/pull/890.

Limitation

getBodyAsString() and getBodyAsStream() doesn't currently work with the callback (throws an exception). It's implementation seem to be a bit complicated - mostly because we are able to produce output only once, but i'm open to ideas.

Usage example from sabre/dav

Snippet from sabre/dav to make generateMultiStatus() method work as a callback:

    function generateMultiStatus($fileProperties, $strip404s = false) {

        $w = $this->xml->getWriter();
        return function() use ($fileProperties, $strip404s, $w) {
            $w->openUri('php://output');
            $w->contextUri = $this->baseUri;
            $w->startDocument();
            $w->startElement('{DAV:}multistatus');

            foreach ($fileProperties as $entry) {
                $href = $entry['href'];
                unset($entry['href']);
                if ($strip404s) {
                    unset($entry[404]);
                }
                $response = new Xml\Element\Response(
                    ltrim($href, '/'),
                    $entry
                );
                $w->write([
                    'name'  => '{DAV:}response',
                    'value' => $response
                ]);
                // maybe we should flush() from time to time here as well
            }

            $w->endElement();
            $w->endDocument();
            $w->flush();
        };

    }

@@ -71,6 +71,11 @@ static function sendResponse(ResponseInterface $response) {
$body = $response->getBody();
if (is_null($body)) return;

if (is_callable($body)) {
$body();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we pass the $response object into the callback so it can handle the case of existing Content-Length header etc? (like wie do for non-callback bodies a few lines below)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure if it's necessary - i think that the same layer which is setting the headers (e.g. content length) is also setting the body (callback), so it can also pass the content length to the callback (e.g. function() use ($contentLength) {...}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at your generateMultiStatus example tells me that this assumption doesnt work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can imagine getting around that (e.g. passing some more context in CorePlugin::httpPropFind() into the generateMultiStatus() method), however, i've added the $response as an argument to the body callback function (see 47c4b1f)

i usually prefer not to add features which are not immediately required, however i understand you want to make it simple for the callback to do the same as Sapi::sendResponse() does without any other "external information"

let me know what you guys think now :)

thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with @petrkotek on this. generateMultiStatus really should move out of Sabre\DAV\Server anyway. It's a leftover from the extremely early days.

Usually I think it's reasonable to assume that the thing that sets the body, also has the ability to access the response object.

@petrkotek
Copy link
Contributor Author

ping @evert / @staabm: will you have a chance to look into this? thanks!

@petrkotek
Copy link
Contributor Author

can i get some 👀 on this? thanks!

Copy link
Member

@evert evert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from those small suggestions, I'm happy with the change. I do think that its time to give the sabre/http package a major version bump though. So I might need some patience until a full release. That doesn't mean we can't have master of sabre/http and sabre/dav reflect these changes though, so hopefully you're able to use just that.

@@ -74,6 +77,9 @@ function getBodyAsString() {
if (is_null($body)) {
return '';
}
if (is_callable($body)) {
throw new \UnexpectedValueException('Callback to string not supported');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can definitely support this with output buffering.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ob_start / ob_get_clean and stuff ;)

@@ -53,6 +53,9 @@ function getBodyAsStream() {
rewind($stream);
return $stream;
}
if (is_callable($body)) {
throw new \UnexpectedValueException('Callback to stream not supported');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also definitely be done, and would be nice to have! With output buffering this can be turned into a stream.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More than 'nice to have' actually ;) I think this needs to be an absolute requirement to not break existing stuff using the HTTP response.

@petrkotek petrkotek force-pushed the support-callback branch 3 times, most recently from 6cbe577 to b907b64 Compare October 6, 2016 12:07
@petrkotek
Copy link
Contributor Author

petrkotek commented Oct 6, 2016

so in the last commit i implemented getBodyAsString() and getBodyAsStream() for the case when body is a callback (using ob_* functions as suggested).

I'm not sure if there is more effective way how to convert the php://output into a stream -- at the moment there is an intermediate string representation used

I also removed the commit 47c4b1f which was changing $body() to $body($response)

throw new \RuntimeException('Cannot start output buffering');
}
$callback();
$content = ob_get_contents();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ob_get_clean() would do the same in 1 step

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay, very good point. i don't use this ob_* stuff too much :) updated

(note that i can always rebase & squash the commits if needed)

private function captureCallbackOutput($callback)
{
$success = ob_start();
if ($success === false) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't really need this check. I've never encountered a situation where this can possibly fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
return function() {
return function() use ($content) {
$fd = fopen('php://output', 'r+');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nitpick and not that important, but instead of opening a stream to php://output and writing the contents there, you can just do:

echo $content;.

And this will have an identical effect, and possibly even a bit faster. Not important for unittests though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, good suggestions; fixed ✅

… doesn't fail in common cases; MessageTest: use echo rather then complicated fwrite to php://output
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants