diff --git a/.travis.yml b/.travis.yml index b3b1698683..c19ff53b8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,9 @@ php: - 5.6 - hhvm - nightly - - hhvm-nightly allow_failures: - php: nightly - - php: hhvm-nightly env: - TWIG_EXT=no @@ -26,7 +24,5 @@ matrix: exclude: - php: hhvm env: TWIG_EXT=yes - - php: hhvm-nightly - env: TWIG_EXT=yes - php: nightly env: TWIG_EXT=yes diff --git a/CHANGELOG b/CHANGELOG index febbbd8c31..b89687c6be 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ * 1.18.2 (2015-XX-XX) + * fixed template/line guessing in exceptions for nested templates * optimized the number of inodes and the size of realpath cache when using the cache * 1.18.1 (2015-04-19) diff --git a/doc/api.rst b/doc/api.rst index 1bceaa73d3..def910a7e2 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -71,29 +71,43 @@ options as the constructor second argument:: The following options are available: -* ``debug``: When set to ``true``, the generated templates have a +* ``debug`` *boolean* + + When set to ``true``, the generated templates have a ``__toString()`` method that you can use to display the generated nodes (default to ``false``). -* ``charset``: The charset used by the templates (default to ``utf-8``). +* ``charset`` *string (default to ``utf-8``)* + + The charset used by the templates. + +* ``base_template_class`` *string (default to ``Twig_Template``)* + + The base template class to use for generated + templates. -* ``base_template_class``: The base template class to use for generated - templates (default to ``Twig_Template``). +* ``cache`` *string|false* -* ``cache``: An absolute path where to store the compiled templates, or + An absolute path where to store the compiled templates, or ``false`` to disable caching (which is the default). -* ``auto_reload``: When developing with Twig, it's useful to recompile the +* ``auto_reload`` *boolean* + + When developing with Twig, it's useful to recompile the template whenever the source code changes. If you don't provide a value for the ``auto_reload`` option, it will be determined automatically based on the ``debug`` value. -* ``strict_variables``: If set to ``false``, Twig will silently ignore invalid +* ``strict_variables`` *boolean* + + If set to ``false``, Twig will silently ignore invalid variables (variables and or attributes/methods that do not exist) and replace them with a ``null`` value. When set to ``true``, Twig throws an exception instead (default to ``false``). -* ``autoescape``: Sets the default auto-escaping strategy (``filename``, +* ``autoescape`` *string* + + Sets the default auto-escaping strategy (``filename``, ``html``, ``js``, ``css``, ``url``, ``html_attr``, or a PHP callback that takes the template "filename" and returns the escaping strategy to use -- the callback cannot be a function name to avoid collision with built-in escaping @@ -102,7 +116,9 @@ The following options are available: based on the template filename extension (this strategy does not incur any overhead at runtime as auto-escaping is done at compilation time.) -* ``optimizations``: A flag that indicates which optimizations to apply +* ``optimizations`` *integer* + + A flag that indicates which optimizations to apply (default to ``-1`` -- all optimizations are enabled; set it to ``0`` to disable). diff --git a/doc/tags/if.rst b/doc/tags/if.rst index b10dcb4b3a..12edf980df 100644 --- a/doc/tags/if.rst +++ b/doc/tags/if.rst @@ -37,8 +37,16 @@ You can also use ``not`` to check for values that evaluate to ``false``:

You are not subscribed to our mailing list.

{% endif %} -For multiple branches ``elseif`` and ``else`` can be used like in PHP. You can use -more complex ``expressions`` there too: +For multiple conditions, ``and`` and ``or`` can be used: + +.. code-block:: jinja + + {% if temperature > 18 and temperature < 27 %} +

It's a nice day for a walk in the park.

