feature #13378 lazy-load fragment renderers (fabpot)

This PR was merged into the 2.7 branch.

Discussion
----------

lazy-load fragment renderers

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

Now that we have more fragment renderers (inline, hinclude, esi, and ssi), it's a waste of time to load them all for every single requests; actually, most of the time, we load them for nothing.

So, like we did for event listeners, I propose to lazy-load them.

I've also move the classes to the HttpKernel component as the corresponding classes for the lazy-loading of listeners were moved to the EventDispatcher component a while ago to make them reusable outside of the Symfony2 context.

Last, but not the least, I've named the class with a `LazyLoading` prefix instead of the usual `ContainerAware` as I think it conveys the goal much better. I'd like to rename the other ones as well when it makes sense.

Commits
-------

2be8b6e [TwigBundle] optimized the hinclude fragement renderer when only Twig is used
6148652 [FrameworkBundle] removed obsolete ContainerAwareHIncludeFragmentRenderer class
e620cbf lazy-load fragment renderers
This commit is contained in:
Fabien Potencier 2015-01-12 22:23:21 +01:00
commit 228be36d01
14 changed files with 349 additions and 38 deletions

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
trigger_error('The '.__NAMESPACE__.'\FragmentRendererPass class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass instead.', E_USER_DEPRECATED);
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@ -19,6 +21,8 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
* Adds services tagged kernel.fragment_renderer as HTTP content rendering strategies.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass instead.
*/
class FragmentRendererPass implements CompilerPassInterface
{

View File

@ -539,6 +539,11 @@ class FrameworkExtension extends Extension
$container->setAlias('templating', 'templating.engine.delegating');
}
$container->getDefinition('fragment.renderer.hinclude')
->addTag('kernel.fragment_renderer', array('alias' => 'hinclude'))
->replaceArgument(0, new Reference('templating'))
;
// configure the PHP engine if needed
if (in_array('php', $config['engines'], true)) {
$loader->load('templating_php.xml');

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\FrameworkBundle\Fragment;
trigger_error('The '.__NAMESPACE__.'\ContainerAwareHIncludeFragmentRenderer class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bundle\FrameworkBundle\Fragment\HIncludeFragmentRenderer instead.', E_USER_DEPRECATED);
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\UriSigner;
@ -20,6 +22,8 @@ use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer;
* Implements the Hinclude rendering strategy.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since version 2.7, to be removed in 3.0. Use Symfony\Bundle\FrameworkBundle\Fragment\HIncludeFragmentRenderer instead.
*/
class ContainerAwareHIncludeFragmentRenderer extends HIncludeFragmentRenderer
{

View File

@ -28,13 +28,13 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilder
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FragmentRendererPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Scope;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Bundle\Bundle;

View File

@ -5,9 +5,9 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="fragment.handler.class">Symfony\Component\HttpKernel\Fragment\FragmentHandler</parameter>
<parameter key="fragment.handler.class">Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler</parameter>
<parameter key="fragment.renderer.inline.class">Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer</parameter>
<parameter key="fragment.renderer.hinclude.class">Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer</parameter>
<parameter key="fragment.renderer.hinclude.class">Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer</parameter>
<parameter key="fragment.renderer.hinclude.global_template"></parameter>
<parameter key="fragment.renderer.esi.class">Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer</parameter>
<parameter key="fragment.path">/_fragment</parameter>
@ -15,28 +15,27 @@
<services>
<service id="fragment.handler" class="%fragment.handler.class%">
<argument type="collection" />
<argument type="service" id="service_container" />
<argument>%kernel.debug%</argument>
<argument type="service" id="request_stack" />
</service>
<service id="fragment.renderer.inline" class="%fragment.renderer.inline.class%">
<tag name="kernel.fragment_renderer" />
<tag name="kernel.fragment_renderer" alias="inline" />
<argument type="service" id="http_kernel" />
<argument type="service" id="event_dispatcher" />
<call method="setFragmentPath"><argument>%fragment.path%</argument></call>
</service>
<service id="fragment.renderer.hinclude" class="%fragment.renderer.hinclude.class%">
<tag name="kernel.fragment_renderer" />
<argument type="service" id="service_container" />
<argument /> <!-- templating or Twig service -->
<argument type="service" id="uri_signer" />
<argument>%fragment.renderer.hinclude.global_template%</argument>
<call method="setFragmentPath"><argument>%fragment.path%</argument></call>
</service>
<service id="fragment.renderer.esi" class="%fragment.renderer.esi.class%">
<tag name="kernel.fragment_renderer" />
<tag name="kernel.fragment_renderer" alias="esi" />
<argument type="service" id="esi" on-invalid="null" />
<argument type="service" id="fragment.renderer.inline" />
<argument type="service" id="uri_signer" />
@ -44,7 +43,7 @@
</service>
<service id="fragment.renderer.ssi" class="Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer">
<tag name="kernel.fragment_renderer" />
<tag name="kernel.fragment_renderer" alias="ssi" />
<argument type="service" id="ssi" on-invalid="null" />
<argument type="service" id="fragment.renderer.inline" />
<argument type="service" id="uri_signer" />

View File

@ -0,0 +1,114 @@
<?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\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FragmentRendererPass;
class LegacyFragmentRendererPassTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);
}
/**
* Tests that content rendering not implementing FragmentRendererInterface
* trigger an exception.
*
* @expectedException \InvalidArgumentException
*/
public function testContentRendererWithoutInterface()
{
// one service, not implementing any interface
$services = array(
'my_content_renderer' => array(),
);
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('stdClass'));
$builder = $this->getMock(
'Symfony\Component\DependencyInjection\ContainerBuilder',
array('hasDefinition', 'findTaggedServiceIds', 'getDefinition')
);
$builder->expects($this->any())
->method('hasDefinition')
->will($this->returnValue(true));
// We don't test kernel.fragment_renderer here
$builder->expects($this->atLeastOnce())
->method('findTaggedServiceIds')
->will($this->returnValue($services));
$builder->expects($this->atLeastOnce())
->method('getDefinition')
->will($this->returnValue($definition));
$pass = new FragmentRendererPass();
$pass->process($builder);
}
public function testValidContentRenderer()
{
$services = array(
'my_content_renderer' => array(),
);
$renderer = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$renderer
->expects($this->once())
->method('addMethodCall')
->with('addRenderer', array(new Reference('my_content_renderer')))
;
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\RendererService'));
$builder = $this->getMock(
'Symfony\Component\DependencyInjection\ContainerBuilder',
array('hasDefinition', 'findTaggedServiceIds', 'getDefinition')
);
$builder->expects($this->any())
->method('hasDefinition')
->will($this->returnValue(true));
// We don't test kernel.fragment_renderer here
$builder->expects($this->atLeastOnce())
->method('findTaggedServiceIds')
->will($this->returnValue($services));
$builder->expects($this->atLeastOnce())
->method('getDefinition')
->will($this->onConsecutiveCalls($renderer, $definition));
$pass = new FragmentRendererPass();
$pass->process($builder);
}
}
class RendererService implements \Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface
{
public function render($uri, Request $request = null, array $options = array())
{
}
public function getName()
{
return 'test';
}
}

