1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2024-12-23 02:04:46 +02:00

Update dependency twig/twig to v3.14.0 (#6071)

This commit is contained in:
Peter 2024-10-16 15:29:16 +02:00 committed by GitHub
parent 8dcaffe925
commit 5a0f20b9ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
184 changed files with 6895 additions and 3455 deletions

View File

@ -1039,6 +1039,73 @@
},
"time": "2017-04-19T22:01:50+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.24.0",
@ -1287,6 +1354,82 @@
],
"time": "2021-09-13T13:58:33+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/translation",
"version": "v6.0.5",
@ -1604,34 +1747,37 @@
},
{
"name": "twig/twig",
"version": "v3.4.3",
"version": "v3.14.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58"
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@ -1664,7 +1810,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.4.3"
"source": "https://github.com/twigphp/Twig/tree/v3.14.0"
},
"funding": [
{
@ -1676,7 +1822,7 @@
"type": "tidelift"
}
],
"time": "2022-09-28T08:42:51+00:00"
"time": "2024-09-09T17:55:12+00:00"
},
{
"name": "yubico/u2flib-server",
@ -1728,5 +1874,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@ -3,8 +3,21 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
exit(1);
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@ -42,35 +42,37 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
/** @var ?string */
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
@ -78,8 +80,7 @@ class ClassLoader
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
* @var array<string, string>
*/
private $classMap = array();
@ -87,29 +88,29 @@ class ClassLoader
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
/** @var string|null */
private $apcuPrefix;
/**
* @var self[]
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return string[]
* @return array<string, list<string>>
*/
public function getPrefixes()
{
@ -121,8 +122,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
@ -130,8 +130,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, string>
* @return list<string>
*/
public function getFallbackDirs()
{
@ -139,8 +138,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, string>
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
@ -148,8 +146,7 @@ class ClassLoader
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
@ -157,8 +154,7 @@ class ClassLoader
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
@ -175,24 +171,25 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
$paths
);
}
@ -201,19 +198,19 @@ class ClassLoader
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
$paths
);
}
}
@ -222,9 +219,9 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
@ -232,17 +229,18 @@ class ClassLoader
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@ -252,18 +250,18 @@ class ClassLoader
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
$paths
);
}
}
@ -272,8 +270,8 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
@ -290,8 +288,8 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
@ -425,7 +423,8 @@ class ClassLoader
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
@ -476,9 +475,9 @@ class ClassLoader
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return self[]
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
@ -555,18 +554,26 @@ class ClassLoader
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{
include $file;
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@ -98,7 +98,7 @@ class InstalledVersions
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
@ -119,7 +119,7 @@ class InstalledVersions
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
@ -328,7 +328,9 @@ class InstalledVersions
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
@ -340,12 +342,17 @@ class InstalledVersions
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
$installed[] = self::$installed;
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}

View File

@ -7,7 +7,9 @@ $baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CURLStringFile' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

View File

@ -10,8 +10,14 @@ return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',
'04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php',
'89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php',
'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php',
'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php',
);

View File

@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir);
return array(
'Twig\\' => array($vendorDir . '/twig/twig/src'),
'Tightenco\\Collect\\' => array($vendorDir . '/tightenco/collect/src/Collect'),
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),

View File