+ {% endif %} + +For multiple branches ``elseif`` and ``else`` can be used like in PHP. You can +use more complex ``expressions`` there too: .. code-block:: jinja diff --git a/doc/tags/spaceless.rst b/doc/tags/spaceless.rst index 12e77b2575..b39cb27efe 100644 --- a/doc/tags/spaceless.rst +++ b/doc/tags/spaceless.rst @@ -33,5 +33,5 @@ quirks under some circumstances. .. tip:: For more information on whitespace control, read the - :doc:`dedicated<../templates>` section of the documentation and learn how + :ref:`dedicated section ` of the documentation and learn how you can also use the whitespace control modifier on your tags. diff --git a/doc/templates.rst b/doc/templates.rst index b6dbaba0dc..ba05f916a2 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -660,6 +660,10 @@ You can combine multiple expressions with the following operators: Twig also support bitwise operators (``b-and``, ``b-xor``, and ``b-or``). +.. note:: + + Operators are case sensitive. + Comparisons ~~~~~~~~~~~ @@ -784,6 +788,8 @@ inserted into the string: {{ "foo #{bar} baz" }} {{ "foo #{1 + 2} baz" }} +.. _templates-whitespace-control: + Whitespace Control ------------------ diff --git a/ext/twig/twig.c b/ext/twig/twig.c index 617660e7f3..6f63413efe 100644 --- a/ext/twig/twig.c +++ b/ext/twig/twig.c @@ -779,12 +779,18 @@ PHP_FUNCTION(twig_template_get_attributes) $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface', $item, get_class($object)); } elseif (is_array($object)) { if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty', $arrayItem); + $message = sprintf('Key "%s" does not exist as the array is empty', $arrayItem); } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))); + $message = sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))); } } elseif (Twig_Template::ARRAY_CALL === $type) { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable', $item); } else { $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s")', $item, gettype($object), $object); } @@ -807,12 +813,21 @@ PHP_FUNCTION(twig_template_get_attributes) } else { char *type_name = zend_zval_type_name(object); Z_ADDREF_P(object); - convert_to_string(object); - TWIG_RUNTIME_ERROR(template TSRMLS_CC, - (strcmp("array", type) == 0) - ? "Impossible to access a key (\"%s\") on a %s variable (\"%s\")" - : "Impossible to access an attribute (\"%s\") on a %s variable (\"%s\")", - item, type_name, Z_STRVAL_P(object)); + if (Z_TYPE_P(object) == IS_NULL) { + convert_to_string(object); + TWIG_RUNTIME_ERROR(template TSRMLS_CC, + (strcmp("array", type) == 0) + ? "Impossible to access a key (\"%s\") on a %s variable" + : "Impossible to access an attribute (\"%s\") on a %s variable", + item, type_name); + } else { + convert_to_string(object); + TWIG_RUNTIME_ERROR(template TSRMLS_CC, + (strcmp("array", type) == 0) + ? "Impossible to access a key (\"%s\") on a %s variable (\"%s\")" + : "Impossible to access an attribute (\"%s\") on a %s variable (\"%s\")", + item, type_name, Z_STRVAL_P(object)); + } zval_ptr_dtor(&object); } efree(item); @@ -836,7 +851,14 @@ PHP_FUNCTION(twig_template_get_attributes) if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { return null; } - throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); + + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + + throw new Twig_Error_Runtime($message, -1, $this->getTemplateName()); } */ if (ignoreStrictCheck || !TWIG_CALL_BOOLEAN(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "isStrictVariables" TSRMLS_CC)) { @@ -846,9 +868,15 @@ PHP_FUNCTION(twig_template_get_attributes) type_name = zend_zval_type_name(object); Z_ADDREF_P(object); - convert_to_string_ex(&object); + if (Z_TYPE_P(object) == IS_NULL) { + convert_to_string_ex(&object); - TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on a %s variable (\"%s\")", item, type_name, Z_STRVAL_P(object)); + TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on a %s variable", item, type_name); + } else { + convert_to_string_ex(&object); + + TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on a %s variable (\"%s\")", item, type_name, Z_STRVAL_P(object)); + } zval_ptr_dtor(&object); efree(item); diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index 630c957082..ba625ebb3f 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -1362,7 +1362,7 @@ function twig_test_iterable($value) * * @return string The rendered template */ -function twig_include(Twig_Environment $env, array $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false) +function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false) { $alreadySandboxed = false; $sandbox = null; diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index 1774da3d9f..28a9249014 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -79,7 +79,7 @@ public function getParent(array $context) return false; } - if ($parent instanceof Twig_Template) { + if ($parent instanceof self) { return $this->parents[$parent->getTemplateName()] = $parent; } @@ -249,13 +249,20 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ return $this->env->resolveTemplate($template); } - if ($template instanceof Twig_Template) { + if ($template instanceof self) { return $template; } return $this->env->loadTemplate($template, $index); } catch (Twig_Error $e) { - $e->setTemplateFile($templateName ? $templateName : $this->getTemplateName()); + if (!$e->getTemplateFile()) { + $e->setTemplateFile($templateName ? $templateName : $this->getTemplateName()); + } + + if ($e->getTemplateLine()) { + throw $e; + } + if (!$line) { $e->guess(); } else { @@ -385,10 +392,10 @@ final protected function getContext($context, $item, $ignoreStrictCheck = false) * * @throws Twig_Error_Runtime if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false */ - protected function getAttribute($object, $item, array $arguments = array(), $type = Twig_Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) + protected function getAttribute($object, $item, array $arguments = array(), $type = self::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) { // array - if (Twig_Template::METHOD_CALL !== $type) { + if (self::METHOD_CALL !== $type) { $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item; if ((is_array($object) && array_key_exists($arrayItem, $object)) @@ -401,7 +408,7 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ return $object[$arrayItem]; } - if (Twig_Template::ARRAY_CALL === $type || !is_object($object)) { + if (self::ARRAY_CALL === $type || !is_object($object)) { if ($isDefinedTest) { return false; } @@ -420,8 +427,14 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ } else { $message = sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))); } - } elseif (Twig_Template::ARRAY_CALL === $type) { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } elseif (self::ARRAY_CALL === $type) { + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable', $item); } else { $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s")', $item, gettype($object), $object); } @@ -439,11 +452,17 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ return; } - throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + + throw new Twig_Error_Runtime($message, -1, $this->getTemplateName()); } // object property - if (Twig_Template::METHOD_CALL !== $type) { + if (self::METHOD_CALL !== $type) { if (isset($object->$item) || array_key_exists((string) $item, $object)) { if ($isDefinedTest) { return true; diff --git a/test/Twig/Tests/Fixtures/exceptions/syntax_error_in_reused_template.test b/test/Twig/Tests/Fixtures/exceptions/syntax_error_in_reused_template.test new file mode 100644 index 0000000000..5dd9f3838e --- /dev/null +++ b/test/Twig/Tests/Fixtures/exceptions/syntax_error_in_reused_template.test @@ -0,0 +1,10 @@ +--TEST-- +Exception for syntax error in reused template +--TEMPLATE-- +{% use 'foo.twig' %} +--TEMPLATE(foo.twig)-- +{% block bar %} + {% do node.data = 5 %} +{% endblock %} +--EXCEPTION-- +Twig_Error_Syntax: Unexpected token "operator" of value "=" ("end of statement block" expected) in "foo.twig" at line 3 diff --git a/test/Twig/Tests/Profiler/Dumper/AbstractTest.php b/test/Twig/Tests/Profiler/Dumper/AbstractTest.php index 555a5e7445..d855664ac4 100644 --- a/test/Twig/Tests/Profiler/Dumper/AbstractTest.php +++ b/test/Twig/Tests/Profiler/Dumper/AbstractTest.php @@ -13,32 +13,88 @@ abstract class Twig_Tests_Profiler_Dumper_AbstractTest extends PHPUnit_Framework { protected function getProfile() { - $profile = new Twig_Profiler_Profile(); - $index = new Twig_Profiler_Profile('index.twig', Twig_Profiler_Profile::TEMPLATE); - $profile->addProfile($index); - $body = new Twig_Profiler_Profile('embedded.twig', Twig_Profiler_Profile::BLOCK, 'body'); - $body->leave(); - $index->addProfile($body); - $embedded = new Twig_Profiler_Profile('embedded.twig', Twig_Profiler_Profile::TEMPLATE); - $included = new Twig_Profiler_Profile('included.twig', Twig_Profiler_Profile::TEMPLATE); - $embedded->addProfile($included); - $index->addProfile($embedded); - $included->leave(); - $embedded->leave(); - - $macro = new Twig_Profiler_Profile('index.twig', Twig_Profiler_Profile::MACRO, 'foo'); - $macro->leave(); - $index->addProfile($macro); - - $embedded = clone $embedded; - $index->addProfile($embedded); - usleep(500); - $embedded->leave(); - - usleep(4500); - $index->leave(); - - $profile->leave(); + $profile = $this->getMockBuilder('Twig_Profiler_Profile')->disableOriginalConstructor()->getMock(); + + $profile->expects($this->any())->method('isRoot')->will($this->returnValue(true)); + $profile->expects($this->any())->method('getName')->will($this->returnValue('main')); + $profile->expects($this->any())->method('getDuration')->will($this->returnValue(1)); + $profile->expects($this->any())->method('getMemoryUsage')->will($this->returnValue(0)); + $profile->expects($this->any())->method('getPeakMemoryUsage')->will($this->returnValue(0)); + + $subProfiles = array( + $this->getIndexProfile( + array( + $this->getEmbeddedBlockProfile(), + $this->getEmbeddedTemplateProfile( + array( + $this->getIncludedTemplateProfile(), + ) + ), + $this->getMacroProfile(), + $this->getEmbeddedTemplateProfile( + array( + $this->getIncludedTemplateProfile(), + ) + ), + ) + ), + ); + + $profile->expects($this->any())->method('getProfiles')->will($this->returnValue($subProfiles)); + $profile->expects($this->any())->method('getIterator')->will($this->returnValue(new ArrayIterator($subProfiles))); + + return $profile; + } + + private function getIndexProfile(array $subProfiles = array()) + { + return $this->generateProfile('main', 1, true, 'template', 'index.twig', $subProfiles); + } + + private function getEmbeddedBlockProfile(array $subProfiles = array()) + { + return $this->generateProfile('body', 0.0001, false, 'block', 'embedded.twig', $subProfiles); + } + + private function getEmbeddedTemplateProfile(array $subProfiles = array()) + { + return $this->generateProfile('main', 0.0001, true, 'template', 'embedded.twig', $subProfiles); + } + + private function getIncludedTemplateProfile(array $subProfiles = array()) + { + return $this->generateProfile('main', 0.0001, true, 'template', 'included.twig', $subProfiles); + } + + private function getMacroProfile(array $subProfiles = array()) + { + return $this->generateProfile('foo', 0.0001, false, 'macro', 'index.twig', $subProfiles); + } + + /** + * @param string $name + * @param float $duration + * @param bool $isTemplate + * @param string $type + * @param string $templateName + * @param array $subProfiles + * + * @return Twig_Profiler_Profile + */ + private function generateProfile($name, $duration, $isTemplate, $type, $templateName, array $subProfiles = array()) + { + $profile = $this->getMockBuilder('Twig_Profiler_Profile')->disableOriginalConstructor()->getMock(); + + $profile->expects($this->any())->method('isRoot')->will($this->returnValue(false)); + $profile->expects($this->any())->method('getName')->will($this->returnValue($name)); + $profile->expects($this->any())->method('getDuration')->will($this->returnValue($duration)); + $profile->expects($this->any())->method('getMemoryUsage')->will($this->returnValue(0)); + $profile->expects($this->any())->method('getPeakMemoryUsage')->will($this->returnValue(0)); + $profile->expects($this->any())->method('isTemplate')->will($this->returnValue($isTemplate)); + $profile->expects($this->any())->method('getType')->will($this->returnValue($type)); + $profile->expects($this->any())->method('getTemplate')->will($this->returnValue($templateName)); + $profile->expects($this->any())->method('getProfiles')->will($this->returnValue($subProfiles)); + $profile->expects($this->any())->method('getIterator')->will($this->returnValue(new ArrayIterator($subProfiles))); return $profile; } diff --git a/test/Twig/Tests/TemplateTest.php b/test/Twig/Tests/TemplateTest.php index eaf7300785..03f929b1c0 100644 --- a/test/Twig/Tests/TemplateTest.php +++ b/test/Twig/Tests/TemplateTest.php @@ -28,6 +28,7 @@ public function testGetAttributeExceptions($template, $message, $useExt) $context = array( 'string' => 'foo', + 'null' => null, 'empty_array' => array(), 'array' => array('foo' => 'foo'), 'array_access' => new Twig_TemplateArrayAccessObject(), @@ -47,11 +48,14 @@ public function getAttributeExceptions() { $tests = array( array('{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1', false), + array('{{ null["a"] }}', 'Impossible to access a key ("a") on a null variable in "%s" at line 1', false), array('{{ empty_array["a"] }}', 'Key "a" does not exist as the array is empty in "%s" at line 1', false), array('{{ array["a"] }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1', false), array('{{ array_access["a"] }}', 'Key "a" in object with ArrayAccess of class "Twig_TemplateArrayAccessObject" does not exist in "%s" at line 1', false), array('{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1', false), array('{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1', false), + array('{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1', false), + array('{{ null.a() }}', 'Impossible to invoke a method ("a") on a null variable in "%s" at line 1', false), array('{{ empty_array.a }}', 'Key "a" does not exist as the array is empty in "%s" at line 1', false), array('{{ array.a }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1', false), array('{{ attribute(array, -10) }}', 'Key "-10" for array with keys "foo" does not exist in "%s" at line 1', false),