View File

@ -15,10 +15,12 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer;
use Symfony\Component\HttpFoundation\Request;
class ContainerAwareHIncludeFragmentRendererTest extends TestCase
class LegacyContainerAwareHIncludeFragmentRendererTest extends TestCase
{
public function testRender()
{
$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->once())
->method('get')

View File

@ -19,23 +19,7 @@ class FragmentController extends ContainerAware
{
public function indexAction(Request $request)
{
$actions = $this->container->get('templating')->get('actions');
$html1 = $actions->render($actions->controller('TestBundle:Fragment:inlined', array(
'options' => array(
'bar' => new Bar(),
'eleven' => 11,
),
)));
$html2 = $actions->render($actions->controller('TestBundle:Fragment:customformat', array('_format' => 'html')));
$html3 = $actions->render($actions->controller('TestBundle:Fragment:customlocale', array('_locale' => 'es')));
$request->setLocale('fr');
$html4 = $actions->render($actions->controller('TestBundle:Fragment:forwardlocale'));
return new Response($html1.'--'.$html2.'--'.$html3.'--'.$html4);
return $this->container->get('templating')->renderResponse('fragment.html.php', array('bar' => new Bar()));
}
public function inlinedAction($options, $_format)

View File

@ -0,0 +1,14 @@
<?php echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:inlined', array(
'options' => array(
'bar' => $bar,
'eleven' => 11,
),
)));
?>--<?php
echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:customformat', array('_format' => 'html')));
?>--<?php
echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:customlocale', array('_locale' => 'es')));
?>--<?php
$app->getRequest()->setLocale('fr');
echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:forwardlocale'));
?>

View File

