From f5193e92c50c89f19121771ca800aad55b97aff7 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 10 Mar 2017 13:14:36 +0100 Subject: [PATCH 1/5] Make "length" filter and "empty" test consider __toString [Twig 1.x] --- CHANGELOG | 7 +++ doc/filters/length.rst | 12 ++++- doc/tests/empty.rst | 13 ++++- lib/Twig/Extension/Core.php | 24 ++++++++- test/Twig/Tests/Fixtures/filters/length.test | 17 ++++-- test/Twig/Tests/Fixtures/tests/empty.test | 53 +++++++++---------- test/Twig/Tests/IntegrationTest.php | 55 ++++++++++++++++++++ 7 files changed, 146 insertions(+), 35 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index baff9e9a3..573e6ffae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +* 1.33.0 (2017-XX-XX) + + * "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 + * 1.32.1 (2017-XX-XX) * n/a diff --git a/doc/filters/length.rst b/doc/filters/length.rst index 1f783b3dd..5620f3b3c 100644 --- a/doc/filters/length.rst +++ b/doc/filters/length.rst @@ -1,8 +1,18 @@ ``length`` ========== +.. versionadded:: 1.33 + + Support for the ``__toString()`` magic method has been added in Twig 1.33. + 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..639cdcc39 100644 --- a/doc/tests/empty.rst +++ b/doc/tests/empty.rst @@ -1,11 +1,22 @@ ``empty`` ========= +.. versionadded:: 1.33 + + Support for the ``__toString()`` magic method has been added in Twig 1.33. + ``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/Extension/Core.php b/lib/Twig/Extension/Core.php index 22824a880..78848777b 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -1259,7 +1259,15 @@ if (function_exists('mb_get_info')) { */ 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); } /** @@ -1342,7 +1350,15 @@ else { */ function twig_length_filter(Twig_Environment $env, $thing) { - return is_scalar($thing) ? strlen($thing) : count($thing); + if (is_scalar($thing)) { + return strlen($thing); + } + + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return strlen((string) $thing); + } + + return count($thing); } /** @@ -1404,6 +1420,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 0647891b6..5d20112de 100644 --- a/test/Twig/Tests/IntegrationTest.php +++ b/test/Twig/Tests/IntegrationTest.php @@ -252,3 +252,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'); + } +} From 8441aaab0318bd21e1e11efa085270922d8b970b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 22 Mar 2017 07:47:32 -0700 Subject: [PATCH 2/5] fixed CHANGELOG --- CHANGELOG | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 863546780..5bd4beb0b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,9 +4,6 @@ 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 - -* 1.32.1 (2017-XX-XX) - * fixed JS escaping for unicode characters with higher code points * 1.32.0 (2017-02-26) From 5040e3888569c86360cfe7a9c6ad0306c7afda1d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 22 Mar 2017 07:48:20 -0700 Subject: [PATCH 3/5] bumped version to 1.33 --- composer.json | 2 +- ext/twig/php_twig.h | 2 +- lib/Twig/Environment.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 1054f3404..b89f14ca8 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.32-dev" + "dev-master": "1.33-dev" } } } diff --git a/ext/twig/php_twig.h b/ext/twig/php_twig.h index a235c9e76..16ec1ef9a 100644 --- a/ext/twig/php_twig.h +++ b/ext/twig/php_twig.h @@ -15,7 +15,7 @@ #ifndef PHP_TWIG_H #define PHP_TWIG_H -#define PHP_TWIG_VERSION "1.32.1-DEV" +#define PHP_TWIG_VERSION "1.33.0-DEV" #include "php.h" diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 53ca9bd7f..43c7bdb52 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -16,11 +16,11 @@ */ class Twig_Environment { - const VERSION = '1.32.1'; - const VERSION_ID = 13201; + const VERSION = '1.33.0'; + const VERSION_ID = 13300; const MAJOR_VERSION = 1; - const MINOR_VERSION = 32; - const RELEASE_VERSION = 1; + const MINOR_VERSION = 33; + const RELEASE_VERSION = 0; const EXTRA_VERSION = 'DEV'; protected $charset; From 96638c0da6394ca6576da2148f670f4f19ab66d3 Mon Sep 17 00:00:00 2001 From: Shigeki Morimoto Date: Fri, 18 Nov 2016 16:07:05 +0900 Subject: [PATCH 4/5] Call clearstatcache() before calling is_dir(). --- lib/Twig/Cache/Filesystem.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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)); From 82ff9d6bb4854763a4eaf716419c86e6260198ee Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 22 Mar 2017 08:06:31 -0700 Subject: [PATCH 5/5] updated CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 5bd4beb0b..04d371309 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 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