Skip to content

Commit bb9fe65

Browse files
committed
lazy-load fragment renderers
1 parent 6885074 commit bb9fe65

File tree

4 files changed

+282
-0
lines changed

4 files changed

+282
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
18+
/**
19+
* Adds services tagged kernel.fragment_renderer as HTTP content rendering strategies.
20+
*
21+
* @author Fabien Potencier <fabien@symfony.com>
22+
*/
23+
class FragmentRendererPass implements CompilerPassInterface
24+
{
25+
private $handlerService;
26+
private $rendererTag;
27+
28+
/**
29+
* @param string $handlerService Service name of the fragment handler in the container
30+
* @param string $rendererTag Tag name used for fragments
31+
*/
32+
public function __construct($handlerService = 'fragment.handler', $rendererTag = 'kernel.fragment_renderer')
33+
{
34+
$this->handlerService = $handlerService;
35+
$this->rendererTag = $rendererTag;
36+
}
37+
38+
public function process(ContainerBuilder $container)
39+
{
40+
if (!$container->hasDefinition($this->handlerService)) {
41+
return;
42+
}
43+
44+
$definition = $container->getDefinition($this->handlerService);
45+
foreach ($container->findTaggedServiceIds($this->rendererTag) as $id => $tags) {
46+
$def = $container->getDefinition($id);
47+
if (!$def->isPublic()) {
48+
throw new \InvalidArgumentException(sprintf('The service "%s" must be public as fragment renderer are lazy-loaded.', $id));
49+
}
50+
51+
if ($def->isAbstract()) {
52+
throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as fragment renderer are lazy-loaded.', $id));
53+
}
54+
55+
$refClass = new \ReflectionClass($container->getParameterBag()->resolveValue($def->getClass()));
56+
$interface = 'Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface';
57+
if (!$refClass->implementsInterface($interface)) {
58+
throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
59+
}
60+
61+
foreach ($tags as $tag) {
62+
if (!isset($tag['alias'])) {
63+
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);
64+
65+
// register the handler as a non-lazy-loaded one
66+
$definition->addMethodCall('addRenderer', array(new Reference($id)));
67+
}
68+
69+
$definition->addMethodCall('addRendererService', array($tag['alias'], $id));
70+
}
71+
}
72+
}
73+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerInterface;
15+
use Symfony\Component\HttpFoundation\RequestStack;
16+
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
17+
18+
/**
19+
* Lazily loads fragment renderers from the dependency injection container.
20+
*
21+
* @author Fabien Potencier <fabien@symfony.com>
22+
*/
23+
class LazyLoadingFragmentHandler extends FragmentHandler
24+
{
25+
private $container;
26+
private $rendererIds = array();
27+
28+
public function __construct(ContainerInterface $container, $debug = false, RequestStack $requestStack = null)
29+
{
30+
$this->container = $container;
31+
32+
parent::__construct(array(), $debug, $requestStack);
33+
}
34+
35+
/**
36+
* Adds a service as a fragment renderer.
37+
*
38+
* @param string $renderer The render service id
39+
*/
40+
public function addRendererService($name, $renderer)
41+
{
42+
$this->rendererIds[$name] = $renderer;
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function render($uri, $renderer = 'inline', array $options = array())
49+
{
50+
if (isset($this->rendererIds[$renderer])) {
51+
$this->addRenderer($this->container->get($this->rendererIds[$renderer]));
52+
53+
unset($this->rendererIds[$renderer]);
54+
}
55+
56+
return parent::render($uri, $renderer, $options);
57+
}
58+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\DependencyInjection;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass;
16+
use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface;
17+
18+
class FragmentRendererPassTest extends \PHPUnit_Framework_TestCase
19+
{
20+
/**
21+
* Tests that content rendering not implementing FragmentRendererInterface
22+
* trigger an exception.
23+
*
24+
* @expectedException \InvalidArgumentException
25+
*/
26+
public function testContentRendererWithoutInterface()
27+
{
28+
// one service, not implementing any interface
29+
$services = array(
30+
'my_content_renderer' => array('alias' => 'foo'),
31+
);
32+
33+
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
34+
35+
$builder = $this->getMock(
36+
'Symfony\Component\DependencyInjection\ContainerBuilder',
37+
array('hasDefinition', 'findTaggedServiceIds', 'getDefinition')
38+
);
39+
$builder->expects($this->any())
40+
->method('hasDefinition')
41+
->will($this->returnValue(true));
42+
43+
// We don't test kernel.fragment_renderer here
44+
$builder->expects($this->atLeastOnce())
45+
->method('findTaggedServiceIds')
46+
->will($this->returnValue($services));
47+
48+
$builder->expects($this->atLeastOnce())
49+
->method('getDefinition')
50+
->will($this->returnValue($definition));
51+
52+
$pass = new FragmentRendererPass();
53+
$pass->process($builder);
54+
}
55+
56+
public function testValidContentRenderer()
57+
{
58+
$services = array(
59+
'my_content_renderer' => array(array('alias' => 'foo')),
60+
);
61+
62+
$renderer = $this->getMock('Symfony\Component\DependencyInjection\Definition');
63+
$renderer
64+
->expects($this->once())
65+
->method('addMethodCall')
66+
->with('addRendererService', array('foo', 'my_content_renderer'))
67+
;
68+
69+
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
70+
$definition->expects($this->atLeastOnce())
71+
->method('getClass')
72+
->will($this->returnValue('Symfony\Component\HttpKernel\Tests\DependencyInjection\RendererService'));
73+
$definition
74+
->expects($this->once())
75+
->method('isPublic')
76+
->will($this->returnValue(true))
77+
;
78+
79+
$builder = $this->getMock(
80+
'Symfony\Component\DependencyInjection\ContainerBuilder',
81+
array('hasDefinition', 'findTaggedServiceIds', 'getDefinition')
82+
);
83+
$builder->expects($this->any())
84+
->method('hasDefinition')
85+
->will($this->returnValue(true));
86+
87+
// We don't test kernel.fragment_renderer here
88+
$builder->expects($this->atLeastOnce())
89+
->method('findTaggedServiceIds')
90+
->will($this->returnValue($services));
91+
92+
$builder->expects($this->atLeastOnce())
93+
->method('getDefinition')
94+
->will($this->onConsecutiveCalls($renderer, $definition));
95+
96+
$pass = new FragmentRendererPass();
97+
$pass->process($builder);
98+
}
99+
}
100+
101+
class RendererService implements FragmentRendererInterface
102+
{
103+
public function render($uri, Request $request = null, array $options = array())
104+
{
105+
}
106+
107+
public function getName()
108+
{
109+
return 'test';
110+
}
111+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\DependencyInjection;
13+
14+
use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
18+
class LazyLoadingFragmentHandlerTest extends \PHPUnit_Framework_TestCase
19+
{
20+
public function test()
21+
{
22+
$renderer = $this->getMock('Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface');
23+
$renderer->expects($this->once())->method('getName')->will($this->returnValue('foo'));
24+
$renderer->expects($this->any())->method('render')->will($this->returnValue(new Response()));
25+
26+
$requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack');
27+
$requestStack->expects($this->any())->method('getCurrentRequest')->will($this->returnValue(Request::create('/')));
28+
29+
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
30+
$container->expects($this->once())->method('get')->will($this->returnValue($renderer));
31+
32+
$handler = new LazyLoadingFragmentHandler($container, false, $requestStack);
33+
$handler->addRendererService('foo', 'foo');
34+
35+
$handler->render('/foo', 'foo');
36+
37+
// second call should not lazy-load anymore (see once() above on the get() method)
38+
$handler->render('/foo', 'foo');
39+
}
40+
}

0 commit comments

Comments
 (0)