Skip to content

Commit abad85c

Browse files
committed
merged branch Seldaek/post_response (PR #2791)
Commits ------- 7c2f11f Merge pull request #1 from pminnieur/post_response 9f4391f [HttpKernel] fixed DocBlocks 2a61714 [HttpKernel] added PostResponseEvent dispatching to HttpKernel 915f440 [HttpKernel] removed BC breaks, introduced new TerminableInterface 7efe4bc [HttpKernel] Add Kernel::terminate() and HttpKernel::terminate() for post-response logic Discussion ---------- [HttpKernel] Add Kernel::terminate() and HttpKernel::terminate() for post-response logic This came out of a discussion on IRC about doing stuff post-response, and the fact that right now there is no best practice, and it basically requires adding code after the `->send()` call. It's an attempt at fixing it in an official way. Of course terminate() would need to be called explicitly, and added to the front controllers, but then it offers a standard way for everyone to listen on that event and do things without slowing down the user response. --------------------------------------------------------------------------- by stof at 2011/12/06 02:41:26 -0800 We discussed it on IRC and I suggested a way to avoid the BC break of the interface: adding a new interface (``TerminableInterface`` or whatever better name you find) containing this method. HttpKernel, Kernel and HttpCache can then implement it without breaking the existing apps using the component (Kernel and HttpCache would need an instanceof check to see if the inner kernel implements the method) For Symfony2 users it will mean they have to change their front controller to benefit from the new event of course, but this is easy to do. Btw, Silex can then be able to use it without *any* change for the end users as it can be done inside ``Application::run()`` --------------------------------------------------------------------------- by pminnieur at 2011/12/06 11:47:03 -0800 @Seldaek: I opened a pull request so that the discussion on IRC is fulfilled and no BC breaks exist: https://github.com/Seldaek/symfony/pull/1/files --------------------------------------------------------------------------- by fabpot at 2011/12/07 07:59:49 -0800 Any real-world use case for this? --------------------------------------------------------------------------- by Seldaek at 2011/12/07 08:10:31 -0800 Doing slow stuff after the user got his response back without having to implement a message queue. I believe @pminnieur wanted to use it to send logs to loggly? --------------------------------------------------------------------------- by pminnieur at 2011/12/07 09:08:41 -0800 Its a good practice to defer code execution without the introduction of a new software layer (like gearman, amqp, whatever tools people use to defer code execution) which may be way too much just for the goal of having fast responses, whatever my code does. My real world use case which made me miss this feature the first time: > I have a calendar with a scheduled Event. For a given period of time, several Event entities will be created, coupled to the scheduled event (the schedule Event just keeps track of `startDate`, `endDate` and the `dateInterval`). Let's say we want this scheduled Event to be on every Monday-Friday, on a weekly basis, for the next 10 years. This means I have to create `10*52*5` Event entities before I could even think about sending a simple redirect response. If I could defer code execution, I'd only save the scheduled Event, send the redirect response and after that, I create the `10*52*5` entities. The other use case was loggly, yes. Sending logging data over the wire before the response is send doesn't make sense in my eyes, so it could be deferred after the response is send (this especially sucks if loggly fails and i get a 500 --the frontend/public user is not interested in a working logging facility, he wants his responses). --------------------------------------------------------------------------- by mvrhov at 2011/12/07 10:07:03 -0800 This would help significantly, but the real problem, that your process is busy and unavailable for the next request, is still there. --------------------------------------------------------------------------- by fabpot at 2011/12/07 10:15:18 -0800 I think this is the wrong solution for a real problem. Saying "Its a good practice to defer code execution without the introduction of a new software layer" is just wrong. It is definitely a good practice to defer code execution, but you should use the right tool for the job. I'm -1. --------------------------------------------------------------------------- by pminnieur at 2011/12/07 10:25:44 -0800 It should just give a possibility to put unimportant but heavy lifting code behind the send request with ease. With little effort people could benefit from the usage of `fastcgi_finish_request` without introducing new software, using `register_shutdown_function` or using `__destruct `(which works for simple things, but may act weird with dependencies). It should not simulate node.js ;-) I agree that the real problem is not solved, but small problems could be solved easily. I personally don't want to setup RabbitMQ or whatever, maintain my crontab or any other software that may allow me to defer code execution. --------------------------------------------------------------------------- by Seldaek at 2011/12/08 01:08:32 -0800 @fabpot: one could say that on shared hostings it is still useful because they generally don't give you gearman or \*MQs. Anyway I think it'd be nice to really complete the HttpKernel event cycle. --------------------------------------------------------------------------- by pminnieur at 2011/12/08 01:48:57 -0800 not only on shared hostings, sometimes teams/projects just don't have the resources or knowledge or time to setup such an infrastructure. --------------------------------------------------------------------------- by videlalvaro at 2011/12/08 01:53:06 -0800 I can say we used `fastcgi_finish_request` quite a lot at poppen with symfony 1.x. It certainly helped us to send data to Graphite, save XHProf runs, send data to RabbitMQ, and so on. For example we used to connect to RabbitMQ and send the messages _after_ calling `fastcgi_finish_request` so the user never had to wait for stuff like that. Also keep in mind that if you are using Gearman or RabbitMQ or whatever tool you use to defer code execution… you are not deferring the network connection handling, sending data over the wire and what not. I know this is obvious but is often overlooked. So it would be nice to have an standard way of doing this. --------------------------------------------------------------------------- by henrikbjorn at 2011/12/13 01:42:23 -0800 This could have been useful recently while implementing a "Poor mans cronjob" system. The solution was to do a custom Response object and do the stuff after send have been called with a Connection: Close header and ignore_user_abort(); (Yes very ugly)
2 parents b7c7ed4 + 7c2f11f commit abad85c

File tree

10 files changed

+245
-3
lines changed

10 files changed

+245
-3
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Event;
13+
14+
use Symfony\Component\HttpKernel\HttpKernelInterface;
15+
use Symfony\Component\EventDispatcher\Event;
16+
17+
/**
18+
* Allows to execute logic after a response was sent
19+
*
20+
* @author Jordi Boggiano <[email protected]>
21+
*/
22+
class PostResponseEvent extends Event
23+
{
24+
/**
25+
* The kernel in which this event was thrown
26+
* @var HttpKernelInterface
27+
*/
28+
private $kernel;
29+
30+
public function __construct(HttpKernelInterface $kernel)
31+
{
32+
$this->kernel = $kernel;
33+
}
34+
35+
/**
36+
* Returns the kernel in which this event was thrown
37+
*
38+
* @return HttpKernelInterface
39+
*/
40+
public function getKernel()
41+
{
42+
return $this->kernel;
43+
}
44+
}

src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
namespace Symfony\Component\HttpKernel\HttpCache;
1717

1818
use Symfony\Component\HttpKernel\HttpKernelInterface;
19+
use Symfony\Component\HttpKernel\TerminableInterface;
1920
use Symfony\Component\HttpFoundation\Request;
2021
use Symfony\Component\HttpFoundation\Response;
2122

@@ -26,7 +27,7 @@
2627
*
2728
* @api
2829
*/
29-
class HttpCache implements HttpKernelInterface
30+
class HttpCache implements HttpKernelInterface, TerminableInterface
3031
{
3132
private $kernel;
3233
private $store;
@@ -215,6 +216,18 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
215216
return $response;
216217
}
217218

219+
/**
220+
* {@inheritdoc}
221+
*
222+
* @api
223+
*/
224+
public function terminate()
225+
{
226+
if ($this->getKernel() instanceof TerminableInterface) {
227+
$this->getKernel()->terminate();
228+
}
229+
}
230+
218231
/**
219232
* Forwards the Request to the backend without storing the Response in the cache.
220233
*

src/Symfony/Component/HttpKernel/HttpKernel.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
1919
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
2020
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
21+
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
2122
use Symfony\Component\HttpFoundation\Request;
2223
use Symfony\Component\HttpFoundation\Response;
2324
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -29,7 +30,7 @@
2930
*
3031
* @api
3132
*/
32-
class HttpKernel implements HttpKernelInterface
33+
class HttpKernel implements HttpKernelInterface, TerminableInterface
3334
{
3435
private $dispatcher;
3536
private $resolver;
@@ -78,6 +79,17 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
7879
}
7980
}
8081

