-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[2.2] [output-stream] Refactor StreamingResponse implementation to use an OutputStream #4146
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
Changes from 1 commit
a2f32a9
7c44cba
56a9192
cc8fca3
27e8732
8c6fcec
6e8cbe6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…utputStream
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpFoundation\OutputStream; | ||
|
||
/** | ||
* OutputStreamInterface defines how streaming responses | ||
* send data to the client. | ||
* | ||
* @author Igor Wiedler <igor@wiedler.ch> | ||
*/ | ||
interface OutputStreamInterface | ||
{ | ||
/** | ||
* Write some data to the stream. | ||
* | ||
* @param string $data The data to write | ||
*/ | ||
function write($data); | ||
|
||
/** | ||
* Close the stream. | ||
* | ||
* This should be called after the sending of data | ||
* has been completed. In case of persistent connections, | ||
* it is never called. | ||
*/ | ||
function close(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpFoundation\OutputStream; | ||
|
||
/** | ||
* StreamOutputStream is an implementation of the OutputStreamInterface | ||
* that uses any writable PHP stream as a target. | ||
* | ||
* Example usage: | ||
* | ||
* $stream = fopen('php://output'); | ||
* $output = new StreamOutputStream($stream, 'w'); | ||
* $output->write('foo'); | ||
* $output->write('bar'); | ||
* $output->close(); | ||
* | ||
* @author Igor Wiedler <igor@wiedler.ch> | ||
*/ | ||
class StreamOutputStream implements OutputStreamInterface | ||
{ | ||
private $stream; | ||
|
||
/** | ||
* @param resource $stream A valid stream resource, such as the return value of | ||
* a fopen() call. | ||
*/ | ||
public function __construct($stream) | ||
{ | ||
if (!is_resource($stream)) { | ||
throw new \InvalidArgumentException('The supplied stream is invalid.'); | ||
} | ||
|
||
$this->stream = $stream; | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
*/ | ||
public function write($data) | ||
{ | ||
fwrite($this->stream, $data); | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
*/ | ||
public function close() | ||
{ | ||
fclose($this->stream); | ||
} | ||
|
||
/** | ||
* Static factory method | ||
*/ | ||
static public function create($filename) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be |
||
{ | ||
$stream = fopen($filename, 'w'); | ||
return new static($stream); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing empty line |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ | |
|
||
namespace Symfony\Component\HttpFoundation; | ||
|
||
use Symfony\Component\HttpFoundation\OutputStream\OutputStreamInterface; | ||
|
||
/** | ||
* StreamedResponse represents a streamed HTTP response. | ||
* | ||
|
@@ -29,6 +31,7 @@ | |
class StreamedResponse extends Response | ||
{ | ||
protected $callback; | ||
protected $outputStream; | ||
protected $streamed; | ||
|
||
/** | ||
|
@@ -71,6 +74,16 @@ public function setCallback($callback) | |
$this->callback = $callback; | ||
} | ||
|
||
/** | ||
* Sets the output stream used for streaming the response. | ||
* | ||
* @param OutputStreamInterface $outputStream stream | ||
*/ | ||
public function setOutputStream(OutputStreamInterface $outputStream) | ||
{ | ||
$this->outputStream = $outputStream; | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
*/ | ||
|
@@ -102,7 +115,11 @@ public function sendContent() | |
throw new \LogicException('The Response callback must not be null.'); | ||
} | ||
|
||
call_user_func($this->callback); | ||
if (null === $this->outputStream) { | ||
throw new \LogicException('The Response output stream must not be null.'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about using stdout by default here instead of throwing an exception? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @igorw what do you think about this ? |
||
} | ||
|
||
call_user_func($this->callback, $this->outputStream); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpFoundation\Tests\OutputStream; | ||
|
||
use Symfony\Component\HttpFoundation\OutputStream\StreamOutputStream; | ||
|
||
class OutputStreamTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
public function testConstructor() | ||
{ | ||
$fp = fopen('php://memory', 'rw'); | ||
$output = new StreamOutputStream($fp); | ||
} | ||
|
||
/** | ||
* @expectedException InvalidArgumentException | ||
*/ | ||
public function testConstructorWithInvalidStream() | ||
{ | ||
$fp = 'foobar'; | ||
$output = new StreamOutputStream($fp); | ||
} | ||
|
||
public function testWrite() | ||
{ | ||
$fp = fopen('php://memory', 'rw'); | ||
$output = new StreamOutputStream($fp); | ||
|
||
$output->write('sample output'); | ||
rewind($fp); | ||
$this->assertEquals('sample output', fread($fp, 50)); | ||
} | ||
|
||
public function testClose() | ||
{ | ||
$fp = fopen('php://memory', 'rw'); | ||
$output = new StreamOutputStream($fp); | ||
|
||
$output->close(); | ||
try { | ||
fread($fp, 1); | ||
$this->fail(); | ||
} catch (\PHPUnit_Framework_Error_Warning $e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is more explicit as it verifies exactly where the notice occurs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eriksencosta thus, I'm not sure we can use expectedException for PHPUnit internal exceptions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stof Yes, we can :) |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
namespace Symfony\Component\HttpKernel\EventListener; | ||
|
||
use Symfony\Component\HttpFoundation\StreamedResponse; | ||
use Symfony\Component\HttpFoundation\OutputStream\OutputStreamInterface; | ||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent; | ||
use Symfony\Component\HttpKernel\HttpKernelInterface; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
|
@@ -25,6 +26,11 @@ | |
*/ | ||
class StreamedResponseListener implements EventSubscriberInterface | ||
{ | ||
public function __construct(OutputStreamInterface $outputStream) | ||
{ | ||
$this->outputStream = $outputStream; | ||
} | ||
|
||
/** | ||
* Filters the Response. | ||
* | ||
|
@@ -39,6 +45,7 @@ public function onKernelResponse(FilterResponseEvent $event) | |
$response = $event->getResponse(); | ||
|
||
if ($response instanceof StreamedResponse) { | ||
$response->setOutputStream($this->outputStream); | ||
$response->send(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not close the stream here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea was to allow async execution where the callback closes the stream later on. But since that doesn't really work properly with HttpKernel it makes sense to close it. |
||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does it work if you have more than one streamed sub-requests? The stream would be already closed for the second one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will indeed be a problem. Is there any way to know if we are handling a sub-request at this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at this point, you are always handling a subrequest. See line 162 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:)