diff --git a/.travis.yml b/.travis.yml index 8503c46e2..2d05b2d67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,16 @@ php: - 5.5 - 5.6 - 7.0 + - 7.1 - hhvm env: - TWIG_EXT=no - TWIG_EXT=yes +before_install: + - if [[ ! $TRAVIS_PHP_VERSION = hhvm* ]]; then phpenv config-rm xdebug.ini || echo "xdebug not available"; fi + install: - travis_retry composer install @@ -29,6 +33,7 @@ matrix: exclude: - php: hhvm env: TWIG_EXT=yes - allow_failures: - php: 7.0 env: TWIG_EXT=yes + - php: 7.1 + env: TWIG_EXT=yes diff --git a/CHANGELOG b/CHANGELOG index 857fbdf62..461de0fa3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,9 +17,9 @@ * improved the performance of the filesystem loader * removed features that were deprecated in 1.x -* 1.27.1 (2016-XX-XX) +* 1.28.0 (2016-XX-XX) - * n/a + * optimized the way attributes are fetched * 1.27.0 (2016-10-25) diff --git a/doc/templates.rst b/doc/templates.rst index 567d81902..4a4843ff4 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -422,10 +422,8 @@ everything by default. Twig supports both, automatic escaping is enabled by default. -.. note:: - - Automatic escaping is only supported if the *escaper* extension has been - enabled (which is the default). +The automatic escaping strategy can be configured via the +:ref:`autoescape` option and defaults to ``html``. Working with Manual Escaping ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index ebb48df17..0c62018fa 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -10,6 +10,10 @@ * file that was distributed with this source code. */ +if (PHP_VERSION_ID >= 50600) { + require_once __DIR__.'/twig_call_method.php'; +} + /** * Default base class for compiled templates. * @@ -21,6 +25,9 @@ abstract class Twig_Template const ARRAY_CALL = 'array'; const METHOD_CALL = 'method'; + /** + * @internal + */ protected static $cache = array(); protected $parent; @@ -425,6 +432,8 @@ abstract class Twig_Template * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true * * @throws Twig_Error_Runtime if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal */ protected function getAttribute($object, $item, array $arguments = array(), $type = self::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) { @@ -432,7 +441,7 @@ abstract class Twig_Template if (self::METHOD_CALL !== $type) { $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item; - if ((is_array($object) && array_key_exists($arrayItem, $object)) + if ((is_array($object) && (isset($object[$arrayItem]) || array_key_exists($arrayItem, $object))) || ($object instanceof ArrayAccess && isset($object[$arrayItem])) ) { if ($isDefinedTest) { @@ -513,34 +522,53 @@ abstract class Twig_Template $class = get_class($object); // object method - if (!isset(self::$cache[$class]['methods'])) { + if (!isset(self::$cache[$class])) { // get_class_methods returns all methods accessible in the scope, but we only want public ones to be accessible in templates if ($object instanceof self) { $ref = new ReflectionClass($class); $methods = array(); - foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) { - $methods[strtolower($refMethod->name)] = true; + $methods[] = $refMethod->name; + } + } else { + $methods = get_class_methods($object); + } + $cache = array(); + + foreach ($methods as $method) { + $cache[$method] = $method; + $cache[$lcName = strtolower($method)] = $method; + + if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } else { + continue; } - self::$cache[$class]['methods'] = $methods; - } else { - self::$cache[$class]['methods'] = array_change_key_case(array_flip(get_class_methods($object))); + if (!isset($cache[$name])) { + $cache[$name] = $method; + } + if (!isset($cache[$lcName])) { + $cache[$lcName] = $method; + } } + self::$cache[$class] = $cache; } $call = false; - $lcItem = strtolower($item); - if (isset(self::$cache[$class]['methods'][$lcItem])) { - $method = (string) $item; - } elseif (isset(self::$cache[$class]['methods']['get'.$lcItem])) { - $method = 'get'.$item; - } elseif (isset(self::$cache[$class]['methods']['is'.$lcItem])) { - $method = 'is'.$item; - } elseif (isset(self::$cache[$class]['methods']['has'.$lcItem])) { - $method = 'has'.$item; - } elseif (isset(self::$cache[$class]['methods']['__call'])) { - $method = (string) $item; + if (isset(self::$cache[$class][$item])) { + $method = self::$cache[$class][$item]; + } elseif (isset(self::$cache[$class][$lcItem = strtolower($item)])) { + $method = self::$cache[$class][$lcItem]; + } elseif (isset(self::$cache[$class]['__call'])) { + $method = $item; $call = true; } else { if ($isDefinedTest) { @@ -551,7 +579,7 @@ abstract class Twig_Template return; } - throw new Twig_Error_Runtime(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, get_class($object)), -1, $this->getTemplateName()); + throw new Twig_Error_Runtime(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), -1, $this->getTemplateName()); } if ($isDefinedTest) { @@ -565,7 +593,13 @@ abstract class Twig_Template // Some objects throw exceptions when they have __call, and the method we try // to call is not supported. If ignoreStrictCheck is true, we should return null. try { - $ret = call_user_func_array(array($object, $method), $arguments); + if (!$arguments) { + $ret = $object->$method(); + } elseif (PHP_VERSION_ID >= 50600) { + $ret = twig_call_method($object, $method, $arguments); + } else { + $ret = call_user_func_array(array($object, $method), $arguments); + } } catch (BadMethodCallException $e) { if ($call && ($ignoreStrictCheck || !$this->env->isStrictVariables())) { return; diff --git a/lib/Twig/twig_call_method.php b/lib/Twig/twig_call_method.php new file mode 100644 index 000000000..652cd82a5 --- /dev/null +++ b/lib/Twig/twig_call_method.php @@ -0,0 +1,9 @@ +$method(...$arguments); +} diff --git a/test/Twig/Tests/Fixtures/functions/date.test b/test/Twig/Tests/Fixtures/functions/date.test index 8be9c0c40..a9ffe40fc 100644 --- a/test/Twig/Tests/Fixtures/functions/date.test +++ b/test/Twig/Tests/Fixtures/functions/date.test @@ -1,7 +1,7 @@ --TEST-- "date" function --TEMPLATE-- -{{ date() == date('now') ? 'OK' : 'KO' }} +{{ date().format('r') == date('now').format('r') ? 'OK' : 'KO' }} {{ date(date1) == date('2010-10-04 13:45') ? 'OK' : 'KO' }} {{ date(date2) == date('2010-10-04 13:45') ? 'OK' : 'KO' }} {{ date(date3) == date('2010-10-04 13:45') ? 'OK' : 'KO' }}