82+
/**
83+
* {@inheritdoc}
84+
*
85+
* @api
86+
*/
87+
public function terminate()
88+
{
89+
$event = new PostResponseEvent($this);
90+
$this->dispatcher->dispatch(KernelEvents::TERMINATE, $event);
91+
}
92+
8193
/**
8294
* Handles a request to convert it to a response.
8395
*

src/Symfony/Component/HttpKernel/Kernel.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
*
4545
* @api
4646
*/
47-
abstract class Kernel implements KernelInterface
47+
abstract class Kernel implements KernelInterface, TerminableInterface
4848
{
4949
protected $bundles;
5050
protected $bundleMap;
@@ -134,6 +134,22 @@ public function boot()
134134
$this->booted = true;
135135
}
136136

137+
/**
138+
* {@inheritdoc}
139+
*
140+
* @api
141+
*/
142+
public function terminate()
143+
{
144+
if (false === $this->booted) {
145+
return;
146+
}
147+
148+
if ($this->getHttpKernel() instanceof TerminableInterface) {
149+
$this->getHttpKernel()->terminate();
150+
}
151+
}
152+
137153
/**
138154
* Shutdowns the kernel.
139155
*

src/Symfony/Component/HttpKernel/KernelEvents.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,15 @@ final class KernelEvents
9191
* @api
9292
*/
9393
const RESPONSE = 'kernel.response';
94+
95+
/**
96+
* The TERMINATE event occurs once a reponse was sent
97+
*
98+
* This event allows you to run expensive post-response jobs.
99+
* The event listener method receives a
100+
* Symfony\Component\HttpKernel\Event\PostResponseEvent instance.
101+
*
102+
* @var string
103+
*/
104+
const TERMINATE = 'kernel.terminate';
94105
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
17+
/**
18+
* Terminable extends the Kernel request/response cycle with dispatching a post
19+
* response event after sending the response and before shutting down the kernel.
20+
*
21+
* @author Jordi Boggiano <[email protected]>
22+
* @author Pierre Minnieur <[email protected]>
23+
*
24+
* @api
25+
*/
26+
interface TerminableInterface
27+
{
28+
/**
29+
* Terminates a request/response cycle.
30+
*
31+
* Should be called after sending the response and before shutting down the kernel.
32+
*
33+
* @api
34+
*/
35+
function terminate();
36+
}

