tests/EnvironmentTest.php
1f00c33a
 <?php
 
2119e60c
 namespace Twig\Tests;
 
1f00c33a
 /*
  * This file is part of Twig.
  *
  * (c) Fabien Potencier
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
34bdab4d
 use PHPUnit\Framework\TestCase;
90d579e4
 use Twig\Cache\CacheInterface;
 use Twig\Cache\FilesystemCache;
 use Twig\Environment;
5ebcecf2
 use Twig\Error\RuntimeError;
90d579e4
 use Twig\Extension\AbstractExtension;
 use Twig\Extension\ExtensionInterface;
 use Twig\Extension\GlobalsInterface;
 use Twig\Loader\ArrayLoader;
 use Twig\Loader\LoaderInterface;
 use Twig\Node\Node;
 use Twig\NodeVisitor\NodeVisitorInterface;
 use Twig\RuntimeLoader\RuntimeLoaderInterface;
 use Twig\Source;
 use Twig\Token;
 use Twig\TokenParser\AbstractTokenParser;
 use Twig\TwigFilter;
 use Twig\TwigFunction;
 use Twig\TwigTest;
 
34bdab4d
 class EnvironmentTest extends TestCase
1f00c33a
 {
     public function testAutoescapeOption()
     {
90d579e4
         $loader = new ArrayLoader([
1f00c33a
             'html' => '{{ foo }} {{ foo }}',
b0c41d42
             'js' => '{{ bar }} {{ bar }}',
5c55243d
         ]);
1f00c33a
 
90d579e4
         $twig = new Environment($loader, [
b0c41d42
             'debug' => true,
             'cache' => false,
5c55243d
             'autoescape' => [$this, 'escapingStrategyCallback'],
         ]);
1f00c33a
 
5c55243d
         $this->assertEquals('foo&lt;br/ &gt; foo&lt;br/ &gt;', $twig->render('html', ['foo' => 'foo<br/ >']));
         $this->assertEquals('foo\u003Cbr\/\u0020\u003E foo\u003Cbr\/\u0020\u003E', $twig->render('js', ['bar' => 'foo<br/ >']));
1f00c33a
     }
 
ae9656a8
     public function escapingStrategyCallback($name)
1f00c33a
     {
ae9656a8
         return $name;
1f00c33a
     }
0a7b37b8
 
2450f79a
     public function testGlobals()
     {
656c295e
         $loader = $this->createMock(LoaderInterface::class);
5a1e33d7
         $loader->expects($this->any())->method('getSourceContext')->willReturn(new Source('', ''));
21ecba8c
 
2450f79a
         // globals can be added after calling getGlobals
90d579e4
         $twig = new Environment($loader);
2450f79a
         $twig->addGlobal('foo', 'foo');
3e506720
         $twig->getGlobals();
2450f79a
         $twig->addGlobal('foo', 'bar');
         $globals = $twig->getGlobals();
         $this->assertEquals('bar', $globals['foo']);
 
f839ad32
         // globals can be modified after a template has been loaded
90d579e4
         $twig = new Environment($loader);
2450f79a
         $twig->addGlobal('foo', 'foo');
2030482e
         $twig->getGlobals();
931a8b5c
         $twig->load('index');
2450f79a
         $twig->addGlobal('foo', 'bar');
         $globals = $twig->getGlobals();
         $this->assertEquals('bar', $globals['foo']);
 
         // globals can be modified after extensions init
90d579e4
         $twig = new Environment($loader);
2450f79a
         $twig->addGlobal('foo', 'foo');
3e506720
         $twig->getGlobals();
2450f79a
         $twig->getFunctions();
         $twig->addGlobal('foo', 'bar');
         $globals = $twig->getGlobals();
         $this->assertEquals('bar', $globals['foo']);
 
f839ad32
         // globals can be modified after extensions and a template has been loaded
90d579e4
         $arrayLoader = new ArrayLoader(['index' => '{{foo}}']);
         $twig = new Environment($arrayLoader);
2450f79a
         $twig->addGlobal('foo', 'foo');
3e506720
         $twig->getGlobals();
2450f79a
         $twig->getFunctions();
931a8b5c
         $twig->load('index');
2450f79a
         $twig->addGlobal('foo', 'bar');
         $globals = $twig->getGlobals();
         $this->assertEquals('bar', $globals['foo']);
 
90d579e4
         $twig = new Environment($arrayLoader);
667b274b
         $twig->getGlobals();
         $twig->addGlobal('foo', 'bar');
931a8b5c
         $template = $twig->load('index');
5c55243d
         $this->assertEquals('bar', $template->render([]));
667b274b
 
f839ad32
         // globals cannot be added after a template has been loaded
90d579e4
         $twig = new Environment($loader);
2450f79a
         $twig->addGlobal('foo', 'foo');
2030482e
         $twig->getGlobals();
931a8b5c
         $twig->load('index');
2450f79a
         try {
             $twig->addGlobal('bar', 'bar');
             $this->fail();
010cfaf4
         } catch (\LogicException $e) {
eac76a84
             $this->assertArrayNotHasKey('bar', $twig->getGlobals());
2450f79a
         }
 
         // globals cannot be added after extensions init
90d579e4
         $twig = new Environment($loader);
2450f79a
         $twig->addGlobal('foo', 'foo');
2030482e
         $twig->getGlobals();
2450f79a
         $twig->getFunctions();
         try {
             $twig->addGlobal('bar', 'bar');
             $this->fail();
010cfaf4
         } catch (\LogicException $e) {
eac76a84
             $this->assertArrayNotHasKey('bar', $twig->getGlobals());
2450f79a
         }
 
f839ad32
         // globals cannot be added after extensions and a template has been loaded
90d579e4
         $twig = new Environment($loader);
2450f79a
         $twig->addGlobal('foo', 'foo');
2030482e
         $twig->getGlobals();
2450f79a
         $twig->getFunctions();
931a8b5c
         $twig->load('index');
2450f79a
         try {
             $twig->addGlobal('bar', 'bar');
             $this->fail();
010cfaf4
         } catch (\LogicException $e) {
eac76a84
             $this->assertArrayNotHasKey('bar', $twig->getGlobals());
2450f79a
         }
9f5fa532
 
f839ad32
         // test adding globals after a template has been loaded without call to getGlobals
90d579e4
         $twig = new Environment($loader);
931a8b5c
         $twig->load('index');
9f5fa532
         try {
             $twig->addGlobal('bar', 'bar');
             $this->fail();
010cfaf4
         } catch (\LogicException $e) {
eac76a84
             $this->assertArrayNotHasKey('bar', $twig->getGlobals());
9f5fa532
         }
2450f79a
     }
 
     public function testExtensionsAreNotInitializedWhenRenderingACompiledTemplate()
     {
90d579e4
         $cache = new FilesystemCache($dir = sys_get_temp_dir().'/twig');
5c55243d
         $options = ['cache' => $cache, 'auto_reload' => false, 'debug' => false];
2450f79a
 
         // force compilation
90d579e4
         $twig = new Environment($loader = new ArrayLoader(['index' => '{{ foo }}']), $options);
cacfb069
 
c2e75ff8
         $key = $cache->generateKey('index', $twig->getTemplateClass('index'));
90d579e4
         $cache->write($key, $twig->compileSource(new Source('{{ foo }}', 'index')));
2450f79a
 
         // check that extensions won't be initialized when rendering a template that is already in the cache
         $twig = $this
90d579e4
             ->getMockBuilder(Environment::class)
5c55243d
             ->setConstructorArgs([$loader, $options])
             ->setMethods(['initExtensions'])
2450f79a
             ->getMock()
         ;
 
         $twig->expects($this->never())->method('initExtensions');
 
         // render template
5c55243d
         $output = $twig->render('index', ['foo' => 'bar']);
2450f79a
         $this->assertEquals('bar', $output);
 
2119e60c
         FilesystemHelper::removeDir($dir);
2450f79a
     }
 
1769f84a
     public function testAutoReloadCacheMiss()
     {
efaf5036
         $templateName = __FUNCTION__;
         $templateContent = __FUNCTION__;
1769f84a
 
c7b0a459
         $cache = $this->createMock(CacheInterface::class);
efaf5036
         $loader = $this->getMockLoader($templateName, $templateContent);
90d579e4
         $twig = new Environment($loader, ['cache' => $cache, 'auto_reload' => true, 'debug' => false]);
1769f84a
 
         // Cache miss: getTimestamp returns 0 and as a result the load() is
         // skipped.
         $cache->expects($this->once())
             ->method('generateKey')
5a1e33d7
             ->willReturn('key');
1769f84a
         $cache->expects($this->once())
             ->method('getTimestamp')
5a1e33d7
             ->willReturn(0);
1769f84a
         $loader->expects($this->never())
             ->method('isFresh');
ff0abbb7
         $cache->expects($this->once())
             ->method('write');
         $cache->expects($this->once())
1769f84a
             ->method('load');
 
931a8b5c
         $twig->load($templateName);
1769f84a
     }
 
     public function testAutoReloadCacheHit()
     {
efaf5036
         $templateName = __FUNCTION__;
         $templateContent = __FUNCTION__;
1769f84a
 
c7b0a459
         $cache = $this->createMock(CacheInterface::class);
efaf5036
         $loader = $this->getMockLoader($templateName, $templateContent);
90d579e4
         $twig = new Environment($loader, ['cache' => $cache, 'auto_reload' => true, 'debug' => false]);
1769f84a
 
         $now = time();
 
         // Cache hit: getTimestamp returns something > extension timestamps and
         // the loader returns true for isFresh().
         $cache->expects($this->once())
             ->method('generateKey')
5a1e33d7
             ->willReturn('key');
1769f84a
         $cache->expects($this->once())
             ->method('getTimestamp')
5a1e33d7
             ->willReturn($now);
1769f84a
         $loader->expects($this->once())
             ->method('isFresh')
5a1e33d7
             ->willReturn(true);
ff0abbb7
         $cache->expects($this->atLeastOnce())
1769f84a
             ->method('load');
 
931a8b5c
         $twig->load($templateName);
1769f84a
     }
 
     public function testAutoReloadOutdatedCacheHit()
     {
efaf5036
         $templateName = __FUNCTION__;
         $templateContent = __FUNCTION__;
1769f84a
 
c7b0a459
         $cache = $this->createMock(CacheInterface::class);
efaf5036
         $loader = $this->getMockLoader($templateName, $templateContent);
90d579e4
         $twig = new Environment($loader, ['cache' => $cache, 'auto_reload' => true, 'debug' => false]);
1769f84a
 
         $now = time();
 
         $cache->expects($this->once())
             ->method('generateKey')
5a1e33d7
             ->willReturn('key');
1769f84a
         $cache->expects($this->once())
             ->method('getTimestamp')
5a1e33d7
             ->willReturn($now);
1769f84a
         $loader->expects($this->once())
             ->method('isFresh')
5a1e33d7
             ->willReturn(false);
ff0abbb7
         $cache->expects($this->once())
             ->method('write');
         $cache->expects($this->once())
1769f84a
             ->method('load');
 
931a8b5c
         $twig->load($templateName);
1769f84a
     }
 
71f03df0
     public function testHasGetExtensionByClassName()
     {
656c295e
         $twig = new Environment($this->createMock(LoaderInterface::class));
2119e60c
         $twig->addExtension($ext = new EnvironmentTest_Extension());
         $this->assertSame($ext, $twig->getExtension('Twig\Tests\EnvironmentTest_Extension'));
         $this->assertSame($ext, $twig->getExtension('\Twig\Tests\EnvironmentTest_Extension'));
71f03df0
     }
 
0a7b37b8
     public function testAddExtension()
     {
656c295e
         $twig = new Environment($this->createMock(LoaderInterface::class));
2119e60c
         $twig->addExtension(new EnvironmentTest_Extension());
0a7b37b8
 
         $this->assertArrayHasKey('test', $twig->getTags());
         $this->assertArrayHasKey('foo_filter', $twig->getFilters());
         $this->assertArrayHasKey('foo_function', $twig->getFunctions());
         $this->assertArrayHasKey('foo_test', $twig->getTests());
         $this->assertArrayHasKey('foo_unary', $twig->getUnaryOperators());
         $this->assertArrayHasKey('foo_binary', $twig->getBinaryOperators());
         $this->assertArrayHasKey('foo_global', $twig->getGlobals());
         $visitors = $twig->getNodeVisitors();
ec7a1c18
         $found = false;
         foreach ($visitors as $visitor) {
2119e60c
             if ($visitor instanceof EnvironmentTest_NodeVisitor) {
ec7a1c18
                 $found = true;
             }
         }
         $this->assertTrue($found);
0a7b37b8
     }
 
a57b1cc3
     public function testAddMockExtension()
     {
c7b0a459
         $extension = $this->createMock(ExtensionInterface::class);
90d579e4
         $loader = new ArrayLoader(['page' => 'hey']);
a57b1cc3
 
90d579e4
         $twig = new Environment($loader);
a57b1cc3
         $twig->addExtension($extension);
 
90d579e4
         $this->assertInstanceOf(ExtensionInterface::class, $twig->getExtension(\get_class($extension)));
a57b1cc3
         $this->assertTrue($twig->isTemplateFresh('page', time()));
     }
 
d4e3adaa
     public function testOverrideExtension()
72485c2e
     {
5ebcecf2
         $this->expectException(\LogicException::class);
         $this->expectExceptionMessage('Unable to register extension "Twig\Tests\EnvironmentTest_Extension" as it is already registered.');
 
656c295e
         $twig = new Environment($this->createMock(LoaderInterface::class));
72485c2e
 
ccf27274
         $twig->addExtension(new EnvironmentTest_Extension());
         $twig->addExtension(new EnvironmentTest_Extension());
72485c2e
     }
 
9cb6860e
     public function testAddRuntimeLoader()
     {
c7b0a459
         $runtimeLoader = $this->createMock(RuntimeLoaderInterface::class);
2119e60c
         $runtimeLoader->expects($this->any())->method('load')->willReturn(new EnvironmentTest_Runtime());
9cb6860e
 
90d579e4
         $loader = new ArrayLoader([
9cb6860e
             'func_array' => '{{ from_runtime_array("foo") }}',
             'func_array_default' => '{{ from_runtime_array() }}',
             'func_array_named_args' => '{{ from_runtime_array(name="foo") }}',
             'func_string' => '{{ from_runtime_string("foo") }}',
             'func_string_default' => '{{ from_runtime_string() }}',
             'func_string_named_args' => '{{ from_runtime_string(name="foo") }}',
5c55243d
         ]);
9cb6860e
 
90d579e4
         $twig = new Environment($loader);
2119e60c
         $twig->addExtension(new EnvironmentTest_ExtensionWithoutRuntime());
9cb6860e
         $twig->addRuntimeLoader($runtimeLoader);
 
         $this->assertEquals('foo', $twig->render('func_array'));
         $this->assertEquals('bar', $twig->render('func_array_default'));
         $this->assertEquals('foo', $twig->render('func_array_named_args'));
         $this->assertEquals('foo', $twig->render('func_string'));
         $this->assertEquals('bar', $twig->render('func_string_default'));
         $this->assertEquals('foo', $twig->render('func_string_named_args'));
     }
 
772373cf
     public function testFailLoadTemplate()
     {
5ebcecf2
         $this->expectException(RuntimeError::class);
         $this->expectExceptionMessage('Failed to load Twig template "testFailLoadTemplate.twig", index "112233": cache might be corrupted in "testFailLoadTemplate.twig".');
 
772373cf
         $template = 'testFailLoadTemplate.twig';
90d579e4
         $twig = new Environment(new ArrayLoader([$template => false]));
653dddd1
         $twig->loadTemplate($twig->getTemplateClass($template), $template, 112233);
772373cf
     }
 
efaf5036
     protected function getMockLoader($templateName, $templateContent)
1769f84a
     {
656c295e
         $loader = $this->createMock(LoaderInterface::class);
1769f84a
         $loader->expects($this->any())
21ecba8c
           ->method('getSourceContext')
efaf5036
           ->with($templateName)
5a1e33d7
           ->willReturn(new Source($templateContent, $templateName));
1769f84a
         $loader->expects($this->any())
           ->method('getCacheKey')
efaf5036
           ->with($templateName)
5a1e33d7
           ->willReturn($templateName);
1769f84a
 
         return $loader;
     }
0a7b37b8
 }
 
2119e60c
 class EnvironmentTest_Extension_WithGlobals extends AbstractExtension
e3a325f2
 {
     public function getGlobals()
     {
5c55243d
         return [
e3a325f2
             'foo_global' => 'foo_global',
5c55243d
         ];
e3a325f2
     }
 }
 
2119e60c
 class EnvironmentTest_Extension extends AbstractExtension implements GlobalsInterface
0a7b37b8
 {
54cec4e3
     public function getTokenParsers(): array
0a7b37b8
     {
5c55243d
         return [
2119e60c
             new EnvironmentTest_TokenParser(),
5c55243d
         ];
0a7b37b8
     }
 
54cec4e3
     public function getNodeVisitors(): array
0a7b37b8
     {
5c55243d
         return [
2119e60c
             new EnvironmentTest_NodeVisitor(),
5c55243d
         ];
0a7b37b8
     }
 
54cec4e3
     public function getFilters(): array
0a7b37b8
     {
5c55243d
         return [
90d579e4
             new TwigFilter('foo_filter'),
5c55243d
         ];
0a7b37b8
     }
 
54cec4e3
     public function getTests(): array
0a7b37b8
     {
5c55243d
         return [
90d579e4
             new TwigTest('foo_test'),
5c55243d
         ];
0a7b37b8
     }
 
54cec4e3
     public function getFunctions(): array
0a7b37b8
     {
5c55243d
         return [
90d579e4
             new TwigFunction('foo_function'),
5c55243d
         ];
0a7b37b8
     }
 
54cec4e3
     public function getOperators(): array
0a7b37b8
     {
5c55243d
         return [
             ['foo_unary' => []],
             ['foo_binary' => []],
         ];
0a7b37b8
     }
 
54cec4e3
     public function getGlobals(): array
0a7b37b8
     {
5c55243d
         return [
0a7b37b8
             'foo_global' => 'foo_global',
5c55243d
         ];
0a7b37b8
     }
71f03df0
 }
0a7b37b8
 
2119e60c
 class EnvironmentTest_TokenParser extends AbstractTokenParser
0a7b37b8
 {
54cec4e3
     public function parse(Token $token): Node
0a7b37b8
     {
     }
 
54cec4e3
     public function getTag(): string
0a7b37b8
     {
         return 'test';
     }
 }
 
2119e60c
 class EnvironmentTest_NodeVisitor implements NodeVisitorInterface
0a7b37b8
 {
54cec4e3
     public function enterNode(Node $node, Environment $env): Node
0a7b37b8
     {
         return $node;
     }
 
54cec4e3
     public function leaveNode(Node $node, Environment $env): ?Node
0a7b37b8
     {
         return $node;
     }
 
54cec4e3
     public function getPriority(): int
0a7b37b8
     {
         return 0;
     }
1f00c33a
 }
9774f4f1
 
2119e60c
 class EnvironmentTest_ExtensionWithoutRuntime extends AbstractExtension
9cb6860e
 {
54cec4e3
     public function getFunctions(): array
9cb6860e
     {
5c55243d
         return [
2119e60c
             new TwigFunction('from_runtime_array', ['Twig\Tests\EnvironmentTest_Runtime', 'fromRuntime']),
             new TwigFunction('from_runtime_string', 'Twig\Tests\EnvironmentTest_Runtime::fromRuntime'),
5c55243d
         ];
9cb6860e
     }
 }
 
2119e60c
 class EnvironmentTest_Runtime
9cb6860e
 {
     public function fromRuntime($name = 'bar')
     {
         return $name;
     }
 }