diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
index ce22481921f5a..8ce564265ec9d 100644
--- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
+++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
@@ -14,6 +14,8 @@
use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
use Symfony\Bridge\Twig\Tests\TestCase;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
class HttpKernelExtensionTest extends TestCase
@@ -23,13 +25,30 @@ class HttpKernelExtensionTest extends TestCase
*/
public function testFragmentWithError()
{
- $kernel = $this->getFragmentHandler($this->throwException(new \Exception('foo')));
+ $renderer = $this->getFragmentHandler($this->throwException(new \Exception('foo')));
- $loader = new \Twig_Loader_Array(array('index' => '{{ fragment("foo") }}'));
- $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false));
- $twig->addExtension(new HttpKernelExtension($kernel));
+ $this->renderTemplate($renderer);
+ }
+
+ public function testRenderFragment()
+ {
+ $renderer = $this->getFragmentHandler($this->returnValue(new Response('html')));
+
+ $response = $this->renderTemplate($renderer);
- $this->renderTemplate($kernel);
+ $this->assertEquals('html', $response);
+ }
+
+ public function testUnknownFragmentRenderer()
+ {
+ $context = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack')
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+ $renderer = new FragmentHandler(array(), false, $context);
+
+ $this->setExpectedException('InvalidArgumentException', 'The "inline" renderer does not exist.');
+ $renderer->render('/foo');
}
protected function getFragmentHandler($return)
@@ -38,8 +57,14 @@ protected function getFragmentHandler($return)
$strategy->expects($this->once())->method('getName')->will($this->returnValue('inline'));
$strategy->expects($this->once())->method('render')->will($return);
- $renderer = new FragmentHandler(array($strategy));
- $renderer->setRequest(Request::create('/'));
+ $context = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack')
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+
+ $context->expects($this->any())->method('getCurrentRequest')->will($this->returnValue(Request::create('/')));
+
+ $renderer = new FragmentHandler(array($strategy), false, $context);
return $renderer;
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml
index 4773339906a4e..a1beee30a1fb8 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml
@@ -17,7 +17,7 @@
%kernel.debug%
-
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml
index 9e21db4519151..6b2f7c9688699 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml
@@ -94,7 +94,7 @@
-
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
index 674e28f1c98a0..608bf42d5253f 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
@@ -12,6 +12,7 @@
Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer
Symfony\Component\HttpKernel\Config\FileLocator
Symfony\Component\HttpKernel\UriSigner
+ Symfony\Component\HttpFoundation\RequestStack
@@ -23,8 +24,11 @@
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
index 177821a5afb24..6c1dd73e2e615 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
@@ -38,7 +38,7 @@
%kernel.default_locale%
-
+
diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
index 954b66ac43c02..061d47e7eec94 100644
--- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md
+++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
2.4.0
-----
+ * added RequestStack
* added Request::getEncodings()
* added accessors methods to session handlers
diff --git a/src/Symfony/Component/HttpFoundation/RequestStack.php b/src/Symfony/Component/HttpFoundation/RequestStack.php
new file mode 100644
index 0000000000000..71bfd106991f8
--- /dev/null
+++ b/src/Symfony/Component/HttpFoundation/RequestStack.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * Request stack that controls the lifecycle of requests.
+ *
+ * @author Benjamin Eberlei
+ */
+class RequestStack
+{
+ /**
+ * @var Request[]
+ */
+ private $requests = array();
+
+ /**
+ * Pushes a Request on the stack.
+ *
+ * This method should generally not be called directly as the stack
+ * management should be taken care of by the application itself.
+ */
+ public function push(Request $request)
+ {
+ $this->requests[] = $request;
+ }
+
+ /**
+ * Pops the current request from the stack.
+ *
+ * This operation lets the current request go out of scope.
+ *
+ * This method should generally not be called directly as the stack
+ * management should be taken care of by the application itself.
+ *
+ * @return Request
+ */
+ public function pop()
+ {
+ if (!$this->requests) {
+ throw new \LogicException('Unable to pop a Request as the stack is already empty.');
+ }
+
+ return array_pop($this->requests);
+ }
+
+ /**
+ * @return Request|null
+ */
+ public function getCurrentRequest()
+ {
+ return end($this->requests) ?: null;
+ }
+
+ /**
+ * Gets the master Request.
+ *
+ * Be warned that making your code aware of the master request
+ * might make it un-compatible with other features of your framework
+ * like ESI support.
+ *
+ * @return Request|null
+ */
+ public function getMasterRequest()
+ {
+ if (!$this->requests) {
+ return null;
+ }
+
+ return $this->requests[0];
+ }
+
+ /**
+ * Returns the parent request of the current.
+ *
+ * Be warned that making your code aware of the parent request
+ * might make it un-compatible with other features of your framework
+ * like ESI support.
+ *
+ * If current Request is the master request, it returns null.
+ *
+ * @return Request|null
+ */
+ public function getParentRequest()
+ {
+ $pos = count($this->requests) - 2;
+
+ if (!isset($this->requests[$pos])) {
+ return null;
+ }
+
+ return $this->requests[$pos];
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md
index c06dd3fa57e07..b36e9358ff13e 100644
--- a/src/Symfony/Component/HttpKernel/CHANGELOG.md
+++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+2.4.0
+-----
+
+ * added the KernelEvents::FINISH_REQUEST event
+
2.3.0
-----
diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php
index c9b8a211d4291..69e7937d18c11 100644
--- a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php
+++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\DependencyInjection;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
@@ -35,10 +36,11 @@ class ContainerAwareHttpKernel extends HttpKernel
* @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
* @param ContainerInterface $container A ContainerInterface instance
* @param ControllerResolverInterface $controllerResolver A ControllerResolverInterface instance
+ * @param RequestStack $requestStack A stack for master/sub requests
*/
- public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver)
+ public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver, RequestStack $requestStack = null)
{
- parent::__construct($dispatcher, $controllerResolver);
+ parent::__construct($dispatcher, $controllerResolver, $requestStack);
$this->container = $container;
diff --git a/src/Symfony/Component/HttpKernel/Event/FinishRequestEvent.php b/src/Symfony/Component/HttpKernel/Event/FinishRequestEvent.php
new file mode 100644
index 0000000000000..ee724843cd843
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Event/FinishRequestEvent.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Event;
+
+/**
+ * Triggered whenever a request is fully processed.
+ *
+ * @author Benjamin Eberlei
+ */
+class FinishRequestEvent extends KernelEvent
+{
+}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
index 0b864c02f2bc4..7cab6aa39b560 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
@@ -12,7 +12,9 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -20,32 +22,48 @@
/**
* Initializes the locale based on the current request.
*
+ * This listener works in 2 modes:
+ *
+ * * 2.3 compatibility mode where you must call setRequest whenever the Request changes.
+ * * 2.4+ mode where you must pass a RequestStack instance in the constructor.
+ *
* @author Fabien Potencier
*/
class LocaleListener implements EventSubscriberInterface
{
private $router;
private $defaultLocale;
+ private $requestStack;
- public function __construct($defaultLocale = 'en', RequestContextAwareInterface $router = null)
+ /**
+ * RequestStack will become required in 3.0.
+ */
+ public function __construct($defaultLocale = 'en', RequestContextAwareInterface $router = null, RequestStack $requestStack = null)
{
$this->defaultLocale = $defaultLocale;
+ $this->requestStack = $requestStack;
$this->router = $router;
}
+ /**
+ * Sets the current Request.
+ *
+ * This method was used to synchronize the Request, but as the HttpKernel
+ * is doing that automatically now, you should never be called it directly.
+ * It is kept public for BC with the 2.3 version.
+ *
+ * @param Request|null $request A Request instance
+ *
+ * @deprecated Deprecated since version 2.4, to be removed in 3.0.
+ */
public function setRequest(Request $request = null)
{
if (null === $request) {
return;
}
- if ($locale = $request->attributes->get('_locale')) {
- $request->setLocale($locale);
- }
-
- if (null !== $this->router) {
- $this->router->getContext()->setParameter('_locale', $request->getLocale());
- }
+ $this->setLocale($request);
+ $this->setRouterContext($request);
}
public function onKernelRequest(GetResponseEvent $event)
@@ -53,7 +71,33 @@ public function onKernelRequest(GetResponseEvent $event)
$request = $event->getRequest();
$request->setDefaultLocale($this->defaultLocale);
- $this->setRequest($request);
+ $this->setLocale($request);
+ $this->setRouterContext($request);
+ }
+
+ public function onKernelFinishRequest(FinishRequestEvent $event)
+ {
+ if (null === $this->requestStack) {
+ throw new \LogicException('You must pass a RequestStack.');
+ }
+
+ if (null !== $parentRequest = $this->requestStack->getParentRequest()) {
+ $this->setRouterContext($parentRequest);
+ }
+ }
+
+ private function setLocale(Request $request)
+ {
+ if ($locale = $request->attributes->get('_locale')) {
+ $request->setLocale($locale);
+ }
+ }
+
+ private function setRouterContext(Request $request)
+ {
+ if (null !== $this->router) {
+ $this->router->getContext()->setParameter('_locale', $request->getLocale());
+ }
}
public static function getSubscribedEvents()
@@ -61,6 +105,7 @@ public static function getSubscribedEvents()
return array(
// must be registered after the Router to have access to the _locale
KernelEvents::REQUEST => array(array('onKernelRequest', 16)),
+ KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)),
);
}
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php
index 777fd11bf40ab..d122388b4b086 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php
@@ -13,9 +13,11 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
@@ -28,6 +30,11 @@
/**
* Initializes the context from the request and sets request attributes based on a matching route.
*
+ * This listener works in 2 modes:
+ *
+ * * 2.3 compatibility mode where you must call setRequest whenever the Request changes.
+ * * 2.4+ mode where you must pass a RequestStack instance in the constructor.
+ *
* @author Fabien Potencier
*/
class RouterListener implements EventSubscriberInterface
@@ -36,17 +43,20 @@ class RouterListener implements EventSubscriberInterface
private $context;
private $logger;
private $request;
+ private $requestStack;
/**
* Constructor.
*
+ * RequestStack will become required in 3.0.
+ *
* @param UrlMatcherInterface|RequestMatcherInterface $matcher The Url or Request matcher
* @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface)
* @param LoggerInterface|null $logger The logger
*
* @throws \InvalidArgumentException
*/
- public function __construct($matcher, RequestContext $context = null, LoggerInterface $logger = null)
+ public function __construct($matcher, RequestContext $context = null, LoggerInterface $logger = null, RequestStack $requestStack = null)
{
if (!$matcher instanceof UrlMatcherInterface && !$matcher instanceof RequestMatcherInterface) {
throw new \InvalidArgumentException('Matcher must either implement UrlMatcherInterface or RequestMatcherInterface.');
@@ -58,18 +68,20 @@ public function __construct($matcher, RequestContext $context = null, LoggerInte
$this->matcher = $matcher;
$this->context = $context ?: $matcher->getContext();
+ $this->requestStack = $requestStack;
$this->logger = $logger;
}
/**
* Sets the current Request.
*
- * The application should call this method whenever the Request
- * object changes (entering a Request scope for instance, but
- * also when leaving a Request scope -- especially when they are
- * nested).
+ * This method was used to synchronize the Request, but as the HttpKernel
+ * is doing that automatically now, you should never be called it directly.
+ * It is kept public for BC with the 2.3 version.
*
* @param Request|null $request A Request instance
+ *
+ * @deprecated Deprecated since version 2.4, to be moved to a private function in 3.0.
*/
public function setRequest(Request $request = null)
{
@@ -79,6 +91,15 @@ public function setRequest(Request $request = null)
$this->request = $request;
}
+ public function onKernelFinishRequest(FinishRequestEvent $event)
+ {
+ if (null === $this->requestStack) {
+ throw new \LogicException('You must pass a RequestStack.');
+ }
+
+ $this->setRequest($this->requestStack->getParentRequest());
+ }
+
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
@@ -86,7 +107,10 @@ public function onKernelRequest(GetResponseEvent $event)
// initialize the context that is also used by the generator (assuming matcher and generator share the same context instance)
// we call setRequest even if most of the time, it has already been done to keep compatibility
// with frameworks which do not use the Symfony service container
- $this->setRequest($request);
+ // when we have a RequestStack, no need to do it
+ if (null !== $this->requestStack) {
+ $this->setRequest($request);
+ }
if ($request->attributes->has('_controller')) {
// routing is already done
@@ -139,6 +163,7 @@ public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array(array('onKernelRequest', 32)),
+ KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)),
);
}
}
diff --git a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php
index af9b9ba98b744..05b6a086e9709 100644
--- a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php
+++ b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php
@@ -14,6 +14,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
/**
@@ -22,6 +23,11 @@
* This class handles the rendering of resource fragments that are included into
* a main resource. The handling of the rendering is managed by specialized renderers.
*
+ * This listener works in 2 modes:
+ *
+ * * 2.3 compatibility mode where you must call setRequest whenever the Request changes.
+ * * 2.4+ mode where you must pass a RequestStack instance in the constructor.
+ *
* @author Fabien Potencier
*
* @see FragmentRendererInterface
@@ -31,15 +37,19 @@ class FragmentHandler
private $debug;
private $renderers;
private $request;
+ private $requestStack;
/**
* Constructor.
*
+ * RequestStack will become required in 3.0.
+ *
* @param FragmentRendererInterface[] $renderers An array of FragmentRendererInterface instances
* @param Boolean $debug Whether the debug mode is enabled or not
*/
- public function __construct(array $renderers = array(), $debug = false)
+ public function __construct(array $renderers = array(), $debug = false, RequestStack $requestStack = null)
{
+ $this->requestStack = $requestStack;
$this->renderers = array();
foreach ($renderers as $renderer) {
$this->addRenderer($renderer);
@@ -60,7 +70,13 @@ public function addRenderer(FragmentRendererInterface $renderer)
/**
* Sets the current Request.
*
- * @param Request $request The current Request
+ * This method was used to synchronize the Request, but as the HttpKernel
+ * is doing that automatically now, you should never be called it directly.
+ * It is kept public for BC with the 2.3 version.
+ *
+ * @param Request|null $request A Request instance
+ *
+ * @deprecated Deprecated since version 2.4, to be removed in 3.0.
*/
public function setRequest(Request $request = null)
{
@@ -93,11 +109,11 @@ public function render($uri, $renderer = 'inline', array $options = array())
throw new \InvalidArgumentException(sprintf('The "%s" renderer does not exist.', $renderer));
}
- if (null === $this->request) {
- throw new \LogicException('Rendering a fragment can only be done when handling a master Request.');
+ if (!$request = $this->getRequest()) {
+ throw new \LogicException('Rendering a fragment can only be done when handling a Request.');
}
- return $this->deliver($this->renderers[$renderer]->render($uri, $this->request, $options));
+ return $this->deliver($this->renderers[$renderer]->render($uri, $request, $options));
}
/**
@@ -115,7 +131,7 @@ public function render($uri, $renderer = 'inline', array $options = array())
protected function deliver(Response $response)
{
if (!$response->isSuccessful()) {
- throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->request->getUri(), $response->getStatusCode()));
+ throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->getRequest()->getUri(), $response->getStatusCode()));
}
if (!$response instanceof StreamedResponse) {
@@ -124,4 +140,9 @@ protected function deliver(Response $response)
$response->sendContent();
}
+
+ private function getRequest()
+ {
+ return $this->requestStack ? $this->requestStack->getCurrentRequest() : $this->request;
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php
index 837a16ff370e9..0be8e1b4dbb7c 100644
--- a/src/Symfony/Component/HttpKernel/HttpKernel.php
+++ b/src/Symfony/Component/HttpKernel/HttpKernel.php
@@ -16,11 +16,13 @@
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -35,19 +37,22 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
{
protected $dispatcher;
protected $resolver;
+ protected $requestStack;
/**
* Constructor
*
- * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
- * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance
+ * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
+ * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance
+ * @param RequestStack $requestStack A stack for master/sub requests
*
* @api
*/
- public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver)
+ public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, RequestStack $requestStack = null)
{
$this->dispatcher = $dispatcher;
$this->resolver = $resolver;
+ $this->requestStack = $requestStack ?: new RequestStack();
}
/**
@@ -61,6 +66,8 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
return $this->handleRaw($request, $type);
} catch (\Exception $e) {
if (false === $catch) {
+ $this->finishRequest($request, $type);
+
throw $e;
}
@@ -93,6 +100,8 @@ public function terminate(Request $request, Response $response)
*/
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
+ $this->requestStack->push($request);
+
// request
$event = new GetResponseEvent($this, $request, $type);
$this->dispatcher->dispatch(KernelEvents::REQUEST, $event);
@@ -156,9 +165,27 @@ private function filterResponse(Response $response, Request $request, $type)
$this->dispatcher->dispatch(KernelEvents::RESPONSE, $event);
+ $this->finishRequest($request, $type);
+
return $event->getResponse();
}
+ /**
+ * Publishes the finish request event, then pop the request from the stack.
+ *
+ * Note that the order of the operations is important here, otherwise
+ * operations such as {@link RequestStack::getParentRequest()} can lead to
+ * weird results.
+ *
+ * @param Request $request
+ * @param int $type
+ */
+ private function finishRequest(Request $request, $type)
+ {
+ $this->dispatcher->dispatch(KernelEvents::FINISH_REQUEST, new FinishRequestEvent($this, $request, $type));
+ $this->requestStack->pop();
+ }
+
/**
* Handles an exception by trying to convert it to a Response.
*
@@ -179,6 +206,8 @@ private function handleException(\Exception $e, $request, $type)
$e = $event->getException();
if (!$event->hasResponse()) {
+ $this->finishRequest($request, $type);
+
throw $e;
}
diff --git a/src/Symfony/Component/HttpKernel/KernelEvents.php b/src/Symfony/Component/HttpKernel/KernelEvents.php
index fce48ac3a6ed8..5e6ebcb8d9926 100644
--- a/src/Symfony/Component/HttpKernel/KernelEvents.php
+++ b/src/Symfony/Component/HttpKernel/KernelEvents.php
@@ -102,4 +102,14 @@ final class KernelEvents
* @var string
*/
const TERMINATE = 'kernel.terminate';
+
+ /**
+ * The REQUEST_FINISHED event occurs when a response was generated for a request.
+ *
+ * This event allows you to reset the global and environmental state of
+ * the application, when it was changed during the request.
+ *
+ * @var string
+ */
+ const FINISH_REQUEST = 'kernel.finish_request';
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php
index 28901dafdd643..2c8a6a28ec149 100644
--- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -26,59 +27,49 @@ public function testHandle($type)
{
$request = new Request();
$expected = new Response();
+ $controller = function() use ($expected) {
+ return $expected;
+ };
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
- $container
- ->expects($this->once())
- ->method('enterScope')
- ->with($this->equalTo('request'))
- ;
- $container
- ->expects($this->once())
- ->method('leaveScope')
- ->with($this->equalTo('request'))
- ;
- $container
- ->expects($this->at(0))
- ->method('hasScope')
- ->with($this->equalTo('request'))
- ->will($this->returnValue(false));
- $container
- ->expects($this->at(1))
- ->method('addScope')
- ->with($this->isInstanceOf('Symfony\Component\DependencyInjection\Scope'));
- // enterScope()
- $container
- ->expects($this->at(3))
- ->method('set')
- ->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request'))
- ;
- $container
- ->expects($this->at(4))
- ->method('set')
- ->with($this->equalTo('request'), $this->equalTo(null), $this->equalTo('request'))
+ $this
+ ->expectsEnterScopeOnce($container)
+ ->expectsLeaveScopeOnce($container)
+ ->expectsSetRequestWithAt($container, $request, 3)
+ ->expectsSetRequestWithAt($container, null, 4)
;
$dispatcher = new EventDispatcher();
- $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
- $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver);
+ $resolver = $this->getResolverMockFor($controller, $request);
+ $stack = new RequestStack();
+ $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver, $stack);
- $controller = function () use ($expected) {
+ $actual = $kernel->handle($request, $type);
+
+ $this->assertSame($expected, $actual, '->handle() returns the response');
+ }
+
+ /**
+ * @dataProvider getProviderTypes
+ */
+ public function testVerifyRequestStackPushPopDuringHandle($type)
+ {
+ $request = new Request();
+ $expected = new Response();
+ $controller = function() use ($expected) {
return $expected;
};
- $resolver->expects($this->once())
- ->method('getController')
- ->with($request)
- ->will($this->returnValue($controller));
- $resolver->expects($this->once())
- ->method('getArguments')
- ->with($request, $controller)
- ->will($this->returnValue(array()));
+ $stack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array('push', 'pop'));
+ $stack->expects($this->at(0))->method('push')->with($this->equalTo($request));
+ $stack->expects($this->at(1))->method('pop');
- $actual = $kernel->handle($request, $type);
+ $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
+ $dispatcher = new EventDispatcher();
+ $resolver = $this->getResolverMockFor($controller, $request);
+ $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver, $stack);
- $this->assertSame($expected, $actual, '->handle() returns the response');
+ $kernel->handle($request, $type);
}
/**
@@ -88,51 +79,23 @@ public function testHandleRestoresThePreviousRequestOnException($type)
{
$request = new Request();
$expected = new \Exception();
+ $controller = function() use ($expected) {
+ throw $expected;
+ };
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
- $container
- ->expects($this->once())
- ->method('enterScope')
- ->with($this->equalTo('request'))
- ;
- $container
- ->expects($this->once())
- ->method('leaveScope')
- ->with($this->equalTo('request'))
- ;
- $container
- ->expects($this->at(0))
- ->method('hasScope')
- ->with($this->equalTo('request'))
- ->will($this->returnValue(true));
- // enterScope()
- $container
- ->expects($this->at(2))
- ->method('set')
- ->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request'))
- ;
- $container
- ->expects($this->at(3))
- ->method('set')
- ->with($this->equalTo('request'), $this->equalTo(null), $this->equalTo('request'))
+ $this
+ ->expectsEnterScopeOnce($container)
+ ->expectsLeaveScopeOnce($container)
+ ->expectsSetRequestWithAt($container, $request, 3)
+ ->expectsSetRequestWithAt($container, null, 4)
;
$dispatcher = new EventDispatcher();
$resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
- $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver);
-
- $controller = function () use ($expected) {
- throw $expected;
- };
-
- $resolver->expects($this->once())
- ->method('getController')
- ->with($request)
- ->will($this->returnValue($controller));
- $resolver->expects($this->once())
- ->method('getArguments')
- ->with($request, $controller)
- ->will($this->returnValue(array()));
+ $resolver = $this->getResolverMockFor($controller, $request);
+ $stack = new RequestStack();
+ $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver, $stack);
try {
$kernel->handle($request, $type);
@@ -151,4 +114,51 @@ public function getProviderTypes()
array(HttpKernelInterface::SUB_REQUEST),
);
}
+
+ private function getResolverMockFor($controller, $request)
+ {
+ $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
+ $resolver->expects($this->once())
+ ->method('getController')
+ ->with($request)
+ ->will($this->returnValue($controller));
+ $resolver->expects($this->once())
+ ->method('getArguments')
+ ->with($request, $controller)
+ ->will($this->returnValue(array()));
+
+ return $resolver;
+ }
+
+ private function expectsSetRequestWithAt($container, $with, $at)
+ {
+ $container
+ ->expects($this->at($at))
+ ->method('set')
+ ->with($this->equalTo('request'), $this->equalTo($with), $this->equalTo('request'))
+ ;
+ return $this;
+ }
+
+ private function expectsEnterScopeOnce($container)
+ {
+ $container
+ ->expects($this->once())
+ ->method('enterScope')
+ ->with($this->equalTo('request'))
+ ;
+
+ return $this;
+ }
+
+ private function expectsLeaveScopeOnce($container)
+ {
+ $container
+ ->expects($this->once())
+ ->method('leaveScope')
+ ->with($this->equalTo('request'))
+ ;
+
+ return $this;
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php
index 36859baa16b0f..d128753ffab06 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php
@@ -11,16 +11,24 @@
namespace Symfony\Component\HttpKernel\Tests\EventListener;
-use Symfony\Component\HttpKernel\EventListener\LocaleListener;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\EventListener\LocaleListener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class LocaleListenerTest extends \PHPUnit_Framework_TestCase
{
+ private $requestStack;
+
+ protected function setUp()
+ {
+ $this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array(), array(), '', false);
+ }
+
public function testDefaultLocaleWithoutSession()
{
- $listener = new LocaleListener('fr');
+ $listener = new LocaleListener('fr', null, $this->requestStack);
$event = $this->getEvent($request = Request::create('/'));
$listener->onKernelRequest($event);
@@ -34,7 +42,7 @@ public function testLocaleFromRequestAttribute()
$request->cookies->set('foo', 'value');
$request->attributes->set('_locale', 'es');
- $listener = new LocaleListener('fr');
+ $listener = new LocaleListener('fr', null, $this->requestStack);
$event = $this->getEvent($request);
$listener->onKernelRequest($event);
@@ -53,15 +61,39 @@ public function testLocaleSetForRoutingContext()
$request = Request::create('/');
$request->attributes->set('_locale', 'es');
- $listener = new LocaleListener('fr', $router);
+ $listener = new LocaleListener('fr', $router, $this->requestStack);
$listener->onKernelRequest($this->getEvent($request));
}
+ public function testRouterResetWithParentRequestOnKernelFinishRequest()
+ {
+ if (!class_exists('Symfony\Component\Routing\Router')) {
+ $this->markTestSkipped('The "Routing" component is not available');
+ }
+
+ // the request context is updated
+ $context = $this->getMock('Symfony\Component\Routing\RequestContext');
+ $context->expects($this->once())->method('setParameter')->with('_locale', 'es');
+
+ $router = $this->getMock('Symfony\Component\Routing\Router', array('getContext'), array(), '', false);
+ $router->expects($this->once())->method('getContext')->will($this->returnValue($context));
+
+ $parentRequest = Request::create('/');
+ $parentRequest->setLocale('es');
+
+ $this->requestStack->expects($this->once())->method('getParentRequest')->will($this->returnValue($parentRequest));
+
+ $event = $this->getMock('Symfony\Component\HttpKernel\Event\FinishRequestEvent', array(), array(), '', false);
+
+ $listener = new LocaleListener('fr', $router, $this->requestStack);
+ $listener->onKernelFinishRequest($event);
+ }
+
public function testRequestLocaleIsNotOverridden()
{
$request = Request::create('/');
$request->setLocale('de');
- $listener = new LocaleListener('fr');
+ $listener = new LocaleListener('fr', null, $this->requestStack);
$event = $this->getEvent($request);
$listener->onKernelRequest($event);
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php
index 66fe6bd55d505..ac742b35e08ce 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php
@@ -11,14 +11,22 @@
namespace Symfony\Component\HttpKernel\Tests\EventListener;
-use Symfony\Component\HttpKernel\EventListener\RouterListener;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Routing\RequestContext;
class RouterListenerTest extends \PHPUnit_Framework_TestCase
{
+ private $requestStack;
+
+ public function setUp()
+ {
+ $this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array(), array(), '', false);
+ }
+
/**
* @dataProvider getPortData
*/
@@ -34,7 +42,7 @@ public function testPort($defaultHttpPort, $defaultHttpsPort, $uri, $expectedHtt
->method('getContext')
->will($this->returnValue($context));
- $listener = new RouterListener($urlMatcher);
+ $listener = new RouterListener($urlMatcher, null, null, $this->requestStack);
$event = $this->createGetResponseEventForUri($uri);
$listener->onKernelRequest($event);
@@ -72,7 +80,7 @@ private function createGetResponseEventForUri($uri)
*/
public function testInvalidMatcher()
{
- new RouterListener(new \stdClass());
+ new RouterListener(new \stdClass(), null, null, $this->requestStack);
}
public function testRequestMatcher()
@@ -87,7 +95,7 @@ public function testRequestMatcher()
->with($this->isInstanceOf('Symfony\Component\HttpFoundation\Request'))
->will($this->returnValue(array()));
- $listener = new RouterListener($requestMatcher, new RequestContext());
+ $listener = new RouterListener($requestMatcher, new RequestContext(), null, $this->requestStack);
$listener->onKernelRequest($event);
}
@@ -108,7 +116,7 @@ public function testSubRequestWithDifferentMethod()
->method('getContext')
->will($this->returnValue($context));
- $listener = new RouterListener($requestMatcher, new RequestContext());
+ $listener = new RouterListener($requestMatcher, new RequestContext(), null, $this->requestStack);
$listener->onKernelRequest($event);
// sub-request with another HTTP method
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php
index cec8ae98403a7..dbf6b20b424ed 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php
@@ -17,12 +17,27 @@
class FragmentHandlerTest extends \PHPUnit_Framework_TestCase
{
+ private $requestStack;
+
+ public function setUp()
+ {
+ $this->requestStack = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack')
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+ $this->requestStack
+ ->expects($this->any())
+ ->method('getCurrentRequest')
+ ->will($this->returnValue(Request::create('/')))
+ ;
+ }
+
/**
* @expectedException \InvalidArgumentException
*/
public function testRenderWhenRendererDoesNotExist()
{
- $handler = new FragmentHandler();
+ $handler = new FragmentHandler(array(), null, $this->requestStack);
$handler->render('/', 'foo');
}
@@ -72,9 +87,8 @@ protected function getHandler($returnValue, $arguments = array())
call_user_func_array(array($e, 'with'), $arguments);
}
- $handler = new FragmentHandler();
+ $handler = new FragmentHandler(array(), null, $this->requestStack);
$handler->addRenderer($renderer);
- $handler->setRequest(Request::create('/'));
return $handler;
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
index 1bfa0e5fa822e..2f69a94575477 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
@@ -238,6 +238,20 @@ public function testTerminate()
$this->assertEquals($response, $capturedResponse);
}
+ public function testVerifyRequestStackPushPopDuringHandle()
+ {
+ $request = new Request();
+
+ $stack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array('push', 'pop'));
+ $stack->expects($this->at(0))->method('push')->with($this->equalTo($request));
+ $stack->expects($this->at(1))->method('pop');
+
+ $dispatcher = new EventDispatcher();
+ $kernel = new HttpKernel($dispatcher, $this->getResolver(), $stack);
+
+ $kernel->handle($request, HttpKernelInterface::MASTER_REQUEST);
+ }
+
protected function getResolver($controller = null)
{
if (null === $controller) {
diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json
index 0e8aac54c6af8..a09b0013271c7 100644
--- a/src/Symfony/Component/HttpKernel/composer.json
+++ b/src/Symfony/Component/HttpKernel/composer.json
@@ -18,7 +18,7 @@
"require": {
"php": ">=5.3.3",
"symfony/event-dispatcher": "~2.1",
- "symfony/http-foundation": "~2.2",
+ "symfony/http-foundation": "~2.4",
"symfony/debug": "~2.3",
"psr/log": "~1.0"
},
diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php
index 36df81a80e629..5a1e9d564749f 100644
--- a/src/Symfony/Component/Security/Http/Firewall.php
+++ b/src/Symfony/Component/Security/Http/Firewall.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -30,6 +31,7 @@ class Firewall implements EventSubscriberInterface
{
private $map;
private $dispatcher;
+ private $exceptionListeners;
/**
* Constructor.
@@ -41,6 +43,7 @@ public function __construct(FirewallMapInterface $map, EventDispatcherInterface
{
$this->map = $map;
$this->dispatcher = $dispatcher;
+ $this->exceptionListeners = new \SplObjectStorage();
}
/**
@@ -57,6 +60,7 @@ public function onKernelRequest(GetResponseEvent $event)
// register listeners for this firewall
list($listeners, $exception) = $this->map->getListeners($event->getRequest());
if (null !== $exception) {
+ $this->exceptionListeners[$event->getRequest()] = $exception;
$exception->register($this->dispatcher);
}
@@ -70,8 +74,21 @@ public function onKernelRequest(GetResponseEvent $event)
}
}
+ public function onKernelFinishRequest(FinishRequestEvent $event)
+ {
+ $request = $event->getRequest();
+
+ if (isset($this->exceptionListeners[$request])) {
+ $this->exceptionListeners[$request]->unregister($this->dispatcher);
+ unset($this->exceptionListeners[$request]);
+ }
+ }
+
public static function getSubscribedEvents()
{
- return array(KernelEvents::REQUEST => array('onKernelRequest', 8));
+ return array(
+ KernelEvents::REQUEST => array('onKernelRequest', 8),
+ KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest',
+ );
}
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
index abbb4606a6e81..0cca0c46cc394 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
@@ -69,6 +69,16 @@ public function register(EventDispatcherInterface $dispatcher)
$dispatcher->addListener(KernelEvents::EXCEPTION, array($this, 'onKernelException'));
}
+ /**
+ * Unregisters the dispatcher.
+ *
+ * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
+ */
+ public function unregister(EventDispatcherInterface $dispatcher)
+ {
+ $dispatcher->removeListener(KernelEvents::EXCEPTION, array($this, 'onKernelException'));
+ }
+
/**
* Handles security related exceptions.
*
@@ -76,10 +86,6 @@ public function register(EventDispatcherInterface $dispatcher)
*/
public function onKernelException(GetResponseForExceptionEvent $event)
{
- // we need to remove ourselves as the exception listener can be
- // different depending on the Request
- $event->getDispatcher()->removeListener(KernelEvents::EXCEPTION, array($this, 'onKernelException'));
-
$exception = $event->getException();
$request = $event->getRequest();
diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json
index b6bbae4515029..fe1299c170e10 100644
--- a/src/Symfony/Component/Security/composer.json
+++ b/src/Symfony/Component/Security/composer.json
@@ -18,8 +18,8 @@
"require": {
"php": ">=5.3.3",
"symfony/event-dispatcher": "~2.1",
- "symfony/http-foundation": "~2.1",
- "symfony/http-kernel": "~2.1"
+ "symfony/http-foundation": "~2.4",
+ "symfony/http-kernel": "~2.4"
},
"require-dev": {
"symfony/form": "~2.0",