tests/Symfony/Tests/Component/HttpKernel/EventListener/ExceptionListenerTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch =
114114
return new Response('foo');
115115
}
116116

117+
public function terminate() {}
117118
}
118119

119120
class TestKernelThatThrowsException implements HttpKernelInterface
@@ -122,4 +123,6 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch =
122123
{
123124
throw new \Exception('bar');
124125
}
126+
127+
public function terminate() {}
125128
}

tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,43 @@
1111

1212
namespace Symfony\Tests\Component\HttpKernel\HttpCache;
1313

14+
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
15+
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
16+
use Symfony\Component\HttpKernel\HttpKernelInterface;
1417
require_once __DIR__.'/HttpCacheTestCase.php';
1518

1619
class HttpCacheTest extends HttpCacheTestCase
1720
{
21+
public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
22+
{
23+
$storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface')
24+
->disableOriginalConstructor()
25+
->getMock();
26+
27+
// does not implement TerminableInterface
28+
$kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpKernelInterface')
29+
->disableOriginalConstructor()
30+
->getMock();
31+
32+
$kernelMock->expects($this->never())
33+
->method('terminate');
34+
35+
$kernel = new HttpCache($kernelMock, $storeMock);
36+
$kernel->terminate();
37+
38+
// implements TerminableInterface
39+
$kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel')
40+
->disableOriginalConstructor()
41+
->setMethods(array('terminate', 'registerBundles', 'registerContainerConfiguration'))
42+
->getMock();
43+
44+
$kernelMock->expects($this->once())
45+
->method('terminate');
46+
47+
$kernel = new HttpCache($kernelMock, $storeMock);
48+
$kernel->terminate();
49+
}
50+
1851
public function testPassesOnNonGetHeadRequests()
1952
{
2053
$this->setNextResponse(200);

tests/Symfony/Tests/Component/HttpKernel/HttpKernelTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,20 @@ public function testHandleWithAResponseListener()
164164
$this->assertEquals('foo', $kernel->handle(new Request())->getContent());
165165
}
166166

167+
public function testTerminate()
168+
{
169+
$dispatcher = new EventDispatcher();
170+
$kernel = new HttpKernel($dispatcher, $this->getResolver());
171+
$dispatcher->addListener(KernelEvents::TERMINATE, function ($event) use (&$called, &$capturedKernel) {
172+
$called = true;
173+
$capturedKernel = $event->getKernel();
174+
});
175+
176+
$kernel->terminate();
177+
$this->assertTrue($called);
178+
$this->assertEquals($kernel, $capturedKernel);
179+
}
180+
167181
protected function getResolver($controller = null)
168182
{
169183
if (null === $controller) {

tests/Symfony/Tests/Component/HttpKernel/KernelTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,66 @@ public function testInitializeBundleThrowsExceptionWhenABundleExtendsItself()
652652
$kernel->initializeBundles();
653653
}
654654

655+
public function testTerminateReturnsSilentlyIfKernelIsNotBooted()
656+
{
657+
$kernel = $this->getMockBuilder('Symfony\Tests\Component\HttpKernel\KernelForTest')
658+
->disableOriginalConstructor()
659+
->setMethods(array('getHttpKernel'))
660+
->getMock();
661+
662+
$kernel->expects($this->never())
663+
->method('getHttpKernel');
664+
665+
$kernel->setIsBooted(false);
666+
$kernel->terminate();
667+
}
668+
669+
public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
670+
{
671+
// does not implement TerminableInterface
672+
$httpKernelMock = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')
673+
->disableOriginalConstructor()
674+
->getMock();
675+
676+
$httpKernelMock
677+
->expects($this->never())
678+
->method('terminate');
679+
680+
$kernel = $this->getMockBuilder('Symfony\Tests\Component\HttpKernel\KernelForTest')
681+
->disableOriginalConstructor()
682+
->setMethods(array('getHttpKernel'))
683+
->getMock();
684+
685+
$kernel->expects($this->once())
686+
->method('getHttpKernel')
687+
->will($this->returnValue($httpKernelMock));
688+
689+
$kernel->setIsBooted(true);
690+
$kernel->terminate();
691+
692+
// implements TerminableInterface
693+
$httpKernelMock = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernel')
694+
->disableOriginalConstructor()
695+
->setMethods(array('terminate'))
696+
->getMock();
697+
698+
$httpKernelMock
699+
->expects($this->once())
700+
->method('terminate');
701+
702+
$kernel = $this->getMockBuilder('Symfony\Tests\Component\HttpKernel\KernelForTest')
703+
->disableOriginalConstructor()
704+
->setMethods(array('getHttpKernel'))
705+
->getMock();
706+
707+
$kernel->expects($this->exactly(2))
708+
->method('getHttpKernel')
709+
->will($this->returnValue($httpKernelMock));
710+
711+
$kernel->setIsBooted(true);
712+
$kernel->terminate();
713+
}
714+
655715
protected function getBundle($dir = null, $parent = null, $className = null, $bundleName = null)
656716
{
657717
$bundle = $this

0 commit comments

Comments
 (0)