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),