@ -33,25 +33,18 @@ class ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b
$loader->register(true);
$includeFiles = \Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::$files;
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire873464e4bd965a3168f133248b1b218b($fileIdentifier, $file);
$filesToLoad = \Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire873464e4bd965a3168f133248b1b218b($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

View File

@ -11,10 +11,16 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
'04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
'89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php',
'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php',
'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php',
);
public static $prefixLengthsPsr4 = array (
@ -25,6 +31,7 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
),
'S' =>
array (
'Symfony\\Polyfill\\Php81\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23,
@ -80,6 +87,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
array (
0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect',
),
'Symfony\\Polyfill\\Php81\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@ -170,7 +181,9 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

View File

@ -1068,6 +1068,76 @@
],
"install-path": "../soundasleep/html2text"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"version_normalized": "3.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"time": "2024-04-18T09:32:20+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/deprecation-contracts"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.24.0",
@ -1325,6 +1395,85 @@
],
"install-path": "../symfony/polyfill-php80"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"version_normalized": "1.31.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2024-09-09T11:45:10+00:00",
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php81"
},
{
"name": "symfony/translation",
"version": "v6.0.5",
@ -1654,37 +1803,40 @@
},
{
"name": "twig/twig",
"version": "v3.4.3",
"version_normalized": "3.4.3.0",
"version": "v3.14.0",
"version_normalized": "3.14.0.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58"
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"time": "2022-09-28T08:42:51+00:00",
"time": "2024-09-09T17:55:12+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@ -1717,7 +1869,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.4.3"
"source": "https://github.com/twigphp/Twig/tree/v3.14.0"
},
"funding": [
{

View File

@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '8e0b1d8aee4af02311692cb031695cc2ac3850fd',
'reference' => '220fdbb168792c07493db330d898b345cc902055',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '8e0b1d8aee4af02311692cb031695cc2ac3850fd',
'reference' => '220fdbb168792c07493db330d898b345cc902055',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -175,6 +175,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.5.0',
'version' => '3.5.0.0',
'reference' => '0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.24.0',
'version' => '1.24.0.0',
@ -202,6 +211,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php81' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => '4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php81',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/translation' => array(
'pretty_version' => 'v6.0.5',
'version' => '6.0.5.0',
@ -245,9 +263,9 @@
'dev_requirement' => false,
),
'twig/twig' => array(
'pretty_version' => 'v3.4.3',
'version' => '3.4.3.0',
'reference' => 'c38fd6b0b7f370c198db91ffd02e23b517426b58',
'pretty_version' => 'v3.14.0',
'version' => '3.14.0.0',
'reference' => '126b2c97818dbff0cdf3fbfc881aedb3d40aae72',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),

View File

@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80002)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.2". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {

View File

@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@ -0,0 +1,19 @@
Copyright (c) 2020-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.

View File

@ -0,0 +1,35 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2021-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php81;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class Php81
{
public static function array_is_list(array $array): bool
{
if ([] === $array || $array === array_values($array)) {
return true;
}
$nextKey = -1;
foreach ($array as $k => $v) {
if ($k !== ++$nextKey) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,18 @@
Symfony Polyfill / Php81
========================
This component provides features added to PHP 8.1 core:
- [`array_is_list`](https://php.net/array_is_list)
- [`enum_exists`](https://php.net/enum-exists)
- [`MYSQLI_REFRESH_REPLICA`](https://php.net/mysqli.constants#constantmysqli-refresh-replica) constant
- [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types)
- [`CURLStringFile`](https://php.net/CURLStringFile) (but only if PHP >= 7.4 is used)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID >= 70400 && extension_loaded('curl')) {
/**
* @property string $data
*/
class CURLStringFile extends CURLFile
{
private $data;
public function __construct(string $data, string $postname, string $mime = 'application/octet-stream')
{
$this->data = $data;
parent::__construct('data://application/octet-stream;base64,'.base64_encode($data), $mime, $postname);
}
public function __set(string $name, $value): void
{
if ('data' !== $name) {
$this->$name = $value;
return;
}
if (is_object($value) ? !method_exists($value, '__toString') : !is_scalar($value)) {
throw new TypeError('Cannot assign '.gettype($value).' to property CURLStringFile::$data of type string');
}
$this->name = 'data://application/octet-stream;base64,'.base64_encode($value);
}
public function __isset(string $name): bool
{
return isset($this->$name);
}
public function &__get(string $name)
{
return $this->$name;
}
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80100) {
#[Attribute(Attribute::TARGET_METHOD)]
final class ReturnTypeWillChange
{
public function __construct()
{
}
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php81 as p;
if (\PHP_VERSION_ID >= 80100) {
return;
}
if (defined('MYSQLI_REFRESH_SLAVE') && !defined('MYSQLI_REFRESH_REPLICA')) {
define('MYSQLI_REFRESH_REPLICA', 64);
}
if (!function_exists('array_is_list')) {
function array_is_list(array $array): bool { return p\Php81::array_is_list($array); }
}
if (!function_exists('enum_exists')) {
function enum_exists(string $enum, bool $autoload = true): bool { return $autoload && class_exists($enum) && false; }
}

View File

@ -0,0 +1,33 @@
{
"name": "symfony/polyfill-php81",
"type": "library",
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"keywords": ["polyfill", "shim", "compatibility", "portable"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Php81\\": "" },
"files": [ "bootstrap.php" ],
"classmap": [ "Resources/stubs" ]
},
"minimum-stability": "dev",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}

View File

@ -1,18 +0,0 @@
; top-most EditorConfig file
root = true
; Unix-style newlines
[*]
end_of_line = LF
[*.php]
indent_style = space
indent_size = 4
[*.test]
indent_style = space
indent_size = 4
[*.rst]
indent_style = space
indent_size = 4

View File

@ -1,4 +0,0 @@
/doc/ export-ignore
/extra/ export-ignore
/tests/ export-ignore
/phpunit.xml.dist export-ignore

View File

@ -1,149 +0,0 @@
name: "CI"
on:
pull_request:
push:
branches:
- '3.x'
env:
SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE: 1
permissions:
contents: read
jobs:
tests:
name: "PHP ${{ matrix.php-version }}"
runs-on: 'ubuntu-latest'
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
php-version:
- '7.2.5'
- '7.3'
- '7.4'
- '8.0'
- '8.1'
experimental: [false]
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: ${{ matrix.php-version }}
ini-values: memory_limit=-1
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- run: composer install
- name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
- name: "PHPUnit version"
run: vendor/bin/simple-phpunit --version
- name: "Run tests"
run: vendor/bin/simple-phpunit
extension-tests:
needs:
- 'tests'
name: "${{ matrix.extension }} with PHP ${{ matrix.php-version }}"
runs-on: 'ubuntu-latest'
continue-on-error: true
strategy:
matrix:
php-version:
- '7.2.5'
- '7.3'
- '7.4'
- '8.0'
- '8.1'
extension:
- 'extra/cache-extra'
- 'extra/cssinliner-extra'
- 'extra/html-extra'
- 'extra/inky-extra'
- 'extra/intl-extra'
- 'extra/markdown-extra'
- 'extra/string-extra'
- 'extra/twig-extra-bundle'
experimental: [false]
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: ${{ matrix.php-version }}
ini-values: memory_limit=-1
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- run: composer install
- name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
- name: "PHPUnit version"
run: vendor/bin/simple-phpunit --version
- name: "Composer install"
working-directory: ${{ matrix.extension}}
run: composer install
- name: "Run tests"
working-directory: ${{ matrix.extension}}
run: ../../vendor/bin/simple-phpunit
#
# Drupal does not support Twig 3 now!
#
# integration-tests:
# needs:
# - 'tests'
#
# name: "Integration tests with PHP ${{ matrix.php-version }}"
#
# runs-on: 'ubuntu-20.04'
#
# continue-on-error: true
#
# strategy:
# matrix:
# php-version:
# - '7.3'
#
# steps:
# - name: "Checkout code"
# uses: actions/checkout@v2
#
# - name: "Install PHP with extensions"
# uses: shivammathur/setup-php@2
# with:
# coverage: "none"
# extensions: "gd, pdo_sqlite"
# php-version: ${{ matrix.php-version }}
# ini-values: memory_limit=-1
# tools: composer:v2
#
# - run: bash ./tests/drupal_test.sh
# shell: "bash"

View File

@ -1,64 +0,0 @@
name: "Documentation"
on:
pull_request:
push:
branches:
- '2.x'
- '3.x'
permissions:
contents: read
jobs:
build:
name: "Build"
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Set-up PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
coverage: none
tools: "composer:v2"
- name: Get composer cache directory
id: composercache
working-directory: doc/_build
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: "Install dependencies"
working-directory: doc/_build
run: composer install --prefer-dist --no-progress
- name: "Build the docs"
working-directory: doc/_build
run: php build.php --disable-cache
doctor-rst:
name: "DOCtor-RST"
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Run DOCtor-RST"
uses: docker://oskarstark/doctor-rst
with:
args: --short
env:
DOCS_DIR: 'doc/'

View File

@ -1,6 +0,0 @@
/doc/_build/vendor
/doc/_build/output
/composer.lock
/phpunit.xml
/vendor
.phpunit.result.cache

View File

@ -1,20 +0,0 @@
<?php
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'@PHPUnit75Migration:risky' => true,
'php_unit_dedicate_assert' => ['target' => '5.6'],
'array_syntax' => ['syntax' => 'short'],
'php_unit_fqcn_annotation' => true,
'no_unreachable_default_argument_value' => false,
'braces' => ['allow_single_line_closure' => true],
'heredoc_to_nowdoc' => false,
'ordered_imports' => true,
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'all'],
])
->setRiskyAllowed(true)
->setFinder((new PhpCsFixer\Finder())->in(__DIR__))
;

View File

@ -1,3 +1,178 @@
# 3.14.0 (2024-09-09)
* Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
* Add the possibility to reset globals via `Environment::resetGlobals()`
* Deprecate `Environment::mergeGlobals()`
# 3.13.0 (2024-09-07)
* Add the `types` tag (experimental)
* Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead.
* Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead.
* Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead.
* Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead.
* Deprecate not overriding `Twig\Test\IntegrationTestCase::getFixturesDirectory()`, this method will be abstract in 4.0
* Marked `Twig\Test\IntegrationTestCase::getTests()` and `getLegacyTests()` as final
# 3.12.0 (2024-08-29)
* Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template.
This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag.
* Deprecate the "tag" constructor argument of the "Twig\Node\Node" class as the tag is now automatically set by the Parser when needed
* Fix precedence of two-word tests when the first word is a valid test
* Deprecate the `spaceless` filter
* Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()`
* Deprecate passing `null` to `Twig\Parser::setParent()`
* Update `Node::__toString()` to include the node tag if set
* Add support for integers in methods of `Twig\Node\Node` that take a Node name
* Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor
* Deprecate returning "null" from "TokenParserInterface::parse()".
* Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES`
* Fix performance regression when `use_yield` is `false` (which is the default)
* Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is)
* Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments
* Add the `html_cva` function (in the HTML extra package)
* Add support for named arguments to the `block` and `attribute` functions
* Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments
* Add a `CallableArgumentsExtractor` class
* Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`;
pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead
* Deprecate all Twig callable attributes on `FunctionExpression`, `FilterExpression`, and `TestExpression`
* Deprecate the `filter` node of `FilterExpression`
* Add the notion of Twig callables (functions, filters, and tests)
* Bump minimum PHP version to 8.0
* Fix integration tests when a test has more than one data/expect section and deprecations
* Add the `enum_cases` function
# 3.11.0 (2024-08-08)
* Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER`
* Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache`
* Add the possibility to deprecate attributes and nodes on `Node`
* Add the possibility to add a package and a version to the `deprecated` tag
* Add the possibility to add a package for filter/function/test deprecations
* Mark `ConstantExpression` as being `@final`
* Add the `find` filter
* Fix optimizer mode validation in `OptimizerNodeVisitor`
* Add the possibility to yield from a generator in `PrintNode`
* Add the `shuffle` filter
* Add the `singular` and `plural` filters in `StringExtension`
* Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()`
* Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of
`Twig\ExpressionParser::parseMappingExpression()`
* Deprecate `Twig\ExpressionParser\parseArrayExpression()` in favor of
`Twig\ExpressionParser::parseSequenceExpression()`
* Add `sequence` and `mapping` tests
* Deprecate `Twig\Node\Expression\NameExpression::isSimple()` and
`Twig\Node\Expression\NameExpression::isSpecial()`
# 3.10.3 (2024-05-16)
* Fix missing ; in generated code
# 3.10.2 (2024-05-14)
* Fix support for the deprecated escaper signature
# 3.10.1 (2024-05-12)
* Fix BC break on escaper extension
* Fix constant return type
# 3.10.0 (2024-05-11)
* Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and
`CoreExtension::formatNumber` part of the public API
* Add `needs_charset` option for filters and functions
* Extract the escaping logic from the `EscaperExtension` class to a new
`EscaperRuntime` class.
The following methods from ``Twig\\Extension\\EscaperExtension`` are
deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``,
``addSafeClasses()``. Use the same methods on the
``Twig\\Runtime\\EscaperRuntime`` class instead.
* Fix capturing output from extensions that still use echo
* Fix a PHP warning in the Lexer on malformed templates
* Fix blocks not available under some circumstances
* Synchronize source context in templates when setting a Node on a Node
# 3.9.3 (2024-04-18)
* Add missing `twig_escape_filter_is_safe` deprecated function
* Fix yield usage with CaptureNode
* Add missing unwrap call when using a TemplateWrapper instance internally
* Ensure Lexer is initialized early on
# 3.9.2 (2024-04-17)
* Fix usage of display_end hook
# 3.9.1 (2024-04-17)
* Fix missing `$blocks` variable in `CaptureNode`
# 3.9.0 (2024-04-16)
* Add support for PHP 8.4
* Deprecate AbstractNodeVisitor
* Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate()
* Add a new "yield" mode for output generation;
Node implementations that use "echo" or "print" should use "yield" instead;
all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield";
the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`;
"yield" will be the only strategy supported in the next major version
* Add return type for Symfony 7 compatibility
* Fix premature loop exit in Security Policy lookup of allowed methods/properties
* Deprecate all internal extension functions in favor of methods on the extension classes
* Mark all extension functions as @internal
* Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
* Throw a proper Twig exception when using cycle on an empty array
# 3.8.0 (2023-11-21)
* Catch errors thrown during template rendering
* Fix IntlExtension::formatDateTime use of date formatter prototype
* Fix premature loop exit in Security Policy lookup of allowed methods/properties
* Remove NumberFormatter::TYPE_CURRENCY (deprecated in PHP 8.3)
* Restore return type annotations
* Allow Symfony 7 packages to be installed
* Deprecate `twig_test_iterable` function. Use the native `is_iterable` instead.
# 3.7.1 (2023-08-28)
* Fix some phpdocs
# 3.7.0 (2023-07-26)
* Add support for the ...spread operator on arrays and hashes
# 3.6.1 (2023-06-08)
* Suppress some native return type deprecation messages
# 3.6.0 (2023-05-03)
* Allow psr/container 2.0
* Add the new PHP 8.0 IntlDateFormatter::RELATIVE_* constants for date formatting
* Make the Lexer initialize itself lazily
# 3.5.1 (2023-02-08)
* Arrow functions passed to the "reduce" filter now accept the current key as a third argument
* Restores the leniency of the matches twig comparison
* Fix error messages in sandboxed mode for "has some" and "has every"
# 3.5.0 (2022-12-27)
* Make Twig\ExpressionParser non-internal
* Add "has some" and "has every" operators
* Add Compile::reset()
* Throw a better runtime error when the "matches" regexp is not valid
* Add "twig *_names" intl functions
* Fix optimizing closures callbacks
* Add a better exception when getting an undefined constant via `constant`
* Fix `if` nodes when outside of a block and with an empty body
# 3.4.3 (2022-09-28)
* Fix a security issue on filesystem loader (possibility to load a template outside a configured directory)
@ -141,7 +316,7 @@
* removed Parser::isReservedMacroName()
* removed SanboxedPrintNode
* removed Node::setTemplateName()
* made classes maked as "@final" final
* made classes marked as "@final" final
* removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface
* removed the "spaceless" tag
* removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass()

View File

@ -1,4 +1,4 @@
Copyright (c) 2009-2022 by the Twig Team.
Copyright (c) 2009-present by the Twig Team.
All rights reserved.

View File

@ -11,7 +11,7 @@ Sponsors
.. raw:: html
<a href="https://blackfire.io/docs/introduction?utm_source=twig&utm_medium=github_readme&utm_campaign=logo">
<a href="https://docs.blackfire.io/introduction?utm_source=twig&utm_medium=github_readme&utm_campaign=logo">
<img src="https://static.blackfire.io/assets/intemporals/logo/png/blackfire-io_secondary_horizontal_transparent.png?1" width="255px" alt="Blackfire.io">
</a>

View File

@ -24,15 +24,23 @@
}
],
"require": {
"php": ">=7.2.5",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-ctype": "^1.8"
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0",
"psr/container": "^1.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
"psr/container": "^1.0|^2.0"
},
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4" : {
"Twig\\" : "src/"
}
@ -41,10 +49,5 @@
"psr-4" : {
"Twig\\Tests\\" : "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
}
}

View File

@ -0,0 +1,136 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractTwigCallable implements TwigCallableInterface
{
protected $options;
private $name;
private $dynamicName;
private $callable;
private $arguments;
public function __construct(string $name, $callable = null, array $options = [])
{
$this->name = $this->dynamicName = $name;
$this->callable = $callable;
$this->arguments = [];
$this->options = array_merge([
'needs_environment' => false,
'needs_context' => false,
'needs_charset' => false,
'is_variadic' => false,
'deprecated' => false,
'deprecating_package' => '',
'alternative' => null,
], $options);
}
public function __toString(): string
{
return \sprintf('%s(%s)', static::class, $this->name);
}
public function getName(): string
{
return $this->name;
}
public function getDynamicName(): string
{
return $this->dynamicName;
}
public function getCallable()
{
return $this->callable;
}
public function getNodeClass(): string
{
return $this->options['node_class'];
}
public function needsCharset(): bool
{
return $this->options['needs_charset'];
}
public function needsEnvironment(): bool
{
return $this->options['needs_environment'];
}
public function needsContext(): bool
{
return $this->options['needs_context'];
}
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self
{
$new = clone $this;
$new->name = $name;
$new->dynamicName = $dynamicName;
$new->arguments = $arguments;
return $new;
}
/**
* @deprecated since Twig 3.12, use withDynamicArguments() instead
*/
public function setArguments(array $arguments): void
{
trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class);
$this->arguments = $arguments;
}
public function getArguments(): array
{
return $this->arguments;
}
public function isVariadic(): bool
{
return $this->options['is_variadic'];
}
public function isDeprecated(): bool
{
return (bool) $this->options['deprecated'];
}
public function getDeprecatingPackage(): string
{
return $this->options['deprecating_package'];
}
public function getDeprecatedVersion(): string
{
return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
}
public function getAlternative(): ?string
{
return $this->options['alternative'];
}
public function getMinimalNumberOfRequiredArguments(): int
{
return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + \count($this->arguments);
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Attribute;
/**
* Marks nodes that are ready to accept a TwigCallable instead of its name.
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class FirstClassTwigCallableReady
{
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Attribute;
/**
* Marks nodes that are ready for using "yield" instead of "echo" or "print()" for rendering.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class YieldReady
{
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Cache;
/**
* Chains several caches together.
*
* Cached items are fetched from the first cache having them in its data store.
* They are saved and deleted in all adapters at once.
*
* @author Quentin Devos <quentin@devos.pm>
*/
final class ChainCache implements CacheInterface
{
/**
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
*/
public function __construct(
private iterable $caches,
) {
}
public function generateKey(string $name, string $className): string
{
return $className.'#'.$name;
}
public function write(string $key, string $content): void
{
$splitKey = $this->splitKey($key);
foreach ($this->caches as $cache) {
$cache->write($cache->generateKey(...$splitKey), $content);
}
}
public function load(string $key): void
{
[$name, $className] = $this->splitKey($key);
foreach ($this->caches as $cache) {
$cache->load($cache->generateKey($name, $className));
if (class_exists($className, false)) {
break;
}
}
}
public function getTimestamp(string $key): int
{
$splitKey = $this->splitKey($key);
foreach ($this->caches as $cache) {
if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) {
return $timestamp;
}
}
return 0;
}
/**
* @return string[]
*/
private function splitKey(string $key): array
{
return array_reverse(explode('#', $key, 2));
}
}

View File

@ -50,11 +50,11 @@ class FilesystemCache implements CacheInterface
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));
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));
throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir));
}
$tmpFile = tempnam($dir, basename($key));
@ -63,7 +63,7 @@ class FilesystemCache implements CacheInterface
if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) {
// Compile cached file into bytecode cache
if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
@opcache_invalidate($key, true);
} elseif (\function_exists('apc_compile_file')) {
apc_compile_file($key);
@ -73,7 +73,7 @@ class FilesystemCache implements CacheInterface
return;
}
throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $key));
throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key));
}
public function getTimestamp(string $key): int

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Cache;
/**
* Implements a cache on the filesystem that can only be read, not written to.
*
* @author Quentin Devos <quentin@devos.pm>
*/
class ReadOnlyFilesystemCache extends FilesystemCache
{
public function write(string $key, string $content): void
{
// Do nothing with the content, it's a read-only filesystem.
}
}

View File

@ -22,15 +22,16 @@ class Compiler
private $lastLine;
private $source;
private $indentation;
private $env;
private $debugInfo = [];
private $sourceOffset;
private $sourceLine;
private $varNameSalt = 0;
private $didUseEcho = false;
private $didUseEchoStack = [];
public function __construct(Environment $env)
{
$this->env = $env;
public function __construct(
private Environment $env,
) {
}
public function getEnvironment(): Environment
@ -46,7 +47,7 @@ class Compiler
/**
* @return $this
*/
public function compile(Node $node, int $indentation = 0)
public function reset(int $indentation = 0)
{
$this->lastLine = null;
$this->source = '';
@ -57,23 +58,54 @@ class Compiler
$this->indentation = $indentation;
$this->varNameSalt = 0;
$node->compile($this);
return $this;
}
/**
* @return $this
*/
public function compile(Node $node, int $indentation = 0)
{
$this->reset($indentation);
$this->didUseEchoStack[] = $this->didUseEcho;
try {
$this->didUseEcho = false;
$node->compile($this);
if ($this->didUseEcho) {
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
}
return $this;
} finally {
$this->didUseEcho = array_pop($this->didUseEchoStack);
}
}
/**
* @return $this
*/
public function subcompile(Node $node, bool $raw = true)
{
if (false === $raw) {
if (!$raw) {
$this->source .= str_repeat(' ', $this->indentation * 4);
}
$node->compile($this);
$this->didUseEchoStack[] = $this->didUseEcho;
return $this;
try {
$this->didUseEcho = false;
$node->compile($this);
if ($this->didUseEcho) {
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
}
return $this;
} finally {
$this->didUseEcho = array_pop($this->didUseEchoStack);
}
}
/**
@ -83,6 +115,7 @@ class Compiler
*/
public function raw(string $string)
{
$this->checkForEcho($string);
$this->source .= $string;
return $this;
@ -96,6 +129,7 @@ class Compiler
public function write(...$strings)
{
foreach ($strings as $string) {
$this->checkForEcho($string);
$this->source .= str_repeat(' ', $this->indentation * 4).$string;
}
@ -109,7 +143,7 @@ class Compiler
*/
public function string(string $value)
{
$this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
$this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
return $this;
}
@ -161,7 +195,7 @@ class Compiler
public function addDebugInfo(Node $node)
{
if ($node->getTemplateLine() != $this->lastLine) {
$this->write(sprintf("// line %d\n", $node->getTemplateLine()));
$this->write(\sprintf("// line %d\n", $node->getTemplateLine()));
$this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
$this->sourceOffset = \strlen($this->source);
@ -209,6 +243,15 @@ class Compiler
public function getVarName(): string
{
return sprintf('__internal_compile_%d', $this->varNameSalt++);
return \sprintf('__internal_compile_%d', $this->varNameSalt++);
}
private function checkForEcho(string $string): void
{
if ($this->didUseEcho) {
return;
}
$this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false;
}
}

View File

@ -22,12 +22,17 @@ use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\OptimizerExtension;
use Twig\Extension\YieldNotReadyExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\LoaderInterface;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\Runtime\EscaperRuntime;
use Twig\RuntimeLoader\FactoryRuntimeLoader;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
use Twig\TokenParser\TokenParserInterface;
@ -38,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
public const VERSION = '3.4.3';
public const VERSION_ID = 30403;
public const VERSION = '3.14.0';
public const VERSION_ID = 31400;
public const MAJOR_VERSION = 3;
public const MINOR_VERSION = 4;
public const RELEASE_VERSION = 3;
public const MINOR_VERSION = 14;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
private $charset;
@ -53,16 +58,19 @@ class Environment
private $lexer;
private $parser;
private $compiler;
/** @var array<string, mixed> */
private $globals = [];
private $resolvedGlobals;
private $loadedTemplates;
private $strictVariables;
private $templateClassPrefix = '__TwigTemplate_';
private $originalCache;
private $extensionSet;
private $runtimeLoaders = [];
private $runtimes = [];
private $optionsHash;
/** @var bool */
private $useYield;
private $defaultRuntimeLoader;
/**
* Constructor.
@ -94,8 +102,12 @@ class Environment
* * optimizations: A flag that indicates which optimizations to apply
* (default to -1 which means that all optimizations are enabled;
* set it to 0 to disable).
*
* * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready)
* false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration
* Switch to "true" when possible as this will be the only supported mode in Twig 4.0
*/
public function __construct(LoaderInterface $loader, $options = [])
public function __construct(LoaderInterface $loader, array $options = [])
{
$this->setLoader($loader);
@ -107,20 +119,38 @@ class Environment
'cache' => false,
'auto_reload' => null,
'optimizations' => -1,
'use_yield' => false,
], $options);
$this->useYield = (bool) $options['use_yield'];
$this->debug = (bool) $options['debug'];
$this->setCharset($options['charset'] ?? 'UTF-8');
$this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
$this->strictVariables = (bool) $options['strict_variables'];
$this->setCache($options['cache']);
$this->extensionSet = new ExtensionSet();
$this->defaultRuntimeLoader = new FactoryRuntimeLoader([
EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); },
]);
$this->addExtension(new CoreExtension());
$this->addExtension(new EscaperExtension($options['autoescape']));
$escaperExt = new EscaperExtension($options['autoescape']);
$escaperExt->setEnvironment($this, false);
$this->addExtension($escaperExt);
if (\PHP_VERSION_ID >= 80000) {
$this->addExtension(new YieldNotReadyExtension($this->useYield));
}
$this->addExtension(new OptimizerExtension($options['optimizations']));
}
/**
* @internal
*/
public function useYield(): bool
{
return $this->useYield;
}
/**
* Enables debugging mode.
*/
@ -246,7 +276,6 @@ class Environment
*
* * The cache key for the given template;
* * The currently enabled extensions;
* * Whether the Twig C extension is available or not;
* * PHP version;
* * Twig version;
* * Options with what environment was created.
@ -256,11 +285,11 @@ class Environment
*
* @internal
*/
public function getTemplateClass(string $name, int $index = null): string
public function getTemplateClass(string $name, ?int $index = null): string
{
$key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
}
/**
@ -305,6 +334,11 @@ class Environment
if ($name instanceof TemplateWrapper) {
return $name;
}
if ($name instanceof Template) {
trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__);
return $name;
}
return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
}
@ -315,8 +349,8 @@ class Environment
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The template name
* @param int $index The index if it is an embedded template
* @param string $name The template name
* @param int|null $index The index if it is an embedded template
*
* @throws LoaderError When the template cannot be found
* @throws RuntimeError When a previously generated cache is corrupted
@ -324,7 +358,7 @@ class Environment
*
* @internal
*/
public function loadTemplate(string $cls, string $name, int $index = null): Template
public function loadTemplate(string $cls, string $name, ?int $index = null): Template
{
$mainCls = $cls;
if (null !== $index) {
@ -342,7 +376,6 @@ class Environment
$this->cache->load($key);
}
$source = null;
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
@ -359,7 +392,7 @@ class Environment
}
if (!class_exists($cls, false)) {
throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
}
}
}
@ -374,19 +407,19 @@ class Environment
*
* This method should not be used as a generic way to load templates.
*
* @param string $template The template source
* @param string $name An optional name of the template to be used in error messages
* @param string $template The template source
* @param string|null $name An optional name of the template to be used in error messages
*
* @throws LoaderError When the template cannot be found
* @throws SyntaxError When an error occurred during compilation
*/
public function createTemplate(string $template, string $name = null): TemplateWrapper
public function createTemplate(string $template, ?string $name = null): TemplateWrapper
{
$hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false);
if (null !== $name) {
$name = sprintf('%s (string template %s)', $name, $hash);
$name = \sprintf('%s (string template %s)', $name, $hash);
} else {
$name = sprintf('__string_template__%s', $hash);
$name = \sprintf('__string_template__%s', $hash);
}
$loader = new ChainLoader([
@ -419,10 +452,10 @@ class Environment
/**
* Tries to load a template consecutively from an array.
*
* Similar to load() but it also accepts instances of \Twig\Template and
* \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded.
* Similar to load() but it also accepts instances of \Twig\TemplateWrapper
* and an array of templates where each is tried to be loaded.
*
* @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively
* @param string|TemplateWrapper|array<string|TemplateWrapper> $names A template or an array of templates to try consecutively
*
* @throws LoaderError When none of the templates can be found
* @throws SyntaxError When an error occurred during compilation
@ -436,7 +469,9 @@ class Environment
$count = \count($names);
foreach ($names as $name) {
if ($name instanceof Template) {
return $name;
trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__);
return new TemplateWrapper($this, $name);
}
if ($name instanceof TemplateWrapper) {
return $name;
@ -449,7 +484,7 @@ class Environment
return $this->load($name);
}
throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
}
public function setLexer(Lexer $lexer)
@ -518,7 +553,7 @@ class Environment
$e->setSourceContext($source);
throw $e;
} catch (\Exception $e) {
throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
}
}
@ -534,7 +569,7 @@ class Environment
public function setCharset(string $charset)
{
if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) {
if ('UTF8' === $charset = strtoupper($charset ?: '')) {
// iconv on Windows requires "UTF-8" instead of "UTF8"
$charset = 'UTF-8';
}
@ -592,7 +627,11 @@ class Environment
}
}
throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class));
if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) {
return $this->runtimes[$class] = $runtime;
}
throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
}
public function addExtension(ExtensionInterface $extension)
@ -763,7 +802,7 @@ class Environment
public function addGlobal(string $name, $value)
{
if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
}
if (null !== $this->resolvedGlobals) {
@ -775,6 +814,8 @@ class Environment
/**
* @internal
*
* @return array<string, mixed>
*/
public function getGlobals(): array
{
@ -789,21 +830,26 @@ class Environment
return array_merge($this->extensionSet->getGlobals(), $this->globals);
}
public function resetGlobals(): void
{
$this->resolvedGlobals = null;
$this->extensionSet->resetGlobals();
}
/**
* @deprecated since Twig 3.14
*/
public function mergeGlobals(array $context): array
{
// we don't use array_merge as the context being generally
// bigger than globals, this code is faster.
foreach ($this->getGlobals() as $key => $value) {
if (!\array_key_exists($key, $context)) {
$context[$key] = $value;
}
}
trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__);
return $context;
return $context + $this->getGlobals();
}
/**
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
*/
public function getUnaryOperators(): array
{
@ -812,6 +858,8 @@ class Environment
/**
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
{
@ -827,6 +875,7 @@ class Environment
self::VERSION,
(int) $this->debug,
(int) $this->strictVariables,
$this->useYield ? '1' : '0',
]);
}
}

View File

@ -53,7 +53,7 @@ class Error extends \Exception
* @param int $lineno The template line where the error occurred
* @param Source|null $source The source context where the error occurred
*/
public function __construct(string $message, int $lineno = -1, Source $source = null, \Exception $previous = null)
public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null)
{
parent::__construct('', 0, $previous);
@ -93,7 +93,7 @@ class Error extends \Exception
return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
}
public function setSourceContext(Source $source = null): void
public function setSourceContext(?Source $source = null): void
{
if (null === $source) {
$this->sourceCode = $this->name = $this->sourcePath = null;
@ -130,28 +130,28 @@ class Error extends \Exception
}
$dot = false;
if ('.' === substr($this->message, -1)) {
if (str_ends_with($this->message, '.')) {
$this->message = substr($this->message, 0, -1);
$dot = true;
}
$questionMark = false;
if ('?' === substr($this->message, -1)) {
if (str_ends_with($this->message, '?')) {
$this->message = substr($this->message, 0, -1);
$questionMark = true;
}
if ($this->name) {
if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) {
$name = sprintf('"%s"', $this->name);
if (\is_string($this->name) || $this->name instanceof \Stringable) {
$name = \sprintf('"%s"', $this->name);
} else {
$name = json_encode($this->name);
}
$this->message .= sprintf(' in %s', $name);
$this->message .= \sprintf(' in %s', $name);
}
if ($this->lineno && $this->lineno >= 0) {
$this->message .= sprintf(' at line %d', $this->lineno);
$this->message .= \sprintf(' at line %d', $this->lineno);
}
if ($dot) {
@ -172,7 +172,7 @@ class Error extends \Exception
foreach ($backtrace as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof Template) {
$currentClass = \get_class($trace['object']);
$isEmbedContainer = null === $templateClass ? false : 0 === strpos($templateClass, $currentClass);
$isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass);
if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
$template = $trace['object'];
$templateClass = \get_class($trace['object']);

View File

@ -30,7 +30,7 @@ class SyntaxError extends Error
$alternatives = [];
foreach ($items as $item) {
$lev = levenshtein($name, $item);
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
$alternatives[$item] = $lev;
}
}
@ -41,6 +41,6 @@ class SyntaxError extends Error
asort($alternatives);
$this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
$this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
}
}

View File

@ -12,20 +12,21 @@
namespace Twig;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary;
@ -40,23 +41,22 @@ use Twig\Node\Node;
* @see https://en.wikipedia.org/wiki/Operator-precedence_parser
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ExpressionParser
{
public const OPERATOR_LEFT = 1;
public const OPERATOR_RIGHT = 2;
private $parser;
private $env;
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
private $binaryOperators;
private $readyNodes = [];
public function __construct(Parser $parser, Environment $env)
{
$this->parser = $parser;
$this->env = $env;
public function __construct(
private Parser $parser,
private Environment $env,
) {
$this->unaryOperators = $env->getUnaryOperators();
$this->binaryOperators = $env->getBinaryOperators();
}
@ -80,7 +80,7 @@ class ExpressionParser
} elseif (isset($op['callable'])) {
$expr = $op['callable']($this->parser, $expr);
} else {
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true);
$class = $op['class'];
$expr = new $class($expr, $expr1, $token->getLine());
}
@ -103,52 +103,52 @@ class ExpressionParser
$stream = $this->parser->getStream();
// short array syntax (one argument, no parentheses)?
if ($stream->look(1)->test(/* Token::ARROW_TYPE */ 12)) {
if ($stream->look(1)->test(Token::ARROW_TYPE)) {
$line = $stream->getCurrent()->getLine();
$token = $stream->expect(/* Token::NAME_TYPE */ 5);
$token = $stream->expect(Token::NAME_TYPE);
$names = [new AssignNameExpression($token->getValue(), $token->getLine())];
$stream->expect(/* Token::ARROW_TYPE */ 12);
$stream->expect(Token::ARROW_TYPE);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
}
// first, determine if we are parsing an arrow function by finding => (long form)
$i = 0;
if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
return null;
}
++$i;
while (true) {
// variable name
++$i;
if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
break;
}
++$i;
}
if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
return null;
}
++$i;
if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) {
if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
return null;
}
// yes, let's parse it properly
$token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(');
$token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
$line = $token->getLine();
$names = [];
while (true) {
$token = $stream->expect(/* Token::NAME_TYPE */ 5);
$token = $stream->expect(Token::NAME_TYPE);
$names[] = new AssignNameExpression($token->getValue(), $token->getLine());
if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')');
$stream->expect(/* Token::ARROW_TYPE */ 12);
$stream->expect(Token::PUNCTUATION_TYPE, ')');
$stream->expect(Token::ARROW_TYPE);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
}
@ -164,10 +164,10 @@ class ExpressionParser
$class = $operator['class'];
return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
} elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
} elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) {
$this->parser->getStream()->next();
$expr = $this->parseExpression();
$this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'An opened parenthesis is not properly closed');
$this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
return $this->parsePostfixExpression($expr);
}
@ -177,15 +177,18 @@ class ExpressionParser
private function parseConditionalExpression($expr): AbstractExpression
{
while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) {
if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$expr2 = $this->parseExpression();
if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
// Ternary operator (expr ? expr2 : expr3)
$expr3 = $this->parseExpression();
} else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
}
} else {
// Ternary without then (expr ?: expr3)
$expr2 = $expr;
$expr3 = $this->parseExpression();
}
@ -198,19 +201,19 @@ class ExpressionParser
private function isUnary(Token $token): bool
{
return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]);
return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
}
private function isBinary(Token $token): bool
{
return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]);
return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
}
public function parsePrimaryExpression()
{
$token = $this->parser->getCurrentToken();
switch ($token->getType()) {
case /* Token::NAME_TYPE */ 5:
case Token::NAME_TYPE:
$this->parser->getStream()->next();
switch ($token->getValue()) {
case 'true':
@ -239,17 +242,17 @@ class ExpressionParser
}
break;
case /* Token::NUMBER_TYPE */ 6:
case Token::NUMBER_TYPE:
$this->parser->getStream()->next();
$node = new ConstantExpression($token->getValue(), $token->getLine());
break;
case /* Token::STRING_TYPE */ 7:
case /* Token::INTERPOLATION_START_TYPE */ 10:
case Token::STRING_TYPE:
case Token::INTERPOLATION_START_TYPE:
$node = $this->parseStringExpression();
break;
case /* Token::OPERATOR_TYPE */ 8:
case Token::OPERATOR_TYPE:
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
// in this context, string operators are variable names
$this->parser->getStream()->next();
@ -260,7 +263,7 @@ class ExpressionParser
if (isset($this->unaryOperators[$token->getValue()])) {
$class = $this->unaryOperators[$token->getValue()]['class'];
if (!\in_array($class, [NegUnary::class, PosUnary::class])) {
throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
}
$this->parser->getStream()->next();
@ -272,14 +275,14 @@ class ExpressionParser
// no break
default:
if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) {
$node = $this->parseArrayExpression();
} elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) {
$node = $this->parseHashExpression();
} elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
throw new SyntaxError(sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
$node = $this->parseSequenceExpression();
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseMappingExpression();
} elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
} else {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
}
}
@ -294,12 +297,12 @@ class ExpressionParser
// a string cannot be followed by another string in a single expression
$nextCanBeString = true;
while (true) {
if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) {
if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
$nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
$nextCanBeString = false;
} elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) {
} elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
$nodes[] = $this->parseExpression();
$stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11);
$stream->expect(Token::INTERPOLATION_END_TYPE);
$nextCanBeString = true;
} else {
break;
@ -314,56 +317,91 @@ class ExpressionParser
return $expr;
}
/**
* @deprecated since 3.11, use parseSequenceExpression() instead
*/
public function parseArrayExpression()
{
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__);
return $this->parseSequenceExpression();
}
public function parseSequenceExpression()
{
$stream = $this->parser->getStream();
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected');
$stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
if (!$first) {
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma');
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
// trailing ,?
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
break;
}
}
$first = false;
$node->addElement($this->parseExpression());
if ($stream->test(Token::SPREAD_TYPE)) {
$stream->next();
$expr = $this->parseExpression();
$expr->setAttribute('spread', true);
$node->addElement($expr);
} else {
$node->addElement($this->parseExpression());
}
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed');
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
return $node;
}
/**
* @deprecated since 3.11, use parseMappingExpression() instead
*/
public function parseHashExpression()
{
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__);
return $this->parseMappingExpression();
}
public function parseMappingExpression()
{
$stream = $this->parser->getStream();
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected');
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
if (!$first) {
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma');
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
// trailing ,?
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
break;
}
}
$first = false;
// a hash key can be:
if ($stream->test(Token::SPREAD_TYPE)) {
$stream->next();
$value = $this->parseExpression();
$value->setAttribute('spread', true);
$node->addElement($value);
continue;
}
// a mapping key can be:
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) {
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$key = new ConstantExpression($token->getValue(), $token->getLine());
// {a} is a shortcut for {a:a}
@ -372,22 +410,22 @@ class ExpressionParser
$node->addElement($value, $key);
continue;
}
} elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) {
} elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
$key = new ConstantExpression($token->getValue(), $token->getLine());
} elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
} elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$key = $this->parseExpression();
} else {
$current = $stream->getCurrent();
throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)');
$value = $this->parseExpression();
$node->addElement($value, $key);
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
return $node;
}
@ -396,7 +434,7 @@ class ExpressionParser
{
while (true) {
$token = $this->parser->getCurrentToken();
if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) {
if (Token::PUNCTUATION_TYPE == $token->getType()) {
if ('.' == $token->getValue() || '[' == $token->getValue()) {
$node = $this->parseSubscriptExpression($node);
} elseif ('|' == $token->getValue()) {
@ -414,50 +452,37 @@ class ExpressionParser
public function getFunctionNode($name, $line)
{
switch ($name) {
case 'parent':
$this->parseArguments();
if (!\count($this->parser->getBlockStack())) {
throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
}
if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
$arguments = new ArrayExpression([], $line);
foreach ($this->parseArguments() as $n) {
$arguments->addElement($n);
}
if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
}
$node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
$node->setAttribute('safe', true);
return new ParentExpression($this->parser->peekBlockStack(), $line);
case 'block':
$args = $this->parseArguments();
if (\count($args) < 1) {
throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
}
return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line);
case 'attribute':
$args = $this->parseArguments();
if (\count($args) < 2) {
throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
}
return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line);
default:
if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
$arguments = new ArrayExpression([], $line);
foreach ($this->parseArguments() as $n) {
$arguments->addElement($n);
}
$node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
$node->setAttribute('safe', true);
return $node;
}
$args = $this->parseArguments(true);
$class = $this->getFunctionNodeClass($name, $line);
return new $class($name, $args, $line);
return $node;
}
$args = $this->parseArguments(true);
$function = $this->getFunction($name, $line);
if ($function->getParserCallable()) {
$fakeNode = new Node(lineno: $line);
$fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
}
if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($ready ? $function : $function->getName(), $args, $line);
}
public function parseSubscriptExpression($node)
@ -470,29 +495,25 @@ class ExpressionParser
if ('.' == $token->getValue()) {
$token = $stream->next();
if (
/* Token::NAME_TYPE */ 5 == $token->getType()
Token::NAME_TYPE == $token->getType()
||
/* Token::NUMBER_TYPE */ 6 == $token->getType()
Token::NUMBER_TYPE == $token->getType()
||
(/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
(Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
) {
$arg = new ConstantExpression($token->getValue(), $lineno);
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Template::METHOD_CALL;
foreach ($this->parseArguments() as $n) {
$arguments->addElement($n);
}
}
} else {
throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
}
if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
if (!$arg instanceof ConstantExpression) {
throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
}
$name = $arg->getAttribute('value');
$node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
@ -505,34 +526,34 @@ class ExpressionParser
// slice?
$slice = false;
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
$arg = new ConstantExpression(0, $token->getLine());
} else {
$arg = $this->parseExpression();
}
if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
}
if ($slice) {
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
$length = new ConstantExpression(null, $token->getLine());
} else {
$length = $this->parseExpression();
}
$class = $this->getFilterNodeClass('slice', $token->getLine());
$filter = $this->getFilter('slice', $token->getLine());
$arguments = new Node([$arg, $length]);
$filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
$filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
$stream->expect(Token::PUNCTUATION_TYPE, ']');
return $filter;
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
$stream->expect(Token::PUNCTUATION_TYPE, ']');
}
return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
@ -545,23 +566,35 @@ class ExpressionParser
return $this->parseFilterExpressionRaw($node);
}
public function parseFilterExpressionRaw($node, $tag = null)
public function parseFilterExpressionRaw($node)
{
while (true) {
$token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5);
if (\func_num_args() > 1) {
trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
}
$name = new ConstantExpression($token->getValue(), $token->getLine());
if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
while (true) {
$token = $this->parser->getStream()->expect(Token::NAME_TYPE);
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = new Node();
} else {
$arguments = $this->parseArguments(true, false, true);
}
$class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
$filter = $this->getFilter($token->getValue(), $token->getLine());
$node = new $class($node, $name, $arguments, $token->getLine(), $tag);
$ready = true;
if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) {
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
$node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine());
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) {
break;
}
@ -575,7 +608,7 @@ class ExpressionParser
* Parses arguments.
*
* @param bool $namedArguments Whether to allow named arguments or not
* @param bool $definition Whether we are parsing arguments for a function definition
* @param bool $definition Whether we are parsing arguments for a function (or macro) definition
*
* @return Node
*
@ -586,28 +619,28 @@ class ExpressionParser
$args = [];
$stream = $this->parser->getStream();
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
$stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
if (!empty($args)) {
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma');
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
// if the comma above was a trailing comma, early exit the argument parse loop
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {
break;
}
}
if ($definition) {
$token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name');
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
$value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else {
$value = $this->parseExpression(0, $allowArrow);
}
$name = null;
if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) {
if (!$value instanceof NameExpression) {
throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
}
$name = $value->getAttribute('name');
@ -615,7 +648,7 @@ class ExpressionParser
$value = $this->parsePrimaryExpression();
if (!$this->checkConstantExpression($value)) {
throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext());
throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());
}
} else {
$value = $this->parseExpression(0, $allowArrow);
@ -626,6 +659,7 @@ class ExpressionParser
if (null === $name) {
$name = $value->getAttribute('name');
$value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
$value->setAttribute('is_implicit', true);
}
$args[$name] = $value;
} else {
@ -636,7 +670,7 @@ class ExpressionParser
}
}
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis');
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
return new Node($args);
}
@ -647,19 +681,19 @@ class ExpressionParser
$targets = [];
while (true) {
$token = $this->parser->getCurrentToken();
if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
// in this context, string operators are variable names
$this->parser->getStream()->next();
} else {
$stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to');
$stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
}
$value = $token->getValue();
if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
}
$targets[] = new AssignNameExpression($value, $token->getLine());
if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
@ -672,7 +706,7 @@ class ExpressionParser
$targets = [];
while (true) {
$targets[] = $this->parseExpression();
if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
@ -688,121 +722,115 @@ class ExpressionParser
private function parseTestExpression(Node $node): TestExpression
{
$stream = $this->parser->getStream();
list($name, $test) = $this->getTest($node->getTemplateLine());
$test = $this->getTest($node->getTemplateLine());
$class = $this->getTestNodeClass($test);
$arguments = null;
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = $this->parseArguments(true);
} elseif ($test->hasOneMandatoryArgument()) {
$arguments = new Node([0 => $this->parsePrimaryExpression()]);
}
if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
$node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
$node->setAttribute('safe', true);
}
return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
}
private function getTest(int $line): array
{
$stream = $this->parser->getStream();
$name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
if ($test = $this->env->getTest($name)) {
return [$name, $test];
$ready = $test instanceof TwigTest;
if (!isset($this->readyNodes[$class = $test->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if ($stream->test(/* Token::NAME_TYPE */ 5)) {
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine());
}
private function getTest(int $line): TwigTest
{
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
if ($stream->test(Token::NAME_TYPE)) {
// try 2-words tests
$name = $name.' '.$this->parser->getCurrentToken()->getValue();
if ($test = $this->env->getTest($name)) {
$stream->next();
return [$name, $test];
}
} else {
$test = $this->env->getTest($name);
}
$e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getTests()));
if (!$test) {
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getTests()));
throw $e;
}
throw $e;
}
private function getTestNodeClass(TwigTest $test): string
{
if ($test->isDeprecated()) {
$stream = $this->parser->getStream();
$message = sprintf('Twig Test "%s" is deprecated', $test->getName());
$message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
if ($test->getDeprecatedVersion()) {
$message .= sprintf(' since version %s', $test->getDeprecatedVersion());
}
if ($test->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $test->getAlternative());
$message .= \sprintf('. Use "%s" instead', $test->getAlternative());
}
$src = $stream->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
@trigger_error($message, \E_USER_DEPRECATED);
trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
}
return $test->getNodeClass();
return $test;
}
private function getFunctionNodeClass(string $name, int $line): string
private function getFunction(string $name, int $line): TwigFunction
{
if (!$function = $this->env->getFunction($name)) {
$e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getFunctions()));
throw $e;
}
if ($function->isDeprecated()) {
$message = sprintf('Twig Function "%s" is deprecated', $function->getName());
if ($function->getDeprecatedVersion()) {
$message .= sprintf(' since version %s', $function->getDeprecatedVersion());
}
$message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
if ($function->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $function->getAlternative());
$message .= \sprintf('. Use "%s" instead', $function->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
@trigger_error($message, \E_USER_DEPRECATED);
trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
}
return $function->getNodeClass();
return $function;
}
private function getFilterNodeClass(string $name, int $line): string
private function getFilter(string $name, int $line): TwigFilter
{
if (!$filter = $this->env->getFilter($name)) {
$e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getFilters()));
throw $e;
}
if ($filter->isDeprecated()) {
$message = sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if ($filter->getDeprecatedVersion()) {
$message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
}
$message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if ($filter->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $filter->getAlternative());
$message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
@trigger_error($message, \E_USER_DEPRECATED);
trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
}
return $filter->getNodeClass();
return $filter;
}
// checks that the node only contains "constant" elements

View File

@ -40,6 +40,6 @@ abstract class AbstractExtension implements ExtensionInterface
public function getOperators()
{
return [];
return [[], []];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,11 @@
* file that was distributed with this source code.
*/
namespace Twig\Extension {
namespace Twig\Extension;
use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
use Twig\TwigFunction;
final class DebugExtension extends AbstractExtension
@ -18,47 +22,41 @@ final class DebugExtension extends AbstractExtension
{
// dump is safe if var_dump is overridden by xdebug
$isDumpOutputHtmlSafe = \extension_loaded('xdebug')
// false means that it was not set (and the default is on) or it explicitly enabled
&& (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump'))
// false means that it was not set (and the default is on) or it explicitly enabled
// xdebug.overload_var_dump produces HTML only when html_errors is also enabled
&& (false === ini_get('html_errors') || ini_get('html_errors'))
// Xdebug overloads var_dump in develop mode when html_errors is enabled
&& str_contains(\ini_get('xdebug.mode'), 'develop')
&& (false === \ini_get('html_errors') || \ini_get('html_errors'))
|| 'cli' === \PHP_SAPI
;
return [
new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
];
}
}
}
namespace {
use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
function twig_var_dump(Environment $env, $context, ...$vars)
{
if (!$env->isDebug()) {
return;
}
ob_start();
if (!$vars) {
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
$vars[$key] = $value;
}
/**
* @internal
*/
public static function dump(Environment $env, $context, ...$vars)
{
if (!$env->isDebug()) {
return;
}
var_dump($vars);
} else {
var_dump(...$vars);
}
ob_start();
return ob_get_clean();
}
if (!$vars) {
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
$vars[$key] = $value;
}
}
var_dump($vars);
} else {
var_dump(...$vars);
}
return ob_get_clean();
}
}

View File

@ -9,22 +9,24 @@
* file that was distributed with this source code.
*/
namespace Twig\Extension {
namespace Twig\Extension;
use Twig\Environment;
use Twig\FileExtensionEscapingStrategy;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Filter\RawFilter;
use Twig\Node\Node;
use Twig\NodeVisitor\EscaperNodeVisitor;
use Twig\Runtime\EscaperRuntime;
use Twig\TokenParser\AutoEscapeTokenParser;
use Twig\TwigFilter;
final class EscaperExtension extends AbstractExtension
{
private $defaultStrategy;
private $environment;
private $escapers = [];
/** @internal */
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
private $escaper;
private $defaultStrategy;
/**
* @param string|false|callable $defaultStrategy An escaping strategy
@ -49,19 +51,43 @@ final class EscaperExtension extends AbstractExtension
public function getFilters(): array
{
return [
new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
];
}
/**
* @deprecated since Twig 3.10
*/
public function setEnvironment(Environment $environment): void
{
$triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
if ($triggerDeprecation) {
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
}
$this->environment = $environment;
$this->escaper = $environment->getRuntime(EscaperRuntime::class);
}
/**
* @deprecated since Twig 3.10
*/
public function setEscaperRuntime(EscaperRuntime $escaper)
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
$this->escaper = $escaper;
}
/**
* Sets the default strategy to use when not defined by the user.
*
* The strategy can be a valid PHP callback that takes the template
* name as an argument and returns the strategy to use.
*
* @param string|false|callable $defaultStrategy An escaping strategy
* @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy
*/
public function setDefaultStrategy($defaultStrategy): void
{
@ -93,324 +119,82 @@ final class EscaperExtension extends AbstractExtension
/**
* Defines a new escaper to be used via the escape filter.
*
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable $callable A valid PHP callable
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable(Environment, string, string): string $callable A valid PHP callable
*
* @deprecated since Twig 3.10
*/
public function setEscaper($strategy, callable $callable)
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__);
if (!isset($this->environment)) {
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
$this->escapers[$strategy] = $callable;
$callable = function ($string, $charset) use ($callable) {
return $callable($this->environment, $string, $charset);
};
$this->escaper->setEscaper($strategy, $callable);
}
/**
* Gets all defined escapers.
*
* @return callable[] An array of escapers
* @return array<string, callable(Environment, string, string): string> An array of escapers
*
* @deprecated since Twig 3.10
*/
public function getEscapers()
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__);
return $this->escapers;
}
/**
* @deprecated since Twig 3.10
*/
public function setSafeClasses(array $safeClasses = [])
{
$this->safeClasses = [];
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $strategies);
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__);
if (!isset($this->escaper)) {
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
$this->escaper->setSafeClasses($safeClasses);
}
/**
* @deprecated since Twig 3.10
*/
public function addSafeClass(string $class, array $strategies)
{
$class = ltrim($class, '\\');
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__);
foreach ($strategies as $strategy) {
$this->safeLookup[$strategy][$class] = true;
}
}
}
}
namespace {
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\EscaperExtension;
use Twig\Markup;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
/**
* Marks a variable as being safe.
*
* @param string $string A PHP variable
*/
function twig_raw_filter($string)
{
return $string;
}
/**
* Escapes a string.
*
* @param mixed $string The value to be escaped
* @param string $strategy The escaping strategy
* @param string $charset The charset
* @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
*
* @return string
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
if ($autoescape && $string instanceof Markup) {
return $string;
}
if (!\is_string($string)) {
if (\is_object($string) && method_exists($string, '__toString')) {
if ($autoescape) {
$c = \get_class($string);
$ext = $env->getExtension(EscaperExtension::class);
if (!isset($ext->safeClasses[$c])) {
$ext->safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($ext->safeClasses[$class])) {
$ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
foreach ($ext->safeClasses[$class] as $s) {
$ext->safeLookup[$s][$c] = true;
}
}
}
}
if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
return (string) $string;
}
}
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
return $string;
}
}
if ('' === $string) {
return '';
}
if (null === $charset) {
$charset = $env->getCharset();
}
switch ($strategy) {
case 'html':
// see https://www.php.net/htmlspecialchars
// Using a static variable to avoid initializing the array
// each time the function is called. Moving the declaration on the
// top of the function slow downs other escaping strategies.
static $htmlspecialcharsCharsets = [
'ISO-8859-1' => true, 'ISO8859-1' => true,
'ISO-8859-15' => true, 'ISO8859-15' => true,
'utf-8' => true, 'UTF-8' => true,
'CP866' => true, 'IBM866' => true, '866' => true,
'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
'1251' => true,
'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
'BIG5' => true, '950' => true,
'GB2312' => true, '936' => true,
'BIG5-HKSCS' => true,
'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
'EUC-JP' => true, 'EUCJP' => true,
'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
];
if (isset($htmlspecialcharsCharsets[$charset])) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
// cache the lowercase variant for future iterations
$htmlspecialcharsCharsets[$charset] = true;
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
$string = twig_convert_encoding($string, 'UTF-8', $charset);
$string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
return iconv('UTF-8', $charset, $string);
case 'js':
// escape all non-alphanumeric characters
// into their \x or \uHHHH representations
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
$char = $matches[0];
/*
* A few characters have short escape sequences in JSON and JavaScript.
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
static $shortMap = [
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
];
if (isset($shortMap[$char])) {
return $shortMap[$char];
}
$codepoint = mb_ord($char, 'UTF-8');
if (0x10000 > $codepoint) {
return sprintf('\u%04X', $codepoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
$u = $codepoint - 0x10000;
$high = 0xD800 | ($u >> 10);
$low = 0xDC00 | ($u & 0x3FF);
return sprintf('\u%04X\u%04X', $high, $low);
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'css':
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
$char = $matches[0];
return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'html_attr':
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
/**
* This function is adapted from code coming from Zend Framework.
*
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://framework.zend.com/license/new-bsd New BSD License
*/
$chr = $matches[0];
$ord = \ord($chr);
/*
* The following replaces characters undefined in HTML with the
* hex entity for the Unicode replacement character.
*/
if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
return '&#xFFFD;';
}
/*
* Check if the current character to escape has a name entity we should
* replace it with while grabbing the hex value of the character.
*/
if (1 === \strlen($chr)) {
/*
* While HTML supports far more named entities, the lowest common denominator
* has become HTML5's XML Serialisation which is restricted to the those named
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*/
static $entityMap = [
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
];
if (isset($entityMap[$ord])) {
return $entityMap[$ord];
}
return sprintf('&#x%02X;', $ord);
}
/*
* Per OWASP recommendations, we'll use hex entities for any other
* characters where a named entity does not exist.
*/
return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'url':
return rawurlencode($string);
default:
$escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
if (array_key_exists($strategy, $escapers)) {
return $escapers[$strategy]($env, $string, $charset);
}
$validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
}
}
/**
* @internal
*/
function twig_escape_filter_is_safe(Node $filterArgs)
{
foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')];
if (!isset($this->escaper)) {
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
return [];
$this->escaper->addSafeClass($class, $strategies);
}
return ['html'];
}
/**
* @internal
*/
public static function escapeFilterIsSafe(Node $filterArgs)
{
foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')];
}
return [];
}
return ['html'];
}
}

View File

@ -11,6 +11,8 @@
namespace Twig\Extension;
use Twig\ExpressionParser;
use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
@ -63,6 +65,11 @@ interface ExtensionInterface
* Returns a list of operators to add to the existing list.
*
* @return array<array> First array of unary operators, second array of binary operators
*
* @psalm-return array{
* array<string, array{precedence: int, class: class-string<AbstractExpression>}>,
* array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
* }
*/
public function getOperators();
}

View File

@ -12,14 +12,14 @@
namespace Twig\Extension;
/**
* Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method.
*
* Explicitly implement this interface if you really need to implement the
* deprecated getGlobals() method in your extensions.
* Allows Twig extensions to add globals to the context.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface GlobalsInterface
{
/**
* @return array<string, mixed>
*/
public function getGlobals(): array;
}

View File

@ -15,11 +15,9 @@ use Twig\NodeVisitor\OptimizerNodeVisitor;
final class OptimizerExtension extends AbstractExtension
{
private $optimizers;
public function __construct(int $optimizers = -1)
{
$this->optimizers = $optimizers;
public function __construct(
private int $optimizers = -1,
) {
}
public function getNodeVisitors(): array

View File

@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SourcePolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;
@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension
private $sandboxedGlobally;
private $sandboxed;
private $policy;
private $sourcePolicy;
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null)
{
$this->policy = $policy;
$this->sandboxedGlobally = $sandboxed;
$this->sourcePolicy = $sourcePolicy;
}
public function getTokenParsers(): array
@ -50,9 +53,9 @@ final class SandboxExtension extends AbstractExtension
$this->sandboxed = false;
}
public function isSandboxed(): bool
public function isSandboxed(?Source $source = null): bool
{
return $this->sandboxedGlobally || $this->sandboxed;
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
}
public function isSandboxedGlobally(): bool
@ -60,6 +63,15 @@ final class SandboxExtension extends AbstractExtension
return $this->sandboxedGlobally;
}
private function isSourceSandboxed(?Source $source): bool
{
if (null === $source || null === $this->sourcePolicy) {
return false;
}
return $this->sourcePolicy->enableSandbox($source);
}
public function setSecurityPolicy(SecurityPolicyInterface $policy)
{
$this->policy = $policy;
@ -70,16 +82,16 @@ final class SandboxExtension extends AbstractExtension
return $this->policy;
}
public function checkSecurity($tags, $filters, $functions): void
public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
$this->policy->checkSecurity($tags, $filters, $functions);
}
}
public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void
public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
@ -91,9 +103,9 @@ final class SandboxExtension extends AbstractExtension
}
}
public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void
public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
@ -105,9 +117,9 @@ final class SandboxExtension extends AbstractExtension
}
}
public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
{
if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
if ($this->isSandboxed($source) && $obj instanceof \Stringable) {
try {
$this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {

View File

@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension
public function addFunction(TwigFunction $function): void
{
if (isset($this->functions[$function->getName()])) {
throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName()));
throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName()));
}
$this->functions[$function->getName()] = $function;
@ -49,7 +49,7 @@ final class StagingExtension extends AbstractExtension
public function addFilter(TwigFilter $filter): void
{
if (isset($this->filters[$filter->getName()])) {
throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName()));
throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName()));
}
$this->filters[$filter->getName()] = $filter;
@ -73,7 +73,7 @@ final class StagingExtension extends AbstractExtension
public function addTokenParser(TokenParserInterface $parser): void
{
if (isset($this->tokenParsers[$parser->getTag()])) {
throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag()));
throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag()));
}
$this->tokenParsers[$parser->getTag()] = $parser;
@ -87,7 +87,7 @@ final class StagingExtension extends AbstractExtension
public function addTest(TwigTest $test): void
{
if (isset($this->tests[$test->getName()])) {
throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName()));
throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName()));
}
$this->tests[$test->getName()] = $test;

View File

@ -9,7 +9,10 @@
* file that was distributed with this source code.
*/
namespace Twig\Extension {
namespace Twig\Extension;
use Twig\Environment;
use Twig\TemplateWrapper;
use Twig\TwigFunction;
final class StringLoaderExtension extends AbstractExtension
@ -17,26 +20,21 @@ final class StringLoaderExtension extends AbstractExtension
public function getFunctions(): array
{
return [
new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]),
new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]),
];
}
}
}
namespace {
use Twig\Environment;
use Twig\TemplateWrapper;
/**
* Loads a template from a string.
*
* {{ include(template_from_string("Hello {{ name }}")) }}
*
* @param string $template A template as a string or object implementing __toString()
* @param string $name An optional name of the template to be used in error messages
*/
function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper
{
return $env->createTemplate((string) $template, $name);
}
/**
* Loads a template from a string.
*
* {{ include(template_from_string("Hello {{ name }}")) }}
*
* @param string|null $name An optional name of the template to be used in error messages
*
* @internal
*/
public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper
{
return $env->createTemplate((string) $template, $name);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Extension;
use Twig\NodeVisitor\YieldNotReadyNodeVisitor;
/**
* @internal to be removed in Twig 4
*/
final class YieldNotReadyExtension extends AbstractExtension
{
public function __construct(
private bool $useYield,
) {
}
public function getNodeVisitors(): array
{
return [new YieldNotReadyNodeVisitor($this->useYield)];
}
}

View File

@ -15,6 +15,9 @@ use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\StagingExtension;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
@ -31,11 +34,23 @@ final class ExtensionSet
private $staging;
private $parsers;
private $visitors;
/** @var array<string, TwigFilter> */
private $filters;
/** @var array<string, TwigFilter> */
private $dynamicFilters;
/** @var array<string, TwigTest> */
private $tests;
/** @var array<string, TwigTest> */
private $dynamicTests;
/** @var array<string, TwigFunction> */
private $functions;
/** @var array<string, TwigFunction> */
private $dynamicFunctions;
/** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
private $binaryOperators;
/** @var array<string, mixed> */
private $globals;
private $functionCallbacks = [];
private $filterCallbacks = [];
@ -62,7 +77,7 @@ final class ExtensionSet
$class = ltrim($class, '\\');
if (!isset($this->extensions[$class])) {
throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class));
throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class));
}
return $this->extensions[$class];
@ -117,11 +132,11 @@ final class ExtensionSet
$class = \get_class($extension);
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
}
if (isset($this->extensions[$class])) {
throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class));
throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class));
}
$this->extensions[$class] = $extension;
@ -130,7 +145,7 @@ final class ExtensionSet
public function addFunction(TwigFunction $function): void
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
}
$this->staging->addFunction($function);
@ -158,14 +173,11 @@ final class ExtensionSet
return $this->functions[$name];
}
foreach ($this->functions as $pattern => $function) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
foreach ($this->dynamicFunctions as $pattern => $function) {
if (preg_match($pattern, $name, $matches)) {
array_shift($matches);
$function->setArguments($matches);
return $function;
return $function->withDynamicArguments($name, $function->getName(), $matches);
}
}
@ -186,7 +198,7 @@ final class ExtensionSet
public function addFilter(TwigFilter $filter): void
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
}
$this->staging->addFilter($filter);
@ -214,14 +226,11 @@ final class ExtensionSet
return $this->filters[$name];
}
foreach ($this->filters as $pattern => $filter) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
foreach ($this->dynamicFilters as $pattern => $filter) {
if (preg_match($pattern, $name, $matches)) {
array_shift($matches);
$filter->setArguments($matches);
return $filter;
return $filter->withDynamicArguments($name, $filter->getName(), $matches);
}
}
@ -305,6 +314,9 @@ final class ExtensionSet
$this->parserCallbacks[] = $callable;
}
/**
* @return array<string, mixed>
*/
public function getGlobals(): array
{
if (null !== $this->globals) {
@ -317,12 +329,7 @@ final class ExtensionSet
continue;
}
$extGlobals = $extension->getGlobals();
if (!\is_array($extGlobals)) {
throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension)));
}
$globals = array_merge($globals, $extGlobals);
$globals = array_merge($globals, $extension->getGlobals());
}
if ($this->initialized) {
@ -332,10 +339,15 @@ final class ExtensionSet
return $globals;
}
public function resetGlobals(): void
{
$this->globals = null;
}
public function addTest(TwigTest $test): void
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
}
$this->staging->addTest($test);
@ -363,22 +375,20 @@ final class ExtensionSet
return $this->tests[$name];
}
foreach ($this->tests as $pattern => $test) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
foreach ($this->dynamicTests as $pattern => $test) {
if (preg_match($pattern, $name, $matches)) {
array_shift($matches);
if ($count) {
if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($matches);
$test->setArguments($matches);
return $test;
}
return $test->withDynamicArguments($name, $test->getName(), $matches);
}
}
return null;
}
/**
* @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
*/
public function getUnaryOperators(): array
{
if (!$this->initialized) {
@ -388,6 +398,9 @@ final class ExtensionSet
return $this->unaryOperators;
}
/**
* @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
{
if (!$this->initialized) {
@ -403,6 +416,9 @@ final class ExtensionSet
$this->filters = [];
$this->functions = [];
$this->tests = [];
$this->dynamicFilters = [];
$this->dynamicFunctions = [];
$this->dynamicTests = [];
$this->visitors = [];
$this->unaryOperators = [];
$this->binaryOperators = [];
@ -419,17 +435,26 @@ final class ExtensionSet
{
// filters
foreach ($extension->getFilters() as $filter) {
$this->filters[$filter->getName()] = $filter;
$this->filters[$name = $filter->getName()] = $filter;
if (str_contains($name, '*')) {
$this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter;
}
}
// functions
foreach ($extension->getFunctions() as $function) {
$this->functions[$function->getName()] = $function;
$this->functions[$name = $function->getName()] = $function;
if (str_contains($name, '*')) {
$this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function;
}
}
// tests
foreach ($extension->getTests() as $test) {
$this->tests[$test->getName()] = $test;
$this->tests[$name = $test->getName()] = $test;
if (str_contains($name, '*')) {
$this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test;
}
}
// token parsers
@ -449,11 +474,11 @@ final class ExtensionSet
// operators
if ($operators = $extension->getOperators()) {
if (!\is_array($operators)) {
throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
}
if (2 !== \count($operators)) {
throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
}
$this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);

View File

@ -37,7 +37,7 @@ class FileExtensionEscapingStrategy
return 'html'; // return html for directories
}
if ('.twig' === substr($name, -5)) {
if (str_ends_with($name, '.twig')) {
$name = substr($name, 0, -5);
}

View File

@ -19,6 +19,8 @@ use Twig\Error\SyntaxError;
*/
class Lexer
{
private $isInitialized = false;
private $tokens;
private $code;
private $cursor;
@ -48,6 +50,14 @@ class Lexer
public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
public const PUNCTUATION = '()[]{}?:.,|';
private const SPECIAL_CHARS = [
'f' => "\f",
'n' => "\n",
'r' => "\r",
't' => "\t",
'v' => "\v",
];
public function __construct(Environment $env, array $options = [])
{
$this->env = $env;
@ -61,6 +71,13 @@ class Lexer
'whitespace_line_chars' => ' \t\0\x0B',
'interpolation' => ['#{', '}'],
], $options);
}
private function initialize()
{
if ($this->isInitialized) {
return;
}
// when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default
$this->regexes = [
@ -149,10 +166,14 @@ class Lexer
'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A',
'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A',
];
$this->isInitialized = true;
}
public function tokenize(Source $source): TokenStream
{
$this->initialize();
$this->source = $source;
$this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
$this->cursor = 0;
@ -194,11 +215,11 @@ class Lexer
}
}
$this->pushToken(/* Token::EOF_TYPE */ -1);
$this->pushToken(Token::EOF_TYPE);
if (!empty($this->brackets)) {
list($expect, $lineno) = array_pop($this->brackets);
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
[$expect, $lineno] = array_pop($this->brackets);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
return new TokenStream($this->tokens, $this->source);
@ -208,7 +229,7 @@ class Lexer
{
// if no matches are left we return the rest of the template as simple text token
if ($this->position == \count($this->positions[0]) - 1) {
$this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor));
$this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor));
$this->cursor = $this->end;
return;
@ -237,7 +258,7 @@ class Lexer
$text = rtrim($text, " \t\0\x0B");
}
}
$this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
$this->pushToken(Token::TEXT_TYPE, $text);
$this->moveCursor($textContent.$position[0]);
switch ($this->positions[1][$this->position][0]) {
@ -255,14 +276,14 @@ class Lexer
$this->moveCursor($match[0]);
$this->lineno = (int) $match[1];
} else {
$this->pushToken(/* Token::BLOCK_START_TYPE */ 1);
$this->pushToken(Token::BLOCK_START_TYPE);
$this->pushState(self::STATE_BLOCK);
$this->currentVarBlockLine = $this->lineno;
}
break;
case $this->options['tag_variable'][0]:
$this->pushToken(/* Token::VAR_START_TYPE */ 2);
$this->pushToken(Token::VAR_START_TYPE);
$this->pushState(self::STATE_VAR);
$this->currentVarBlockLine = $this->lineno;
break;
@ -272,7 +293,7 @@ class Lexer
private function lexBlock(): void
{
if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::BLOCK_END_TYPE */ 3);
$this->pushToken(Token::BLOCK_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@ -283,7 +304,7 @@ class Lexer
private function lexVar(): void
{
if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::VAR_END_TYPE */ 4);
$this->pushToken(Token::VAR_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@ -298,23 +319,28 @@ class Lexer
$this->moveCursor($match[0]);
if ($this->cursor >= $this->end) {
throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
}
}
// spread operator
if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
$this->pushToken(Token::SPREAD_TYPE, '...');
$this->moveCursor('...');
}
// arrow function
if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) {
$this->pushToken(Token::ARROW_TYPE, '=>');
$this->moveCursor('=>');
}
// operators
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0]));
$this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0]));
$this->moveCursor($match[0]);
}
// names
elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]);
$this->pushToken(Token::NAME_TYPE, $match[0]);
$this->moveCursor($match[0]);
}
// numbers
@ -323,33 +349,33 @@ class Lexer
if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
$number = (int) $match[0]; // integers lower than the maximum
}
$this->pushToken(/* Token::NUMBER_TYPE */ 6, $number);
$this->pushToken(Token::NUMBER_TYPE, $number);
$this->moveCursor($match[0]);
}
// punctuation
elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) {
elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
// opening bracket
if (false !== strpos('([{', $this->code[$this->cursor])) {
if (str_contains('([{', $this->code[$this->cursor])) {
$this->brackets[] = [$this->code[$this->cursor], $this->lineno];
}
// closing bracket
elseif (false !== strpos(')]}', $this->code[$this->cursor])) {
elseif (str_contains(')]}', $this->code[$this->cursor])) {
if (empty($this->brackets)) {
throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
list($expect, $lineno) = array_pop($this->brackets);
[$expect, $lineno] = array_pop($this->brackets);
if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
}
$this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]);
$this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]);
++$this->cursor;
}
// strings
elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1)));
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1)));
$this->moveCursor($match[0]);
}
// opening double quoted string
@ -360,10 +386,67 @@ class Lexer
}
// unlexable
else {
throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
}
private function stripcslashes(string $str, string $quoteType): string
{
$result = '';
$length = \strlen($str);
$i = 0;
while ($i < $length) {
if (false === $pos = strpos($str, '\\', $i)) {
$result .= substr($str, $i);
break;
}
$result .= substr($str, $i, $pos - $i);
$i = $pos + 1;
if ($i >= $length) {
$result .= '\\';
break;
}
$nextChar = $str[$i];
if (isset(self::SPECIAL_CHARS[$nextChar])) {
$result .= self::SPECIAL_CHARS[$nextChar];
} elseif ('\\' === $nextChar) {
$result .= $nextChar;
} elseif ("'" === $nextChar || '"' === $nextChar) {
if ($nextChar !== $quoteType) {
trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
}
$result .= $nextChar;
} elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) {
$result .= '#{';
++$i;
} elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) {
$hex = $str[++$i];
if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) {
$hex .= $str[++$i];
}
$result .= \chr(hexdec($hex));
} elseif (ctype_digit($nextChar) && $nextChar < '8') {
$octal = $nextChar;
while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) {
$octal .= $str[++$i];
}
$result .= \chr(octdec($octal));
} else {
trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
$result .= $nextChar;
}
++$i;
}
return $result;
}
private function lexRawData(): void
{
if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
@ -385,7 +468,7 @@ class Lexer
}
}
$this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
$this->pushToken(Token::TEXT_TYPE, $text);
}
private function lexComment(): void
@ -401,23 +484,23 @@ class Lexer
{
if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
$this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
$this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10);
$this->pushToken(Token::INTERPOLATION_START_TYPE);
$this->moveCursor($match[0]);
$this->pushState(self::STATE_INTERPOLATION);
} elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) {
$this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0]));
} elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) {
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"'));
$this->moveCursor($match[0]);
} elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
list($expect, $lineno) = array_pop($this->brackets);
[$expect, $lineno] = array_pop($this->brackets);
if ('"' != $this->code[$this->cursor]) {
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
$this->popState();
++$this->cursor;
} else {
// unlexable
throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
}
@ -426,7 +509,7 @@ class Lexer
$bracket = end($this->brackets);
if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
array_pop($this->brackets);
$this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11);
$this->pushToken(Token::INTERPOLATION_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@ -437,7 +520,7 @@ class Lexer
private function pushToken($type, $value = ''): void
{
// do not push empty text tokens
if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) {
if (Token::TEXT_TYPE === $type && '' === $value) {
return;
}

View File

@ -28,14 +28,12 @@ use Twig\Source;
*/
final class ArrayLoader implements LoaderInterface
{
private $templates = [];
/**
* @param array $templates An array of templates (keys are the names, and values are the source code)
*/
public function __construct(array $templates = [])
{
$this->templates = $templates;
public function __construct(
private array $templates = [],
) {
}
public function setTemplate(string $name, string $template): void
@ -46,7 +44,7 @@ final class ArrayLoader implements LoaderInterface
public function getSourceContext(string $name): Source
{
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
}
return new Source($this->templates[$name], $name);
@ -60,7 +58,7 @@ final class ArrayLoader implements LoaderInterface
public function getCacheKey(string $name): string
{
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
}
return $name.':'.$this->templates[$name];
@ -69,7 +67,7 @@ final class ArrayLoader implements LoaderInterface
public function isFresh(string $name, int $time): bool
{
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
}
return true;

View File

@ -21,22 +21,28 @@ use Twig\Source;
*/
final class ChainLoader implements LoaderInterface
{
/**
* @var array<string, bool>
*/
private $hasSourceCache = [];
private $loaders = [];
/**
* @param LoaderInterface[] $loaders
* @param iterable<LoaderInterface> $loaders
*/
public function __construct(array $loaders = [])
{
foreach ($loaders as $loader) {
$this->addLoader($loader);
}
public function __construct(
private iterable $loaders = [],
) {
}
public function addLoader(LoaderInterface $loader): void
{
$this->loaders[] = $loader;
$current = $this->loaders;
$this->loaders = (static function () use ($current, $loader): \Generator {
yield from $current;
yield $loader;
})();
$this->hasSourceCache = [];
}
@ -45,13 +51,18 @@ final class ChainLoader implements LoaderInterface
*/
public function getLoaders(): array
{
if (!\is_array($this->loaders)) {
$this->loaders = iterator_to_array($this->loaders, false);
}
return $this->loaders;
}
public function getSourceContext(string $name): Source
{
$exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) {
continue;
}
@ -63,7 +74,7 @@ final class ChainLoader implements LoaderInterface
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
public function exists(string $name): bool
@ -72,7 +83,7 @@ final class ChainLoader implements LoaderInterface
return $this->hasSourceCache[$name];
}
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if ($loader->exists($name)) {
return $this->hasSourceCache[$name] = true;
}
@ -84,7 +95,8 @@ final class ChainLoader implements LoaderInterface
public function getCacheKey(string $name): string
{
$exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) {
continue;
}
@ -96,13 +108,14 @@ final class ChainLoader implements LoaderInterface
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
public function isFresh(string $name, int $time): bool
{
$exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) {
continue;
}
@ -114,6 +127,6 @@ final class ChainLoader implements LoaderInterface
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
}

View File

@ -34,9 +34,9 @@ class FilesystemLoader implements LoaderInterface
* @param string|array $paths A path or an array of paths where to look for templates
* @param string|null $rootPath The root path common to all relative paths (null for getcwd())
*/
public function __construct($paths = [], string $rootPath = null)
public function __construct($paths = [], ?string $rootPath = null)
{
$this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR;
$this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR;
if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
$this->rootPath = $realPath.\DIRECTORY_SEPARATOR;
}
@ -89,7 +89,7 @@ class FilesystemLoader implements LoaderInterface
$checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
if (!is_dir($checkPath)) {
throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
}
$this->paths[$namespace][] = rtrim($path, '/\\');
@ -105,7 +105,7 @@ class FilesystemLoader implements LoaderInterface
$checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
if (!is_dir($checkPath)) {
throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
}
$path = rtrim($path, '/\\');
@ -183,7 +183,7 @@ class FilesystemLoader implements LoaderInterface
}
try {
list($namespace, $shortname) = $this->parseName($name);
[$namespace, $shortname] = $this->parseName($name);
$this->validateName($shortname);
} catch (LoaderError $e) {
@ -195,7 +195,7 @@ class FilesystemLoader implements LoaderInterface
}
if (!isset($this->paths[$namespace])) {
$this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace);
$this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace);
if (!$throw) {
return null;
@ -218,7 +218,7 @@ class FilesystemLoader implements LoaderInterface
}
}
$this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
$this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
if (!$throw) {
return null;
@ -236,7 +236,7 @@ class FilesystemLoader implements LoaderInterface
{
if (isset($name[0]) && '@' == $name[0]) {
if (false === $pos = strpos($name, '/')) {
throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
}
$namespace = substr($name, 1, $pos - 1);
@ -250,7 +250,7 @@ class FilesystemLoader implements LoaderInterface
private function validateName(string $name): void
{
if (false !== strpos($name, "\0")) {
if (str_contains($name, "\0")) {
throw new LoaderError('A template name cannot contain NUL bytes.');
}
@ -265,7 +265,7 @@ class FilesystemLoader implements LoaderInterface
}
if ($level < 0) {
throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
}
}
}

View File

@ -16,10 +16,10 @@ namespace Twig;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Markup implements \Countable, \JsonSerializable
class Markup implements \Countable, \JsonSerializable, \Stringable
{
private $content;
private $charset;
private ?string $charset;
public function __construct($content, $charset)
{

View File

@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@ -24,11 +25,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class AutoEscapeNode extends Node
{
public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape')
public function __construct($value, Node $body, int $lineno)
{
parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag);
parent::__construct(['body' => $body], ['value' => $value], $lineno);
}
public function compile(Compiler $compiler): void

View File

@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@ -19,24 +20,29 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class BlockNode extends Node
{
public function __construct(string $name, Node $body, int $lineno, string $tag = null)
public function __construct(string $name, Node $body, int $lineno)
{
parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag);
parent::__construct(['body' => $body], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n")
->write("/**\n")
->write(" * @return iterable<null|scalar|\Stringable>\n")
->write(" */\n")
->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n")
->indent()
->write("\$macros = \$this->macros;\n")
;
$compiler
->subcompile($this->getNode('body'))
->write("yield from [];\n")
->outdent()
->write("}\n\n")
;

View File

@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@ -19,18 +20,19 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class BlockReferenceNode extends Node implements NodeOutputInterface
{
public function __construct(string $name, int $lineno, string $tag = null)
public function __construct(string $name, int $lineno)
{
parent::__construct([], ['name' => $name], $lineno, $tag);
parent::__construct([], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
;
}
}

View File

@ -11,11 +11,14 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
/**
* Represents a body node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class BodyNode extends Node
{
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* Represents a node for which we need to capture the output.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CaptureNode extends Node
{
public function __construct(Node $body, int $lineno)
{
parent::__construct(['body' => $body], ['raw' => false], $lineno);
}
public function compile(Compiler $compiler): void
{
$useYield = $compiler->getEnvironment()->useYield();
if (!$this->getAttribute('raw')) {
$compiler->raw("('' === \$tmp = ");
}
$compiler
->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput(')
->raw("(function () use (&\$context, \$macros, \$blocks) {\n")
->indent()
->subcompile($this->getNode('body'))
->write("yield from [];\n")
->outdent()
->write('})()')
;
if ($useYield) {
$compiler->raw(', false))');
} else {
$compiler->raw(')');
}
if (!$this->getAttribute('raw')) {
$compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());");
} else {
$compiler->raw(';');
}
}
}

View File

@ -11,17 +11,19 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckSecurityCallNode extends Node
{
public function compile(Compiler $compiler)
{
$compiler
->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n")
->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n")
->write("\$this->checkSecurity();\n")
;
}

View File

@ -11,17 +11,24 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckSecurityNode extends Node
{
private $usedFilters;
private $usedTags;
private $usedFunctions;
/**
* @param array<string, int> $usedFilters
* @param array<string, int> $usedTags
* @param array<string, int> $usedFunctions
*/
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
{
$this->usedFilters = $usedFilters;
@ -33,32 +40,22 @@ class CheckSecurityNode extends Node
public function compile(Compiler $compiler): void
{
$tags = $filters = $functions = [];
foreach (['tags', 'filters', 'functions'] as $type) {
foreach ($this->{'used'.ucfirst($type)} as $name => $node) {
if ($node instanceof Node) {
${$type}[$name] = $node->getTemplateLine();
} else {
${$type}[$node] = null;
}
}
}
$compiler
->write("\n")
->write("public function checkSecurity()\n")
->write("{\n")
->indent()
->write('static $tags = ')->repr(array_filter($tags))->raw(";\n")
->write('static $filters = ')->repr(array_filter($filters))->raw(";\n")
->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n")
->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n")
->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n")
->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n")
->write("try {\n")
->indent()
->write("\$this->sandbox->checkSecurity(\n")
->indent()
->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n")
->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n")
->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n")
->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n")
->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n")
->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n")
->write("\$this->source\n")
->outdent()
->write(");\n")
->outdent()

View File

@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@ -24,11 +25,12 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckToStringNode extends AbstractExpression
{
public function __construct(AbstractExpression $expr)
{
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag());
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine());
}
public function compile(Compiler $compiler): void

View File

@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@ -20,11 +21,12 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
#[YieldReady]
class DeprecatedNode extends Node
{
public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
public function __construct(AbstractExpression $expr, int $lineno)
{
parent::__construct(['expr' => $expr], [], $lineno, $tag);
parent::__construct(['expr' => $expr], [], $lineno);
}
public function compile(Compiler $compiler): void
@ -33,21 +35,39 @@ class DeprecatedNode extends Node
$expr = $this->getNode('expr');
if ($expr instanceof ConstantExpression) {
$compiler->write('@trigger_error(')
->subcompile($expr);
} else {
if (!$expr instanceof ConstantExpression) {
$varName = $compiler->getVarName();
$compiler->write(sprintf('$%s = ', $varName))
$compiler
->write(\sprintf('$%s = ', $varName))
->subcompile($expr)
->raw(";\n")
->write(sprintf('@trigger_error($%s', $varName));
;
}
$compiler->write('trigger_deprecation(');
if ($this->hasNode('package')) {
$compiler->subcompile($this->getNode('package'));
} else {
$compiler->raw("''");
}
$compiler->raw(', ');
if ($this->hasNode('version')) {
$compiler->subcompile($this->getNode('version'));
} else {
$compiler->raw("''");
}
$compiler->raw(', ');
if ($expr instanceof ConstantExpression) {
$compiler->subcompile($expr);
} else {
$compiler->write(\sprintf('$%s', $varName));
}
$compiler
->raw('.')
->string(sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine()))
->raw(", E_USER_DEPRECATED);\n")
->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine()))
->raw(");\n")
;
}
}

View File

@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@ -19,11 +20,12 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class DoNode extends Node
{
public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
public function __construct(AbstractExpression $expr, int $lineno)
{
parent::__construct(['expr' => $expr], [], $lineno, $tag);
parent::__construct(['expr' => $expr], [], $lineno);
}
public function compile(Compiler $compiler): void

View File

@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@ -20,12 +21,13 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class EmbedNode extends IncludeNode
{
// we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
{
parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);
parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno);
$this->setAttribute('name', $name);
$this->setAttribute('index', $index);

View File

@ -21,4 +21,8 @@ use Twig\Node\Node;
*/
abstract class AbstractExpression extends Node
{
public function isGenerator(): bool
{
return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator');
}
}

View File

@ -55,7 +55,7 @@ class ArrayExpression extends AbstractExpression
return false;
}
public function addElement(AbstractExpression $value, AbstractExpression $key = null): void
public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
{
if (null === $key) {
$key = new ConstantExpression(++$this->index, $value->getTemplateLine());
@ -66,20 +66,70 @@ class ArrayExpression extends AbstractExpression
public function compile(Compiler $compiler): void
{
$keyValuePairs = $this->getKeyValuePairs();
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
if ($needsArrayMergeSpread) {
$compiler->raw('CoreExtension::merge(');
}
$compiler->raw('[');
$first = true;
foreach ($this->getKeyValuePairs() as $pair) {
$reopenAfterMergeSpread = false;
$nextIndex = 0;
foreach ($keyValuePairs as $pair) {
if ($reopenAfterMergeSpread) {
$compiler->raw(', [');
$reopenAfterMergeSpread = false;
}
if ($needsArrayMergeSpread && $pair['value']->hasAttribute('spread')) {
$compiler->raw('], ')->subcompile($pair['value']);
$first = true;
$reopenAfterMergeSpread = true;
continue;
}
if (!$first) {
$compiler->raw(', ');
}
$first = false;
$compiler
->subcompile($pair['key'])
->raw(' => ')
->subcompile($pair['value'])
;
if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) {
$compiler->raw('...')->subcompile($pair['value']);
++$nextIndex;
} else {
$key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
if ($nextIndex !== $key) {
if (\is_int($key)) {
$nextIndex = $key + 1;
}
$compiler
->subcompile($pair['key'])
->raw(' => ')
;
} else {
++$nextIndex;
}
$compiler->subcompile($pair['value']);
}
}
$compiler->raw(']');
if (!$reopenAfterMergeSpread) {
$compiler->raw(']');
}
if ($needsArrayMergeSpread) {
$compiler->raw(')');
}
}
private function hasSpreadItem(array $pairs): bool
{
foreach ($pairs as $pair) {
if ($pair['value']->hasAttribute('spread')) {
return true;
}
}
return false;
}
}

View File

@ -21,9 +21,9 @@ use Twig\Node\Node;
*/
class ArrowFunctionExpression extends AbstractExpression
{
public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null)
public function __construct(AbstractExpression $expr, Node $names, $lineno)
{
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag);
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno);
}
public function compile(Compiler $compiler): void

View File

@ -20,11 +20,11 @@ class EndsWithBinary extends AbstractBinary
$left = $compiler->getVarName();
$right = $compiler->getVarName();
$compiler
->raw(sprintf('(is_string($%s = ', $left))
->raw(\sprintf('(is_string($%s = ', $left))
->subcompile($this->getNode('left'))
->raw(sprintf(') && is_string($%s = ', $right))
->raw(\sprintf(') && is_string($%s = ', $right))
->subcompile($this->getNode('right'))
->raw(sprintf(') && (\'\' === $%2$s || $%2$s === substr($%1$s, -strlen($%2$s))))', $left, $right))
->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right))
;
}

View File

@ -24,7 +24,7 @@ class EqualBinary extends AbstractBinary
}
$compiler
->raw('(0 === twig_compare(')
->raw('(0 === CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -24,7 +24,7 @@ class GreaterBinary extends AbstractBinary
}
$compiler
->raw('(1 === twig_compare(')
->raw('(1 === CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -24,7 +24,7 @@ class GreaterEqualBinary extends AbstractBinary
}
$compiler
->raw('(0 <= twig_compare(')
->raw('(0 <= CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
class HasEveryBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->raw('CoreExtension::arrayEvery($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('');
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
class HasSomeBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->raw('CoreExtension::arraySome($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('');
}
}

View File

@ -18,7 +18,7 @@ class InBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('twig_in_filter(')
->raw('CoreExtension::inFilter(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -24,7 +24,7 @@ class LessBinary extends AbstractBinary
}
$compiler
->raw('(-1 === twig_compare(')
->raw('(-1 === CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -24,7 +24,7 @@ class LessEqualBinary extends AbstractBinary
}
$compiler
->raw('(0 >= twig_compare(')
->raw('(0 >= CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('preg_match(')
->raw('CoreExtension::matches(')
->subcompile($this->getNode('right'))
->raw(', ')
->subcompile($this->getNode('left'))

View File

@ -24,7 +24,7 @@ class NotEqualBinary extends AbstractBinary
}
$compiler
->raw('(0 !== twig_compare(')
->raw('(0 !== CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('!twig_in_filter(')
->raw('!CoreExtension::inFilter(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@ -20,11 +20,11 @@ class StartsWithBinary extends AbstractBinary
$left = $compiler->getVarName();
$right = $compiler->getVarName();
$compiler
->raw(sprintf('(is_string($%s = ', $left))
->raw(\sprintf('(is_string($%s = ', $left))
->subcompile($this->getNode('left'))
->raw(sprintf(') && is_string($%s = ', $right))
->raw(\sprintf(') && is_string($%s = ', $right))
->subcompile($this->getNode('right'))
->raw(sprintf(') && (\'\' === $%2$s || 0 === strpos($%1$s, $%2$s)))', $left, $right))
->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right))
;
}

View File

@ -22,14 +22,14 @@ use Twig\Node\Node;
*/
class BlockReferenceExpression extends AbstractExpression
{
public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null)
public function __construct(Node $name, ?Node $template, int $lineno)
{
$nodes = ['name' => $name];
if (null !== $template) {
$nodes['template'] = $template;
}
parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno, $tag);
parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno);
}
public function compile(Compiler $compiler): void
@ -40,8 +40,9 @@ class BlockReferenceExpression extends AbstractExpression
if ($this->getAttribute('output')) {
$compiler->addDebugInfo($this);
$compiler->write('yield from ');
$this
->compileTemplateCall($compiler, 'displayBlock')
->compileTemplateCall($compiler, 'yieldBlock')
->raw(";\n");
} else {
$this->compileTemplateCall($compiler, 'renderBlock');
@ -65,7 +66,7 @@ class BlockReferenceExpression extends AbstractExpression
;
}
$compiler->raw(sprintf('->%s', $method));
$compiler->raw(\sprintf('->unwrap()->%s', $method));
return $this->compileBlockArguments($compiler);
}

View File

@ -15,40 +15,49 @@ use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Extension\ExtensionInterface;
use Twig\Node\Node;
use Twig\TwigCallableInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
use Twig\Util\CallableArgumentsExtractor;
use Twig\Util\ReflectionCallable;
abstract class CallExpression extends AbstractExpression
{
private $reflector;
private $reflector = null;
protected function compileCallable(Compiler $compiler)
{
$callable = $this->getAttribute('callable');
$twigCallable = $this->getTwigCallable();
$callable = $twigCallable->getCallable();
if (\is_string($callable) && false === strpos($callable, '::')) {
if (\is_string($callable) && !str_contains($callable, '::')) {
$compiler->raw($callable);
} else {
[$r, $callable] = $this->reflectCallable($callable);
$rc = $this->reflectCallable($twigCallable);
$r = $rc->getReflector();
$callable = $rc->getCallable();
if (\is_string($callable)) {
$compiler->raw($callable);
} elseif (\is_array($callable) && \is_string($callable[0])) {
if (!$r instanceof \ReflectionMethod || $r->isStatic()) {
$compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
$compiler->raw(\sprintf('%s::%s', $callable[0], $callable[1]));
} else {
$compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
$compiler->raw(\sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
}
} elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) {
$class = \get_class($callable[0]);
if (!$compiler->getEnvironment()->hasExtension($class)) {
// Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class));
$compiler->raw(\sprintf('$this->env->getExtension(\'%s\')', $class));
} else {
$compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
$compiler->raw(\sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
}
$compiler->raw(sprintf('->%s', $callable[1]));
$compiler->raw(\sprintf('->%s', $callable[1]));
} else {
$compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
$compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $twigCallable->getDynamicName()));
}
}
@ -57,16 +66,30 @@ abstract class CallExpression extends AbstractExpression
protected function compileArguments(Compiler $compiler, $isArray = false): void
{
if (\func_num_args() >= 2) {
trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
}
$compiler->raw($isArray ? '[' : '(');
$first = true;
if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
$twigCallable = $this->getAttribute('twig_callable');
if ($twigCallable->needsCharset()) {
$compiler->raw('$this->env->getCharset()');
$first = false;
}
if ($twigCallable->needsEnvironment()) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->raw('$this->env');
$first = false;
}
if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
if ($twigCallable->needsContext()) {
if (!$first) {
$compiler->raw(', ');
}
@ -74,14 +97,12 @@ abstract class CallExpression extends AbstractExpression
$first = false;
}
if ($this->hasAttribute('arguments')) {
foreach ($this->getAttribute('arguments') as $argument) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->string($argument);
$first = false;
foreach ($twigCallable->getArguments() as $argument) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->string($argument);
$first = false;
}
if ($this->hasNode('node')) {
@ -93,8 +114,7 @@ abstract class CallExpression extends AbstractExpression
}
if ($this->hasNode('arguments')) {
$callable = $this->getAttribute('callable');
$arguments = $this->getArguments($callable, $this->getNode('arguments'));
$arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments'));
foreach ($arguments as $node) {
if (!$first) {
$compiler->raw(', ');
@ -107,8 +127,13 @@ abstract class CallExpression extends AbstractExpression
$compiler->raw($isArray ? ']' : ')');
}
/**
* @deprecated since 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead
*/
protected function getArguments($callable, $arguments)
{
trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated, use Twig\Util\CallableArgumentsExtractor::getArguments() instead.', __METHOD__);
$callType = $this->getAttribute('type');
$callName = $this->getAttribute('name');
@ -119,28 +144,28 @@ abstract class CallExpression extends AbstractExpression
$named = true;
$name = $this->normalizeName($name);
} elseif ($named) {
throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
}
$parameters[$name] = $node;
}
$isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic');
$isVariadic = $this->getAttribute('twig_callable')->isVariadic();
if (!$named && !$isVariadic) {
return $parameters;
}
if (!$callable) {
if ($named) {
$message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
$message = \sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
} else {
$message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
$message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
}
throw new \LogicException($message);
}
list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic);
[$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic);
$arguments = [];
$names = [];
$missingArguments = [];
@ -160,11 +185,11 @@ abstract class CallExpression extends AbstractExpression
if (\array_key_exists($name, $parameters)) {
if (\array_key_exists($pos, $parameters)) {
throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
}
if (\count($missingArguments)) {
throw new SyntaxError(sprintf(
throw new SyntaxError(\sprintf(
'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
$name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
), $this->getTemplateLine(), $this->getSourceContext());
@ -189,7 +214,7 @@ abstract class CallExpression extends AbstractExpression
$missingArguments[] = $name;
}
} else {
throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
}
}
@ -220,7 +245,7 @@ abstract class CallExpression extends AbstractExpression
}
throw new SyntaxError(
sprintf(
\sprintf(
'Unknown argument%s "%s" for %s "%s(%s)".',
\count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
),
@ -232,88 +257,106 @@ abstract class CallExpression extends AbstractExpression
return $arguments;
}
/**
* @deprecated since 3.12
*/
protected function normalizeName(string $name): string
{
trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated.', __METHOD__);
return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name));
}
// To be removed in 4.0
private function getCallableParameters($callable, bool $isVariadic): array
{
[$r, , $callableName] = $this->reflectCallable($callable);
$twigCallable = $this->getAttribute('twig_callable');
$rc = $this->reflectCallable($twigCallable);
$r = $rc->getReflector();
$callableName = $rc->getName();
$parameters = $r->getParameters();
if ($this->hasNode('node')) {
array_shift($parameters);
}
if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
if ($twigCallable->needsCharset()) {
array_shift($parameters);
}
if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
if ($twigCallable->needsEnvironment()) {
array_shift($parameters);
}
if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) {
foreach ($this->getAttribute('arguments') as $argument) {
array_shift($parameters);
}
if ($twigCallable->needsContext()) {
array_shift($parameters);
}
foreach ($twigCallable->getArguments() as $argument) {
array_shift($parameters);
}
$isPhpVariadic = false;
if ($isVariadic) {
$argument = end($parameters);
$isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName();
$isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName();
if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
array_pop($parameters);
} elseif ($argument && $argument->isVariadic()) {
array_pop($parameters);
$isPhpVariadic = true;
} else {
throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name')));
throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $twigCallable->getName()));
}
}
return [$parameters, $isPhpVariadic];
}
private function reflectCallable($callable)
private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable
{
if (null !== $this->reflector) {
return $this->reflector;
if (!$this->reflector) {
$this->reflector = new ReflectionCallable($callable);
}
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
}
return $this->reflector;
}
if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$r = new \ReflectionMethod($callable[0], $callable[1]);
/**
* Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node).
*
* To be removed in 4.0 and replace by $this->getAttribute('twig_callable').
*/
private function getTwigCallable(): TwigCallableInterface
{
$current = $this->getAttribute('twig_callable');
return $this->reflector = [$r, $callable, $r->class.'::'.$r->name];
}
$this->setAttribute('twig_callable', match ($this->getAttribute('type')) {
'test' => (new TwigTest(
$this->getAttribute('name'),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()),
'function' => (new TwigFunction(
$this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(),
'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(),
'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(),
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()),
'filter' => (new TwigFilter(
$this->getAttribute('name'),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(),
'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(),
'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(),
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()),
});
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e);
}
$r = new \ReflectionFunction($closure);
if (false !== strpos($r->name, '{closure}')) {
return $this->reflector = [$r, $callable, 'Closure'];
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name;
} elseif ($class = $r->getClosureScopeClass()) {
$callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else {
$callable = $callableName = $r->name;
}
if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
$callable = $r->getClosure();
}
return $this->reflector = [$r, $callable, $callableName];
return $this->getAttribute('twig_callable');
}
}

View File

@ -23,14 +23,23 @@ class ConditionalExpression extends AbstractExpression
public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->subcompile($this->getNode('expr1'))
->raw(') ? (')
->subcompile($this->getNode('expr2'))
->raw(') : (')
->subcompile($this->getNode('expr3'))
->raw('))')
;
// Ternary with no then uses Elvis operator
if ($this->getNode('expr1') === $this->getNode('expr2')) {
$compiler
->raw('((')
->subcompile($this->getNode('expr1'))
->raw(') ?: (')
->subcompile($this->getNode('expr3'))
->raw('))');
} else {
$compiler
->raw('((')
->subcompile($this->getNode('expr1'))
->raw(') ? (')
->subcompile($this->getNode('expr2'))
->raw(') : (')
->subcompile($this->getNode('expr3'))
->raw('))');
}
}
}

View File

@ -14,6 +14,9 @@ namespace Twig\Node\Expression;
use Twig\Compiler;
/**
* @final
*/
class ConstantExpression extends AbstractExpression
{
public function __construct($value, int $lineno)

View File

@ -11,7 +11,9 @@
namespace Twig\Node\Expression\Filter;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Extension\CoreExtension;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
@ -19,6 +21,8 @@ use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Node;
use Twig\TwigFilter;
use Twig\TwigTest;
/**
* Returns the value or the default value when it is undefined or empty.
@ -29,20 +33,27 @@ use Twig\Node\Node;
*/
class DefaultFilter extends FilterExpression
{
public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno)
{
$default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine());
if ($filter instanceof TwigFilter) {
$name = $filter->getName();
$default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine());
} else {
$name = $filter->getAttribute('value');
$default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine());
}
if ('default' === $filterName->getAttribute('value') && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) {
$test = new DefinedTest(clone $node, 'defined', new Node(), $node->getTemplateLine());
$false = \count($arguments) ? $arguments->getNode(0) : new ConstantExpression('', $node->getTemplateLine());
if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) {
$test = new DefinedTest(clone $node, new TwigTest('defined'), new Node(), $node->getTemplateLine());
$false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine());
$node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine());
} else {
$node = $default;
}
parent::__construct($node, $filterName, $arguments, $lineno, $tag);
parent::__construct($node, $filter, $arguments, $lineno);
}
public function compile(Compiler $compiler): void

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Filter;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Node;
use Twig\TwigFilter;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RawFilter extends FilterExpression
{
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0)
{
parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine());
}
public function compile(Compiler $compiler): void
{
$compiler->subcompile($this->getNode('node'));
}
}

View File

@ -12,28 +12,61 @@
namespace Twig\Node\Expression;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\NameDeprecation;
use Twig\Node\Node;
use Twig\TwigFilter;
class FilterExpression extends CallExpression
{
public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno)
{
parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag);
if ($filter instanceof TwigFilter) {
$name = $filter->getName();
$filterName = new ConstantExpression($name, $lineno);
} else {
$name = $filter->getAttribute('value');
$filterName = $filter;
trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class);
}
parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno);
if ($filter instanceof TwigFilter) {
$this->setAttribute('twig_callable', $filter);
}
$this->deprecateNode('filter', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
}
public function compile(Compiler $compiler): void
{
$name = $this->getNode('filter')->getAttribute('value');
$filter = $compiler->getEnvironment()->getFilter($name);
$name = $this->getNode('filter', false)->getAttribute('value');
if ($name !== $this->getAttribute('name')) {
trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.');
$this->removeAttribute('twig_callable');
}
if ('raw' === $name) {
trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.');
$this->setAttribute('name', $name);
$this->setAttribute('type', 'filter');
$this->setAttribute('needs_environment', $filter->needsEnvironment());
$this->setAttribute('needs_context', $filter->needsContext());
$this->setAttribute('arguments', $filter->getArguments());
$this->setAttribute('callable', $filter->getCallable());
$this->setAttribute('is_variadic', $filter->isVariadic());
$compiler->subcompile($this->getNode('node'));
return;
}
if (!$this->hasAttribute('twig_callable')) {
$this->setAttribute('twig_callable', $compiler->getEnvironment()->getFilter($name));
}
$this->compileCallable($compiler);
}

View File

@ -11,32 +11,57 @@
namespace Twig\Node\Expression;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\NameDeprecation;
use Twig\Node\Node;
use Twig\TwigFunction;
class FunctionExpression extends CallExpression
{
public function __construct(string $name, Node $arguments, int $lineno)
#[FirstClassTwigCallableReady]
public function __construct(TwigFunction|string $function, Node $arguments, int $lineno)
{
parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
if ($function instanceof TwigFunction) {
$name = $function->getName();
} else {
$name = $function;
trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class);
}
parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno);
if ($function instanceof TwigFunction) {
$this->setAttribute('twig_callable', $function);
}
$this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
}
public function compile(Compiler $compiler)
{
$name = $this->getAttribute('name');
$function = $compiler->getEnvironment()->getFunction($name);
$this->setAttribute('name', $name);
$this->setAttribute('type', 'function');
$this->setAttribute('needs_environment', $function->needsEnvironment());
$this->setAttribute('needs_context', $function->needsContext());
$this->setAttribute('arguments', $function->getArguments());
$callable = $function->getCallable();
if ('constant' === $name && $this->getAttribute('is_defined_test')) {
$callable = 'twig_constant_is_defined';
if ($this->hasAttribute('twig_callable')) {
$name = $this->getAttribute('twig_callable')->getName();
if ($name !== $this->getAttribute('name')) {
trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "function" node in a NodeVisitor class is not supported anymore.');
$this->removeAttribute('twig_callable');
}
}
if (!$this->hasAttribute('twig_callable')) {
$this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name));
}
if ('constant' === $name && $this->getAttribute('is_defined_test')) {
$this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine()));
}
$this->setAttribute('callable', $callable);
$this->setAttribute('is_variadic', $function->isVariadic());
$this->compileCallable($compiler);
}

View File

@ -0,0 +1,41 @@
<?php
namespace Twig\Node\Expression\FunctionNode;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
class EnumCasesFunction extends FunctionExpression
{
public function compile(Compiler $compiler): void
{
$arguments = $this->getNode('arguments');
if ($arguments->hasNode('enum')) {
$firstArgument = $arguments->getNode('enum');
} elseif ($arguments->hasNode('0')) {
$firstArgument = $arguments->getNode('0');
} else {
$firstArgument = null;
}
if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) {
parent::compile($compiler);
return;
}
$value = $firstArgument->getAttribute('value');
if (!\is_string($value)) {
throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext());
}
if (!enum_exists($value)) {
throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext());
}
$compiler->raw(\sprintf('%s::cases()', $value));
}
}

View File

@ -57,7 +57,7 @@ class GetAttrExpression extends AbstractExpression
return;
}
$compiler->raw('twig_get_attribute($this->env, $this->source, ');
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
if ($this->getAttribute('ignore_strict_check')) {
$this->getNode('node')->setAttribute('ignore_strict_check', true);

Some files were not shown because too many files have changed in this diff Show More