mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-11 18:06:46 +00:00
feature #2420 Make "length" filter and "empty" test consider __toString [Twig 1.x] (mpdude)
This PR was squashed before being merged into the 1.x branch (closes #2420).
Discussion
----------
Make "length" filter and "empty" test consider __toString [Twig 1.x]
Use case: When you have variables in your views that are actually objects but implement `__toString`, they feel like strings: For example, `{{ something }}` will make use of that to-string-conversion.
What does *not* work is
**a)** `{{ something | length }}`, because that will only have a meaningful return value for objects implementing `\Countable`. This interface, however, may have a totally different semantic/purpose for the object in question.
**b)** `{% if something is empty %}`, because
> empty checks if a variable is an empty string, an empty array, an empty hash, exactly false, or exactly null `[http://twig.sensiolabs.org/doc/2.x/tests/empty.html]`
... and obviously `something !== null` in this case.
For template designers, this may be surprising if they don't actually care about the object-or-string difference, they just "use" the variable.
This change tries to address this as it changes the behavior for such objects that have a `__toString` method and are *not* `\Countable`.
*Yes*, it's a BC break in edge cases:
For a), objects that implement a `__toString` but not `\Countable` would previously yield `1` for `{{ object | length }}`, and now would return the length of the string returned by `__toString`.
For b), testing (defined) variables that are objects implementing `__toString` and that return `''`, the test now is `false`.
Commits
-------
f5193e92 Make "length" filter and "empty" test consider __toString [Twig 1.x]
This commit is contained in:
@@ -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)
|
||||
|
||||
* fixed JS escaping for unicode characters with higher code points
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
|
||||
+12
-1
@@ -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 %}
|
||||
|
||||
|
||||
@@ -1264,7 +1264,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1347,7 +1355,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1409,6 +1425,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user