diff --git a/CHANGELOG b/CHANGELOG index 683cb4424..de96df643 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ -* 2.2.1 (2017-XX-XX) +* 2.3.0 (2017-XX-XX) + * fixed a race condition handling when writing cache files + * "length" filter now returns string length when applied to an object that does + not implement \Countable but provides __toString() + * "empty" test will now consider the return value of the __toString() method for + objects implement __toString() but not \Countable * fixed JS escaping for unicode characters with higher code points * added error message when calling `parent()` in a block that doesn't exist in the parent template @@ -34,8 +39,13 @@ * improved the performance of the filesystem loader * removed features that were deprecated in 1.x -* 1.32.1 (2017-XX-XX) +* 1.33.0 (2017-XX-XX) + * fixed a race condition handling when writing cache files + * "length" filter now returns string length when applied to an object that does + not implement \Countable but provides __toString() + * "empty" test will now consider the return value of the __toString() method for + objects implement __toString() but not \Countable * fixed JS escaping for unicode characters with higher code points * 1.32.0 (2017-02-26) diff --git a/composer.json b/composer.json index 10d081ef5..9b5d0bcc4 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } } } diff --git a/doc/filters/length.rst b/doc/filters/length.rst index 1f783b3dd..6f5a10711 100644 --- a/doc/filters/length.rst +++ b/doc/filters/length.rst @@ -1,8 +1,18 @@ ``length`` ========== +.. versionadded:: 2.3 + + Support for the ``__toString()`` magic method has been added in Twig 2.3. + The ``length`` filter returns the number of items of a sequence or mapping, or -the length of a string: +the length of a string. + +For objects that implement the ``Countable`` interface, ``length`` will use the +return value of the ``count()`` method. + +For objects that implement the ``__toString()`` magic method (and not ``Countable``), +it will return the length of the string provided by that method. .. code-block:: jinja diff --git a/doc/tests/empty.rst b/doc/tests/empty.rst index c214d50ae..1a5495707 100644 --- a/doc/tests/empty.rst +++ b/doc/tests/empty.rst @@ -1,11 +1,22 @@ ``empty`` ========= +.. versionadded:: 2.3 + + Support for the ``__toString()`` magic method has been added in Twig 2.3. + ``empty`` checks if a variable is an empty string, an empty array, an empty -hash, exactly ``false``, or exactly ``null``: +hash, exactly ``false``, or exactly ``null``. + +For objects that implement the ``Countable`` interface, ``empty`` will check the +return value of the ``count()`` method. + +For objects that implement the ``__toString()`` magic method (and not ``Countable``), +it will check if an empty string is returned. .. code-block:: jinja {% if foo is empty %} ... {% endif %} + diff --git a/lib/Twig/Cache/Filesystem.php b/lib/Twig/Cache/Filesystem.php index de4e50729..9230d7282 100644 --- a/lib/Twig/Cache/Filesystem.php +++ b/lib/Twig/Cache/Filesystem.php @@ -49,8 +49,11 @@ class Twig_Cache_Filesystem implements Twig_CacheInterface { $dir = dirname($key); if (!is_dir($dir)) { - if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) { - throw new RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir)); + if (false === @mkdir($dir, 0777, true)) { + clearstatcache(true, $dir); + if (!is_dir($dir)) { + throw new RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir)); + } } } elseif (!is_writable($dir)) { throw new RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir)); diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index ad8f51db6..2fc4ac8df 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -16,11 +16,11 @@ */ class Twig_Environment { - const VERSION = '2.2.1'; - const VERSION_ID = 20201; + const VERSION = '2.3.0'; + const VERSION_ID = 20300; const MAJOR_VERSION = 2; - const MINOR_VERSION = 2; - const RELEASE_VERSION = 1; + const MINOR_VERSION = 3; + const RELEASE_VERSION = 0; const EXTRA_VERSION = 'DEV'; private $charset; diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index e40ce3082..234c8c1c0 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -1144,7 +1144,15 @@ function twig_convert_encoding($string, $to, $from) */ function twig_length_filter(Twig_Environment $env, $thing) { - return is_scalar($thing) ? mb_strlen($thing, $env->getCharset()) : count($thing); + if (is_scalar($thing)) { + return mb_strlen($thing, $env->getCharset()); + } + + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return mb_strlen((string) $thing, $env->getCharset()); + } + + return count($thing); } /** @@ -1233,6 +1241,10 @@ function twig_test_empty($value) return 0 == count($value); } + if (method_exists($value, '__toString')) { + return '' === (string) $value; + } + return '' === $value || false === $value || null === $value || array() === $value; } diff --git a/test/Twig/Tests/Fixtures/filters/length.test b/test/Twig/Tests/Fixtures/filters/length.test index 3347474d5..599c3fd52 100644 --- a/test/Twig/Tests/Fixtures/filters/length.test +++ b/test/Twig/Tests/Fixtures/filters/length.test @@ -4,11 +4,22 @@ {{ array|length }} {{ string|length }} {{ number|length }} -{{ markup|length }} +{{ to_string_able|length }} +{{ countable|length }} +{{ magic|length }} --DATA-- -return array('array' => array(1, 4), 'string' => 'foo', 'number' => 1000, 'markup' => new Twig_Markup('foo', 'UTF-8')) +return array( + 'array' => array(1, 4), + 'string' => 'foo', + 'number' => 1000, + 'to_string_able' => new ToStringStub('foobar'), + 'countable' => new CountableStub(42), /* also asserts we do *not* call __toString() */ + 'magic' => new MagicCallStub(), /* used to assert we do *not* call __call */ +); --EXPECT-- 2 3 4 -3 +6 +42 +1 diff --git a/test/Twig/Tests/Fixtures/tests/empty.test b/test/Twig/Tests/Fixtures/tests/empty.test index a776d032b..807c0ed4a 100644 --- a/test/Twig/Tests/Fixtures/tests/empty.test +++ b/test/Twig/Tests/Fixtures/tests/empty.test @@ -1,35 +1,28 @@ --TEST-- "empty" test --TEMPLATE-- -{{ foo is empty ? 'ok' : 'ko' }} -{{ bar is empty ? 'ok' : 'ko' }} -{{ foobar is empty ? 'ok' : 'ko' }} -{{ array is empty ? 'ok' : 'ko' }} -{{ zero is empty ? 'ok' : 'ko' }} -{{ string is empty ? 'ok' : 'ko' }} +{{ string_empty is empty ? 'ok' : 'ko' }} +{{ string_zero is empty ? 'ko' : 'ok' }} +{{ value_null is empty ? 'ok' : 'ko' }} +{{ value_false is empty ? 'ok' : 'ko' }} +{{ value_int_zero is empty ? 'ko' : 'ok' }} +{{ array_empty is empty ? 'ok' : 'ko' }} +{{ array_not_empty is empty ? 'ko' : 'ok' }} +{{ magically_callable is empty ? 'ko' : 'ok' }} {{ countable_empty is empty ? 'ok' : 'ko' }} -{{ countable_not_empty is empty ? 'ok' : 'ko' }} +{{ countable_not_empty is empty ? 'ko' : 'ok' }} +{{ tostring_empty is empty ? 'ok' : 'ko' }} +{{ tostring_not_empty is empty ? 'ko' : 'ok' }} {{ markup_empty is empty ? 'ok' : 'ko' }} -{{ markup_not_empty is empty ? 'ok' : 'ko' }} +{{ markup_not_empty is empty ? 'ko' : 'ok' }} --DATA-- - -class CountableStub implements Countable -{ - private $items; - - public function __construct(array $items) - { - $this->items = $items; - } - - public function count() - { - return count($this->items); - } -} return array( - 'foo' => '', 'bar' => null, 'foobar' => false, 'array' => array(), 'zero' => 0, 'string' => '0', + 'string_empty' => '', 'string_zero' => '0', + 'value_null' => null, 'value_false' => false, 'value_int_zero' => 0, + 'array_empty' => array(), 'array_not_empty' => array(1, 2), + 'magically_callable' => new MagicCallStub(), 'countable_empty' => new CountableStub(array()), 'countable_not_empty' => new CountableStub(array(1, 2)), + 'tostring_empty' => new ToStringStub(''), 'tostring_not_empty' => new ToStringStub('0' /* edge case of using "0" as the string */), 'markup_empty' => new Twig_Markup('', 'UTF-8'), 'markup_not_empty' => new Twig_Markup('test', 'UTF-8'), ); --EXPECT-- @@ -37,9 +30,13 @@ ok ok ok ok -ko -ko ok -ko ok -ko +ok +ok +ok +ok +ok +ok +ok +ok diff --git a/test/Twig/Tests/IntegrationTest.php b/test/Twig/Tests/IntegrationTest.php index ffede5f59..812449117 100644 --- a/test/Twig/Tests/IntegrationTest.php +++ b/test/Twig/Tests/IntegrationTest.php @@ -254,3 +254,58 @@ class TwigTestExtension extends Twig_Extension return 'static_magic_'.$arguments[0]; } } + +/** + * This class is used in tests for the "length" filter and "empty" test. It asserts that __call is not + * used to convert such objects to strings. + */ +class MagicCallStub +{ + public function __call($name, $args) + { + throw new Exception('__call shall not be called'); + } +} + +class ToStringStub +{ + /** + * @var string + */ + private $string; + + public function __construct($string) + { + $this->string = $string; + } + + public function __toString() + { + return $this->string; + } +} + +/** + * This class is used in tests for the length filter and empty test to show + * that when \Countable is implemented, it is preferred over the __toString() + * method. + */ +class CountableStub implements \Countable +{ + private $count; + + public function __construct($count) + { + $this->count = $count; + } + + public function count() + { + return $this->count; + } + + public function __toString() + { + throw new Exception('__toString shall not be called on \Countables'); + } +}