bug #25756 [TwigBundle] Register TwigBridge extensions first (fancyweb)

This PR was squashed before being merged into the 3.4 branch (closes #25756).

Discussion
----------

[TwigBundle] Register TwigBridge extensions first

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | https://github.com/symfony/symfony/issues/25610
| License       | MIT
| Doc PR        | -

The only extension that is really needed to display the current exception page is the `CodeExtension` so we could only prepend this one. However, prepending all of them seems safer to me in the long term.

Also I deeply looked into why this problem only appeared in 3.4 and found the reason. Before 3.4 it actually never reaches the `ExceptionController` for this kind of error because it cannot be resolved because it needs a twig instance in its constructor. This instance is directly taken from the container. Before 3.4 when an exception is thrown when you try to get a service from the container, the instance stored in the `$services` array is unset which is not the case in further versions. So in 3.4+, the `ExceptionController` can be resolved because the instance of twig is still in the container even after the initial exception.

It also means these kind of exceptions are displayed with bugs on all versions before 3.4 I guess. Actually it shows the message 2 times : one for the initial exception and the other one when it tries to resolve the `ExceptionController`.

Maybe another solution might be to use a dedicated twig instance with the right settings just for the exception page ?

Commits
-------

c8465ed97f [TwigBundle] Register TwigBridge extensions first
This commit is contained in:
Fabien Potencier 2018-02-07 06:32:49 +01:00
commit abb913283e
2 changed files with 104 additions and 4 deletions

View File

@ -34,11 +34,22 @@ class TwigEnvironmentPass implements CompilerPassInterface
// For instance, global variable definitions must be registered
// afterward. If not, the globals from the extensions will never
// be registered.
$calls = $definition->getMethodCalls();
$definition->setMethodCalls(array());
$currentMethodCalls = $definition->getMethodCalls();
$twigBridgeExtensionsMethodCalls = array();
$othersExtensionsMethodCalls = array();
foreach ($container->findTaggedServiceIds('twig.extension', true) as $id => $attributes) {
$definition->addMethodCall('addExtension', array(new Reference($id)));
$methodCall = array('addExtension', array(new Reference($id)));
$extensionClass = $container->getDefinition($id)->getClass();
if (is_string($extensionClass) && 0 === strpos($extensionClass, 'Symfony\Bridge\Twig\Extension')) {
$twigBridgeExtensionsMethodCalls[] = $methodCall;
} else {
$othersExtensionsMethodCalls[] = $methodCall;
}
}
if (!empty($twigBridgeExtensionsMethodCalls) || !empty($othersExtensionsMethodCalls)) {
$definition->setMethodCalls(array_merge($twigBridgeExtensionsMethodCalls, $othersExtensionsMethodCalls, $currentMethodCalls));
}
$definition->setMethodCalls(array_merge($definition->getMethodCalls(), $calls));
}
}

View File

@ -0,0 +1,89 @@
<?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\TwigBundle\Tests\DependencyInjection\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigEnvironmentPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class TwigEnvironmentPassTest extends TestCase
{
public function testTwigBridgeExtensionsAreRegisteredFirst()
{
$twigDefinition = new Definition('twig');
$containerBuilderMock = $this->getMockBuilder(ContainerBuilder::class)
->setMethods(array('hasDefinition', 'get', 'findTaggedServiceIds', 'getDefinition'))
->getMock();
$containerBuilderMock
->expects($this->once())
->method('hasDefinition')
->with('twig')
->will($this->returnValue(true));
$containerBuilderMock
->expects($this->once())
->method('findTaggedServiceIds')
->with('twig.extension')
->will($this->returnValue(array(
'other_extension' => array(
array()
),
'twig_bridge_extension' => array(
array()
)
)));
$otherExtensionDefinitionMock = $this->getMockBuilder(Definition::class)
->setMethods(array('getClass'))
->getMock();
$otherExtensionDefinitionMock
->expects($this->once())
->method('getClass')
->will($this->returnValue('Foo\\Bar'));
$twigExtensionDefinitionMock = $this->getMockBuilder(Definition::class)
->setMethods(array('getClass'))
->getMock();
$twigExtensionDefinitionMock
->expects($this->once())
->method('getClass')
->will($this->returnValue('Symfony\\Bridge\\Twig\\Extension\\Foo'));
$containerBuilderMock
->expects($this->exactly(3))
->method('getDefinition')
->withConsecutive(array('twig'), array('other_extension'), array('twig_bridge_extension'))
->willReturnOnConsecutiveCalls(
$this->returnValue($twigDefinition),
$this->returnValue($otherExtensionDefinitionMock),
$this->returnValue($twigExtensionDefinitionMock)
);
$twigEnvironmentPass = new TwigEnvironmentPass();
$twigEnvironmentPass->process($containerBuilderMock);
$methodCalls = $twigDefinition->getMethodCalls();
$this->assertCount(2, $methodCalls);
$twigBridgeExtensionReference = $methodCalls[0][1][0];
$this->assertInstanceOf(Reference::class, $twigBridgeExtensionReference);
/* @var Reference $twigBridgeExtensionReference */
$this->assertEquals('twig_bridge_extension', $twigBridgeExtensionReference->__toString());
$otherExtensionReference = $methodCalls[1][1][0];
$this->assertInstanceOf(Reference::class, $otherExtensionReference);
/* @var Reference $otherExtensionReference */
$this->assertEquals('other_extension', $otherExtensionReference->__toString());
}
}