@ -13,6 +13,7 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
@ -41,6 +42,17 @@ class ExtensionPass implements CompilerPassInterface
if ($container->has('fragment.handler')) {
$container->getDefinition('twig.extension.httpkernel')->addTag('twig.extension');
// inject Twig in the hinclude service if Twig is the only registered templating engine
if (
!$container->hasParameter('templating.engines')
|| array('twig') == $container->getParameter('templating.engines')
) {
$container->getDefinition('fragment.renderer.hinclude')
->addTag('kernel.fragment_renderer', array('alias' => 'hinclude'))
->replaceArgument(0, new Reference('twig'))
;
}
}
if ($container->has('request_stack')) {

View File

@ -0,0 +1,73 @@
<?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\HttpKernel\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services tagged kernel.fragment_renderer as HTTP content rendering strategies.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FragmentRendererPass implements CompilerPassInterface
{
private $handlerService;
private $rendererTag;
/**
* @param string $handlerService Service name of the fragment handler in the container
* @param string $rendererTag Tag name used for fragments
*/
public function __construct($handlerService = 'fragment.handler', $rendererTag = 'kernel.fragment_renderer')
{
$this->handlerService = $handlerService;
$this->rendererTag = $rendererTag;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->handlerService)) {
return;
}
$definition = $container->getDefinition($this->handlerService);
foreach ($container->findTaggedServiceIds($this->rendererTag) as $id => $tags) {
$def = $container->getDefinition($id);
if (!$def->isPublic()) {
throw new \InvalidArgumentException(sprintf('The service "%s" must be public as fragment renderer are lazy-loaded.', $id));
}
if ($def->isAbstract()) {
throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as fragment renderer are lazy-loaded.', $id));
}
$refClass = new \ReflectionClass($container->getParameterBag()->resolveValue($def->getClass()));
$interface = 'Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface';
if (!$refClass->implementsInterface($interface)) {
throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}
foreach ($tags as $tag) {
if (!isset($tag['alias'])) {
trigger_error(sprintf('Service "%s" will have to define the "alias" attribute on the "%s" tag as of Symfony 3.0.', $id, $this->fragmentTag), E_USER_DEPRECATED);
// register the handler as a non-lazy-loaded one
$definition->addMethodCall('addRenderer', array(new Reference($id)));
}
$definition->addMethodCall('addRendererService', array($tag['alias'], $id));
}
}
}
}

View File

@ -0,0 +1,58 @@
<?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\HttpKernel\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
/**
* Lazily loads fragment renderers from the dependency injection container.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class LazyLoadingFragmentHandler extends FragmentHandler
{
private $container;
private $rendererIds = array();
public function __construct(ContainerInterface $container, $debug = false, RequestStack $requestStack = null)
{
$this->container = $container;
parent::__construct(array(), $debug, $requestStack);
}
/**
* Adds a service as a fragment renderer.
*
* @param string $renderer The render service id
*/
public function addRendererService($name, $renderer)
{
$this->rendererIds[$name] = $renderer;
}
/**
* {@inheritdoc}
*/
public function render($uri, $renderer = 'inline', array $options = array())
{
if (isset($this->rendererIds[$renderer])) {
$this->addRenderer($this->container->get($this->rendererIds[$renderer]));
unset($this->rendererIds[$renderer]);
}
return parent::render($uri, $renderer, $options);
}
}

View File

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler;
namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FragmentRendererPass;
use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass;
use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface;
class FragmentRendererPassTest extends \PHPUnit_Framework_TestCase
{
@ -27,13 +27,10 @@ class FragmentRendererPassTest extends \PHPUnit_Framework_TestCase
{
// one service, not implementing any interface
$services = array(
'my_content_renderer' => array(),
'my_content_renderer' => array('alias' => 'foo'),
);
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('stdClass'));
$builder = $this->getMock(
'Symfony\Component\DependencyInjection\ContainerBuilder',
@ -59,20 +56,25 @@ class FragmentRendererPassTest extends \PHPUnit_Framework_TestCase
public function testValidContentRenderer()
{
$services = array(
'my_content_renderer' => array(),
'my_content_renderer' => array(array('alias' => 'foo')),
);
$renderer = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$renderer
->expects($this->once())
->method('addMethodCall')
->with('addRenderer', array(new Reference('my_content_renderer')))
->with('addRendererService', array('foo', 'my_content_renderer'))
;
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\RendererService'));
->will($this->returnValue('Symfony\Component\HttpKernel\Tests\DependencyInjection\RendererService'));
$definition
->expects($this->once())
->method('isPublic')
->will($this->returnValue(true))
;
$builder = $this->getMock(
'Symfony\Component\DependencyInjection\ContainerBuilder',
@ -96,7 +98,7 @@ class FragmentRendererPassTest extends \PHPUnit_Framework_TestCase
}
}
class RendererService implements \Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface
class RendererService implements FragmentRendererInterface
{
public function render($uri, Request $request = null, array $options = array())
{

View File

@ -0,0 +1,40 @@
<?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\HttpKernel\Tests\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class LazyLoadingFragmentHandlerTest extends \PHPUnit_Framework_TestCase
{
public function test()
{
$renderer = $this->getMock('Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface');
$renderer->expects($this->once())->method('getName')->will($this->returnValue('foo'));
$renderer->expects($this->any())->method('render')->will($this->returnValue(new Response()));
$requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack');
$requestStack->expects($this->any())->method('getCurrentRequest')->will($this->returnValue(Request::create('/')));
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->once())->method('get')->will($this->returnValue($renderer));
$handler = new LazyLoadingFragmentHandler($container, false, $requestStack);
$handler->addRendererService('foo', 'foo');
$handler->render('/foo', 'foo');
// second call should not lazy-load anymore (see once() above on the get() method)
$handler->render('/foo', 'foo');
}
}