diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php
index e9ead975b..9463da203 100644
--- a/addons/userrights/userrightsprofile.class.inc.php
+++ b/addons/userrights/userrightsprofile.class.inc.php
@@ -825,49 +825,38 @@ class UserRightsProfile extends UserRightsAddOnAPI
{
// We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading
// We have to answer NO for objects shared for reading purposes
- if (self::HasSharing())
- {
- $aClassProps = SharedObject::GetSharedClassProperties($sClass);
- if ($aClassProps)
- {
- // This class is shared, GetSelectFilter may allow some objects for read only
- // But currently we are checking wether the objects might be written...
- // Let's exclude the objects based on the relevant criteria
+ if (self::HasSharing() && SharedObject::GetSharedClassProperties($sClass)) {
+ // This class is shared, GetSelectFilter may allow some objects for read only
+ // But currently we are checking whether the objects might be written...
+ // Let's exclude the objects based on the relevant criteria
- $sOrgAttCode = self::GetOwnerOrganizationAttCode($sClass);
- if (!is_null($sOrgAttCode))
- {
- $aUserOrgs = $this->GetUserOrgs($oUser, $sClass);
- if (!is_null($aUserOrgs) && count($aUserOrgs) > 0)
- {
- $iCountNO = 0;
- $iCountYES = 0;
- $oInstanceSet->Rewind();
- while($oObject = $oInstanceSet->Fetch())
- {
- $iOrg = $oObject->Get($sOrgAttCode);
- if (in_array($iOrg, $aUserOrgs))
- {
- $iCountYES++;
- }
- else
- {
- $iCountNO++;
- }
- }
- if ($iCountNO == 0)
- {
- $iPermission = UR_ALLOWED_YES;
- }
- elseif ($iCountYES == 0)
- {
- $iPermission = UR_ALLOWED_NO;
- }
- else
- {
- $iPermission = UR_ALLOWED_DEPENDS;
+ // Use $oInstanceSet only if sClass is the main class
+ if (!is_a($oInstanceSet->GetClass(), $sClass, true)) {
+ /** @var \DBObjectSet $oInstanceSet */
+ throw new CoreException(__FUNCTION__.': Expecting object set to be of class '.$sClass.' but it is of class '.$oInstanceSet->GetClass(), ['OQL_Query' => $oInstanceSet->GetFilter()->ToOQL(), 'classes' => $oInstanceSet->GetSelectedClasses()]);
+ }
+ $sOrgAttCode = self::GetOwnerOrganizationAttCode($sClass);
+ if (!is_null($sOrgAttCode)) {
+ $aUserOrgs = $this->GetUserOrgs($oUser, $sClass);
+ if (!is_null($aUserOrgs) && count($aUserOrgs) > 0) {
+ $iCountNO = 0;
+ $iCountYES = 0;
+ $oInstanceSet->Rewind();
+ while ($oObject = $oInstanceSet->Fetch()) {
+ $iOrg = $oObject->Get($sOrgAttCode);
+ if (in_array($iOrg, $aUserOrgs)) {
+ $iCountYES++;
+ } else {
+ $iCountNO++;
}
}
+ if ($iCountNO == 0) {
+ $iPermission = UR_ALLOWED_YES;
+ } elseif ($iCountYES == 0) {
+ $iPermission = UR_ALLOWED_NO;
+ } else {
+ $iPermission = UR_ALLOWED_DEPENDS;
+ }
}
}
}
@@ -982,4 +971,3 @@ class UserRightsProfile extends UserRightsAddOnAPI
UserRights::SelectModule('UserRightsProfile');
-?>
diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php
index 68b2ce516..7c158ec02 100644
--- a/application/displayblock.class.inc.php
+++ b/application/displayblock.class.inc.php
@@ -2030,8 +2030,8 @@ class MenuBlock extends DisplayBlock
$sSelectedClassName = MetaModel::GetName($sSelectedClass);
// Check rights on class
- $bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sSelectedClass)) && UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_MODIFY, $oSet) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
- $bIsBulkDeleteAllowed = (bool) UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_DELETE, $sSelectedClass);
+ $bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sSelectedClass)) && UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_MODIFY) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
+ $bIsBulkDeleteAllowed = (bool) UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_DELETE);
// Refine filter on selected class so bullk actions occur on the right class
$oSelectedClassFilter = $this->GetFilter()->DeepClone();
diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php
index 59fca8528..81927e921 100644
--- a/application/menunode.class.inc.php
+++ b/application/menunode.class.inc.php
@@ -1159,11 +1159,11 @@ class OQLMenuNode extends MenuNode
{
$sUsageId = utils::GetSafeId($sUsageId);
$oSearch = DBObjectSearch::FromOQL($sOql);
- $sClass= $oSearch->GetClass();
+ $sClass= $oSearch->GetClass();
$sIcon = MetaModel::GetClassIcon($sClass, false);
if ($bSearchPane) {
$aParams = array_merge(['open' => $bSearchOpen, 'table_id' => $sUsageId, 'submit_on_load' => false], $aExtraParams);
- $oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams);
+ $oBlock = new DisplayBlock($oSearch, DisplayBlock::ENUM_STYLE_LIST_SEARCH, false /* Asynchronous */, $aParams);
$oBlock->Display($oPage, 0);
$oPage->add("
");
}
diff --git a/composer.lock b/composer.lock
index 4ebeb258e..7acfc83de 100644
--- a/composer.lock
+++ b/composer.lock
@@ -3929,6 +3929,82 @@
],
"time": "2023-01-26T09:26:14+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": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "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/polyfill-php83",
"version": "v1.28.0",
@@ -4976,30 +5052,38 @@
},
{
"name": "twig/twig",
- "version": "v3.8.0",
+ "version": "v3.16.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
+ "reference": "475ad2dc97d65d8631393e721e7e44fb544f0561"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
- "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/475ad2dc97d65d8631393e721e7e44fb544f0561",
+ "reference": "475ad2dc97d65d8631393e721e7e44fb544f0561",
"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-php80": "^1.22"
+ "symfony/polyfill-php81": "^1.29"
},
"require-dev": {
+ "phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
- "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
+ "files": [
+ "src/Resources/core.php",
+ "src/Resources/debug.php",
+ "src/Resources/escaper.php",
+ "src/Resources/string_loader.php"
+ ],
"psr-4": {
"Twig\\": "src/"
}
@@ -5032,7 +5116,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
- "source": "https://github.com/twigphp/Twig/tree/v3.8.0"
+ "source": "https://github.com/twigphp/Twig/tree/v3.16.0"
},
"funding": [
{
@@ -5044,7 +5128,7 @@
"type": "tidelift"
}
],
- "time": "2023-11-21T18:54:41+00:00"
+ "time": "2024-11-29T08:27:05+00:00"
},
{
"name": "webmozart/assert",
diff --git a/datamodels/2.x/itop-core-update/templates/SelectUpdateFile.ready.js.twig b/datamodels/2.x/itop-core-update/templates/SelectUpdateFile.ready.js.twig
index 1e731f6c4..ad7d8624a 100644
--- a/datamodels/2.x/itop-core-update/templates/SelectUpdateFile.ready.js.twig
+++ b/datamodels/2.x/itop-core-update/templates/SelectUpdateFile.ready.js.twig
@@ -134,20 +134,29 @@ $("#launch-fast-setup").on("click", function(e) {
route: "core_update_ajax.rebuild_toolkit_environment"
},
dataType: "json",
+ complete: function(jqXHR, textStatus) {
+ $("#fast-setup-wait").addClass("ibo-is-hidden");
+ $("#launch-fast-setup").prop("disabled", false);
+ fast_setup_wait.addClass("ibo-is-hidden");
+ },
success: function (data) {
oMessage.removeClass("ibo-is-information");
if (data.bStatus) {
+ oMessage.removeClass("ibo-is-failure");
oMessage.addClass("ibo-is-success");
oContent.html("{{ 'iTopUpdate:UI:SetupMessage:UpdateDone'|dict_s }}");
} else {
+ oMessage.removeClass("ibo-is-success");
oMessage.addClass("ibo-is-failure");
oContent.html(data.sError);
}
-
- $("#fast-setup-wait").addClass("ibo-is-hidden");
- $("#launch-fast-setup").prop("disabled", false);
- fast_setup_wait.addClass("ibo-is-hidden");
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ oMessage.removeClass("ibo-is-information");
+ oMessage.removeClass("ibo-is-success");
+ oMessage.addClass("ibo-is-failure");
+ oContent.html(textStatus + ' ' + errorThrown);
}
});
diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php
index e30582e5f..270f7b0bd 100644
--- a/lib/composer/autoload_classmap.php
+++ b/lib/composer/autoload_classmap.php
@@ -178,6 +178,7 @@ return array(
'CSVBulkExport' => $baseDir . '/core/csvbulkexport.class.inc.php',
'CSVParser' => $baseDir . '/core/csvparser.class.inc.php',
'CSVParserException' => $baseDir . '/application/exceptions/CSVParserException.php',
+ 'CURLStringFile' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'CellChangeSpec' => $baseDir . '/core/bulkchange.class.inc.php',
'CellStatus_Ambiguous' => $baseDir . '/core/bulkchange.class.inc.php',
'CellStatus_Issue' => $baseDir . '/core/bulkchange.class.inc.php',
@@ -1503,6 +1504,7 @@ return array(
'RestResultWithObjects' => $baseDir . '/core/restservices.class.inc.php',
'RestResultWithRelations' => $baseDir . '/core/restservices.class.inc.php',
'RestUtils' => $baseDir . '/application/applicationextension.inc.php',
+ 'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'RotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
'RowStatus' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => $baseDir . '/core/bulkchange.class.inc.php',
@@ -2869,6 +2871,7 @@ return array(
'Symfony\\Polyfill\\Php72\\Php72' => $vendorDir . '/symfony/polyfill-php72/Php72.php',
'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php',
'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php',
+ 'Symfony\\Polyfill\\Php81\\Php81' => $vendorDir . '/symfony/polyfill-php81/Php81.php',
'Symfony\\Polyfill\\Php83\\Php83' => $vendorDir . '/symfony/polyfill-php83/Php83.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\ApplicationRuntime' => $vendorDir . '/symfony/runtime/Internal/Console/ApplicationRuntime.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\Command\\CommandRuntime' => $vendorDir . '/symfony/runtime/Internal/Console/Command/CommandRuntime.php',
@@ -2916,10 +2919,17 @@ return array(
'TriggerOnStateLeave' => $baseDir . '/core/trigger.class.inc.php',
'TriggerOnThresholdReached' => $baseDir . '/core/trigger.class.inc.php',
'TrueExpression' => $baseDir . '/core/oql/expression.class.inc.php',
+ 'Twig\\AbstractTwigCallable' => $vendorDir . '/twig/twig/src/AbstractTwigCallable.php',
+ 'Twig\\Attribute\\FirstClassTwigCallableReady' => $vendorDir . '/twig/twig/src/Attribute/FirstClassTwigCallableReady.php',
+ 'Twig\\Attribute\\YieldReady' => $vendorDir . '/twig/twig/src/Attribute/YieldReady.php',
'Twig\\Cache\\CacheInterface' => $vendorDir . '/twig/twig/src/Cache/CacheInterface.php',
+ 'Twig\\Cache\\ChainCache' => $vendorDir . '/twig/twig/src/Cache/ChainCache.php',
'Twig\\Cache\\FilesystemCache' => $vendorDir . '/twig/twig/src/Cache/FilesystemCache.php',
'Twig\\Cache\\NullCache' => $vendorDir . '/twig/twig/src/Cache/NullCache.php',
+ 'Twig\\Cache\\ReadOnlyFilesystemCache' => $vendorDir . '/twig/twig/src/Cache/ReadOnlyFilesystemCache.php',
+ 'Twig\\Cache\\RemovableCacheInterface' => $vendorDir . '/twig/twig/src/Cache/RemovableCacheInterface.php',
'Twig\\Compiler' => $vendorDir . '/twig/twig/src/Compiler.php',
+ 'Twig\\DeprecatedCallableInfo' => $vendorDir . '/twig/twig/src/DeprecatedCallableInfo.php',
'Twig\\Environment' => $vendorDir . '/twig/twig/src/Environment.php',
'Twig\\Error\\Error' => $vendorDir . '/twig/twig/src/Error/Error.php',
'Twig\\Error\\LoaderError' => $vendorDir . '/twig/twig/src/Error/LoaderError.php',
@@ -2939,6 +2949,7 @@ return array(
'Twig\\Extension\\SandboxExtension' => $vendorDir . '/twig/twig/src/Extension/SandboxExtension.php',
'Twig\\Extension\\StagingExtension' => $vendorDir . '/twig/twig/src/Extension/StagingExtension.php',
'Twig\\Extension\\StringLoaderExtension' => $vendorDir . '/twig/twig/src/Extension/StringLoaderExtension.php',
+ 'Twig\\Extension\\YieldNotReadyExtension' => $vendorDir . '/twig/twig/src/Extension/YieldNotReadyExtension.php',
'Twig\\FileExtensionEscapingStrategy' => $vendorDir . '/twig/twig/src/FileExtensionEscapingStrategy.php',
'Twig\\Lexer' => $vendorDir . '/twig/twig/src/Lexer.php',
'Twig\\Loader\\ArrayLoader' => $vendorDir . '/twig/twig/src/Loader/ArrayLoader.php',
@@ -2949,21 +2960,23 @@ return array(
'Twig\\NodeTraverser' => $vendorDir . '/twig/twig/src/NodeTraverser.php',
'Twig\\NodeVisitor\\AbstractNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php',
'Twig\\NodeVisitor\\EscaperNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php',
- 'Twig\\NodeVisitor\\MacroAutoImportNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php',
'Twig\\NodeVisitor\\NodeVisitorInterface' => $vendorDir . '/twig/twig/src/NodeVisitor/NodeVisitorInterface.php',
'Twig\\NodeVisitor\\OptimizerNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php',
'Twig\\NodeVisitor\\SafeAnalysisNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php',
'Twig\\NodeVisitor\\SandboxNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php',
+ 'Twig\\NodeVisitor\\YieldNotReadyNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php',
'Twig\\Node\\AutoEscapeNode' => $vendorDir . '/twig/twig/src/Node/AutoEscapeNode.php',
'Twig\\Node\\BlockNode' => $vendorDir . '/twig/twig/src/Node/BlockNode.php',
'Twig\\Node\\BlockReferenceNode' => $vendorDir . '/twig/twig/src/Node/BlockReferenceNode.php',
'Twig\\Node\\BodyNode' => $vendorDir . '/twig/twig/src/Node/BodyNode.php',
+ 'Twig\\Node\\CaptureNode' => $vendorDir . '/twig/twig/src/Node/CaptureNode.php',
'Twig\\Node\\CheckSecurityCallNode' => $vendorDir . '/twig/twig/src/Node/CheckSecurityCallNode.php',
'Twig\\Node\\CheckSecurityNode' => $vendorDir . '/twig/twig/src/Node/CheckSecurityNode.php',
'Twig\\Node\\CheckToStringNode' => $vendorDir . '/twig/twig/src/Node/CheckToStringNode.php',
'Twig\\Node\\DeprecatedNode' => $vendorDir . '/twig/twig/src/Node/DeprecatedNode.php',
'Twig\\Node\\DoNode' => $vendorDir . '/twig/twig/src/Node/DoNode.php',
'Twig\\Node\\EmbedNode' => $vendorDir . '/twig/twig/src/Node/EmbedNode.php',
+ 'Twig\\Node\\EmptyNode' => $vendorDir . '/twig/twig/src/Node/EmptyNode.php',
'Twig\\Node\\Expression\\AbstractExpression' => $vendorDir . '/twig/twig/src/Node/Expression/AbstractExpression.php',
'Twig\\Node\\Expression\\ArrayExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ArrayExpression.php',
'Twig\\Node\\Expression\\ArrowFunctionExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ArrowFunctionExpression.php',
@@ -2997,15 +3010,20 @@ return array(
'Twig\\Node\\Expression\\Binary\\SpaceshipBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/SpaceshipBinary.php',
'Twig\\Node\\Expression\\Binary\\StartsWithBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php',
'Twig\\Node\\Expression\\Binary\\SubBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/SubBinary.php',
+ 'Twig\\Node\\Expression\\Binary\\XorBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/XorBinary.php',
'Twig\\Node\\Expression\\BlockReferenceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/BlockReferenceExpression.php',
'Twig\\Node\\Expression\\CallExpression' => $vendorDir . '/twig/twig/src/Node/Expression/CallExpression.php',
'Twig\\Node\\Expression\\ConditionalExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ConditionalExpression.php',
'Twig\\Node\\Expression\\ConstantExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ConstantExpression.php',
'Twig\\Node\\Expression\\FilterExpression' => $vendorDir . '/twig/twig/src/Node/Expression/FilterExpression.php',
'Twig\\Node\\Expression\\Filter\\DefaultFilter' => $vendorDir . '/twig/twig/src/Node/Expression/Filter/DefaultFilter.php',
+ 'Twig\\Node\\Expression\\Filter\\RawFilter' => $vendorDir . '/twig/twig/src/Node/Expression/Filter/RawFilter.php',
'Twig\\Node\\Expression\\FunctionExpression' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionExpression.php',
+ 'Twig\\Node\\Expression\\FunctionNode\\EnumCasesFunction' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php',
+ 'Twig\\Node\\Expression\\FunctionNode\\EnumFunction' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php',
'Twig\\Node\\Expression\\GetAttrExpression' => $vendorDir . '/twig/twig/src/Node/Expression/GetAttrExpression.php',
'Twig\\Node\\Expression\\InlinePrint' => $vendorDir . '/twig/twig/src/Node/Expression/InlinePrint.php',
+ 'Twig\\Node\\Expression\\MacroReferenceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/MacroReferenceExpression.php',
'Twig\\Node\\Expression\\MethodCallExpression' => $vendorDir . '/twig/twig/src/Node/Expression/MethodCallExpression.php',
'Twig\\Node\\Expression\\NameExpression' => $vendorDir . '/twig/twig/src/Node/Expression/NameExpression.php',
'Twig\\Node\\Expression\\NullCoalesceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/NullCoalesceExpression.php',
@@ -3023,6 +3041,13 @@ return array(
'Twig\\Node\\Expression\\Unary\\NegUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/NegUnary.php',
'Twig\\Node\\Expression\\Unary\\NotUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/NotUnary.php',
'Twig\\Node\\Expression\\Unary\\PosUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/PosUnary.php',
+ 'Twig\\Node\\Expression\\Unary\\SpreadUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/SpreadUnary.php',
+ 'Twig\\Node\\Expression\\Unary\\StringCastUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/StringCastUnary.php',
+ 'Twig\\Node\\Expression\\Variable\\AssignContextVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\AssignTemplateVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/AssignTemplateVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\ContextVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/ContextVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\LocalVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/LocalVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\TemplateVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/TemplateVariable.php',
'Twig\\Node\\Expression\\VariadicExpression' => $vendorDir . '/twig/twig/src/Node/Expression/VariadicExpression.php',
'Twig\\Node\\FlushNode' => $vendorDir . '/twig/twig/src/Node/FlushNode.php',
'Twig\\Node\\ForLoopNode' => $vendorDir . '/twig/twig/src/Node/ForLoopNode.php',
@@ -3032,14 +3057,18 @@ return array(
'Twig\\Node\\IncludeNode' => $vendorDir . '/twig/twig/src/Node/IncludeNode.php',
'Twig\\Node\\MacroNode' => $vendorDir . '/twig/twig/src/Node/MacroNode.php',
'Twig\\Node\\ModuleNode' => $vendorDir . '/twig/twig/src/Node/ModuleNode.php',
+ 'Twig\\Node\\NameDeprecation' => $vendorDir . '/twig/twig/src/Node/NameDeprecation.php',
'Twig\\Node\\Node' => $vendorDir . '/twig/twig/src/Node/Node.php',
'Twig\\Node\\NodeCaptureInterface' => $vendorDir . '/twig/twig/src/Node/NodeCaptureInterface.php',
'Twig\\Node\\NodeOutputInterface' => $vendorDir . '/twig/twig/src/Node/NodeOutputInterface.php',
+ 'Twig\\Node\\Nodes' => $vendorDir . '/twig/twig/src/Node/Nodes.php',
'Twig\\Node\\PrintNode' => $vendorDir . '/twig/twig/src/Node/PrintNode.php',
'Twig\\Node\\SandboxNode' => $vendorDir . '/twig/twig/src/Node/SandboxNode.php',
'Twig\\Node\\SetNode' => $vendorDir . '/twig/twig/src/Node/SetNode.php',
'Twig\\Node\\TextNode' => $vendorDir . '/twig/twig/src/Node/TextNode.php',
+ 'Twig\\Node\\TypesNode' => $vendorDir . '/twig/twig/src/Node/TypesNode.php',
'Twig\\Node\\WithNode' => $vendorDir . '/twig/twig/src/Node/WithNode.php',
+ 'Twig\\OperatorPrecedenceChange' => $vendorDir . '/twig/twig/src/OperatorPrecedenceChange.php',
'Twig\\Parser' => $vendorDir . '/twig/twig/src/Parser.php',
'Twig\\Profiler\\Dumper\\BaseDumper' => $vendorDir . '/twig/twig/src/Profiler/Dumper/BaseDumper.php',
'Twig\\Profiler\\Dumper\\BlackfireDumper' => $vendorDir . '/twig/twig/src/Profiler/Dumper/BlackfireDumper.php',
@@ -3052,6 +3081,7 @@ return array(
'Twig\\RuntimeLoader\\ContainerRuntimeLoader' => $vendorDir . '/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php',
'Twig\\RuntimeLoader\\FactoryRuntimeLoader' => $vendorDir . '/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php',
'Twig\\RuntimeLoader\\RuntimeLoaderInterface' => $vendorDir . '/twig/twig/src/RuntimeLoader/RuntimeLoaderInterface.php',
+ 'Twig\\Runtime\\EscaperRuntime' => $vendorDir . '/twig/twig/src/Runtime/EscaperRuntime.php',
'Twig\\Sandbox\\SecurityError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityError.php',
'Twig\\Sandbox\\SecurityNotAllowedFilterError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php',
'Twig\\Sandbox\\SecurityNotAllowedFunctionError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php',
@@ -3060,6 +3090,7 @@ return array(
'Twig\\Sandbox\\SecurityNotAllowedTagError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php',
'Twig\\Sandbox\\SecurityPolicy' => $vendorDir . '/twig/twig/src/Sandbox/SecurityPolicy.php',
'Twig\\Sandbox\\SecurityPolicyInterface' => $vendorDir . '/twig/twig/src/Sandbox/SecurityPolicyInterface.php',
+ 'Twig\\Sandbox\\SourcePolicyInterface' => $vendorDir . '/twig/twig/src/Sandbox/SourcePolicyInterface.php',
'Twig\\Source' => $vendorDir . '/twig/twig/src/Source.php',
'Twig\\Template' => $vendorDir . '/twig/twig/src/Template.php',
'Twig\\TemplateWrapper' => $vendorDir . '/twig/twig/src/TemplateWrapper.php',
@@ -3075,6 +3106,7 @@ return array(
'Twig\\TokenParser\\FlushTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/FlushTokenParser.php',
'Twig\\TokenParser\\ForTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ForTokenParser.php',
'Twig\\TokenParser\\FromTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/FromTokenParser.php',
+ 'Twig\\TokenParser\\GuardTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/GuardTokenParser.php',
'Twig\\TokenParser\\IfTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/IfTokenParser.php',
'Twig\\TokenParser\\ImportTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ImportTokenParser.php',
'Twig\\TokenParser\\IncludeTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/IncludeTokenParser.php',
@@ -3082,13 +3114,17 @@ return array(
'Twig\\TokenParser\\SandboxTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/SandboxTokenParser.php',
'Twig\\TokenParser\\SetTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/SetTokenParser.php',
'Twig\\TokenParser\\TokenParserInterface' => $vendorDir . '/twig/twig/src/TokenParser/TokenParserInterface.php',
+ 'Twig\\TokenParser\\TypesTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/TypesTokenParser.php',
'Twig\\TokenParser\\UseTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/UseTokenParser.php',
'Twig\\TokenParser\\WithTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/WithTokenParser.php',
'Twig\\TokenStream' => $vendorDir . '/twig/twig/src/TokenStream.php',
+ 'Twig\\TwigCallableInterface' => $vendorDir . '/twig/twig/src/TwigCallableInterface.php',
'Twig\\TwigFilter' => $vendorDir . '/twig/twig/src/TwigFilter.php',
'Twig\\TwigFunction' => $vendorDir . '/twig/twig/src/TwigFunction.php',
'Twig\\TwigTest' => $vendorDir . '/twig/twig/src/TwigTest.php',
+ 'Twig\\Util\\CallableArgumentsExtractor' => $vendorDir . '/twig/twig/src/Util/CallableArgumentsExtractor.php',
'Twig\\Util\\DeprecationCollector' => $vendorDir . '/twig/twig/src/Util/DeprecationCollector.php',
+ 'Twig\\Util\\ReflectionCallable' => $vendorDir . '/twig/twig/src/Util/ReflectionCallable.php',
'Twig\\Util\\TemplateDirIterator' => $vendorDir . '/twig/twig/src/Util/TemplateDirIterator.php',
'UIExtKeyWidget' => $baseDir . '/application/ui.extkeywidget.class.inc.php',
'UIHTMLEditorWidget' => $baseDir . '/application/ui.htmleditorwidget.class.inc.php',
diff --git a/lib/composer/autoload_files.php b/lib/composer/autoload_files.php
index 4124e1e53..b3a813be2 100644
--- a/lib/composer/autoload_files.php
+++ b/lib/composer/autoload_files.php
@@ -12,6 +12,11 @@ return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
+ '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.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',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php
index 7666d905f..c79e6f1dc 100644
--- a/lib/composer/autoload_psr4.php
+++ b/lib/composer/autoload_psr4.php
@@ -11,6 +11,7 @@ return array(
'TheNetworg\\OAuth2\\Client\\' => array($vendorDir . '/thenetworg/oauth2-azure/src'),
'Symfony\\Runtime\\Symfony\\Component\\' => array($vendorDir . '/symfony/runtime/Internal'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
+ 'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php
index ac3a90c8e..9ecf11d57 100644
--- a/lib/composer/autoload_static.php
+++ b/lib/composer/autoload_static.php
@@ -13,6 +13,11 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
+ '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.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',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
@@ -39,6 +44,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
array (
'Symfony\\Runtime\\Symfony\\Component\\' => 34,
'Symfony\\Polyfill\\Php83\\' => 23,
+ 'Symfony\\Polyfill\\Php81\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Php72\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
@@ -132,6 +138,10 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
),
+ 'Symfony\\Polyfill\\Php81\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
+ ),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -558,6 +568,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'CSVBulkExport' => __DIR__ . '/../..' . '/core/csvbulkexport.class.inc.php',
'CSVParser' => __DIR__ . '/../..' . '/core/csvparser.class.inc.php',
'CSVParserException' => __DIR__ . '/../..' . '/application/exceptions/CSVParserException.php',
+ 'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'CellChangeSpec' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'CellStatus_Ambiguous' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'CellStatus_Issue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
@@ -1883,6 +1894,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'RestResultWithObjects' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestResultWithRelations' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestUtils' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
+ 'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'RotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
'RowStatus' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
@@ -3249,6 +3261,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Symfony\\Polyfill\\Php72\\Php72' => __DIR__ . '/..' . '/symfony/polyfill-php72/Php72.php',
'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php',
'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php',
+ 'Symfony\\Polyfill\\Php81\\Php81' => __DIR__ . '/..' . '/symfony/polyfill-php81/Php81.php',
'Symfony\\Polyfill\\Php83\\Php83' => __DIR__ . '/..' . '/symfony/polyfill-php83/Php83.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\ApplicationRuntime' => __DIR__ . '/..' . '/symfony/runtime/Internal/Console/ApplicationRuntime.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\Command\\CommandRuntime' => __DIR__ . '/..' . '/symfony/runtime/Internal/Console/Command/CommandRuntime.php',
@@ -3296,10 +3309,17 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'TriggerOnStateLeave' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
'TriggerOnThresholdReached' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
'TrueExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
+ 'Twig\\AbstractTwigCallable' => __DIR__ . '/..' . '/twig/twig/src/AbstractTwigCallable.php',
+ 'Twig\\Attribute\\FirstClassTwigCallableReady' => __DIR__ . '/..' . '/twig/twig/src/Attribute/FirstClassTwigCallableReady.php',
+ 'Twig\\Attribute\\YieldReady' => __DIR__ . '/..' . '/twig/twig/src/Attribute/YieldReady.php',
'Twig\\Cache\\CacheInterface' => __DIR__ . '/..' . '/twig/twig/src/Cache/CacheInterface.php',
+ 'Twig\\Cache\\ChainCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/ChainCache.php',
'Twig\\Cache\\FilesystemCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/FilesystemCache.php',
'Twig\\Cache\\NullCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/NullCache.php',
+ 'Twig\\Cache\\ReadOnlyFilesystemCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/ReadOnlyFilesystemCache.php',
+ 'Twig\\Cache\\RemovableCacheInterface' => __DIR__ . '/..' . '/twig/twig/src/Cache/RemovableCacheInterface.php',
'Twig\\Compiler' => __DIR__ . '/..' . '/twig/twig/src/Compiler.php',
+ 'Twig\\DeprecatedCallableInfo' => __DIR__ . '/..' . '/twig/twig/src/DeprecatedCallableInfo.php',
'Twig\\Environment' => __DIR__ . '/..' . '/twig/twig/src/Environment.php',
'Twig\\Error\\Error' => __DIR__ . '/..' . '/twig/twig/src/Error/Error.php',
'Twig\\Error\\LoaderError' => __DIR__ . '/..' . '/twig/twig/src/Error/LoaderError.php',
@@ -3319,6 +3339,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Extension\\SandboxExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/SandboxExtension.php',
'Twig\\Extension\\StagingExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/StagingExtension.php',
'Twig\\Extension\\StringLoaderExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/StringLoaderExtension.php',
+ 'Twig\\Extension\\YieldNotReadyExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/YieldNotReadyExtension.php',
'Twig\\FileExtensionEscapingStrategy' => __DIR__ . '/..' . '/twig/twig/src/FileExtensionEscapingStrategy.php',
'Twig\\Lexer' => __DIR__ . '/..' . '/twig/twig/src/Lexer.php',
'Twig\\Loader\\ArrayLoader' => __DIR__ . '/..' . '/twig/twig/src/Loader/ArrayLoader.php',
@@ -3329,21 +3350,23 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\NodeTraverser' => __DIR__ . '/..' . '/twig/twig/src/NodeTraverser.php',
'Twig\\NodeVisitor\\AbstractNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php',
'Twig\\NodeVisitor\\EscaperNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php',
- 'Twig\\NodeVisitor\\MacroAutoImportNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php',
'Twig\\NodeVisitor\\NodeVisitorInterface' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/NodeVisitorInterface.php',
'Twig\\NodeVisitor\\OptimizerNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php',
'Twig\\NodeVisitor\\SafeAnalysisNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php',
'Twig\\NodeVisitor\\SandboxNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php',
+ 'Twig\\NodeVisitor\\YieldNotReadyNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php',
'Twig\\Node\\AutoEscapeNode' => __DIR__ . '/..' . '/twig/twig/src/Node/AutoEscapeNode.php',
'Twig\\Node\\BlockNode' => __DIR__ . '/..' . '/twig/twig/src/Node/BlockNode.php',
'Twig\\Node\\BlockReferenceNode' => __DIR__ . '/..' . '/twig/twig/src/Node/BlockReferenceNode.php',
'Twig\\Node\\BodyNode' => __DIR__ . '/..' . '/twig/twig/src/Node/BodyNode.php',
+ 'Twig\\Node\\CaptureNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CaptureNode.php',
'Twig\\Node\\CheckSecurityCallNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckSecurityCallNode.php',
'Twig\\Node\\CheckSecurityNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckSecurityNode.php',
'Twig\\Node\\CheckToStringNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckToStringNode.php',
'Twig\\Node\\DeprecatedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/DeprecatedNode.php',
'Twig\\Node\\DoNode' => __DIR__ . '/..' . '/twig/twig/src/Node/DoNode.php',
'Twig\\Node\\EmbedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/EmbedNode.php',
+ 'Twig\\Node\\EmptyNode' => __DIR__ . '/..' . '/twig/twig/src/Node/EmptyNode.php',
'Twig\\Node\\Expression\\AbstractExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/AbstractExpression.php',
'Twig\\Node\\Expression\\ArrayExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ArrayExpression.php',
'Twig\\Node\\Expression\\ArrowFunctionExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ArrowFunctionExpression.php',
@@ -3377,15 +3400,20 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Node\\Expression\\Binary\\SpaceshipBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/SpaceshipBinary.php',
'Twig\\Node\\Expression\\Binary\\StartsWithBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php',
'Twig\\Node\\Expression\\Binary\\SubBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/SubBinary.php',
+ 'Twig\\Node\\Expression\\Binary\\XorBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/XorBinary.php',
'Twig\\Node\\Expression\\BlockReferenceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/BlockReferenceExpression.php',
'Twig\\Node\\Expression\\CallExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/CallExpression.php',
'Twig\\Node\\Expression\\ConditionalExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ConditionalExpression.php',
'Twig\\Node\\Expression\\ConstantExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ConstantExpression.php',
'Twig\\Node\\Expression\\FilterExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FilterExpression.php',
'Twig\\Node\\Expression\\Filter\\DefaultFilter' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Filter/DefaultFilter.php',
+ 'Twig\\Node\\Expression\\Filter\\RawFilter' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Filter/RawFilter.php',
'Twig\\Node\\Expression\\FunctionExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionExpression.php',
+ 'Twig\\Node\\Expression\\FunctionNode\\EnumCasesFunction' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php',
+ 'Twig\\Node\\Expression\\FunctionNode\\EnumFunction' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php',
'Twig\\Node\\Expression\\GetAttrExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/GetAttrExpression.php',
'Twig\\Node\\Expression\\InlinePrint' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/InlinePrint.php',
+ 'Twig\\Node\\Expression\\MacroReferenceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/MacroReferenceExpression.php',
'Twig\\Node\\Expression\\MethodCallExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/MethodCallExpression.php',
'Twig\\Node\\Expression\\NameExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/NameExpression.php',
'Twig\\Node\\Expression\\NullCoalesceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/NullCoalesceExpression.php',
@@ -3403,6 +3431,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Node\\Expression\\Unary\\NegUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/NegUnary.php',
'Twig\\Node\\Expression\\Unary\\NotUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/NotUnary.php',
'Twig\\Node\\Expression\\Unary\\PosUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/PosUnary.php',
+ 'Twig\\Node\\Expression\\Unary\\SpreadUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/SpreadUnary.php',
+ 'Twig\\Node\\Expression\\Unary\\StringCastUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/StringCastUnary.php',
+ 'Twig\\Node\\Expression\\Variable\\AssignContextVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\AssignTemplateVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/AssignTemplateVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\ContextVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/ContextVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\LocalVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/LocalVariable.php',
+ 'Twig\\Node\\Expression\\Variable\\TemplateVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/TemplateVariable.php',
'Twig\\Node\\Expression\\VariadicExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/VariadicExpression.php',
'Twig\\Node\\FlushNode' => __DIR__ . '/..' . '/twig/twig/src/Node/FlushNode.php',
'Twig\\Node\\ForLoopNode' => __DIR__ . '/..' . '/twig/twig/src/Node/ForLoopNode.php',
@@ -3412,14 +3447,18 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Node\\IncludeNode' => __DIR__ . '/..' . '/twig/twig/src/Node/IncludeNode.php',
'Twig\\Node\\MacroNode' => __DIR__ . '/..' . '/twig/twig/src/Node/MacroNode.php',
'Twig\\Node\\ModuleNode' => __DIR__ . '/..' . '/twig/twig/src/Node/ModuleNode.php',
+ 'Twig\\Node\\NameDeprecation' => __DIR__ . '/..' . '/twig/twig/src/Node/NameDeprecation.php',
'Twig\\Node\\Node' => __DIR__ . '/..' . '/twig/twig/src/Node/Node.php',
'Twig\\Node\\NodeCaptureInterface' => __DIR__ . '/..' . '/twig/twig/src/Node/NodeCaptureInterface.php',
'Twig\\Node\\NodeOutputInterface' => __DIR__ . '/..' . '/twig/twig/src/Node/NodeOutputInterface.php',
+ 'Twig\\Node\\Nodes' => __DIR__ . '/..' . '/twig/twig/src/Node/Nodes.php',
'Twig\\Node\\PrintNode' => __DIR__ . '/..' . '/twig/twig/src/Node/PrintNode.php',
'Twig\\Node\\SandboxNode' => __DIR__ . '/..' . '/twig/twig/src/Node/SandboxNode.php',
'Twig\\Node\\SetNode' => __DIR__ . '/..' . '/twig/twig/src/Node/SetNode.php',
'Twig\\Node\\TextNode' => __DIR__ . '/..' . '/twig/twig/src/Node/TextNode.php',
+ 'Twig\\Node\\TypesNode' => __DIR__ . '/..' . '/twig/twig/src/Node/TypesNode.php',
'Twig\\Node\\WithNode' => __DIR__ . '/..' . '/twig/twig/src/Node/WithNode.php',
+ 'Twig\\OperatorPrecedenceChange' => __DIR__ . '/..' . '/twig/twig/src/OperatorPrecedenceChange.php',
'Twig\\Parser' => __DIR__ . '/..' . '/twig/twig/src/Parser.php',
'Twig\\Profiler\\Dumper\\BaseDumper' => __DIR__ . '/..' . '/twig/twig/src/Profiler/Dumper/BaseDumper.php',
'Twig\\Profiler\\Dumper\\BlackfireDumper' => __DIR__ . '/..' . '/twig/twig/src/Profiler/Dumper/BlackfireDumper.php',
@@ -3432,6 +3471,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\RuntimeLoader\\ContainerRuntimeLoader' => __DIR__ . '/..' . '/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php',
'Twig\\RuntimeLoader\\FactoryRuntimeLoader' => __DIR__ . '/..' . '/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php',
'Twig\\RuntimeLoader\\RuntimeLoaderInterface' => __DIR__ . '/..' . '/twig/twig/src/RuntimeLoader/RuntimeLoaderInterface.php',
+ 'Twig\\Runtime\\EscaperRuntime' => __DIR__ . '/..' . '/twig/twig/src/Runtime/EscaperRuntime.php',
'Twig\\Sandbox\\SecurityError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityError.php',
'Twig\\Sandbox\\SecurityNotAllowedFilterError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php',
'Twig\\Sandbox\\SecurityNotAllowedFunctionError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php',
@@ -3440,6 +3480,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Sandbox\\SecurityNotAllowedTagError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php',
'Twig\\Sandbox\\SecurityPolicy' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityPolicy.php',
'Twig\\Sandbox\\SecurityPolicyInterface' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityPolicyInterface.php',
+ 'Twig\\Sandbox\\SourcePolicyInterface' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SourcePolicyInterface.php',
'Twig\\Source' => __DIR__ . '/..' . '/twig/twig/src/Source.php',
'Twig\\Template' => __DIR__ . '/..' . '/twig/twig/src/Template.php',
'Twig\\TemplateWrapper' => __DIR__ . '/..' . '/twig/twig/src/TemplateWrapper.php',
@@ -3455,6 +3496,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\TokenParser\\FlushTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/FlushTokenParser.php',
'Twig\\TokenParser\\ForTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ForTokenParser.php',
'Twig\\TokenParser\\FromTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/FromTokenParser.php',
+ 'Twig\\TokenParser\\GuardTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/GuardTokenParser.php',
'Twig\\TokenParser\\IfTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/IfTokenParser.php',
'Twig\\TokenParser\\ImportTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ImportTokenParser.php',
'Twig\\TokenParser\\IncludeTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/IncludeTokenParser.php',
@@ -3462,13 +3504,17 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\TokenParser\\SandboxTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/SandboxTokenParser.php',
'Twig\\TokenParser\\SetTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/SetTokenParser.php',
'Twig\\TokenParser\\TokenParserInterface' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/TokenParserInterface.php',
+ 'Twig\\TokenParser\\TypesTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/TypesTokenParser.php',
'Twig\\TokenParser\\UseTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/UseTokenParser.php',
'Twig\\TokenParser\\WithTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/WithTokenParser.php',
'Twig\\TokenStream' => __DIR__ . '/..' . '/twig/twig/src/TokenStream.php',
+ 'Twig\\TwigCallableInterface' => __DIR__ . '/..' . '/twig/twig/src/TwigCallableInterface.php',
'Twig\\TwigFilter' => __DIR__ . '/..' . '/twig/twig/src/TwigFilter.php',
'Twig\\TwigFunction' => __DIR__ . '/..' . '/twig/twig/src/TwigFunction.php',
'Twig\\TwigTest' => __DIR__ . '/..' . '/twig/twig/src/TwigTest.php',
+ 'Twig\\Util\\CallableArgumentsExtractor' => __DIR__ . '/..' . '/twig/twig/src/Util/CallableArgumentsExtractor.php',
'Twig\\Util\\DeprecationCollector' => __DIR__ . '/..' . '/twig/twig/src/Util/DeprecationCollector.php',
+ 'Twig\\Util\\ReflectionCallable' => __DIR__ . '/..' . '/twig/twig/src/Util/ReflectionCallable.php',
'Twig\\Util\\TemplateDirIterator' => __DIR__ . '/..' . '/twig/twig/src/Util/TemplateDirIterator.php',
'UIExtKeyWidget' => __DIR__ . '/../..' . '/application/ui.extkeywidget.class.inc.php',
'UIHTMLEditorWidget' => __DIR__ . '/../..' . '/application/ui.htmleditorwidget.class.inc.php',
diff --git a/lib/composer/installed.json b/lib/composer/installed.json
index dba4ce6ab..274f34f90 100644
--- a/lib/composer/installed.json
+++ b/lib/composer/installed.json
@@ -4162,6 +4162,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": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "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/polyfill-php83",
"version": "v1.28.0",
@@ -5398,33 +5477,41 @@
},
{
"name": "twig/twig",
- "version": "v3.8.0",
- "version_normalized": "3.8.0.0",
+ "version": "v3.16.0",
+ "version_normalized": "3.16.0.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
+ "reference": "475ad2dc97d65d8631393e721e7e44fb544f0561"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
- "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/475ad2dc97d65d8631393e721e7e44fb544f0561",
+ "reference": "475ad2dc97d65d8631393e721e7e44fb544f0561",
"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-php80": "^1.22"
+ "symfony/polyfill-php81": "^1.29"
},
"require-dev": {
+ "phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
- "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
- "time": "2023-11-21T18:54:41+00:00",
+ "time": "2024-11-29T08:27:05+00:00",
"type": "library",
"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/"
}
@@ -5457,7 +5544,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
- "source": "https://github.com/twigphp/Twig/tree/v3.8.0"
+ "source": "https://github.com/twigphp/Twig/tree/v3.16.0"
},
"funding": [
{
diff --git a/lib/composer/installed.php b/lib/composer/installed.php
index b30a7b375..d8fd41591 100644
--- a/lib/composer/installed.php
+++ b/lib/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'combodo/itop',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
- 'reference' => '5ae2fdee94b925808451355b98b941351e0b4fcd',
+ 'reference' => '1bf53bae2a481c76bba07e2f6d3447b4008efacd',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -22,7 +22,7 @@
'combodo/itop' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
- 'reference' => '5ae2fdee94b925808451355b98b941351e0b4fcd',
+ 'reference' => '1bf53bae2a481c76bba07e2f6d3447b4008efacd',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -587,6 +587,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/polyfill-php83' => array(
'pretty_version' => 'v1.28.0',
'version' => '1.28.0.0',
@@ -729,9 +738,9 @@
'dev_requirement' => false,
),
'twig/twig' => array(
- 'pretty_version' => 'v3.8.0',
- 'version' => '3.8.0.0',
- 'reference' => '9d15f0ac07f44dc4217883ec6ae02fd555c6f71d',
+ 'pretty_version' => 'v3.16.0',
+ 'version' => '3.16.0.0',
+ 'reference' => '475ad2dc97d65d8631393e721e7e44fb544f0561',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),
diff --git a/lib/symfony/polyfill-php81/LICENSE b/lib/symfony/polyfill-php81/LICENSE
new file mode 100644
index 000000000..99c6bdf35
--- /dev/null
+++ b/lib/symfony/polyfill-php81/LICENSE
@@ -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.
diff --git a/lib/symfony/polyfill-php81/Php81.php b/lib/symfony/polyfill-php81/Php81.php
new file mode 100644
index 000000000..f0507b765
--- /dev/null
+++ b/lib/symfony/polyfill-php81/Php81.php
@@ -0,0 +1,37 @@
+
+ *
+ * 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
+ *
+ * @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;
+ }
+}
diff --git a/lib/symfony/polyfill-php81/README.md b/lib/symfony/polyfill-php81/README.md
new file mode 100644
index 000000000..c07ef7820
--- /dev/null
+++ b/lib/symfony/polyfill-php81/README.md
@@ -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).
diff --git a/lib/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php b/lib/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php
new file mode 100644
index 000000000..5ff93fcaf
--- /dev/null
+++ b/lib/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php
@@ -0,0 +1,51 @@
+
+ *
+ * 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;
+ }
+ }
+}
diff --git a/lib/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php b/lib/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php
new file mode 100644
index 000000000..cb7720a8d
--- /dev/null
+++ b/lib/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php
@@ -0,0 +1,20 @@
+
+ *
+ * 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()
+ {
+ }
+ }
+}
diff --git a/lib/symfony/polyfill-php81/bootstrap.php b/lib/symfony/polyfill-php81/bootstrap.php
new file mode 100644
index 000000000..9f872e02f
--- /dev/null
+++ b/lib/symfony/polyfill-php81/bootstrap.php
@@ -0,0 +1,28 @@
+
+ *
+ * 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; }
+}
diff --git a/lib/symfony/polyfill-php81/composer.json b/lib/symfony/polyfill-php81/composer.json
new file mode 100644
index 000000000..28b6408ea
--- /dev/null
+++ b/lib/symfony/polyfill-php81/composer.json
@@ -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"
+ }
+ }
+}
diff --git a/lib/twig/twig/CHANGELOG b/lib/twig/twig/CHANGELOG
index 2b8341fd8..6f3062d3f 100644
--- a/lib/twig/twig/CHANGELOG
+++ b/lib/twig/twig/CHANGELOG
@@ -1,3 +1,202 @@
+# 3.16.0 (2024-XX-XX)
+
+ * Deprecate `InlinePrint`
+ * Fix having macro variables starting with an underscore
+ * Deprecate not passing a `Source` instance to `TokenStream`
+ * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead
+
+# 3.15.0 (2024-11-17)
+
+ * [BC BREAK] Add support for accessing class constants with the dot operator;
+ this can be a BC break if you don't use UPPERCASE constant names
+ * Add Spanish inflector support for the `plural` and `singular` filters in the String extension
+ * Deprecate `TempNameExpression` in favor of `LocalVariable`
+ * Deprecate `NameExpression` in favor of `ContextVariable`
+ * Deprecate `AssignNameExpression` in favor of `AssignContextVariable`
+ * Remove `MacroAutoImportNodeVisitor`
+ * Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression`
+ * Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros
+ * Fix support for the "is defined" test on inherited macros
+ * Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`)
+ * Add named arguments support for macros
+ * Add a new `guard` tag that allows to test if some Twig callables are available at compilation time
+ * Allow arrow functions everywhere
+ * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`)
+ * Add support for triggering deprecations for future operator precedence changes
+ * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence
+ * Deprecate using the `??` binary operator without explicit parentheses
+ * Deprecate using the `~` binary operator in an expression with `+` or `-` without using parentheses to clarify precedence
+ * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression`
+ * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`)
+ * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead.
+ * Add support for inline comments
+ * Add `Profile::getStartTime()` and `Profile::getEndTime()`
+ * Fix "ignore missing" when used on an "embed" tag
+ * Fix the possibility to override an aliased block (via use)
+ * Add template cache hot reload
+ * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature
+ They were automatically converted to snake-cased before
+ * Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead
+ * Add support for argument unpackaging
+ * Add JSON support for the file extension escaping strategy
+ * Support Markup instances (and any other \Stringable) as dynamic mapping keys
+ * Deprecate the `sandbox` tag
+ * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options)
+ * Add the `enum` function
+ * Add support for logical `xor` operator
+
+# 3.14.2 (2024-11-07)
+
+ * Fix an infinite recursion in the sandbox code
+
+# 3.14.1 (2024-11-06)
+
+ * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
+ They are now checked via the property policy
+ * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
+ under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
+
+# 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.2 (2024-11-06)
+
+ * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
+ They are now checked via the property policy
+ * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
+ under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
+
+# 3.11.1 (2024-09-10)
+
+ * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
+
+# 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
@@ -186,7 +385,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()
diff --git a/lib/twig/twig/README.rst b/lib/twig/twig/README.rst
index fbe7e9a9f..7bf8c673e 100644
--- a/lib/twig/twig/README.rst
+++ b/lib/twig/twig/README.rst
@@ -11,7 +11,7 @@ Sponsors
.. raw:: html
-
+
diff --git a/lib/twig/twig/composer.json b/lib/twig/twig/composer.json
index 1b1726fe8..079f1daf3 100644
--- a/lib/twig/twig/composer.json
+++ b/lib/twig/twig/composer.json
@@ -24,16 +24,24 @@
}
],
"require": {
- "php": ">=7.2.5",
- "symfony/polyfill-php80": "^1.22",
+ "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": "^5.4.9|^6.3|^7.0",
- "psr/container": "^1.0|^2.0"
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
+ "psr/container": "^1.0|^2.0",
+ "phpstan/phpstan": "^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/"
}
diff --git a/lib/twig/twig/phpstan-baseline.neon b/lib/twig/twig/phpstan-baseline.neon
new file mode 100644
index 000000000..1121ae1b2
--- /dev/null
+++ b/lib/twig/twig/phpstan-baseline.neon
@@ -0,0 +1,25 @@
+parameters:
+ ignoreErrors:
+ - # The method is dynamically generated by the CheckSecurityNode
+ message: '#^Call to an undefined method Twig\\Template\:\:checkSecurity\(\)\.$#'
+ identifier: method.notFound
+ count: 1
+ path: src/Extension/CoreExtension.php
+
+ - # Avoid BC-break
+ message: '#^Constructor of class Twig\\Node\\ForNode has an unused parameter \$ifexpr\.$#'
+ identifier: constructor.unusedParameter
+ count: 1
+ path: src/Node/ForNode.php
+
+ - # 2 parameters will be required
+ message: '#^Method Twig\\Node\\IncludeNode\:\:addGetTemplate\(\) invoked with 2 parameters, 1 required\.$#'
+ identifier: arguments.count
+ count: 1
+ path: src/Node/IncludeNode.php
+
+ - # int|string will be supported in 4.x
+ message: '#^PHPDoc tag @param for parameter $name with type int|string is not subtype of native type string\.$#'
+ identifier: parameter.phpDocType
+ count: 5
+ path: src/Node/Node.php
diff --git a/lib/twig/twig/phpstan.neon.dist b/lib/twig/twig/phpstan.neon.dist
new file mode 100644
index 000000000..6d94e4109
--- /dev/null
+++ b/lib/twig/twig/phpstan.neon.dist
@@ -0,0 +1,9 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: 3
+ paths:
+ - src
+ excludePaths:
+ - src/Test
diff --git a/lib/twig/twig/src/AbstractTwigCallable.php b/lib/twig/twig/src/AbstractTwigCallable.php
new file mode 100644
index 000000000..d85f0f861
--- /dev/null
+++ b/lib/twig/twig/src/AbstractTwigCallable.php
@@ -0,0 +1,184 @@
+
+ */
+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,
+ 'deprecation_info' => null,
+ 'deprecated' => false,
+ 'deprecating_package' => '',
+ 'alternative' => null,
+ ], $options);
+
+ if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) {
+ throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class));
+ }
+
+ if ($this->options['deprecated']) {
+ if ($this->options['deprecation_info']) {
+ throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.');
+ }
+
+ trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.');
+
+ $this->options['deprecation_info'] = new DeprecatedCallableInfo(
+ $this->options['deprecating_package'],
+ $this->options['deprecated'],
+ null,
+ $this->options['alternative'],
+ );
+ }
+
+ if ($this->options['deprecation_info']) {
+ $this->options['deprecation_info']->setName($name);
+ $this->options['deprecation_info']->setType($this->getType());
+ }
+ }
+
+ 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'];
+ }
+
+ /**
+ * @return static
+ */
+ 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['deprecation_info'];
+ }
+
+ public function triggerDeprecation(?string $file = null, ?int $line = null): void
+ {
+ $this->options['deprecation_info']->triggerDeprecation($file, $line);
+ }
+
+ /**
+ * @deprecated since Twig 3.15
+ */
+ public function getDeprecatingPackage(): string
+ {
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
+
+ return $this->options['deprecating_package'];
+ }
+
+ /**
+ * @deprecated since Twig 3.15
+ */
+ public function getDeprecatedVersion(): string
+ {
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
+
+ return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
+ }
+
+ /**
+ * @deprecated since Twig 3.15
+ */
+ public function getAlternative(): ?string
+ {
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
+
+ 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);
+ }
+}
diff --git a/lib/twig/twig/src/Attribute/FirstClassTwigCallableReady.php b/lib/twig/twig/src/Attribute/FirstClassTwigCallableReady.php
new file mode 100644
index 000000000..ffd8cffc8
--- /dev/null
+++ b/lib/twig/twig/src/Attribute/FirstClassTwigCallableReady.php
@@ -0,0 +1,20 @@
+
+ */
+final class ChainCache implements CacheInterface, RemovableCacheInterface
+{
+ /**
+ * @param iterable $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;
+ }
+
+ public function remove(string $name, string $cls): void
+ {
+ foreach ($this->caches as $cache) {
+ if ($cache instanceof RemovableCacheInterface) {
+ $cache->remove($name, $cls);
+ }
+ }
+ }
+
+ /**
+ * @return string[]
+ */
+ private function splitKey(string $key): array
+ {
+ return array_reverse(explode('#', $key, 2));
+ }
+}
diff --git a/lib/twig/twig/src/Cache/FilesystemCache.php b/lib/twig/twig/src/Cache/FilesystemCache.php
index 4024adbd7..5840585e3 100644
--- a/lib/twig/twig/src/Cache/FilesystemCache.php
+++ b/lib/twig/twig/src/Cache/FilesystemCache.php
@@ -16,7 +16,7 @@ namespace Twig\Cache;
*
* @author Andrew Tch
*/
-class FilesystemCache implements CacheInterface
+class FilesystemCache implements CacheInterface, RemovableCacheInterface
{
public const FORCE_BYTECODE_INVALIDATION = 1;
@@ -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));
@@ -73,7 +73,15 @@ 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 remove(string $name, string $cls): void
+ {
+ $key = $this->generateKey($name, $cls);
+ if (!@unlink($key) && file_exists($key)) {
+ throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key));
+ }
}
public function getTimestamp(string $key): int
diff --git a/lib/twig/twig/src/Cache/NullCache.php b/lib/twig/twig/src/Cache/NullCache.php
index 8d20d59d8..1ae216928 100644
--- a/lib/twig/twig/src/Cache/NullCache.php
+++ b/lib/twig/twig/src/Cache/NullCache.php
@@ -16,7 +16,7 @@ namespace Twig\Cache;
*
* @author Fabien Potencier
*/
-final class NullCache implements CacheInterface
+final class NullCache implements CacheInterface, RemovableCacheInterface
{
public function generateKey(string $name, string $className): string
{
@@ -35,4 +35,8 @@ final class NullCache implements CacheInterface
{
return 0;
}
+
+ public function remove(string $name, string $cls): void
+ {
+ }
}
diff --git a/lib/twig/twig/src/Cache/ReadOnlyFilesystemCache.php b/lib/twig/twig/src/Cache/ReadOnlyFilesystemCache.php
new file mode 100644
index 000000000..3ba6514c9
--- /dev/null
+++ b/lib/twig/twig/src/Cache/ReadOnlyFilesystemCache.php
@@ -0,0 +1,25 @@
+
+ */
+class ReadOnlyFilesystemCache extends FilesystemCache
+{
+ public function write(string $key, string $content): void
+ {
+ // Do nothing with the content, it's a read-only filesystem.
+ }
+}
diff --git a/lib/twig/twig/src/Cache/RemovableCacheInterface.php b/lib/twig/twig/src/Cache/RemovableCacheInterface.php
new file mode 100644
index 000000000..05da56913
--- /dev/null
+++ b/lib/twig/twig/src/Cache/RemovableCacheInterface.php
@@ -0,0 +1,20 @@
+
+ */
+interface RemovableCacheInterface
+{
+ public function remove(string $name, string $cls): void;
+}
diff --git a/lib/twig/twig/src/Compiler.php b/lib/twig/twig/src/Compiler.php
index eb652c61a..3166841e3 100644
--- a/lib/twig/twig/src/Compiler.php
+++ b/lib/twig/twig/src/Compiler.php
@@ -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
@@ -66,9 +67,20 @@ class Compiler
public function compile(Node $node, int $indentation = 0)
{
$this->reset($indentation);
- $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 #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node));
+ }
+
+ return $this;
+ } finally {
+ $this->didUseEcho = array_pop($this->didUseEchoStack);
+ }
}
/**
@@ -76,13 +88,24 @@ class Compiler
*/
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 #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node));
+ }
+
+ return $this;
+ } finally {
+ $this->didUseEcho = array_pop($this->didUseEchoStack);
+ }
}
/**
@@ -92,6 +115,7 @@ class Compiler
*/
public function raw(string $string)
{
+ $this->checkForEcho($string);
$this->source .= $string;
return $this;
@@ -105,6 +129,7 @@ class Compiler
public function write(...$strings)
{
foreach ($strings as $string) {
+ $this->checkForEcho($string);
$this->source .= str_repeat(' ', $this->indentation * 4).$string;
}
@@ -118,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;
}
@@ -170,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);
@@ -218,6 +243,15 @@ class Compiler
public function getVarName(): string
{
- return sprintf('__internal_compile_%d', $this->varNameSalt++);
+ return \sprintf('_v%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;
}
}
diff --git a/lib/twig/twig/src/DeprecatedCallableInfo.php b/lib/twig/twig/src/DeprecatedCallableInfo.php
new file mode 100644
index 000000000..2db9f3d28
--- /dev/null
+++ b/lib/twig/twig/src/DeprecatedCallableInfo.php
@@ -0,0 +1,67 @@
+
+ */
+final class DeprecatedCallableInfo
+{
+ private string $type;
+ private string $name;
+
+ public function __construct(
+ private string $package,
+ private string $version,
+ private ?string $altName = null,
+ private ?string $altPackage = null,
+ private ?string $altVersion = null,
+ ) {
+ }
+
+ public function setType(string $type): void
+ {
+ $this->type = $type;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ public function triggerDeprecation(?string $file = null, ?int $line = null): void
+ {
+ $message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name);
+
+ if ($this->altName) {
+ $message .= \sprintf('; use "%s"', $this->altName);
+ if ($this->altPackage) {
+ $message .= \sprintf(' from the "%s" package', $this->altPackage);
+ }
+ if ($this->altVersion) {
+ $message .= \sprintf(' (available since version %s)', $this->altVersion);
+ }
+ $message .= ' instead';
+ }
+
+ if ($file) {
+ $message .= \sprintf(' in %s', $file);
+ if ($line) {
+ $message .= \sprintf(' at line %d', $line);
+ }
+ }
+
+ $message .= '.';
+
+ trigger_deprecation($this->package, $this->version, $message);
+ }
+}
diff --git a/lib/twig/twig/src/Environment.php b/lib/twig/twig/src/Environment.php
index d7d51cdb1..bc064e445 100644
--- a/lib/twig/twig/src/Environment.php
+++ b/lib/twig/twig/src/Environment.php
@@ -14,6 +14,7 @@ namespace Twig;
use Twig\Cache\CacheInterface;
use Twig\Cache\FilesystemCache;
use Twig\Cache\NullCache;
+use Twig\Cache\RemovableCacheInterface;
use Twig\Error\Error;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -22,6 +23,7 @@ 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;
@@ -30,6 +32,8 @@ 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;
@@ -40,10 +44,10 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
- public const VERSION = '3.8.0';
- public const VERSION_ID = 30800;
+ public const VERSION = '3.16.0';
+ public const VERSION_ID = 31600;
public const MAJOR_VERSION = 3;
- public const MINOR_VERSION = 8;
+ public const MINOR_VERSION = 16;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
@@ -60,12 +64,15 @@ class Environment
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;
+ private array $hotCache = [];
/**
* Constructor.
@@ -97,8 +104,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);
@@ -110,20 +121,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.
*/
@@ -206,6 +235,18 @@ class Environment
return $this->strictVariables;
}
+ public function removeCache(string $name): void
+ {
+ $cls = $this->getTemplateClass($name);
+ $this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16));
+
+ if ($this->cache instanceof RemovableCacheInterface) {
+ $this->cache->remove($name, $cls);
+ } else {
+ throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache)));
+ }
+ }
+
/**
* Gets the current cache implementation.
*
@@ -249,7 +290,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.
@@ -259,11 +299,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;
+ $key = ($this->hotCache[$name] ?? $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);
}
/**
@@ -308,6 +348,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));
}
@@ -318,8 +363,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
@@ -327,7 +372,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) {
@@ -348,8 +393,10 @@ class Environment
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
- $this->cache->write($key, $content);
- $this->cache->load($key);
+ if (!isset($this->hotCache[$name])) {
+ $this->cache->write($key, $content);
+ $this->cache->load($key);
+ }
if (!class_exists($mainCls, false)) {
/* Last line of defense if either $this->bcWriteCacheFile was used,
@@ -361,7 +408,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);
}
}
}
@@ -376,19 +423,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([
@@ -421,10 +468,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 $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
@@ -438,7 +485,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;
@@ -451,7 +500,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)
@@ -520,7 +569,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);
}
}
@@ -536,7 +585,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';
}
@@ -594,7 +643,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)
@@ -765,7 +818,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) {
@@ -776,8 +829,6 @@ class Environment
}
/**
- * @internal
- *
* @return array
*/
public function getGlobals(): array
@@ -793,17 +844,20 @@ 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();
}
/**
@@ -835,6 +889,7 @@ class Environment
self::VERSION,
(int) $this->debug,
(int) $this->strictVariables,
+ $this->useYield ? '1' : '0',
]);
}
}
diff --git a/lib/twig/twig/src/Error/Error.php b/lib/twig/twig/src/Error/Error.php
index bca1fa64c..61c309fa1 100644
--- a/lib/twig/twig/src/Error/Error.php
+++ b/lib/twig/twig/src/Error/Error.php
@@ -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, \Throwable $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;
@@ -142,16 +142,16 @@ class Error extends \Exception
}
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) {
diff --git a/lib/twig/twig/src/Error/SyntaxError.php b/lib/twig/twig/src/Error/SyntaxError.php
index 77c437c68..841b653f5 100644
--- a/lib/twig/twig/src/Error/SyntaxError.php
+++ b/lib/twig/twig/src/Error/SyntaxError.php
@@ -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))));
}
}
diff --git a/lib/twig/twig/src/ExpressionParser.php b/lib/twig/twig/src/ExpressionParser.php
index 13e0f0876..2fb528518 100644
--- a/lib/twig/twig/src/ExpressionParser.php
+++ b/lib/twig/twig/src/ExpressionParser.php
@@ -12,26 +12,31 @@
namespace Twig;
+use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Error\SyntaxError;
+use Twig\Node\EmptyNode;
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\MacroReferenceExpression;
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;
+use Twig\Node\Expression\Unary\SpreadUnary;
+use Twig\Node\Expression\Variable\AssignContextVariable;
+use Twig\Node\Expression\Variable\ContextVariable;
+use Twig\Node\Expression\Variable\LocalVariable;
+use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\Node;
+use Twig\Node\Nodes;
/**
* Parses expressions.
@@ -48,24 +53,50 @@ class ExpressionParser
public const OPERATOR_LEFT = 1;
public const OPERATOR_RIGHT = 2;
- private $parser;
- private $env;
- /** @var array}> */
+ /** @var array}> */
private $unaryOperators;
- /** @var array, associativity: self::OPERATOR_*}> */
+ /** @var array, associativity: self::OPERATOR_*}> */
private $binaryOperators;
+ private $readyNodes = [];
+ private array $precedenceChanges = [];
+ private bool $deprecationCheck = true;
- 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();
+
+ $ops = [];
+ foreach ($this->unaryOperators as $n => $c) {
+ $ops[] = $c + ['name' => $n, 'type' => 'unary'];
+ }
+ foreach ($this->binaryOperators as $n => $c) {
+ $ops[] = $c + ['name' => $n, 'type' => 'binary'];
+ }
+ foreach ($ops as $config) {
+ if (!isset($config['precedence_change'])) {
+ continue;
+ }
+ $name = $config['type'].'_'.$config['name'];
+ $min = min($config['precedence_change']->getNewPrecedence(), $config['precedence']);
+ $max = max($config['precedence_change']->getNewPrecedence(), $config['precedence']);
+ foreach ($ops as $c) {
+ if ($c['precedence'] > $min && $c['precedence'] < $max) {
+ $this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name;
+ }
+ }
+ }
}
- public function parseExpression($precedence = 0, $allowArrow = false)
+ public function parseExpression($precedence = 0)
{
- if ($allowArrow && $arrow = $this->parseArrow()) {
+ if (func_num_args() > 1) {
+ trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__);
+ }
+
+ if ($arrow = $this->parseArrow()) {
return $arrow;
}
@@ -82,11 +113,20 @@ 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'], true);
+ $previous = $this->setDeprecationCheck(true);
+ try {
+ $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
+ } finally {
+ $this->setDeprecationCheck($previous);
+ }
$class = $op['class'];
$expr = new $class($expr, $expr1, $token->getLine());
}
+ $expr->setAttribute('operator', 'binary_'.$token->getValue());
+
+ $this->triggerPrecedenceDeprecations($expr);
+
$token = $this->parser->getCurrentToken();
}
@@ -97,6 +137,43 @@ class ExpressionParser
return $expr;
}
+ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void
+ {
+ // Check that the all nodes that are between the 2 precedences have explicit parentheses
+ if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) {
+ return;
+ }
+
+ if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) {
+ if ($expr->hasExplicitParentheses()) {
+ return;
+ }
+ $target = explode('_', $unaryOp)[1];
+ $change = $this->unaryOperators[$target]['precedence_change'];
+ /** @var AbstractExpression $node */
+ $node = $expr->getNode('node');
+ foreach ($this->precedenceChanges as $operatorName => $changes) {
+ if (!in_array($unaryOp, $changes)) {
+ continue;
+ }
+ if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) {
+ trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
+ }
+ }
+ } else {
+ foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) {
+ foreach ($expr as $node) {
+ /** @var AbstractExpression $node */
+ if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) {
+ $op = explode('_', $operatorName)[1];
+ $change = $this->binaryOperators[$op]['precedence_change'];
+ trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
+ }
+ }
+ }
+ }
+ }
+
/**
* @return ArrowFunctionExpression|null
*/
@@ -105,54 +182,54 @@ 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);
- $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
- $stream->expect(/* Token::ARROW_TYPE */ 12);
+ $token = $stream->expect(Token::NAME_TYPE);
+ $names = [new AssignContextVariable($token->getValue(), $token->getLine())];
+ $stream->expect(Token::ARROW_TYPE);
- return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
+ return new ArrowFunctionExpression($this->parseExpression(), new Nodes($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);
- $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
+ $token = $stream->expect(Token::NAME_TYPE);
+ $names[] = new AssignContextVariable($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);
+ return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line);
}
private function getPrimary(): AbstractExpression
@@ -165,11 +242,23 @@ class ExpressionParser
$expr = $this->parseExpression($operator['precedence']);
$class = $operator['class'];
- return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
- } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+ $expr = new $class($expr, $token->getLine());
+ $expr->setAttribute('operator', 'unary_'.$token->getValue());
+
+ if ($this->deprecationCheck) {
+ $this->triggerPrecedenceDeprecations($expr);
+ }
+
+ return $this->parsePostfixExpression($expr);
+ } 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');
+ $previous = $this->setDeprecationCheck(false);
+ try {
+ $expr = $this->parseExpression()->setExplicitParentheses();
+ } finally {
+ $this->setDeprecationCheck($previous);
+ }
+ $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
return $this->parsePostfixExpression($expr);
}
@@ -179,10 +268,10 @@ 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 {
@@ -203,19 +292,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,53 +328,44 @@ class ExpressionParser
if ('(' === $this->parser->getCurrentToken()->getValue()) {
$node = $this->getFunctionNode($token->getValue(), $token->getLine());
} else {
- $node = new NameExpression($token->getValue(), $token->getLine());
+ $node = new ContextVariable($token->getValue(), $token->getLine());
}
}
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::PUNCTUATION_TYPE:
+ $node = match ($token->getValue()) {
+ '[' => $this->parseSequenceExpression(),
+ '{' => $this->parseMappingExpression(),
+ default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()),
+ };
+ break;
+
+ 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();
- $node = new NameExpression($token->getValue(), $token->getLine());
+ $node = new ContextVariable($token->getValue(), $token->getLine());
break;
}
- 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());
- }
-
- $this->parser->getStream()->next();
- $expr = $this->parsePrimaryExpression();
-
- $node = new $class($expr, $token->getLine());
- break;
+ if ('=' === $token->getValue() && ('==' === $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());
}
// 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());
- } 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());
}
return $this->parsePostfixExpression($node);
@@ -299,12 +379,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;
@@ -319,26 +399,35 @@ class ExpressionParser
return $expr;
}
+ /**
+ * @deprecated since Twig 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;
- if ($stream->test(/* Token::SPREAD_TYPE */ 13)) {
- $stream->next();
+ if ($stream->nextIf(Token::SPREAD_TYPE)) {
$expr = $this->parseExpression();
$expr->setAttribute('spread', true);
$node->addElement($expr);
@@ -346,68 +435,77 @@ class ExpressionParser
$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 Twig 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;
- if ($stream->test(/* Token::SPREAD_TYPE */ 13)) {
- $stream->next();
+ if ($stream->nextIf(Token::SPREAD_TYPE)) {
$value = $this->parseExpression();
$value->setAttribute('spread', true);
$node->addElement($value);
continue;
}
- // a hash key can be:
+ // 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}
if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
- $value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine());
+ $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine());
$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;
}
@@ -416,7 +514,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()) {
@@ -434,124 +532,38 @@ 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 (!$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());
- }
-
- 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);
+ if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
+ return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line);
}
+
+ $args = $this->parseOnlyArguments();
+ $function = $this->getFunction($name, $line);
+
+ if ($function->getParserCallable()) {
+ $fakeNode = new EmptyNode($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)
{
- $stream = $this->parser->getStream();
- $token = $stream->next();
- $lineno = $token->getLine();
- $arguments = new ArrayExpression([], $lineno);
- $type = Template::ANY_CALL;
- if ('.' == $token->getValue()) {
- $token = $stream->next();
- if (
- /* Token::NAME_TYPE */ 5 == $token->getType()
- ||
- /* Token::NUMBER_TYPE */ 6 == $token->getType()
- ||
- (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
- ) {
- $arg = new ConstantExpression($token->getValue(), $lineno);
-
- if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
- $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());
- }
-
- if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
- $name = $arg->getAttribute('value');
-
- $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
- $node->setAttribute('safe', true);
-
- return $node;
- }
- } else {
- $type = Template::ARRAY_CALL;
-
- // slice?
- $slice = false;
- if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
- $slice = true;
- $arg = new ConstantExpression(0, $token->getLine());
- } else {
- $arg = $this->parseExpression();
- }
-
- if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
- $slice = true;
- }
-
- if ($slice) {
- if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
- $length = new ConstantExpression(null, $token->getLine());
- } else {
- $length = $this->parseExpression();
- }
-
- $class = $this->getFilterNodeClass('slice', $token->getLine());
- $arguments = new Node([$arg, $length]);
- $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
-
- $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
-
- return $filter;
- }
-
- $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
+ if ('.' === $this->parser->getStream()->next()->getValue()) {
+ return $this->parseSubscriptExpressionDot($node);
}
- return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
+ return $this->parseSubscriptExpressionArray($node);
}
public function parseFilterExpression($node)
@@ -561,23 +573,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, '(')) {
- $arguments = new Node();
+ while (true) {
+ $token = $this->parser->getStream()->expect(Token::NAME_TYPE);
+
+ if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
+ $arguments = new EmptyNode();
} else {
- $arguments = $this->parseArguments(true, false, true);
+ $arguments = $this->parseOnlyArguments();
}
- $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;
}
@@ -590,51 +614,66 @@ 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
- *
* @return Node
*
* @throws SyntaxError
*/
- public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
+ public function parseArguments()
{
+ $namedArguments = false;
+ $definition = false;
+ if (func_num_args() > 1) {
+ $definition = func_get_arg(1);
+ }
+ if (func_num_args() > 0) {
+ trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__);
+ $namedArguments = func_get_arg(0);
+ }
+
$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, ')')) {
- if (!empty($args)) {
- $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma');
+ $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
+ $hasSpread = false;
+ while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
+ if ($args) {
+ $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');
- $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
+ $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
+ $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else {
- $value = $this->parseExpression(0, $allowArrow);
+ if ($stream->nextIf(Token::SPREAD_TYPE)) {
+ $hasSpread = true;
+ $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
+ } elseif ($hasSpread) {
+ throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
+ } else {
+ $value = $this->parseExpression();
+ }
}
$name = null;
- if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
+ if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $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');
if ($definition) {
- $value = $this->parsePrimaryExpression();
+ $value = $this->getPrimary();
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);
+ $value = $this->parseExpression();
}
}
@@ -642,6 +681,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 {
@@ -652,9 +692,9 @@ 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);
+ return new Nodes($args);
}
public function parseAssignmentExpression()
@@ -663,24 +703,20 @@ 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());
- }
- $targets[] = new AssignNameExpression($value, $token->getLine());
+ $targets[] = new AssignContextVariable($token->getValue(), $token->getLine());
- if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+ if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
- return new Node($targets);
+ return new Nodes($targets);
}
public function parseMultitargetExpression()
@@ -688,12 +724,12 @@ 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;
}
}
- return new Node($targets);
+ return new Nodes($targets);
}
private function parseNotTestExpression(Node $node): NotUnary
@@ -704,124 +740,108 @@ 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, '(')) {
- $arguments = $this->parseArguments(true);
+ if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
+ $arguments = $this->parseOnlyArguments();
} elseif ($test->hasOneMandatoryArgument()) {
- $arguments = new Node([0 => $this->parsePrimaryExpression()]);
+ $arguments = new Nodes([0 => $this->getPrimary()]);
}
- if ('defined' === $name && $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);
+ if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
+ $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
}
- return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
+ $ready = $test instanceof TwigTest;
+ if (!isset($this->readyNodes[$class = $test->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 "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): array
+ private function getTest(int $line): TwigTest
{
$stream = $this->parser->getStream();
- $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+ $name = $stream->expect(Token::NAME_TYPE)->getValue();
- if ($test = $this->env->getTest($name)) {
- return [$name, $test];
- }
-
- if ($stream->test(/* Token::NAME_TYPE */ 5)) {
+ 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) {
+ if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
+ return new TwigTest($name, fn () => '');
+ }
+ $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());
-
- if ($test->getDeprecatedVersion()) {
- $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
- }
- if ($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());
-
- @trigger_error($message, \E_USER_DEPRECATED);
+ $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
}
- 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());
+ if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
+ return new TwigFunction($name, fn () => '');
+ }
+ $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());
- }
- if ($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);
-
- @trigger_error($message, \E_USER_DEPRECATED);
+ $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
}
- 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());
+ if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
+ return new TwigFilter($name, fn () => '');
+ }
+ $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());
- }
- if ($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);
-
- @trigger_error($message, \E_USER_DEPRECATED);
+ $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
}
- return $filter->getNodeClass();
+ return $filter;
}
// checks that the node only contains "constant" elements
+ // to be removed in 4.0
private function checkConstantExpression(Node $node): bool
{
if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
@@ -838,4 +858,154 @@ class ExpressionParser
return true;
}
+
+ private function setDeprecationCheck(bool $deprecationCheck): bool
+ {
+ $current = $this->deprecationCheck;
+ $this->deprecationCheck = $deprecationCheck;
+
+ return $current;
+ }
+
+ private function createArguments(int $line): ArrayExpression
+ {
+ $arguments = new ArrayExpression([], $line);
+ foreach ($this->parseOnlyArguments() as $k => $n) {
+ $arguments->addElement($n, new LocalVariable($k, $line));
+ }
+
+ return $arguments;
+ }
+
+ public function parseOnlyArguments()
+ {
+ $args = [];
+ $stream = $this->parser->getStream();
+ $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
+ $hasSpread = false;
+ while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
+ if ($args) {
+ $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, ')')) {
+ break;
+ }
+ }
+
+ if ($stream->nextIf(Token::SPREAD_TYPE)) {
+ $hasSpread = true;
+ $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
+ } elseif ($hasSpread) {
+ throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
+ } else {
+ $value = $this->parseExpression();
+ }
+
+ $name = null;
+ if (($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());
+ }
+ $name = $value->getAttribute('name');
+ $value = $this->parseExpression();
+ }
+
+ if (null === $name) {
+ $args[] = $value;
+ } else {
+ $args[$name] = $value;
+ }
+ }
+ $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
+
+ return new Nodes($args);
+ }
+
+ private function parseSubscriptExpressionDot(Node $node): AbstractExpression
+ {
+ $stream = $this->parser->getStream();
+ $token = $stream->getCurrent();
+ $lineno = $token->getLine();
+ $arguments = new ArrayExpression([], $lineno);
+ $type = Template::ANY_CALL;
+
+ if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) {
+ $attribute = $this->parseExpression();
+ $stream->expect(Token::PUNCTUATION_TYPE, ')');
+ } else {
+ $token = $stream->next();
+ if (
+ Token::NAME_TYPE == $token->getType()
+ ||
+ Token::NUMBER_TYPE == $token->getType()
+ ||
+ (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
+ ) {
+ $attribute = new ConstantExpression($token->getValue(), $token->getLine());
+ } else {
+ throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $token->getLine(), $stream->getSourceContext());
+ }
+ }
+
+ if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
+ $type = Template::METHOD_CALL;
+ $arguments = $this->createArguments($token->getLine());
+ }
+
+ if (
+ $node instanceof NameExpression
+ &&
+ (
+ null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))
+ ||
+ '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression
+ )
+ ) {
+ return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine());
+ }
+
+ return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno);
+ }
+
+ private function parseSubscriptExpressionArray(Node $node): AbstractExpression
+ {
+ $stream = $this->parser->getStream();
+ $token = $stream->getCurrent();
+ $lineno = $token->getLine();
+ $arguments = new ArrayExpression([], $lineno);
+
+ // slice?
+ $slice = false;
+ if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
+ $slice = true;
+ $attribute = new ConstantExpression(0, $token->getLine());
+ } else {
+ $attribute = $this->parseExpression();
+ }
+
+ if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
+ $slice = true;
+ }
+
+ if ($slice) {
+ if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
+ $length = new ConstantExpression(null, $token->getLine());
+ } else {
+ $length = $this->parseExpression();
+ }
+
+ $filter = $this->getFilter('slice', $token->getLine());
+ $arguments = new Nodes([$attribute, $length]);
+ $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
+
+ $stream->expect(Token::PUNCTUATION_TYPE, ']');
+
+ return $filter;
+ }
+
+ $stream->expect(Token::PUNCTUATION_TYPE, ']');
+
+ return new GetAttrExpression($node, $attribute, $arguments, Template::ARRAY_CALL, $lineno);
+ }
}
diff --git a/lib/twig/twig/src/Extension/AbstractExtension.php b/lib/twig/twig/src/Extension/AbstractExtension.php
index 422925f31..a1b083b68 100644
--- a/lib/twig/twig/src/Extension/AbstractExtension.php
+++ b/lib/twig/twig/src/Extension/AbstractExtension.php
@@ -40,6 +40,6 @@ abstract class AbstractExtension implements ExtensionInterface
public function getOperators()
{
- return [];
+ return [[], []];
}
}
diff --git a/lib/twig/twig/src/Extension/CoreExtension.php b/lib/twig/twig/src/Extension/CoreExtension.php
index 36aa8f10a..429763ad1 100644
--- a/lib/twig/twig/src/Extension/CoreExtension.php
+++ b/lib/twig/twig/src/Extension/CoreExtension.php
@@ -9,8 +9,16 @@
* file that was distributed with this source code.
*/
-namespace Twig\Extension {
+namespace Twig\Extension;
+
+use Twig\DeprecatedCallableInfo;
+use Twig\Environment;
+use Twig\Error\LoaderError;
+use Twig\Error\RuntimeError;
+use Twig\Error\SyntaxError;
use Twig\ExpressionParser;
+use Twig\Markup;
+use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Binary\AddBinary;
use Twig\Node\Expression\Binary\AndBinary;
use Twig\Node\Expression\Binary\BitwiseAndBinary;
@@ -39,8 +47,14 @@ use Twig\Node\Expression\Binary\RangeBinary;
use Twig\Node\Expression\Binary\SpaceshipBinary;
use Twig\Node\Expression\Binary\StartsWithBinary;
use Twig\Node\Expression\Binary\SubBinary;
+use Twig\Node\Expression\Binary\XorBinary;
+use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\Filter\DefaultFilter;
+use Twig\Node\Expression\FunctionNode\EnumCasesFunction;
+use Twig\Node\Expression\FunctionNode\EnumFunction;
+use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NullCoalesceExpression;
+use Twig\Node\Expression\ParentExpression;
use Twig\Node\Expression\Test\ConstantTest;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Test\DivisiblebyTest;
@@ -51,7 +65,14 @@ use Twig\Node\Expression\Test\SameasTest;
use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary;
-use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
+use Twig\Node\Node;
+use Twig\OperatorPrecedenceChange;
+use Twig\Parser;
+use Twig\Sandbox\SecurityNotAllowedMethodError;
+use Twig\Sandbox\SecurityNotAllowedPropertyError;
+use Twig\Source;
+use Twig\Template;
+use Twig\TemplateWrapper;
use Twig\TokenParser\ApplyTokenParser;
use Twig\TokenParser\BlockTokenParser;
use Twig\TokenParser\DeprecatedTokenParser;
@@ -61,19 +82,38 @@ use Twig\TokenParser\ExtendsTokenParser;
use Twig\TokenParser\FlushTokenParser;
use Twig\TokenParser\ForTokenParser;
use Twig\TokenParser\FromTokenParser;
+use Twig\TokenParser\GuardTokenParser;
use Twig\TokenParser\IfTokenParser;
use Twig\TokenParser\ImportTokenParser;
use Twig\TokenParser\IncludeTokenParser;
use Twig\TokenParser\MacroTokenParser;
use Twig\TokenParser\SetTokenParser;
+use Twig\TokenParser\TypesTokenParser;
use Twig\TokenParser\UseTokenParser;
use Twig\TokenParser\WithTokenParser;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
+use Twig\Util\CallableArgumentsExtractor;
final class CoreExtension extends AbstractExtension
{
+ public const ARRAY_LIKE_CLASSES = [
+ 'ArrayIterator',
+ 'ArrayObject',
+ 'CachingIterator',
+ 'RecursiveArrayIterator',
+ 'RecursiveCachingIterator',
+ 'SplDoublyLinkedList',
+ 'SplFixedArray',
+ 'SplObjectStorage',
+ 'SplQueue',
+ 'SplStack',
+ 'WeakMap',
+ ];
+
+ private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B";
+
private $dateFormats = ['F j, Y H:i', '%d days'];
private $numberFormat = [0, '.', ','];
private $timezone = null;
@@ -81,8 +121,8 @@ final class CoreExtension extends AbstractExtension
/**
* Sets the default format to be used by the date filter.
*
- * @param string $format The default date format string
- * @param string $dateIntervalFormat The default date interval format string
+ * @param string|null $format The default date format string
+ * @param string|null $dateIntervalFormat The default date interval format string
*/
public function setDateFormat($format = null, $dateIntervalFormat = null)
{
@@ -165,11 +205,13 @@ final class CoreExtension extends AbstractExtension
new ImportTokenParser(),
new FromTokenParser(),
new SetTokenParser(),
+ new TypesTokenParser(),
new FlushTokenParser(),
new DoTokenParser(),
new EmbedTokenParser(),
new WithTokenParser(),
new DeprecatedTokenParser(),
+ new GuardTokenParser(),
];
}
@@ -177,65 +219,72 @@ final class CoreExtension extends AbstractExtension
{
return [
// formatting filters
- new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]),
- new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]),
- new TwigFilter('format', 'twig_sprintf'),
- new TwigFilter('replace', 'twig_replace_filter'),
- new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]),
+ new TwigFilter('date', [$this, 'formatDate']),
+ new TwigFilter('date_modify', [$this, 'modifyDate']),
+ new TwigFilter('format', [self::class, 'sprintf']),
+ new TwigFilter('replace', [self::class, 'replace']),
+ new TwigFilter('number_format', [$this, 'formatNumber']),
new TwigFilter('abs', 'abs'),
- new TwigFilter('round', 'twig_round'),
+ new TwigFilter('round', [self::class, 'round']),
// encoding
- new TwigFilter('url_encode', 'twig_urlencode_filter'),
+ new TwigFilter('url_encode', [self::class, 'urlencode']),
new TwigFilter('json_encode', 'json_encode'),
- new TwigFilter('convert_encoding', 'twig_convert_encoding'),
+ new TwigFilter('convert_encoding', [self::class, 'convertEncoding']),
// string filters
- new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]),
- new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]),
- new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]),
- new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]),
- new TwigFilter('striptags', 'twig_striptags'),
- new TwigFilter('trim', 'twig_trim_filter'),
- new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]),
- new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]),
+ new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]),
+ new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]),
+ new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]),
+ new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]),
+ new TwigFilter('striptags', [self::class, 'striptags']),
+ new TwigFilter('trim', [self::class, 'trim']),
+ new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
+ new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]),
// array helpers
- new TwigFilter('join', 'twig_join_filter'),
- new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
- new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]),
- new TwigFilter('merge', 'twig_array_merge'),
- new TwigFilter('batch', 'twig_array_batch'),
- new TwigFilter('column', 'twig_array_column'),
- new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]),
- new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]),
- new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]),
+ new TwigFilter('join', [self::class, 'join']),
+ new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]),
+ new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]),
+ new TwigFilter('merge', [self::class, 'merge']),
+ new TwigFilter('batch', [self::class, 'batch']),
+ new TwigFilter('column', [self::class, 'column']),
+ new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]),
+ new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]),
+ new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]),
+ new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true]),
// string/array filters
- new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
- new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]),
- new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]),
- new TwigFilter('first', 'twig_first', ['needs_environment' => true]),
- new TwigFilter('last', 'twig_last', ['needs_environment' => true]),
+ new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]),
+ new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]),
+ new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]),
+ new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]),
+ new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]),
+ new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]),
// iteration and runtime
- new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]),
- new TwigFilter('keys', 'twig_get_array_keys_filter'),
+ new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]),
+ new TwigFilter('keys', [self::class, 'keys']),
];
}
public function getFunctions(): array
{
return [
+ new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]),
+ new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]),
+ new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]),
new TwigFunction('max', 'max'),
new TwigFunction('min', 'min'),
new TwigFunction('range', 'range'),
- new TwigFunction('constant', 'twig_constant'),
- new TwigFunction('cycle', 'twig_cycle'),
- new TwigFunction('random', 'twig_random', ['needs_environment' => true]),
- new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]),
- new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
- new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]),
+ new TwigFunction('constant', [self::class, 'constant']),
+ new TwigFunction('cycle', [self::class, 'cycle']),
+ new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]),
+ new TwigFunction('date', [$this, 'convertDate']),
+ new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
+ new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]),
+ new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]),
+ new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]),
];
}
@@ -250,26 +299,29 @@ final class CoreExtension extends AbstractExtension
new TwigTest('null', null, ['node_class' => NullTest::class]),
new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
- new TwigTest('empty', 'twig_test_empty'),
+ new TwigTest('empty', [self::class, 'testEmpty']),
new TwigTest('iterable', 'is_iterable'),
+ new TwigTest('sequence', [self::class, 'testSequence']),
+ new TwigTest('mapping', [self::class, 'testMapping']),
];
}
public function getNodeVisitors(): array
{
- return [new MacroAutoImportNodeVisitor()];
+ return [];
}
public function getOperators(): array
{
return [
[
- 'not' => ['precedence' => 50, 'class' => NotUnary::class],
+ 'not' => ['precedence' => 50, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 70), 'class' => NotUnary::class],
'-' => ['precedence' => 500, 'class' => NegUnary::class],
'+' => ['precedence' => 500, 'class' => PosUnary::class],
],
[
'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
+ 'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
@@ -291,7 +343,7 @@ final class CoreExtension extends AbstractExtension
'..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
- '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
+ '~' => ['precedence' => 40, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 27), 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
@@ -299,196 +351,230 @@ final class CoreExtension extends AbstractExtension
'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
- '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
+ '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
],
];
}
-}
-}
-namespace {
- use Twig\Environment;
- use Twig\Error\LoaderError;
- use Twig\Error\RuntimeError;
- use Twig\Extension\CoreExtension;
- use Twig\Extension\SandboxExtension;
- use Twig\Markup;
- use Twig\Source;
- use Twig\Template;
- use Twig\TemplateWrapper;
+ /**
+ * Cycles over a sequence.
+ *
+ * @param array|\ArrayAccess $values A non-empty sequence of values
+ * @param int<0, max> $position The position of the value to return in the cycle
+ *
+ * @return mixed The value at the given position in the sequence, wrapping around as needed
+ *
+ * @internal
+ */
+ public static function cycle($values, $position): mixed
+ {
+ if (!\is_array($values)) {
+ if (!$values instanceof \ArrayAccess) {
+ throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.');
+ }
-/**
- * Cycles over a value.
- *
- * @param \ArrayAccess|array $values
- * @param int $position The cycle position
- *
- * @return string The next value in the cycle
- */
-function twig_cycle($values, $position)
-{
- if (!\is_array($values) && !$values instanceof \ArrayAccess) {
- return $values;
+ if (!is_countable($values)) {
+ // To be uncommented in 4.0
+ // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.');
+
+ trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__);
+
+ return $values;
+ }
+
+ $values = self::toArray($values, false);
+ }
+
+ if (!$count = \count($values)) {
+ throw new RuntimeError('The "cycle" function expects a non-empty sequence.');
+ }
+
+ return $values[$position % $count];
}
- return $values[$position % \count($values)];
-}
+ /**
+ * Returns a random value depending on the supplied parameter type:
+ * - a random item from a \Traversable or array
+ * - a random character from a string
+ * - a random integer between 0 and the integer parameter.
+ *
+ * @param \Traversable|array|int|float|string $values The values to pick a random item from
+ * @param int|null $max Maximum value used when $values is an int
+ *
+ * @return mixed A random value from the given sequence
+ *
+ * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
+ *
+ * @internal
+ */
+ public static function random(string $charset, $values = null, $max = null)
+ {
+ if (null === $values) {
+ return null === $max ? mt_rand() : mt_rand(0, (int) $max);
+ }
-/**
- * Returns a random value depending on the supplied parameter type:
- * - a random item from a \Traversable or array
- * - a random character from a string
- * - a random integer between 0 and the integer parameter.
- *
- * @param \Traversable|array|int|float|string $values The values to pick a random item from
- * @param int|null $max Maximum value used when $values is an int
- *
- * @return mixed A random value from the given sequence
- *
- * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
- */
-function twig_random(Environment $env, $values = null, $max = null)
-{
- if (null === $values) {
- return null === $max ? mt_rand() : mt_rand(0, (int) $max);
- }
-
- if (\is_int($values) || \is_float($values)) {
- if (null === $max) {
- if ($values < 0) {
- $max = 0;
- $min = $values;
+ if (\is_int($values) || \is_float($values)) {
+ if (null === $max) {
+ if ($values < 0) {
+ $max = 0;
+ $min = $values;
+ } else {
+ $max = $values;
+ $min = 0;
+ }
} else {
- $max = $values;
- $min = 0;
+ $min = $values;
}
+
+ return mt_rand((int) $min, (int) $max);
+ }
+
+ if (\is_string($values)) {
+ if ('' === $values) {
+ return '';
+ }
+
+ if ('UTF-8' !== $charset) {
+ $values = self::convertEncoding($values, 'UTF-8', $charset);
+ }
+
+ // unicode version of str_split()
+ // split at all positions, but not after the start and not before the end
+ $values = preg_split('/(? $value) {
+ $values[$i] = self::convertEncoding($value, $charset, 'UTF-8');
+ }
+ }
+ }
+
+ if (!is_iterable($values)) {
+ return $values;
+ }
+
+ $values = self::toArray($values);
+
+ if (0 === \count($values)) {
+ throw new RuntimeError('The "random" function cannot pick from an empty sequence or mapping.');
+ }
+
+ return $values[array_rand($values, 1)];
+ }
+
+ /**
+ * Formats a date.
+ *
+ * {{ post.published_at|date("m/d/Y") }}
+ *
+ * @param \DateTimeInterface|\DateInterval|string|int|null $date A date, a timestamp or null to use the current time
+ * @param string|null $format The target format, null to use the default
+ * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
+ */
+ public function formatDate($date, $format = null, $timezone = null): string
+ {
+ if (null === $format) {
+ $formats = $this->getDateFormat();
+ $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
+ }
+
+ if ($date instanceof \DateInterval) {
+ return $date->format($format);
+ }
+
+ return $this->convertDate($date, $timezone)->format($format);
+ }
+
+ /**
+ * Returns a new date object modified.
+ *
+ * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
+ *
+ * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time
+ * @param string $modifier A modifier string
+ *
+ * @return \DateTime|\DateTimeImmutable
+ *
+ * @internal
+ */
+ public function modifyDate($date, $modifier)
+ {
+ return $this->convertDate($date, false)->modify($modifier);
+ }
+
+ /**
+ * Returns a formatted string.
+ *
+ * @param string|null $format
+ * @param ...$values
+ *
+ * @internal
+ */
+ public static function sprintf($format, ...$values): string
+ {
+ return \sprintf($format ?? '', ...$values);
+ }
+
+ /**
+ * @internal
+ */
+ public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string
+ {
+ return $env->getExtension(self::class)->formatDate($date, $format, $timezone);
+ }
+
+ /**
+ * Converts an input to a \DateTime instance.
+ *
+ * {% if date(user.created_at) < date('+2days') %}
+ * {# do something #}
+ * {% endif %}
+ *
+ * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time
+ * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
+ *
+ * @return \DateTime|\DateTimeImmutable
+ */
+ public function convertDate($date = null, $timezone = null)
+ {
+ // determine the timezone
+ if (false !== $timezone) {
+ if (null === $timezone) {
+ $timezone = $this->getTimezone();
+ } elseif (!$timezone instanceof \DateTimeZone) {
+ $timezone = new \DateTimeZone($timezone);
+ }
+ }
+
+ // immutable dates
+ if ($date instanceof \DateTimeImmutable) {
+ return false !== $timezone ? $date->setTimezone($timezone) : $date;
+ }
+
+ if ($date instanceof \DateTime) {
+ $date = clone $date;
+ if (false !== $timezone) {
+ $date->setTimezone($timezone);
+ }
+
+ return $date;
+ }
+
+ if (null === $date || 'now' === $date) {
+ if (null === $date) {
+ $date = 'now';
+ }
+
+ return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone());
+ }
+
+ $asString = (string) $date;
+ if (ctype_digit($asString) || ('' !== $asString && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
+ $date = new \DateTime('@'.$date);
} else {
- $min = $values;
+ $date = new \DateTime($date, $this->getTimezone());
}
- return mt_rand((int) $min, (int) $max);
- }
-
- if (\is_string($values)) {
- if ('' === $values) {
- return '';
- }
-
- $charset = $env->getCharset();
-
- if ('UTF-8' !== $charset) {
- $values = twig_convert_encoding($values, 'UTF-8', $charset);
- }
-
- // unicode version of str_split()
- // split at all positions, but not after the start and not before the end
- $values = preg_split('/(? $value) {
- $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
- }
- }
- }
-
- if (!is_iterable($values)) {
- return $values;
- }
-
- $values = twig_to_array($values);
-
- if (0 === \count($values)) {
- throw new RuntimeError('The random function cannot pick from an empty array.');
- }
-
- return $values[array_rand($values, 1)];
-}
-
-/**
- * Converts a date to the given format.
- *
- * {{ post.published_at|date("m/d/Y") }}
- *
- * @param \DateTimeInterface|\DateInterval|string $date A date
- * @param string|null $format The target format, null to use the default
- * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
- *
- * @return string The formatted date
- */
-function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
-{
- if (null === $format) {
- $formats = $env->getExtension(CoreExtension::class)->getDateFormat();
- $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
- }
-
- if ($date instanceof \DateInterval) {
- return $date->format($format);
- }
-
- return twig_date_converter($env, $date, $timezone)->format($format);
-}
-
-/**
- * Returns a new date object modified.
- *
- * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
- *
- * @param \DateTimeInterface|string $date A date
- * @param string $modifier A modifier string
- *
- * @return \DateTimeInterface
- */
-function twig_date_modify_filter(Environment $env, $date, $modifier)
-{
- $date = twig_date_converter($env, $date, false);
-
- return $date->modify($modifier);
-}
-
-/**
- * Returns a formatted string.
- *
- * @param string|null $format
- * @param ...$values
- *
- * @return string
- */
-function twig_sprintf($format, ...$values)
-{
- return sprintf($format ?? '', ...$values);
-}
-
-/**
- * Converts an input to a \DateTime instance.
- *
- * {% if date(user.created_at) < date('+2days') %}
- * {# do something #}
- * {% endif %}
- *
- * @param \DateTimeInterface|string|null $date A date or null to use the current time
- * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
- *
- * @return \DateTimeInterface
- */
-function twig_date_converter(Environment $env, $date = null, $timezone = null)
-{
- // determine the timezone
- if (false !== $timezone) {
- if (null === $timezone) {
- $timezone = $env->getExtension(CoreExtension::class)->getTimezone();
- } elseif (!$timezone instanceof \DateTimeZone) {
- $timezone = new \DateTimeZone($timezone);
- }
- }
-
- // immutable dates
- if ($date instanceof \DateTimeImmutable) {
- return false !== $timezone ? $date->setTimezone($timezone) : $date;
- }
-
- if ($date instanceof \DateTimeInterface) {
- $date = clone $date;
if (false !== $timezone) {
$date->setTimezone($timezone);
}
@@ -496,993 +582,1126 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null)
return $date;
}
- if (null === $date || 'now' === $date) {
- if (null === $date) {
- $date = 'now';
+ /**
+ * Replaces strings within a string.
+ *
+ * @param string|null $str String to replace in
+ * @param array|\Traversable $from Replace values
+ *
+ * @internal
+ */
+ public static function replace($str, $from): string
+ {
+ if (!is_iterable($from)) {
+ throw new RuntimeError(\sprintf('The "replace" filter expects a sequence or a mapping, got "%s".', get_debug_type($from)));
}
- return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone());
+ return strtr($str ?? '', self::toArray($from));
}
- $asString = (string) $date;
- if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
- $date = new \DateTime('@'.$date);
- } else {
- $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone());
- }
+ /**
+ * Rounds a number.
+ *
+ * @param int|float|string|null $value The value to round
+ * @param int|float $precision The rounding precision
+ * @param 'common'|'ceil'|'floor' $method The method to use for rounding
+ *
+ * @return float The rounded number
+ *
+ * @internal
+ */
+ public static function round($value, $precision = 0, $method = 'common')
+ {
+ $value = (float) $value;
- if (false !== $timezone) {
- $date->setTimezone($timezone);
- }
-
- return $date;
-}
-
-/**
- * Replaces strings within a string.
- *
- * @param string|null $str String to replace in
- * @param array|\Traversable $from Replace values
- *
- * @return string
- */
-function twig_replace_filter($str, $from)
-{
- if (!is_iterable($from)) {
- throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
- }
-
- return strtr($str ?? '', twig_to_array($from));
-}
-
-/**
- * Rounds a number.
- *
- * @param int|float|string|null $value The value to round
- * @param int|float $precision The rounding precision
- * @param string $method The method to use for rounding
- *
- * @return int|float The rounded number
- */
-function twig_round($value, $precision = 0, $method = 'common')
-{
- $value = (float) $value;
-
- if ('common' === $method) {
- return round($value, $precision);
- }
-
- if ('ceil' !== $method && 'floor' !== $method) {
- throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
- }
-
- return $method($value * 10 ** $precision) / 10 ** $precision;
-}
-
-/**
- * Number format filter.
- *
- * All of the formatting options can be left null, in that case the defaults will
- * be used. Supplying any of the parameters will override the defaults set in the
- * environment object.
- *
- * @param mixed $number A float/int/string of the number to format
- * @param int $decimal the number of decimal points to display
- * @param string $decimalPoint the character(s) to use for the decimal point
- * @param string $thousandSep the character(s) to use for the thousands separator
- *
- * @return string The formatted number
- */
-function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
-{
- $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat();
- if (null === $decimal) {
- $decimal = $defaults[0];
- }
-
- if (null === $decimalPoint) {
- $decimalPoint = $defaults[1];
- }
-
- if (null === $thousandSep) {
- $thousandSep = $defaults[2];
- }
-
- return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
-}
-
-/**
- * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
- *
- * @param string|array|null $url A URL or an array of query parameters
- *
- * @return string The URL encoded value
- */
-function twig_urlencode_filter($url)
-{
- if (\is_array($url)) {
- return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
- }
-
- return rawurlencode($url ?? '');
-}
-
-/**
- * Merges any number of arrays or Traversable objects.
- *
- * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
- *
- * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %}
- *
- * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #}
- *
- * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge
- *
- * @return array The merged array
- */
-function twig_array_merge(...$arrays)
-{
- $result = [];
-
- foreach ($arrays as $argNumber => $array) {
- if (!is_iterable($array)) {
- throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1));
+ if ('common' === $method) {
+ return round($value, $precision);
}
- $result = array_merge($result, twig_to_array($array));
- }
-
- return $result;
-}
-
-
-/**
- * Slices a variable.
- *
- * @param mixed $item A variable
- * @param int $start Start of the slice
- * @param int $length Size of the slice
- * @param bool $preserveKeys Whether to preserve key or not (when the input is an array)
- *
- * @return mixed The sliced variable
- */
-function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
-{
- if ($item instanceof \Traversable) {
- while ($item instanceof \IteratorAggregate) {
- $item = $item->getIterator();
+ if ('ceil' !== $method && 'floor' !== $method) {
+ throw new RuntimeError('The "round" filter only supports the "common", "ceil", and "floor" methods.');
}
- if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
- try {
- return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys);
- } catch (\OutOfBoundsException $e) {
- return [];
+ return $method($value * 10 ** $precision) / 10 ** $precision;
+ }
+
+ /**
+ * Formats a number.
+ *
+ * All of the formatting options can be left null, in that case the defaults will
+ * be used. Supplying any of the parameters will override the defaults set in the
+ * environment object.
+ *
+ * @param mixed $number A float/int/string of the number to format
+ * @param int|null $decimal the number of decimal points to display
+ * @param string|null $decimalPoint the character(s) to use for the decimal point
+ * @param string|null $thousandSep the character(s) to use for the thousands separator
+ */
+ public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string
+ {
+ $defaults = $this->getNumberFormat();
+ if (null === $decimal) {
+ $decimal = $defaults[0];
+ }
+
+ if (null === $decimalPoint) {
+ $decimalPoint = $defaults[1];
+ }
+
+ if (null === $thousandSep) {
+ $thousandSep = $defaults[2];
+ }
+
+ return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
+ }
+
+ /**
+ * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
+ *
+ * @param string|array|null $url A URL or an array of query parameters
+ *
+ * @internal
+ */
+ public static function urlencode($url): string
+ {
+ if (\is_array($url)) {
+ return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
+ }
+
+ return rawurlencode($url ?? '');
+ }
+
+ /**
+ * Merges any number of arrays or Traversable objects.
+ *
+ * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
+ *
+ * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %}
+ *
+ * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #}
+ *
+ * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge
+ *
+ * @internal
+ */
+ public static function merge(...$arrays): array
+ {
+ $result = [];
+
+ foreach ($arrays as $argNumber => $array) {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "merge" filter expects a sequence or a mapping, got "%s" for argument %d.', get_debug_type($array), $argNumber + 1));
}
+
+ $result = array_merge($result, self::toArray($array));
}
- $item = iterator_to_array($item, $preserveKeys);
+ return $result;
}
- if (\is_array($item)) {
- return \array_slice($item, $start, $length, $preserveKeys);
- }
+ /**
+ * Slices a variable.
+ *
+ * @param mixed $item A variable
+ * @param int $start Start of the slice
+ * @param int $length Size of the slice
+ * @param bool $preserveKeys Whether to preserve key or not (when the input is an array)
+ *
+ * @return mixed The sliced variable
+ *
+ * @internal
+ */
+ public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false)
+ {
+ if ($item instanceof \Traversable) {
+ while ($item instanceof \IteratorAggregate) {
+ $item = $item->getIterator();
+ }
- return mb_substr((string) $item, $start, $length, $env->getCharset());
-}
+ if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
+ try {
+ return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys);
+ } catch (\OutOfBoundsException $e) {
+ return [];
+ }
+ }
-/**
- * Returns the first element of the item.
- *
- * @param mixed $item A variable
- *
- * @return mixed The first element of the item
- */
-function twig_first(Environment $env, $item)
-{
- $elements = twig_slice($env, $item, 0, 1, false);
-
- return \is_string($elements) ? $elements : current($elements);
-}
-
-/**
- * Returns the last element of the item.
- *
- * @param mixed $item A variable
- *
- * @return mixed The last element of the item
- */
-function twig_last(Environment $env, $item)
-{
- $elements = twig_slice($env, $item, -1, 1, false);
-
- return \is_string($elements) ? $elements : current($elements);
-}
-
-/**
- * Joins the values to a string.
- *
- * The separators between elements are empty strings per default, you can define them with the optional parameters.
- *
- * {{ [1, 2, 3]|join(', ', ' and ') }}
- * {# returns 1, 2 and 3 #}
- *
- * {{ [1, 2, 3]|join('|') }}
- * {# returns 1|2|3 #}
- *
- * {{ [1, 2, 3]|join }}
- * {# returns 123 #}
- *
- * @param array $value An array
- * @param string $glue The separator
- * @param string|null $and The separator for the last pair
- *
- * @return string The concatenated string
- */
-function twig_join_filter($value, $glue = '', $and = null)
-{
- if (!is_iterable($value)) {
- $value = (array) $value;
- }
-
- $value = twig_to_array($value, false);
-
- if (0 === \count($value)) {
- return '';
- }
-
- if (null === $and || $and === $glue) {
- return implode($glue, $value);
- }
-
- if (1 === \count($value)) {
- return $value[0];
- }
-
- return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
-}
-
-/**
- * Splits the string into an array.
- *
- * {{ "one,two,three"|split(',') }}
- * {# returns [one, two, three] #}
- *
- * {{ "one,two,three,four,five"|split(',', 3) }}
- * {# returns [one, two, "three,four,five"] #}
- *
- * {{ "123"|split('') }}
- * {# returns [1, 2, 3] #}
- *
- * {{ "aabbcc"|split('', 2) }}
- * {# returns [aa, bb, cc] #}
- *
- * @param string|null $value A string
- * @param string $delimiter The delimiter
- * @param int $limit The limit
- *
- * @return array The split string as an array
- */
-function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
-{
- $value = $value ?? '';
-
- if ('' !== $delimiter) {
- return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
- }
-
- if ($limit <= 1) {
- return preg_split('/(?getCharset());
- if ($length < $limit) {
- return [$value];
- }
-
- $r = [];
- for ($i = 0; $i < $length; $i += $limit) {
- $r[] = mb_substr($value, $i, $limit, $env->getCharset());
- }
-
- return $r;
-}
-
-// The '_default' filter is used internally to avoid using the ternary operator
-// which costs a lot for big contexts (before PHP 5.4). So, on average,
-// a function call is cheaper.
-/**
- * @internal
- */
-function _twig_default_filter($value, $default = '')
-{
- if (twig_test_empty($value)) {
- return $default;
- }
-
- return $value;
-}
-
-/**
- * Returns the keys for the given array.
- *
- * It is useful when you want to iterate over the keys of an array:
- *
- * {% for key in array|keys %}
- * {# ... #}
- * {% endfor %}
- *
- * @param array $array An array
- *
- * @return array The keys
- */
-function twig_get_array_keys_filter($array)
-{
- if ($array instanceof \Traversable) {
- while ($array instanceof \IteratorAggregate) {
- $array = $array->getIterator();
+ $item = iterator_to_array($item, $preserveKeys);
}
- $keys = [];
- if ($array instanceof \Iterator) {
- $array->rewind();
- while ($array->valid()) {
- $keys[] = $array->key();
- $array->next();
+ if (\is_array($item)) {
+ return \array_slice($item, $start, $length, $preserveKeys);
+ }
+
+ return mb_substr((string) $item, $start, $length, $charset);
+ }
+
+ /**
+ * Returns the first element of the item.
+ *
+ * @param mixed $item A variable
+ *
+ * @return mixed The first element of the item
+ *
+ * @internal
+ */
+ public static function first(string $charset, $item)
+ {
+ $elements = self::slice($charset, $item, 0, 1, false);
+
+ return \is_string($elements) ? $elements : current($elements);
+ }
+
+ /**
+ * Returns the last element of the item.
+ *
+ * @param mixed $item A variable
+ *
+ * @return mixed The last element of the item
+ *
+ * @internal
+ */
+ public static function last(string $charset, $item)
+ {
+ $elements = self::slice($charset, $item, -1, 1, false);
+
+ return \is_string($elements) ? $elements : current($elements);
+ }
+
+ /**
+ * Joins the values to a string.
+ *
+ * The separators between elements are empty strings per default, you can define them with the optional parameters.
+ *
+ * {{ [1, 2, 3]|join(', ', ' and ') }}
+ * {# returns 1, 2 and 3 #}
+ *
+ * {{ [1, 2, 3]|join('|') }}
+ * {# returns 1|2|3 #}
+ *
+ * {{ [1, 2, 3]|join }}
+ * {# returns 123 #}
+ *
+ * @param iterable|array|string|float|int|bool|null $value An array
+ * @param string $glue The separator
+ * @param string|null $and The separator for the last pair
+ *
+ * @internal
+ */
+ public static function join($value, $glue = '', $and = null): string
+ {
+ if (!is_iterable($value)) {
+ $value = (array) $value;
+ }
+
+ $value = self::toArray($value, false);
+
+ if (0 === \count($value)) {
+ return '';
+ }
+
+ if (null === $and || $and === $glue) {
+ return implode($glue, $value);
+ }
+
+ if (1 === \count($value)) {
+ return $value[0];
+ }
+
+ return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
+ }
+
+ /**
+ * Splits the string into an array.
+ *
+ * {{ "one,two,three"|split(',') }}
+ * {# returns [one, two, three] #}
+ *
+ * {{ "one,two,three,four,five"|split(',', 3) }}
+ * {# returns [one, two, "three,four,five"] #}
+ *
+ * {{ "123"|split('') }}
+ * {# returns [1, 2, 3] #}
+ *
+ * {{ "aabbcc"|split('', 2) }}
+ * {# returns [aa, bb, cc] #}
+ *
+ * @param string|null $value A string
+ * @param string $delimiter The delimiter
+ * @param int|null $limit The limit
+ *
+ * @internal
+ */
+ public static function split(string $charset, $value, $delimiter, $limit = null): array
+ {
+ $value = $value ?? '';
+
+ if ('' !== $delimiter) {
+ return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
+ }
+
+ if ($limit <= 1) {
+ return preg_split('/(?getIterator();
+ }
+
+ $keys = [];
+ if ($array instanceof \Iterator) {
+ $array->rewind();
+ while ($array->valid()) {
+ $keys[] = $array->key();
+ $array->next();
+ }
+
+ return $keys;
+ }
+
+ foreach ($array as $key => $item) {
+ $keys[] = $key;
}
return $keys;
}
- foreach ($array as $key => $item) {
- $keys[] = $key;
+ if (!\is_array($array)) {
+ return [];
}
- return $keys;
+ return array_keys($array);
}
- if (!\is_array($array)) {
- return [];
- }
-
- return array_keys($array);
-}
-
-/**
- * Reverses a variable.
- *
- * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string
- * @param bool $preserveKeys Whether to preserve key or not
- *
- * @return mixed The reversed input
- */
-function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
-{
- if ($item instanceof \Traversable) {
- return array_reverse(iterator_to_array($item), $preserveKeys);
- }
-
- if (\is_array($item)) {
- return array_reverse($item, $preserveKeys);
- }
-
- $string = (string) $item;
-
- $charset = $env->getCharset();
-
- if ('UTF-8' !== $charset) {
- $string = twig_convert_encoding($string, 'UTF-8', $charset);
- }
-
- preg_match_all('/./us', $string, $matches);
-
- $string = implode('', array_reverse($matches[0]));
-
- if ('UTF-8' !== $charset) {
- $string = twig_convert_encoding($string, $charset, 'UTF-8');
- }
-
- return $string;
-}
-
-/**
- * Sorts an array.
- *
- * @param array|\Traversable $array
- *
- * @return array
- */
-function twig_sort_filter(Environment $env, $array, $arrow = null)
-{
- if ($array instanceof \Traversable) {
- $array = iterator_to_array($array);
- } elseif (!\is_array($array)) {
- throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
- }
-
- if (null !== $arrow) {
- twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter');
-
- uasort($array, $arrow);
- } else {
- asort($array);
- }
-
- return $array;
-}
-
-/**
- * @internal
- */
-function twig_in_filter($value, $compare)
-{
- if ($value instanceof Markup) {
- $value = (string) $value;
- }
- if ($compare instanceof Markup) {
- $compare = (string) $compare;
- }
-
- if (\is_string($compare)) {
- if (\is_string($value) || \is_int($value) || \is_float($value)) {
- return '' === $value || str_contains($compare, (string) $value);
+ /**
+ * Reverses a variable.
+ *
+ * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string
+ * @param bool $preserveKeys Whether to preserve key or not
+ *
+ * @return mixed The reversed input
+ *
+ * @internal
+ */
+ public static function reverse(string $charset, $item, $preserveKeys = false)
+ {
+ if ($item instanceof \Traversable) {
+ return array_reverse(iterator_to_array($item), $preserveKeys);
}
- return false;
+ if (\is_array($item)) {
+ return array_reverse($item, $preserveKeys);
+ }
+
+ $string = (string) $item;
+
+ if ('UTF-8' !== $charset) {
+ $string = self::convertEncoding($string, 'UTF-8', $charset);
+ }
+
+ preg_match_all('/./us', $string, $matches);
+
+ $string = implode('', array_reverse($matches[0]));
+
+ if ('UTF-8' !== $charset) {
+ $string = self::convertEncoding($string, $charset, 'UTF-8');
+ }
+
+ return $string;
}
- if (!is_iterable($compare)) {
- return false;
+ /**
+ * Shuffles an array, a \Traversable instance, or a string.
+ * The function does not preserve keys.
+ *
+ * @param array|\Traversable|string|null $item
+ *
+ * @return mixed
+ *
+ * @internal
+ */
+ public static function shuffle(string $charset, $item)
+ {
+ if (\is_string($item)) {
+ if ('UTF-8' !== $charset) {
+ $item = self::convertEncoding($item, 'UTF-8', $charset);
+ }
+
+ $item = preg_split('/(? string
+ if (\is_int($a) && \is_string($b)) {
+ $bTrim = trim($b, " \t\n\r\v\f");
+ if (!is_numeric($bTrim)) {
+ return (string) $a <=> $b;
+ }
+ if ((int) $bTrim == $bTrim) {
+ return $a <=> (int) $bTrim;
+ } else {
+ return (float) $a <=> (float) $bTrim;
+ }
+ }
+ if (\is_string($a) && \is_int($b)) {
+ $aTrim = trim($a, " \t\n\r\v\f");
+ if (!is_numeric($aTrim)) {
+ return $a <=> (string) $b;
+ }
+ if ((int) $aTrim == $aTrim) {
+ return (int) $aTrim <=> $b;
+ } else {
+ return (float) $aTrim <=> (float) $b;
+ }
+ }
+
+ // float <=> string
+ if (\is_float($a) && \is_string($b)) {
+ if (is_nan($a)) {
+ return 1;
+ }
+ $bTrim = trim($b, " \t\n\r\v\f");
+ if (!is_numeric($bTrim)) {
+ return (string) $a <=> $b;
+ }
+
+ return $a <=> (float) $bTrim;
+ }
+ if (\is_string($a) && \is_float($b)) {
+ if (is_nan($b)) {
+ return 1;
+ }
+ $aTrim = trim($a, " \t\n\r\v\f");
+ if (!is_numeric($aTrim)) {
+ return $a <=> (string) $b;
+ }
+
+ return (float) $aTrim <=> $b;
+ }
+
+ // fallback to <=>
+ return $a <=> $b;
+ }
+
+ /**
+ * @throws RuntimeError When an invalid pattern is used
+ *
+ * @internal
+ */
+ public static function matches(string $regexp, ?string $str): int
+ {
+ set_error_handler(function ($t, $m) use ($regexp) {
+ throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
+ });
+ try {
+ return preg_match($regexp, $str ?? '');
+ } finally {
+ restore_error_handler();
}
}
- return false;
-}
-
-/**
- * Compares two values using a more strict version of the PHP non-strict comparison operator.
- *
- * @see https://wiki.php.net/rfc/string_to_number_comparison
- * @see https://wiki.php.net/rfc/trailing_whitespace_numerics
- *
- * @internal
- */
-function twig_compare($a, $b)
-{
- // int <=> string
- if (\is_int($a) && \is_string($b)) {
- $bTrim = trim($b, " \t\n\r\v\f");
- if (!is_numeric($bTrim)) {
- return (string) $a <=> $b;
- }
- if ((int) $bTrim == $bTrim) {
- return $a <=> (int) $bTrim;
- } else {
- return (float) $a <=> (float) $bTrim;
- }
- }
- if (\is_string($a) && \is_int($b)) {
- $aTrim = trim($a, " \t\n\r\v\f");
- if (!is_numeric($aTrim)) {
- return $a <=> (string) $b;
- }
- if ((int) $aTrim == $aTrim) {
- return (int) $aTrim <=> $b;
- } else {
- return (float) $aTrim <=> (float) $b;
- }
- }
-
- // float <=> string
- if (\is_float($a) && \is_string($b)) {
- if (is_nan($a)) {
- return 1;
- }
- $bTrim = trim($b, " \t\n\r\v\f");
- if (!is_numeric($bTrim)) {
- return (string) $a <=> $b;
+ /**
+ * Returns a trimmed string.
+ *
+ * @param string|\Stringable|null $string
+ * @param string|null $characterMask
+ * @param string $side left, right, or both
+ *
+ * @throws RuntimeError When an invalid trimming side is used
+ *
+ * @internal
+ */
+ public static function trim($string, $characterMask = null, $side = 'both'): string|\Stringable
+ {
+ if (null === $characterMask) {
+ $characterMask = self::DEFAULT_TRIM_CHARS;
}
- return $a <=> (float) $bTrim;
+ $trimmed = match ($side) {
+ 'both' => trim($string ?? '', $characterMask),
+ 'left' => ltrim($string ?? '', $characterMask),
+ 'right' => rtrim($string ?? '', $characterMask),
+ default => throw new RuntimeError('Trimming side must be "left", "right" or "both".'),
+ };
+
+ // trimming a safe string with the default character mask always returns a safe string (independently of the context)
+ return $string instanceof Markup && self::DEFAULT_TRIM_CHARS === $characterMask ? new Markup($trimmed, $string->getCharset()) : $trimmed;
}
- if (\is_string($a) && \is_float($b)) {
- if (is_nan($b)) {
- return 1;
- }
- $aTrim = trim($a, " \t\n\r\v\f");
- if (!is_numeric($aTrim)) {
- return $a <=> (string) $b;
+
+ /**
+ * Inserts HTML line breaks before all newlines in a string.
+ *
+ * @param string|null $string
+ *
+ * @internal
+ */
+ public static function nl2br($string): string
+ {
+ return nl2br($string ?? '');
+ }
+
+ /**
+ * Removes whitespaces between HTML tags.
+ *
+ * @param string|null $content
+ *
+ * @internal
+ */
+ public static function spaceless($content): string
+ {
+ return trim(preg_replace('/>\s+', '><', $content ?? ''));
+ }
+
+ /**
+ * @param string|null $string
+ * @param string $to
+ * @param string $from
+ *
+ * @internal
+ */
+ public static function convertEncoding($string, $to, $from): string
+ {
+ if (!\function_exists('iconv')) {
+ throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
}
- return (float) $aTrim <=> $b;
+ return iconv($from, $to, $string ?? '');
}
- // fallback to <=>
- return $a <=> $b;
-}
+ /**
+ * Returns the length of a variable.
+ *
+ * @param mixed $thing A variable
+ *
+ * @internal
+ */
+ public static function length(string $charset, $thing): int
+ {
+ if (null === $thing) {
+ return 0;
+ }
-/**
- * @return int
- *
- * @throws RuntimeError When an invalid pattern is used
- */
-function twig_matches(string $regexp, ?string $str)
-{
- set_error_handler(function ($t, $m) use ($regexp) {
- throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
- });
- try {
- return preg_match($regexp, $str ?? '');
- } finally {
- restore_error_handler();
- }
-}
+ if (\is_scalar($thing)) {
+ return mb_strlen($thing, $charset);
+ }
-/**
- * Returns a trimmed string.
- *
- * @param string|null $string
- * @param string|null $characterMask
- * @param string $side
- *
- * @return string
- *
- * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
- */
-function twig_trim_filter($string, $characterMask = null, $side = 'both')
-{
- if (null === $characterMask) {
- $characterMask = " \t\n\r\0\x0B";
+ if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
+ return \count($thing);
+ }
+
+ if ($thing instanceof \Traversable) {
+ return iterator_count($thing);
+ }
+
+ if ($thing instanceof \Stringable) {
+ return mb_strlen((string) $thing, $charset);
+ }
+
+ return 1;
}
- switch ($side) {
- case 'both':
- return trim($string ?? '', $characterMask);
- case 'left':
- return ltrim($string ?? '', $characterMask);
- case 'right':
- return rtrim($string ?? '', $characterMask);
- default:
- throw new RuntimeError('Trimming side must be "left", "right" or "both".');
- }
-}
-
-/**
- * Inserts HTML line breaks before all newlines in a string.
- *
- * @param string|null $string
- *
- * @return string
- */
-function twig_nl2br($string)
-{
- return nl2br($string ?? '');
-}
-
-/**
- * Removes whitespaces between HTML tags.
- *
- * @param string|null $string
- *
- * @return string
- */
-function twig_spaceless($content)
-{
- return trim(preg_replace('/>\s+', '><', $content ?? ''));
-}
-
-/**
- * @param string|null $string
- * @param string $to
- * @param string $from
- *
- * @return string
- */
-function twig_convert_encoding($string, $to, $from)
-{
- if (!\function_exists('iconv')) {
- throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
+ /**
+ * Converts a string to uppercase.
+ *
+ * @param string|null $string A string
+ *
+ * @internal
+ */
+ public static function upper(string $charset, $string): string
+ {
+ return mb_strtoupper($string ?? '', $charset);
}
- return iconv($from, $to, $string ?? '');
-}
-
-/**
- * Returns the length of a variable.
- *
- * @param mixed $thing A variable
- *
- * @return int The length of the value
- */
-function twig_length_filter(Environment $env, $thing)
-{
- if (null === $thing) {
- return 0;
+ /**
+ * Converts a string to lowercase.
+ *
+ * @param string|null $string A string
+ *
+ * @internal
+ */
+ public static function lower(string $charset, $string): string
+ {
+ return mb_strtolower($string ?? '', $charset);
}
- if (\is_scalar($thing)) {
- return mb_strlen($thing, $env->getCharset());
+ /**
+ * Strips HTML and PHP tags from a string.
+ *
+ * @param string|null $string
+ * @param string[]|string|null $allowable_tags
+ *
+ * @internal
+ */
+ public static function striptags($string, $allowable_tags = null): string
+ {
+ return strip_tags($string ?? '', $allowable_tags);
}
- if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
- return \count($thing);
- }
-
- if ($thing instanceof \Traversable) {
- return iterator_count($thing);
- }
-
- if (method_exists($thing, '__toString') && !$thing instanceof \Countable) {
- return mb_strlen((string) $thing, $env->getCharset());
- }
-
- return 1;
-}
-
-/**
- * Converts a string to uppercase.
- *
- * @param string|null $string A string
- *
- * @return string The uppercased string
- */
-function twig_upper_filter(Environment $env, $string)
-{
- return mb_strtoupper($string ?? '', $env->getCharset());
-}
-
-/**
- * Converts a string to lowercase.
- *
- * @param string|null $string A string
- *
- * @return string The lowercased string
- */
-function twig_lower_filter(Environment $env, $string)
-{
- return mb_strtolower($string ?? '', $env->getCharset());
-}
-
-/**
- * Strips HTML and PHP tags from a string.
- *
- * @param string|null $string
- * @param string[]|string|null $string
- *
- * @return string
- */
-function twig_striptags($string, $allowable_tags = null)
-{
- return strip_tags($string ?? '', $allowable_tags);
-}
-
-/**
- * Returns a titlecased string.
- *
- * @param string|null $string A string
- *
- * @return string The titlecased string
- */
-function twig_title_string_filter(Environment $env, $string)
-{
- if (null !== $charset = $env->getCharset()) {
+ /**
+ * Returns a titlecased string.
+ *
+ * @param string|null $string A string
+ *
+ * @internal
+ */
+ public static function titleCase(string $charset, $string): string
+ {
return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset);
}
- return ucwords(strtolower($string ?? ''));
-}
+ /**
+ * Returns a capitalized string.
+ *
+ * @param string|null $string A string
+ *
+ * @internal
+ */
+ public static function capitalize(string $charset, $string): string
+ {
+ return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset);
+ }
-/**
- * Returns a capitalized string.
- *
- * @param string|null $string A string
- *
- * @return string The capitalized string
- */
-function twig_capitalize_string_filter(Environment $env, $string)
-{
- $charset = $env->getCharset();
+ /**
+ * @internal
+ *
+ * to be removed in 4.0
+ */
+ public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
+ {
+ if (!method_exists($template, $method)) {
+ $parent = $template;
+ while ($parent = $parent->getParent($context)) {
+ if (method_exists($parent, $method)) {
+ return $parent->$method(...$args);
+ }
+ }
- return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset);
-}
+ throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
+ }
-/**
- * @internal
- */
-function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
-{
- if (!method_exists($template, $method)) {
- $parent = $template;
- while ($parent = $parent->getParent($context)) {
- if (method_exists($parent, $method)) {
- return $parent->$method(...$args);
+ return $template->$method(...$args);
+ }
+
+ /**
+ * @template TSequence
+ *
+ * @param TSequence $seq
+ *
+ * @return ($seq is iterable ? TSequence : array{})
+ *
+ * @internal
+ */
+ public static function ensureTraversable($seq)
+ {
+ if (is_iterable($seq)) {
+ return $seq;
+ }
+
+ return [];
+ }
+
+ /**
+ * @internal
+ */
+ public static function toArray($seq, $preserveKeys = true)
+ {
+ if ($seq instanceof \Traversable) {
+ return iterator_to_array($seq, $preserveKeys);
+ }
+
+ if (!\is_array($seq)) {
+ return $seq;
+ }
+
+ return $preserveKeys ? $seq : array_values($seq);
+ }
+
+ /**
+ * Checks if a variable is empty.
+ *
+ * {# evaluates to true if the foo variable is null, false, or the empty string #}
+ * {% if foo is empty %}
+ * {# ... #}
+ * {% endif %}
+ *
+ * @param mixed $value A variable
+ *
+ * @internal
+ */
+ public static function testEmpty($value): bool
+ {
+ if ($value instanceof \Countable) {
+ return 0 === \count($value);
+ }
+
+ if ($value instanceof \Traversable) {
+ return !iterator_count($value);
+ }
+
+ if ($value instanceof \Stringable) {
+ return '' === (string) $value;
+ }
+
+ return '' === $value || false === $value || null === $value || [] === $value;
+ }
+
+ /**
+ * Checks if a variable is a sequence.
+ *
+ * {# evaluates to true if the foo variable is a sequence #}
+ * {% if foo is sequence %}
+ * {# ... #}
+ * {% endif %}
+ *
+ * @param mixed $value
+ *
+ * @internal
+ */
+ public static function testSequence($value): bool
+ {
+ if ($value instanceof \ArrayObject) {
+ $value = $value->getArrayCopy();
+ }
+
+ if ($value instanceof \Traversable) {
+ $value = iterator_to_array($value);
+ }
+
+ return \is_array($value) && array_is_list($value);
+ }
+
+ /**
+ * Checks if a variable is a mapping.
+ *
+ * {# evaluates to true if the foo variable is a mapping #}
+ * {% if foo is mapping %}
+ * {# ... #}
+ * {% endif %}
+ *
+ * @param mixed $value
+ *
+ * @internal
+ */
+ public static function testMapping($value): bool
+ {
+ if ($value instanceof \ArrayObject) {
+ $value = $value->getArrayCopy();
+ }
+
+ if ($value instanceof \Traversable) {
+ $value = iterator_to_array($value);
+ }
+
+ return (\is_array($value) && !array_is_list($value)) || \is_object($value);
+ }
+
+ /**
+ * Renders a template.
+ *
+ * @param array $context
+ * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively
+ * @param array $variables The variables to pass to the template
+ * @param bool $withContext
+ * @param bool $ignoreMissing Whether to ignore missing templates or not
+ * @param bool $sandboxed Whether to sandbox the template or not
+ *
+ * @internal
+ */
+ public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string
+ {
+ $alreadySandboxed = false;
+ $sandbox = null;
+ if ($withContext) {
+ $variables = array_merge($context, $variables);
+ }
+
+ if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
+ $sandbox = $env->getExtension(SandboxExtension::class);
+ if (!$alreadySandboxed = $sandbox->isSandboxed()) {
+ $sandbox->enableSandbox();
}
}
- throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
- }
-
- return $template->$method(...$args);
-}
-
-/**
- * @internal
- */
-function twig_ensure_traversable($seq)
-{
- if (is_iterable($seq)) {
- return $seq;
- }
-
- return [];
-}
-
-/**
- * @internal
- */
-function twig_to_array($seq, $preserveKeys = true)
-{
- if ($seq instanceof \Traversable) {
- return iterator_to_array($seq, $preserveKeys);
- }
-
- if (!\is_array($seq)) {
- return $seq;
- }
-
- return $preserveKeys ? $seq : array_values($seq);
-}
-
-/**
- * Checks if a variable is empty.
- *
- * {# evaluates to true if the foo variable is null, false, or the empty string #}
- * {% if foo is empty %}
- * {# ... #}
- * {% endif %}
- *
- * @param mixed $value A variable
- *
- * @return bool true if the value is empty, false otherwise
- */
-function twig_test_empty($value)
-{
- if ($value instanceof \Countable) {
- return 0 === \count($value);
- }
-
- if ($value instanceof \Traversable) {
- return !iterator_count($value);
- }
-
- if (\is_object($value) && method_exists($value, '__toString')) {
- return '' === (string) $value;
- }
-
- return '' === $value || false === $value || null === $value || [] === $value;
-}
-
-/**
- * Checks if a variable is traversable.
- *
- * {# evaluates to true if the foo variable is an array or a traversable object #}
- * {% if foo is iterable %}
- * {# ... #}
- * {% endif %}
- *
- * @param mixed $value A variable
- *
- * @return bool true if the value is traversable
- *
- * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead)
- */
-function twig_test_iterable($value)
-{
- return is_iterable($value);
-}
-
-/**
- * Renders a template.
- *
- * @param array $context
- * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively
- * @param array $variables The variables to pass to the template
- * @param bool $withContext
- * @param bool $ignoreMissing Whether to ignore missing templates or not
- * @param bool $sandboxed Whether to sandbox the template or not
- *
- * @return string The rendered template
- */
-function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
-{
- $alreadySandboxed = false;
- $sandbox = null;
- if ($withContext) {
- $variables = array_merge($context, $variables);
- }
-
- if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
- $sandbox = $env->getExtension(SandboxExtension::class);
- if (!$alreadySandboxed = $sandbox->isSandboxed()) {
- $sandbox->enableSandbox();
- }
-
- foreach ((\is_array($template) ? $template : [$template]) as $name) {
- // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security
- if ($name instanceof TemplateWrapper || $name instanceof Template) {
- $name->unwrap()->checkSecurity();
- }
- }
- }
-
- try {
- $loaded = null;
try {
- $loaded = $env->resolveTemplate($template);
+ $loaded = null;
+ try {
+ $loaded = $env->resolveTemplate($template);
+ } catch (LoaderError $e) {
+ if (!$ignoreMissing) {
+ throw $e;
+ }
+
+ return '';
+ }
+
+ if ($isSandboxed) {
+ $loaded->unwrap()->checkSecurity();
+ }
+
+ return $loaded->render($variables);
+ } finally {
+ if ($isSandboxed && !$alreadySandboxed) {
+ $sandbox->disableSandbox();
+ }
+ }
+ }
+
+ /**
+ * Returns a template content without rendering it.
+ *
+ * @param string $name The template name
+ * @param bool $ignoreMissing Whether to ignore missing templates or not
+ *
+ * @internal
+ */
+ public static function source(Environment $env, $name, $ignoreMissing = false): string
+ {
+ $loader = $env->getLoader();
+ try {
+ return $loader->getSourceContext($name)->getCode();
} catch (LoaderError $e) {
if (!$ignoreMissing) {
throw $e;
}
- }
- return $loaded ? $loaded->render($variables) : '';
- } finally {
- if ($isSandboxed && !$alreadySandboxed) {
- $sandbox->disableSandbox();
- }
- }
-}
-
-/**
- * Returns a template content without rendering it.
- *
- * @param string $name The template name
- * @param bool $ignoreMissing Whether to ignore missing templates or not
- *
- * @return string The template source
- */
-function twig_source(Environment $env, $name, $ignoreMissing = false)
-{
- $loader = $env->getLoader();
- try {
- return $loader->getSourceContext($name)->getCode();
- } catch (LoaderError $e) {
- if (!$ignoreMissing) {
- throw $e;
- }
- }
-}
-
-/**
- * Provides the ability to get constants from instances as well as class/global constants.
- *
- * @param string $constant The name of the constant
- * @param object|null $object The object to get the constant from
- *
- * @return string
- */
-function twig_constant($constant, $object = null)
-{
- if (null !== $object) {
- if ('class' === $constant) {
- return \get_class($object);
- }
-
- $constant = \get_class($object).'::'.$constant;
- }
-
- if (!\defined($constant)) {
- throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant));
- }
-
- return \constant($constant);
-}
-
-/**
- * Checks if a constant exists.
- *
- * @param string $constant The name of the constant
- * @param object|null $object The object to get the constant from
- *
- * @return bool
- */
-function twig_constant_is_defined($constant, $object = null)
-{
- if (null !== $object) {
- if ('class' === $constant) {
- return true;
- }
-
- $constant = \get_class($object).'::'.$constant;
- }
-
- return \defined($constant);
-}
-
-/**
- * Batches item.
- *
- * @param array $items An array of items
- * @param int $size The size of the batch
- * @param mixed $fill A value used to fill missing items
- *
- * @return array
- */
-function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
-{
- if (!is_iterable($items)) {
- throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items)));
- }
-
- $size = ceil($size);
-
- $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys);
-
- if (null !== $fill && $result) {
- $last = \count($result) - 1;
- if ($fillCount = $size - \count($result[$last])) {
- for ($i = 0; $i < $fillCount; ++$i) {
- $result[$last][] = $fill;
- }
+ return '';
}
}
- return $result;
-}
+ /**
+ * Returns the list of cases of the enum.
+ *
+ * @template T of \UnitEnum
+ *
+ * @param class-string $enum
+ *
+ * @return list
+ *
+ * @internal
+ */
+ public static function enumCases(string $enum): array
+ {
+ if (!enum_exists($enum)) {
+ throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum));
+ }
-/**
- * Returns the attribute value for a given array/object.
- *
- * @param mixed $object The object or array from where to get the item
- * @param mixed $item The item to get from the array or object
- * @param array $arguments An array of arguments to pass if the item is an object method
- * @param string $type The type of attribute (@see \Twig\Template constants)
- * @param bool $isDefinedTest Whether this is only a defined check
- * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not
- * @param int $lineno The template line where the attribute was called
- *
- * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
- *
- * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
- *
- * @internal
- */
-function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
-{
- // array
- if (/* Template::METHOD_CALL */ 'method' !== $type) {
- $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
+ return $enum::cases();
+ }
- if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
- || ($object instanceof ArrayAccess && isset($object[$arrayItem]))
- ) {
- if ($isDefinedTest) {
- return true;
+ /**
+ * Provides the ability to access enums by their class names.
+ *
+ * @template T of \UnitEnum
+ *
+ * @param class-string $enum
+ *
+ * @return T
+ *
+ * @internal
+ */
+ public static function enum(string $enum): \UnitEnum
+ {
+ if (!enum_exists($enum)) {
+ throw new RuntimeError(sprintf('"%s" is not an enum.', $enum));
+ }
+
+ if (!$cases = $enum::cases()) {
+ throw new RuntimeError(sprintf('"%s" is an empty enum.', $enum));
+ }
+
+ return $cases[0];
+ }
+
+ /**
+ * Provides the ability to get constants from instances as well as class/global constants.
+ *
+ * @param string $constant The name of the constant
+ * @param object|null $object The object to get the constant from
+ * @param bool $checkDefined Whether to check if the constant is defined or not
+ *
+ * @return mixed Class constants can return many types like scalars, arrays, and
+ * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.)
+ * When $checkDefined is true, returns true when the constant is defined, false otherwise
+ *
+ * @internal
+ */
+ public static function constant($constant, $object = null, bool $checkDefined = false)
+ {
+ if (null !== $object) {
+ if ('class' === $constant) {
+ return $checkDefined ? true : \get_class($object);
}
- return $object[$arrayItem];
+ $constant = \get_class($object).'::'.$constant;
}
- if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) {
+ if (!\defined($constant)) {
+ if ($checkDefined) {
+ return false;
+ }
+
+ if ('::class' === strtolower(substr($constant, -7))) {
+ throw new RuntimeError(\sprintf('You cannot use the Twig function "constant" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant));
+ }
+
+ throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant));
+ }
+
+ return $checkDefined ? true : \constant($constant);
+ }
+
+ /**
+ * Batches item.
+ *
+ * @param array $items An array of items
+ * @param int $size The size of the batch
+ * @param mixed $fill A value used to fill missing items
+ *
+ * @internal
+ */
+ public static function batch($items, $size, $fill = null, $preserveKeys = true): array
+ {
+ if (!is_iterable($items)) {
+ throw new RuntimeError(\sprintf('The "batch" filter expects a sequence or a mapping, got "%s".', get_debug_type($items)));
+ }
+
+ $size = (int) ceil($size);
+
+ $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys);
+
+ if (null !== $fill && $result) {
+ $last = \count($result) - 1;
+ if ($fillCount = $size - \count($result[$last])) {
+ for ($i = 0; $i < $fillCount; ++$i) {
+ $result[$last][] = $fill;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the attribute value for a given array/object.
+ *
+ * @param mixed $object The object or array from where to get the item
+ * @param mixed $item The item to get from the array or object
+ * @param array $arguments An array of arguments to pass if the item is an object method
+ * @param string $type The type of attribute (@see \Twig\Template constants)
+ * @param bool $isDefinedTest Whether this is only a defined check
+ * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not
+ * @param int $lineno The template line where the attribute was called
+ *
+ * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
+ *
+ * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
+ *
+ * @internal
+ */
+ public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
+ {
+ $propertyNotAllowedError = null;
+
+ // array
+ if (Template::METHOD_CALL !== $type) {
+ $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item = (string) $item;
+
+ if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) {
+ try {
+ $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
+ } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
+ goto methodCheck;
+ }
+ }
+
+ if (match (true) {
+ \is_array($object) => \array_key_exists($arrayItem, $object),
+ $object instanceof \ArrayAccess => $object->offsetExists($arrayItem),
+ default => false,
+ }) {
+ if ($isDefinedTest) {
+ return true;
+ }
+
+ return $object[$arrayItem];
+ }
+
+ if (Template::ARRAY_CALL === $type || !\is_object($object)) {
+ if ($isDefinedTest) {
+ return false;
+ }
+
+ if ($ignoreStrictCheck || !$env->isStrictVariables()) {
+ return;
+ }
+
+ if ($object instanceof \ArrayAccess) {
+ $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object));
+ } elseif (\is_object($object)) {
+ $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object));
+ } elseif (\is_array($object)) {
+ if (!$object) {
+ $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem);
+ } else {
+ $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
+ }
+ } elseif (Template::ARRAY_CALL === $type) {
+ if (null === $object) {
+ $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item);
+ } else {
+ $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object);
+ }
+ } elseif (null === $object) {
+ $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
+ } else {
+ $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object);
+ }
+
+ throw new RuntimeError($message, $lineno, $source);
+ }
+ }
+
+ $item = (string) $item;
+
+ if (!\is_object($object)) {
if ($isDefinedTest) {
return false;
}
@@ -1491,260 +1710,432 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar
return;
}
- if ($object instanceof ArrayAccess) {
- $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object));
- } elseif (\is_object($object)) {
- $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object));
+ if (null === $object) {
+ $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
} elseif (\is_array($object)) {
- if (empty($object)) {
- $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem);
- } else {
- $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
- }
- } elseif (/* Template::ARRAY_CALL */ 'array' === $type) {
- if (null === $object) {
- $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item);
- } else {
- $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
- }
- } elseif (null === $object) {
- $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
+ $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item);
} else {
- $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
+ $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object);
}
throw new RuntimeError($message, $lineno, $source);
}
- }
- if (!\is_object($object)) {
- if ($isDefinedTest) {
- return false;
+ if ($object instanceof Template) {
+ throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
}
- if ($ignoreStrictCheck || !$env->isStrictVariables()) {
- return;
- }
-
- if (null === $object) {
- $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
- } elseif (\is_array($object)) {
- $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item);
- } else {
- $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
- }
-
- throw new RuntimeError($message, $lineno, $source);
- }
-
- if ($object instanceof Template) {
- throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
- }
-
- // object property
- if (/* Template::METHOD_CALL */ 'method' !== $type) {
- if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
- if ($isDefinedTest) {
- return true;
- }
-
+ // object property
+ if (Template::METHOD_CALL !== $type) {
if ($sandboxed) {
- $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
+ try {
+ $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
+ } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
+ goto methodCheck;
+ }
}
- return $object->$item;
+ static $propertyCheckers = [];
+
+ if (isset($object->$item)
+ || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item)
+ ) {
+ if ($isDefinedTest) {
+ return true;
+ }
+
+ return $object->$item;
+ }
+
+ if ($object instanceof \DateTimeInterface && \in_array($item, ['date', 'timezone', 'timezone_type'], true)) {
+ if ($isDefinedTest) {
+ return true;
+ }
+
+ return ((array) $object)[$item];
+ }
+
+ if (\defined($object::class.'::'.$item)) {
+ if ($isDefinedTest) {
+ return true;
+ }
+
+ return \constant($object::class.'::'.$item);
+ }
}
- }
- static $cache = [];
+ methodCheck:
- $class = \get_class($object);
+ static $cache = [];
- // object method
- // precedence: getXxx() > isXxx() > hasXxx()
- if (!isset($cache[$class])) {
- $methods = get_class_methods($object);
- sort($methods);
- $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods);
- $classCache = [];
- foreach ($methods as $i => $method) {
- $classCache[$method] = $method;
- $classCache[$lcName = $lcMethods[$i]] = $method;
+ $class = \get_class($object);
- if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) {
- $name = substr($method, 3);
- $lcName = substr($lcName, 3);
- } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) {
- $name = substr($method, 2);
- $lcName = substr($lcName, 2);
- } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) {
- $name = substr($method, 3);
- $lcName = substr($lcName, 3);
- if (\in_array('is'.$lcName, $lcMethods)) {
+ // object method
+ // precedence: getXxx() > isXxx() > hasXxx()
+ if (!isset($cache[$class])) {
+ $methods = get_class_methods($object);
+ sort($methods);
+ $lcMethods = array_map('strtolower', $methods);
+ $classCache = [];
+ foreach ($methods as $i => $method) {
+ $classCache[$method] = $method;
+ $classCache[$lcName = $lcMethods[$i]] = $method;
+
+ if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) {
+ $name = substr($method, 3);
+ $lcName = substr($lcName, 3);
+ } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) {
+ $name = substr($method, 2);
+ $lcName = substr($lcName, 2);
+ } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) {
+ $name = substr($method, 3);
+ $lcName = substr($lcName, 3);
+ if (\in_array('is'.$lcName, $lcMethods)) {
+ continue;
+ }
+ } else {
continue;
}
- } else {
- continue;
+
+ // skip get() and is() methods (in which case, $name is empty)
+ if ($name) {
+ if (!isset($classCache[$name])) {
+ $classCache[$name] = $method;
+ }
+
+ if (!isset($classCache[$lcName])) {
+ $classCache[$lcName] = $method;
+ }
+ }
+ }
+ $cache[$class] = $classCache;
+ }
+
+ $call = false;
+ if (isset($cache[$class][$item])) {
+ $method = $cache[$class][$item];
+ } elseif (isset($cache[$class][$lcItem = strtolower($item)])) {
+ $method = $cache[$class][$lcItem];
+ } elseif (isset($cache[$class]['__call'])) {
+ $method = $item;
+ $call = true;
+ } else {
+ if ($isDefinedTest) {
+ return false;
}
- // skip get() and is() methods (in which case, $name is empty)
- if ($name) {
- if (!isset($classCache[$name])) {
- $classCache[$name] = $method;
+ if ($propertyNotAllowedError) {
+ throw $propertyNotAllowedError;
+ }
+
+ if ($ignoreStrictCheck || !$env->isStrictVariables()) {
+ return;
+ }
+
+ throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
+ }
+
+ if ($sandboxed) {
+ try {
+ $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
+ } catch (SecurityNotAllowedMethodError $e) {
+ if ($isDefinedTest) {
+ return false;
}
- if (!isset($classCache[$lcName])) {
- $classCache[$lcName] = $method;
+ if ($propertyNotAllowedError) {
+ throw $propertyNotAllowedError;
}
+
+ throw $e;
}
}
- $cache[$class] = $classCache;
- }
- $call = false;
- if (isset($cache[$class][$item])) {
- $method = $cache[$class][$item];
- } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) {
- $method = $cache[$class][$lcItem];
- } elseif (isset($cache[$class]['__call'])) {
- $method = $item;
- $call = true;
- } else {
if ($isDefinedTest) {
- return false;
+ return true;
}
- if ($ignoreStrictCheck || !$env->isStrictVariables()) {
- return;
+ // Some objects throw exceptions when they have __call, and the method we try
+ // to call is not supported. If ignoreStrictCheck is true, we should return null.
+ try {
+ $ret = $object->$method(...$arguments);
+ } catch (\BadMethodCallException $e) {
+ if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
+ return;
+ }
+ throw $e;
}
- throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
+ return $ret;
}
- if ($isDefinedTest) {
+ /**
+ * Returns the values from a single column in the input array.
+ *
+ *
+ * {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+ *
+ * {% set fruits = items|column('fruit') %}
+ *
+ * {# fruits now contains ['apple', 'orange'] #}
+ *
+ *
+ * @param array|\Traversable $array An array
+ * @param int|string $name The column name
+ * @param int|string|null $index The column to use as the index/keys for the returned array
+ *
+ * @return array The array of values
+ *
+ * @internal
+ */
+ public static function column($array, $name, $index = null): array
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "column" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
+ }
+
+ if ($array instanceof \Traversable) {
+ $array = iterator_to_array($array);
+ }
+
+ return array_column($array, $name, $index);
+ }
+
+ /**
+ * @param \Closure $arrow
+ *
+ * @internal
+ */
+ public static function filter(Environment $env, $array, $arrow)
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array)));
+ }
+
+ self::checkArrow($env, $arrow, 'filter', 'filter');
+
+ if (\is_array($array)) {
+ return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
+ }
+
+ // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
+ return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
+ }
+
+ /**
+ * @param \Closure $arrow
+ *
+ * @internal
+ */
+ public static function find(Environment $env, $array, $arrow)
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
+ }
+
+ self::checkArrow($env, $arrow, 'find', 'filter');
+
+ foreach ($array as $k => $v) {
+ if ($arrow($v, $k)) {
+ return $v;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param \Closure $arrow
+ *
+ * @internal
+ */
+ public static function map(Environment $env, $array, $arrow)
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
+ }
+
+ self::checkArrow($env, $arrow, 'map', 'filter');
+
+ $r = [];
+ foreach ($array as $k => $v) {
+ $r[$k] = $arrow($v, $k);
+ }
+
+ return $r;
+ }
+
+ /**
+ * @param \Closure $arrow
+ *
+ * @internal
+ */
+ public static function reduce(Environment $env, $array, $arrow, $initial = null)
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
+ }
+
+ self::checkArrow($env, $arrow, 'reduce', 'filter');
+
+ $accumulator = $initial;
+ foreach ($array as $key => $value) {
+ $accumulator = $arrow($accumulator, $value, $key);
+ }
+
+ return $accumulator;
+ }
+
+ /**
+ * @param \Closure $arrow
+ *
+ * @internal
+ */
+ public static function arraySome(Environment $env, $array, $arrow)
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array)));
+ }
+
+ self::checkArrow($env, $arrow, 'has some', 'operator');
+
+ foreach ($array as $k => $v) {
+ if ($arrow($v, $k)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param \Closure $arrow
+ *
+ * @internal
+ */
+ public static function arrayEvery(Environment $env, $array, $arrow)
+ {
+ if (!is_iterable($array)) {
+ throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array)));
+ }
+
+ self::checkArrow($env, $arrow, 'has every', 'operator');
+
+ foreach ($array as $k => $v) {
+ if (!$arrow($v, $k)) {
+ return false;
+ }
+ }
+
return true;
}
- if ($sandboxed) {
- $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
- }
-
- // Some objects throw exceptions when they have __call, and the method we try
- // to call is not supported. If ignoreStrictCheck is true, we should return null.
- try {
- $ret = $object->$method(...$arguments);
- } catch (\BadMethodCallException $e) {
- if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
+ /**
+ * @internal
+ */
+ public static function checkArrow(Environment $env, $arrow, $thing, $type)
+ {
+ if ($arrow instanceof \Closure) {
return;
}
- throw $e;
- }
- return $ret;
-}
-
-/**
- * Returns the values from a single column in the input array.
- *
- *
- * {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- * {% set fruits = items|column('fruit') %}
- *
- * {# fruits now contains ['apple', 'orange'] #}
- *
- *
- * @param array|Traversable $array An array
- * @param mixed $name The column name
- * @param mixed $index The column to use as the index/keys for the returned array
- *
- * @return array The array of values
- */
-function twig_array_column($array, $name, $index = null): array
-{
- if ($array instanceof Traversable) {
- $array = iterator_to_array($array);
- } elseif (!\is_array($array)) {
- throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
- }
-
- return array_column($array, $name, $index);
-}
-
-function twig_array_filter(Environment $env, $array, $arrow)
-{
- if (!is_iterable($array)) {
- throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
- }
-
- twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter');
-
- if (\is_array($array)) {
- return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
- }
-
- // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
- return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
-}
-
-function twig_array_map(Environment $env, $array, $arrow)
-{
- twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter');
-
- $r = [];
- foreach ($array as $k => $v) {
- $r[$k] = $arrow($v, $k);
- }
-
- return $r;
-}
-
-function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
-{
- twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter');
-
- if (!\is_array($array) && !$array instanceof \Traversable) {
- throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
- }
-
- $accumulator = $initial;
- foreach ($array as $key => $value) {
- $accumulator = $arrow($accumulator, $value, $key);
- }
-
- return $accumulator;
-}
-
-function twig_array_some(Environment $env, $array, $arrow)
-{
- twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator');
-
- foreach ($array as $k => $v) {
- if ($arrow($v, $k)) {
- return true;
+ if ($env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) {
+ throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
}
+
+ trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \Closure as an argument to the "%s" %s is deprecated.', $thing, $type);
}
- return false;
-}
+ /**
+ * @internal to be removed in Twig 4
+ */
+ public static function captureOutput(iterable $body): string
+ {
+ $level = ob_get_level();
+ ob_start();
-function twig_array_every(Environment $env, $array, $arrow)
-{
- twig_check_arrow_in_sandbox($env, $arrow, 'has every', 'operator');
+ try {
+ foreach ($body as $data) {
+ echo $data;
+ }
+ } catch (\Throwable $e) {
+ while (ob_get_level() > $level) {
+ ob_end_clean();
+ }
- foreach ($array as $k => $v) {
- if (!$arrow($v, $k)) {
- return false;
+ throw $e;
}
+
+ return ob_get_clean();
}
- return true;
-}
+ /**
+ * @internal
+ */
+ public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
+ {
+ if (!$blockName = $parser->peekBlockStack()) {
+ throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext());
+ }
-function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
-{
- if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
- throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
+ if (!$parser->hasInheritance()) {
+ throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext());
+ }
+
+ return new ParentExpression($blockName, $line);
+ }
+
+ /**
+ * @internal
+ */
+ public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
+ {
+ $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null);
+ $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
+
+ return new BlockReferenceExpression($args[0], $args[1] ?? null, $line);
+ }
+
+ /**
+ * @internal
+ */
+ public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
+ {
+ $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null);
+ $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
+
+ /*
+ Deprecation to uncomment sometimes during the lifetime of the 4.x branch
+ $src = $parser->getStream()->getSourceContext();
+ $dep = new DeprecatedCallableInfo('twig/twig', '3.15', 'The "attribute" function is deprecated, use the "." notation instead.');
+ $dep->setName('attribute');
+ $dep->setType('function');
+ $dep->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
+ */
+
+ return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line);
+ }
+
+ private static function getPropertyChecker(string $class, string $property): \Closure
+ {
+ static $classReflectors = [];
+
+ $class = $classReflectors[$class] ??= new \ReflectionClass($class);
+
+ if (!$class->hasProperty($property)) {
+ static $propertyExists;
+
+ return $propertyExists ??= \Closure::fromCallable('property_exists');
+ }
+
+ $property = $class->getProperty($property);
+
+ if (!$property->isPublic()) {
+ static $false;
+
+ return $false ??= static fn () => false;
+ }
+
+ return static fn ($object) => $property->isInitialized($object);
}
}
-}
diff --git a/lib/twig/twig/src/Extension/DebugExtension.php b/lib/twig/twig/src/Extension/DebugExtension.php
index c0f10d5a3..dac21c317 100644
--- a/lib/twig/twig/src/Extension/DebugExtension.php
+++ b/lib/twig/twig/src/Extension/DebugExtension.php
@@ -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
+ // 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();
+ }
}
diff --git a/lib/twig/twig/src/Extension/EscaperExtension.php b/lib/twig/twig/src/Extension/EscaperExtension.php
index ef8879dbd..52531c436 100644
--- a/lib/twig/twig/src/Extension/EscaperExtension.php
+++ b/lib/twig/twig/src/Extension/EscaperExtension.php
@@ -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 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 '�';
- }
-
- /*
- * 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 => '"', /* quotation mark */
- 38 => '&', /* ampersand */
- 60 => '<', /* less-than sign */
- 62 => '>', /* greater-than sign */
- ];
-
- if (isset($entityMap[$ord])) {
- return $entityMap[$ord];
- }
-
- return sprintf('%02X;', $ord);
- }
-
- /*
- * Per OWASP recommendations, we'll use hex entities for any other
- * characters where a named entity does not exist.
- */
- return sprintf('%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'];
+ }
}
diff --git a/lib/twig/twig/src/Extension/ExtensionInterface.php b/lib/twig/twig/src/Extension/ExtensionInterface.php
index ab9c2c37c..717ef9cec 100644
--- a/lib/twig/twig/src/Extension/ExtensionInterface.php
+++ b/lib/twig/twig/src/Extension/ExtensionInterface.php
@@ -12,9 +12,9 @@
namespace Twig\Extension;
use Twig\ExpressionParser;
-use Twig\Node\Expression\Binary\AbstractBinary;
-use Twig\Node\Expression\Unary\AbstractUnary;
+use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
+use Twig\OperatorPrecedenceChange;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -68,8 +68,8 @@ interface ExtensionInterface
* @return array First array of unary operators, second array of binary operators
*
* @psalm-return array{
- * array}>,
- * array, associativity: ExpressionParser::OPERATOR_*}>
+ * array}>,
+ * array, associativity: ExpressionParser::OPERATOR_*}>
* }
*/
public function getOperators();
diff --git a/lib/twig/twig/src/Extension/GlobalsInterface.php b/lib/twig/twig/src/Extension/GlobalsInterface.php
index 6f1dfe8a7..d52cd107e 100644
--- a/lib/twig/twig/src/Extension/GlobalsInterface.php
+++ b/lib/twig/twig/src/Extension/GlobalsInterface.php
@@ -12,10 +12,7 @@
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
*/
diff --git a/lib/twig/twig/src/Extension/OptimizerExtension.php b/lib/twig/twig/src/Extension/OptimizerExtension.php
index 965bfdb04..d3fe46a67 100644
--- a/lib/twig/twig/src/Extension/OptimizerExtension.php
+++ b/lib/twig/twig/src/Extension/OptimizerExtension.php
@@ -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
diff --git a/lib/twig/twig/src/Extension/SandboxExtension.php b/lib/twig/twig/src/Extension/SandboxExtension.php
index c861159b6..bbf739645 100644
--- a/lib/twig/twig/src/Extension/SandboxExtension.php
+++ b/lib/twig/twig/src/Extension/SandboxExtension.php
@@ -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,15 @@ 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 (\is_array($obj)) {
+ $this->ensureToStringAllowedForArray($obj, $lineno, $source);
+
+ return $obj;
+ }
+
+ if ($obj instanceof \Stringable && $this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {
@@ -120,4 +138,28 @@ final class SandboxExtension extends AbstractExtension
return $obj;
}
+
+ private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void
+ {
+ foreach ($obj as $k => $v) {
+ if (!$v) {
+ continue;
+ }
+
+ if (!\is_array($v)) {
+ $this->ensureToStringAllowed($v, $lineno, $source);
+ continue;
+ }
+
+ if ($r = \ReflectionReference::fromArrayElement($obj, $k)) {
+ if (isset($stack[$r->getId()])) {
+ continue;
+ }
+
+ $stack[$r->getId()] = true;
+ }
+
+ $this->ensureToStringAllowedForArray($v, $lineno, $source, $stack);
+ }
+ }
}
diff --git a/lib/twig/twig/src/Extension/StagingExtension.php b/lib/twig/twig/src/Extension/StagingExtension.php
index 0ea47f90c..59db2ca7d 100644
--- a/lib/twig/twig/src/Extension/StagingExtension.php
+++ b/lib/twig/twig/src/Extension/StagingExtension.php
@@ -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;
diff --git a/lib/twig/twig/src/Extension/StringLoaderExtension.php b/lib/twig/twig/src/Extension/StringLoaderExtension.php
index 7b4514710..698d181f1 100644
--- a/lib/twig/twig/src/Extension/StringLoaderExtension.php
+++ b/lib/twig/twig/src/Extension/StringLoaderExtension.php
@@ -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);
+ }
}
diff --git a/lib/twig/twig/src/Extension/YieldNotReadyExtension.php b/lib/twig/twig/src/Extension/YieldNotReadyExtension.php
new file mode 100644
index 000000000..49dfb8085
--- /dev/null
+++ b/lib/twig/twig/src/Extension/YieldNotReadyExtension.php
@@ -0,0 +1,30 @@
+useYield)];
+ }
+}
diff --git a/lib/twig/twig/src/ExtensionSet.php b/lib/twig/twig/src/ExtensionSet.php
index d32200ceb..99fcfe56b 100644
--- a/lib/twig/twig/src/ExtensionSet.php
+++ b/lib/twig/twig/src/ExtensionSet.php
@@ -15,8 +15,7 @@ use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\StagingExtension;
-use Twig\Node\Expression\Binary\AbstractBinary;
-use Twig\Node\Expression\Unary\AbstractUnary;
+use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
@@ -35,15 +34,21 @@ final class ExtensionSet
private $visitors;
/** @var array */
private $filters;
+ /** @var array */
+ private $dynamicFilters;
/** @var array */
private $tests;
+ /** @var array */
+ private $dynamicTests;
/** @var array */
private $functions;
- /** @var array}> */
+ /** @var array */
+ private $dynamicFunctions;
+ /** @var array}> */
private $unaryOperators;
- /** @var array, associativity: ExpressionParser::OPERATOR_*}> */
+ /** @var array, associativity: ExpressionParser::OPERATOR_*}> */
private $binaryOperators;
- /** @var array */
+ /** @var array|null */
private $globals;
private $functionCallbacks = [];
private $filterCallbacks = [];
@@ -70,7 +75,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];
@@ -125,11 +130,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;
@@ -138,7 +143,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);
@@ -166,14 +171,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);
}
}
@@ -194,7 +196,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);
@@ -222,14 +224,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);
}
}
@@ -328,12 +327,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) {
@@ -343,10 +337,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);
@@ -374,16 +373,11 @@ 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);
}
}
@@ -391,7 +385,7 @@ final class ExtensionSet
}
/**
- * @return array}>
+ * @return array}>
*/
public function getUnaryOperators(): array
{
@@ -403,7 +397,7 @@ final class ExtensionSet
}
/**
- * @return array, associativity: ExpressionParser::OPERATOR_*}>
+ * @return array, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
{
@@ -420,6 +414,9 @@ final class ExtensionSet
$this->filters = [];
$this->functions = [];
$this->tests = [];
+ $this->dynamicFilters = [];
+ $this->dynamicFunctions = [];
+ $this->dynamicTests = [];
$this->visitors = [];
$this->unaryOperators = [];
$this->binaryOperators = [];
@@ -436,17 +433,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
@@ -466,11 +472,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), get_debug_type($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]);
diff --git a/lib/twig/twig/src/FileExtensionEscapingStrategy.php b/lib/twig/twig/src/FileExtensionEscapingStrategy.php
index 812071bf9..5308158d3 100644
--- a/lib/twig/twig/src/FileExtensionEscapingStrategy.php
+++ b/lib/twig/twig/src/FileExtensionEscapingStrategy.php
@@ -45,6 +45,7 @@ class FileExtensionEscapingStrategy
switch ($extension) {
case 'js':
+ case 'json':
return 'js';
case 'css':
diff --git a/lib/twig/twig/src/Lexer.php b/lib/twig/twig/src/Lexer.php
index b23080f58..4983313cf 100644
--- a/lib/twig/twig/src/Lexer.php
+++ b/lib/twig/twig/src/Lexer.php
@@ -48,8 +48,17 @@ class Lexer
public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
public const REGEX_DQ_STRING_DELIM = '/"/A';
public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
+ public const REGEX_INLINE_COMMENT = '/#[^\n]*/A';
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;
@@ -71,8 +80,6 @@ class Lexer
return;
}
- $this->isInitialized = true;
-
// 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 = [
// }}
@@ -160,6 +167,8 @@ 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
@@ -207,11 +216,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);
+ if ($this->brackets) {
+ [$expect, $lineno] = array_pop($this->brackets);
+ throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
return new TokenStream($this->tokens, $this->source);
@@ -221,7 +230,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;
@@ -250,7 +259,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]) {
@@ -268,14 +277,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;
@@ -284,8 +293,8 @@ 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);
+ if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
+ $this->pushToken(Token::BLOCK_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@@ -295,8 +304,8 @@ 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);
+ if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
+ $this->pushToken(Token::VAR_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@@ -311,7 +320,7 @@ 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);
}
}
@@ -321,18 +330,18 @@ class Lexer
$this->moveCursor('...');
}
// arrow function
- elseif ('=' === $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
@@ -341,7 +350,7 @@ 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
@@ -352,22 +361,22 @@ class Lexer
}
// closing bracket
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);
+ if (!$this->brackets) {
+ 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
@@ -376,12 +385,73 @@ class Lexer
$this->pushState(self::STATE_STRING);
$this->moveCursor($match[0]);
}
+ // inline comment
+ elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
+ $this->moveCursor($match[0]);
+ }
// 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" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno);
+ }
+ $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" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno);
+ $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)) {
@@ -403,7 +473,7 @@ class Lexer
}
}
- $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
+ $this->pushToken(Token::TEXT_TYPE, $text);
}
private function lexComment(): void
@@ -419,23 +489,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) && '' !== $match[0]) {
- $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($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);
}
}
@@ -444,7 +514,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 {
@@ -455,7 +525,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;
}
diff --git a/lib/twig/twig/src/Loader/ArrayLoader.php b/lib/twig/twig/src/Loader/ArrayLoader.php
index 5d726c35a..2bb54b7a8 100644
--- a/lib/twig/twig/src/Loader/ArrayLoader.php
+++ b/lib/twig/twig/src/Loader/ArrayLoader.php
@@ -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;
diff --git a/lib/twig/twig/src/Loader/ChainLoader.php b/lib/twig/twig/src/Loader/ChainLoader.php
index fbf4f3a06..6e4f9511c 100644
--- a/lib/twig/twig/src/Loader/ChainLoader.php
+++ b/lib/twig/twig/src/Loader/ChainLoader.php
@@ -21,22 +21,28 @@ use Twig\Source;
*/
final class ChainLoader implements LoaderInterface
{
+ /**
+ * @var array
+ */
private $hasSourceCache = [];
- private $loaders = [];
/**
- * @param LoaderInterface[] $loaders
+ * @param iterable $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).')' : ''));
}
}
diff --git a/lib/twig/twig/src/Loader/FilesystemLoader.php b/lib/twig/twig/src/Loader/FilesystemLoader.php
index 1073a406a..cad163ead 100644
--- a/lib/twig/twig/src/Loader/FilesystemLoader.php
+++ b/lib/twig/twig/src/Loader/FilesystemLoader.php
@@ -24,6 +24,9 @@ class FilesystemLoader implements LoaderInterface
/** Identifier of the main namespace. */
public const MAIN_NAMESPACE = '__main__';
+ /**
+ * @var array>
+ */
protected $paths = [];
protected $cache = [];
protected $errorCache = [];
@@ -31,10 +34,10 @@ class FilesystemLoader implements LoaderInterface
private $rootPath;
/**
- * @param string|array $paths A path or an array of paths where to look for templates
+ * @param string|string[] $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 = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR;
if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
@@ -48,6 +51,8 @@ class FilesystemLoader implements LoaderInterface
/**
* Returns the paths to the templates.
+ *
+ * @return list
*/
public function getPaths(string $namespace = self::MAIN_NAMESPACE): array
{
@@ -58,6 +63,8 @@ class FilesystemLoader implements LoaderInterface
* Returns the path namespaces.
*
* The main namespace is always defined.
+ *
+ * @return list
*/
public function getNamespaces(): array
{
@@ -65,7 +72,7 @@ class FilesystemLoader implements LoaderInterface
}
/**
- * @param string|array $paths A path or an array of paths where to look for templates
+ * @param string|string[] $paths A path or an array of paths where to look for templates
*/
public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void
{
@@ -89,7 +96,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 +112,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 +190,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 +202,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 +225,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 +243,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);
@@ -265,7 +272,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));
}
}
}
diff --git a/lib/twig/twig/src/Markup.php b/lib/twig/twig/src/Markup.php
index 1788acc4f..c7aa65bda 100644
--- a/lib/twig/twig/src/Markup.php
+++ b/lib/twig/twig/src/Markup.php
@@ -16,10 +16,10 @@ namespace Twig;
*
* @author Fabien Potencier
*/
-class Markup implements \Countable, \JsonSerializable
+class Markup implements \Countable, \JsonSerializable, \Stringable
{
private $content;
- private $charset;
+ private ?string $charset;
public function __construct($content, $charset)
{
@@ -32,6 +32,11 @@ class Markup implements \Countable, \JsonSerializable
return $this->content;
}
+ public function getCharset(): string
+ {
+ return $this->charset;
+ }
+
/**
* @return int
*/
diff --git a/lib/twig/twig/src/Node/AutoEscapeNode.php b/lib/twig/twig/src/Node/AutoEscapeNode.php
index cd970411b..ee806396e 100644
--- a/lib/twig/twig/src/Node/AutoEscapeNode.php
+++ b/lib/twig/twig/src/Node/AutoEscapeNode.php
@@ -11,6 +11,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -24,11 +25,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[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
diff --git a/lib/twig/twig/src/Node/BlockNode.php b/lib/twig/twig/src/Node/BlockNode.php
index 0632ba747..b4f939cf6 100644
--- a/lib/twig/twig/src/Node/BlockNode.php
+++ b/lib/twig/twig/src/Node/BlockNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,24 +20,29 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[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\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")
;
diff --git a/lib/twig/twig/src/Node/BlockReferenceNode.php b/lib/twig/twig/src/Node/BlockReferenceNode.php
index cc8af5b52..7c313a04c 100644
--- a/lib/twig/twig/src/Node/BlockReferenceNode.php
+++ b/lib/twig/twig/src/Node/BlockReferenceNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,18 +20,19 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[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')))
;
}
}
diff --git a/lib/twig/twig/src/Node/BodyNode.php b/lib/twig/twig/src/Node/BodyNode.php
index 041cbf685..08115b3bd 100644
--- a/lib/twig/twig/src/Node/BodyNode.php
+++ b/lib/twig/twig/src/Node/BodyNode.php
@@ -11,11 +11,14 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
+
/**
* Represents a body node.
*
* @author Fabien Potencier
*/
+#[YieldReady]
class BodyNode extends Node
{
}
diff --git a/lib/twig/twig/src/Node/CaptureNode.php b/lib/twig/twig/src/Node/CaptureNode.php
new file mode 100644
index 000000000..3b7f0b6d8
--- /dev/null
+++ b/lib/twig/twig/src/Node/CaptureNode.php
@@ -0,0 +1,57 @@
+
+ */
+#[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(';');
+ }
+ }
+}
diff --git a/lib/twig/twig/src/Node/CheckSecurityCallNode.php b/lib/twig/twig/src/Node/CheckSecurityCallNode.php
index a78a38d80..9c162d129 100644
--- a/lib/twig/twig/src/Node/CheckSecurityCallNode.php
+++ b/lib/twig/twig/src/Node/CheckSecurityCallNode.php
@@ -11,17 +11,19 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* @author Fabien Potencier
*/
+#[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")
;
}
diff --git a/lib/twig/twig/src/Node/CheckSecurityNode.php b/lib/twig/twig/src/Node/CheckSecurityNode.php
index 472732796..6e591aad4 100644
--- a/lib/twig/twig/src/Node/CheckSecurityNode.php
+++ b/lib/twig/twig/src/Node/CheckSecurityNode.php
@@ -11,17 +11,24 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* @author Fabien Potencier
*/
+#[YieldReady]
class CheckSecurityNode extends Node
{
private $usedFilters;
private $usedTags;
private $usedFunctions;
+ /**
+ * @param array $usedFilters
+ * @param array $usedTags
+ * @param array $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()
diff --git a/lib/twig/twig/src/Node/CheckToStringNode.php b/lib/twig/twig/src/Node/CheckToStringNode.php
index c7a9d6984..937240c1d 100644
--- a/lib/twig/twig/src/Node/CheckToStringNode.php
+++ b/lib/twig/twig/src/Node/CheckToStringNode.php
@@ -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
*/
+#[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
diff --git a/lib/twig/twig/src/Node/DeprecatedNode.php b/lib/twig/twig/src/Node/DeprecatedNode.php
index 5ff44307f..0772adfc3 100644
--- a/lib/twig/twig/src/Node/DeprecatedNode.php
+++ b/lib/twig/twig/src/Node/DeprecatedNode.php
@@ -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
*/
+#[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")
;
}
}
diff --git a/lib/twig/twig/src/Node/DoNode.php b/lib/twig/twig/src/Node/DoNode.php
index f7783d19f..1593fd050 100644
--- a/lib/twig/twig/src/Node/DoNode.php
+++ b/lib/twig/twig/src/Node/DoNode.php
@@ -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
*/
+#[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
diff --git a/lib/twig/twig/src/Node/EmbedNode.php b/lib/twig/twig/src/Node/EmbedNode.php
index 903c3f6c7..597f95e44 100644
--- a/lib/twig/twig/src/Node/EmbedNode.php
+++ b/lib/twig/twig/src/Node/EmbedNode.php
@@ -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,21 +21,22 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Fabien Potencier
*/
+#[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);
}
- protected function addGetTemplate(Compiler $compiler): void
+ protected function addGetTemplate(Compiler $compiler, string $template = ''): void
{
$compiler
- ->write('$this->loadTemplate(')
+ ->raw('$this->loadTemplate(')
->string($this->getAttribute('name'))
->raw(', ')
->repr($this->getTemplateName())
@@ -44,5 +46,11 @@ class EmbedNode extends IncludeNode
->string($this->getAttribute('index'))
->raw(')')
;
+ if ($this->getAttribute('ignore_missing')) {
+ $compiler
+ ->raw(";\n")
+ ->write(\sprintf("\$%s->getParent(\$context);\n", $template))
+ ;
+ }
}
}
diff --git a/lib/twig/twig/src/Node/EmptyNode.php b/lib/twig/twig/src/Node/EmptyNode.php
new file mode 100644
index 000000000..fd4717ff4
--- /dev/null
+++ b/lib/twig/twig/src/Node/EmptyNode.php
@@ -0,0 +1,33 @@
+
+ */
+#[YieldReady]
+final class EmptyNode extends Node
+{
+ public function __construct(int $lineno = 0)
+ {
+ parent::__construct([], [], $lineno);
+ }
+
+ public function setNode(string $name, Node $node): void
+ {
+ throw new \LogicException('EmptyNode cannot have children.');
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/AbstractExpression.php b/lib/twig/twig/src/Node/Expression/AbstractExpression.php
index 42da0559d..22d8617cd 100644
--- a/lib/twig/twig/src/Node/Expression/AbstractExpression.php
+++ b/lib/twig/twig/src/Node/Expression/AbstractExpression.php
@@ -21,4 +21,23 @@ use Twig\Node\Node;
*/
abstract class AbstractExpression extends Node
{
+ public function isGenerator(): bool
+ {
+ return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator');
+ }
+
+ /**
+ * @return static
+ */
+ public function setExplicitParentheses(): self
+ {
+ $this->setAttribute('with_parentheses', true);
+
+ return $this;
+ }
+
+ public function hasExplicitParentheses(): bool
+ {
+ return $this->hasAttribute('with_parentheses') && $this->getAttribute('with_parentheses');
+ }
}
diff --git a/lib/twig/twig/src/Node/Expression/ArrayExpression.php b/lib/twig/twig/src/Node/Expression/ArrayExpression.php
index 444283802..9769b719e 100644
--- a/lib/twig/twig/src/Node/Expression/ArrayExpression.php
+++ b/lib/twig/twig/src/Node/Expression/ArrayExpression.php
@@ -12,6 +12,7 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
+use Twig\Node\Expression\Unary\StringCastUnary;
class ArrayExpression extends AbstractExpression
{
@@ -55,7 +56,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());
@@ -70,7 +71,7 @@ class ArrayExpression extends AbstractExpression
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
if ($needsArrayMergeSpread) {
- $compiler->raw('twig_array_merge(');
+ $compiler->raw('CoreExtension::merge(');
}
$compiler->raw('[');
$first = true;
@@ -97,7 +98,17 @@ class ArrayExpression extends AbstractExpression
$compiler->raw('...')->subcompile($pair['value']);
++$nextIndex;
} else {
- $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
+ $key = null;
+ if ($pair['key'] instanceof NameExpression) {
+ $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
+ }
+ if ($pair['key'] instanceof TempNameExpression) {
+ $key = $pair['key']->getAttribute('name');
+ $pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
+ }
+ if ($pair['key'] instanceof ConstantExpression) {
+ $key = $pair['key']->getAttribute('value');
+ }
if ($nextIndex !== $key) {
if (\is_int($key)) {
diff --git a/lib/twig/twig/src/Node/Expression/ArrowFunctionExpression.php b/lib/twig/twig/src/Node/Expression/ArrowFunctionExpression.php
index eaad03c9c..2bae4edd7 100644
--- a/lib/twig/twig/src/Node/Expression/ArrowFunctionExpression.php
+++ b/lib/twig/twig/src/Node/Expression/ArrowFunctionExpression.php
@@ -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
diff --git a/lib/twig/twig/src/Node/Expression/AssignNameExpression.php b/lib/twig/twig/src/Node/Expression/AssignNameExpression.php
index 7dd1bc4a3..c2cbb8e4a 100644
--- a/lib/twig/twig/src/Node/Expression/AssignNameExpression.php
+++ b/lib/twig/twig/src/Node/Expression/AssignNameExpression.php
@@ -13,9 +13,25 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
+use Twig\Error\SyntaxError;
+use Twig\Node\Expression\Variable\AssignContextVariable;
class AssignNameExpression extends NameExpression
{
+ public function __construct(string $name, int $lineno)
+ {
+ if (self::class === static::class) {
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, AssignContextVariable::class);
+ }
+
+ // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded
+ if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) {
+ throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno);
+ }
+
+ parent::__construct($name, $lineno);
+ }
+
public function compile(Compiler $compiler): void
{
$compiler
diff --git a/lib/twig/twig/src/Node/Expression/Binary/AbstractBinary.php b/lib/twig/twig/src/Node/Expression/Binary/AbstractBinary.php
index c424e5cc5..ccd3be44f 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/AbstractBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/AbstractBinary.php
@@ -18,8 +18,19 @@ use Twig\Node\Node;
abstract class AbstractBinary extends AbstractExpression
{
+ /**
+ * @param AbstractExpression $left
+ * @param AbstractExpression $right
+ */
public function __construct(Node $left, Node $right, int $lineno)
{
+ if (!$left instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left));
+ }
+ if (!$right instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right));
+ }
+
parent::__construct(['left' => $left, 'right' => $right], [], $lineno);
}
diff --git a/lib/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php b/lib/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php
index 73fa20b1f..a73a5608d 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php
@@ -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(') && str_ends_with($%1$s, $%2$s))', $left, $right))
+ ->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right))
;
}
diff --git a/lib/twig/twig/src/Node/Expression/Binary/EqualBinary.php b/lib/twig/twig/src/Node/Expression/Binary/EqualBinary.php
index 6b48549ef..5f423196f 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/EqualBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/EqualBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/GreaterBinary.php b/lib/twig/twig/src/Node/Expression/Binary/GreaterBinary.php
index e1dd06780..f42de3f86 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/GreaterBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/GreaterBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php b/lib/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php
index df9bfcfbf..0c4f43fd9 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php b/lib/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php
index adfabd44c..c57bb20e9 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php
@@ -18,7 +18,7 @@ class HasEveryBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
- ->raw('twig_array_every($this->env, ')
+ ->raw('CoreExtension::arrayEvery($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php b/lib/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php
index 270da3692..12293f84c 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php
@@ -18,7 +18,7 @@ class HasSomeBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
- ->raw('twig_array_some($this->env, ')
+ ->raw('CoreExtension::arraySome($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/InBinary.php b/lib/twig/twig/src/Node/Expression/Binary/InBinary.php
index 6dbfa97f0..68a98fe15 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/InBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/InBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/LessBinary.php b/lib/twig/twig/src/Node/Expression/Binary/LessBinary.php
index 598e62913..fb3264a2d 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/LessBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/LessBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php b/lib/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php
index e3c4af58d..8f3653892 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/MatchesBinary.php b/lib/twig/twig/src/Node/Expression/Binary/MatchesBinary.php
index a8bce6f4e..0a523c216 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/MatchesBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/MatchesBinary.php
@@ -12,13 +12,31 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
+use Twig\Error\SyntaxError;
+use Twig\Node\Expression\ConstantExpression;
+use Twig\Node\Node;
class MatchesBinary extends AbstractBinary
{
+ public function __construct(Node $left, Node $right, int $lineno)
+ {
+ if ($right instanceof ConstantExpression) {
+ $regexp = $right->getAttribute('value');
+ set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno));
+ try {
+ preg_match($regexp, '');
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ parent::__construct($left, $right, $lineno);
+ }
+
public function compile(Compiler $compiler): void
{
$compiler
- ->raw('twig_matches(')
+ ->raw('CoreExtension::matches(')
->subcompile($this->getNode('right'))
->raw(', ')
->subcompile($this->getNode('left'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php b/lib/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php
index db47a2890..d137ef627 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/NotInBinary.php b/lib/twig/twig/src/Node/Expression/Binary/NotInBinary.php
index fcba6cca1..80c8755d8 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/NotInBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/NotInBinary.php
@@ -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'))
diff --git a/lib/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php b/lib/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php
index 22eff92a7..4519f30d9 100644
--- a/lib/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php
+++ b/lib/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php
@@ -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(') && str_starts_with($%1$s, $%2$s))', $left, $right))
+ ->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right))
;
}
diff --git a/lib/twig/twig/src/Node/Expression/Binary/XorBinary.php b/lib/twig/twig/src/Node/Expression/Binary/XorBinary.php
new file mode 100644
index 000000000..d8ccd7853
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/Binary/XorBinary.php
@@ -0,0 +1,23 @@
+raw('xor');
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/BlockReferenceExpression.php b/lib/twig/twig/src/Node/Expression/BlockReferenceExpression.php
index b1e2a8f7b..0094c7adb 100644
--- a/lib/twig/twig/src/Node/Expression/BlockReferenceExpression.php
+++ b/lib/twig/twig/src/Node/Expression/BlockReferenceExpression.php
@@ -22,14 +22,21 @@ use Twig\Node\Node;
*/
class BlockReferenceExpression extends AbstractExpression
{
- public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null)
+ /**
+ * @param AbstractExpression $name
+ */
+ public function __construct(Node $name, ?Node $template, int $lineno)
{
+ if (!$name instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($name));
+ }
+
$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 +47,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 +73,7 @@ class BlockReferenceExpression extends AbstractExpression
;
}
- $compiler->raw(sprintf('->%s', $method));
+ $compiler->raw(\sprintf('->unwrap()->%s', $method));
return $this->compileBlockArguments($compiler);
}
diff --git a/lib/twig/twig/src/Node/Expression/CallExpression.php b/lib/twig/twig/src/Node/Expression/CallExpression.php
index 3a2d7a4fc..8e999c7eb 100644
--- a/lib/twig/twig/src/Node/Expression/CallExpression.php
+++ b/lib/twig/twig/src/Node/Expression/CallExpression.php
@@ -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) && !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 Twig 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());
@@ -183,13 +208,13 @@ abstract class CallExpression extends AbstractExpression
} elseif ($callableParameter->isDefaultValueAvailable()) {
$optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1);
} elseif ($callableParameter->isOptional()) {
- if (empty($parameters)) {
+ if (!$parameters) {
break;
} else {
$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());
}
}
@@ -210,7 +235,7 @@ abstract class CallExpression extends AbstractExpression
}
}
- if (!empty($parameters)) {
+ if ($parameters) {
$unknownParameter = null;
foreach ($parameters as $parameter) {
if ($parameter instanceof Node) {
@@ -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,90 +257,106 @@ abstract class CallExpression extends AbstractExpression
return $arguments;
}
+ /**
+ * @deprecated since Twig 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') ? $this->getAttribute('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') ? $this->getAttribute('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') ? $this->getAttribute('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 (str_contains($r->name, '{closure}')) {
- return $this->reflector = [$r, $callable, 'Closure'];
- }
-
- if ($object = $r->getClosureThis()) {
- $callable = [$object, $r->name];
- $callableName = get_debug_type($object).'::'.$r->name;
- } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
- $callableName = $class->name.'::'.$r->name;
- } elseif (\PHP_VERSION_ID < 80111 && $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');
}
}
diff --git a/lib/twig/twig/src/Node/Expression/ConstantExpression.php b/lib/twig/twig/src/Node/Expression/ConstantExpression.php
index 7ddbcc6fa..2a8909d54 100644
--- a/lib/twig/twig/src/Node/Expression/ConstantExpression.php
+++ b/lib/twig/twig/src/Node/Expression/ConstantExpression.php
@@ -14,6 +14,9 @@ namespace Twig\Node\Expression;
use Twig\Compiler;
+/**
+ * @final
+ */
class ConstantExpression extends AbstractExpression
{
public function __construct($value, int $lineno)
diff --git a/lib/twig/twig/src/Node/Expression/Filter/DefaultFilter.php b/lib/twig/twig/src/Node/Expression/Filter/DefaultFilter.php
index 6a572d488..a3ee66e47 100644
--- a/lib/twig/twig/src/Node/Expression/Filter/DefaultFilter.php
+++ b/lib/twig/twig/src/Node/Expression/Filter/DefaultFilter.php
@@ -11,7 +11,11 @@
namespace Twig\Node\Expression\Filter;
+use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
+use Twig\Extension\CoreExtension;
+use Twig\Node\EmptyNode;
+use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
@@ -19,6 +23,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 +35,34 @@ use Twig\Node\Node;
*/
class DefaultFilter extends FilterExpression
{
- public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
+ /**
+ * @param AbstractExpression $node
+ */
+ #[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 (!$node instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
+ }
- 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 ($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' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) {
+ $test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $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
diff --git a/lib/twig/twig/src/Node/Expression/Filter/RawFilter.php b/lib/twig/twig/src/Node/Expression/Filter/RawFilter.php
new file mode 100644
index 000000000..e70501e24
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/Filter/RawFilter.php
@@ -0,0 +1,45 @@
+
+ */
+class RawFilter extends FilterExpression
+{
+ /**
+ * @param AbstractExpression $node
+ */
+ #[FirstClassTwigCallableReady]
+ public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0)
+ {
+ if (!$node instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
+ }
+
+ parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine());
+ }
+
+ public function compile(Compiler $compiler): void
+ {
+ $compiler->subcompile($this->getNode('node'));
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/FilterExpression.php b/lib/twig/twig/src/Node/Expression/FilterExpression.php
index 0fc158869..31fb90f1e 100644
--- a/lib/twig/twig/src/Node/Expression/FilterExpression.php
+++ b/lib/twig/twig/src/Node/Expression/FilterExpression.php
@@ -12,28 +12,68 @@
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)
+ /**
+ * @param AbstractExpression $node
+ */
+ #[FirstClassTwigCallableReady]
+ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno)
{
- parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag);
+ if (!$node instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
+ }
+
+ 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);
}
diff --git a/lib/twig/twig/src/Node/Expression/FunctionExpression.php b/lib/twig/twig/src/Node/Expression/FunctionExpression.php
index 71269775c..6215c6abf 100644
--- a/lib/twig/twig/src/Node/Expression/FunctionExpression.php
+++ b/lib/twig/twig/src/Node/Expression/FunctionExpression.php
@@ -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);
}
diff --git a/lib/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/lib/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php
new file mode 100644
index 000000000..7e5c25ff4
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php
@@ -0,0 +1,41 @@
+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));
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php b/lib/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php
new file mode 100644
index 000000000..1f8b0ecf1
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php
@@ -0,0 +1,45 @@
+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" function must be a string.', $this->getTemplateLine(), $this->getSourceContext());
+ }
+
+ if (!enum_exists($value)) {
+ throw new SyntaxError(\sprintf('The first argument of the "enum" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext());
+ }
+
+ if (!$cases = $value::cases()) {
+ throw new SyntaxError(\sprintf('The first argument of the "enum" function must be a non-empty enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext());
+ }
+
+ $compiler->raw(\sprintf('%s::%s', $value, $cases[0]->name));
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/GetAttrExpression.php b/lib/twig/twig/src/Node/Expression/GetAttrExpression.php
index e6a75ce94..e7373de11 100644
--- a/lib/twig/twig/src/Node/Expression/GetAttrExpression.php
+++ b/lib/twig/twig/src/Node/Expression/GetAttrExpression.php
@@ -18,6 +18,10 @@ use Twig\Template;
class GetAttrExpression extends AbstractExpression
{
+
+ /**
+ * @param ArrayExpression|NameExpression|null $arguments
+ */
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno)
{
$nodes = ['node' => $node, 'attribute' => $attribute];
@@ -25,12 +29,17 @@ class GetAttrExpression extends AbstractExpression
$nodes['arguments'] = $arguments;
}
+ if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof NameExpression) {
+ trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
+ }
+
parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno);
}
public function compile(Compiler $compiler): void
{
$env = $compiler->getEnvironment();
+ $arrayAccessSandbox = false;
// optimize array calls
if (
@@ -44,20 +53,38 @@ class GetAttrExpression extends AbstractExpression
->raw('(('.$var.' = ')
->subcompile($this->getNode('node'))
->raw(') && is_array(')
- ->raw($var)
+ ->raw($var);
+
+ if (!$env->hasExtension(SandboxExtension::class)) {
+ $compiler
+ ->raw(') || ')
+ ->raw($var)
+ ->raw(' instanceof ArrayAccess ? (')
+ ->raw($var)
+ ->raw('[')
+ ->subcompile($this->getNode('attribute'))
+ ->raw('] ?? null) : null)')
+ ;
+
+ return;
+ }
+
+ $arrayAccessSandbox = true;
+
+ $compiler
->raw(') || ')
->raw($var)
- ->raw(' instanceof ArrayAccess ? (')
+ ->raw(' instanceof ArrayAccess && in_array(')
+ ->raw($var.'::class')
+ ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
- ->raw('] ?? null) : null)')
+ ->raw('] ?? null) : ')
;
-
- 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);
@@ -83,5 +110,9 @@ class GetAttrExpression extends AbstractExpression
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
->raw(')')
;
+
+ if ($arrayAccessSandbox) {
+ $compiler->raw(')');
+ }
}
}
diff --git a/lib/twig/twig/src/Node/Expression/InlinePrint.php b/lib/twig/twig/src/Node/Expression/InlinePrint.php
index 1ad4751e4..5509f7942 100644
--- a/lib/twig/twig/src/Node/Expression/InlinePrint.php
+++ b/lib/twig/twig/src/Node/Expression/InlinePrint.php
@@ -19,17 +19,21 @@ use Twig\Node\Node;
*/
final class InlinePrint extends AbstractExpression
{
+ /**
+ * @param AbstractExpression $node
+ */
public function __construct(Node $node, int $lineno)
{
+ trigger_deprecation('twig/twig', '3.16', \sprintf('The "%s" class is deprecated with no replacement.', static::class));
+
parent::__construct(['node' => $node], [], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
- ->raw('print (')
+ ->raw('yield ')
->subcompile($this->getNode('node'))
- ->raw(')')
;
}
}
diff --git a/lib/twig/twig/src/Node/Expression/MacroReferenceExpression.php b/lib/twig/twig/src/Node/Expression/MacroReferenceExpression.php
new file mode 100644
index 000000000..abe99aa35
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/MacroReferenceExpression.php
@@ -0,0 +1,56 @@
+
+ */
+class MacroReferenceExpression extends AbstractExpression
+{
+ public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno)
+ {
+ parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
+ }
+
+ public function compile(Compiler $compiler): void
+ {
+ if ($this->getAttribute('is_defined_test')) {
+ $compiler
+ ->subcompile($this->getNode('template'))
+ ->raw('->hasMacro(')
+ ->repr($this->getAttribute('name'))
+ ->raw(', $context')
+ ->raw(')')
+ ;
+
+ return;
+ }
+
+ $compiler
+ ->subcompile($this->getNode('template'))
+ ->raw('->getTemplateForMacro(')
+ ->repr($this->getAttribute('name'))
+ ->raw(', $context, ')
+ ->repr($this->getTemplateLine())
+ ->raw(', $this->getSourceContext())')
+ ->raw(\sprintf('->%s', $this->getAttribute('name')))
+ ->raw('(...')
+ ->subcompile($this->getNode('arguments'))
+ ->raw(')')
+ ;
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/MethodCallExpression.php b/lib/twig/twig/src/Node/Expression/MethodCallExpression.php
index d5ec0b6ef..9aede826c 100644
--- a/lib/twig/twig/src/Node/Expression/MethodCallExpression.php
+++ b/lib/twig/twig/src/Node/Expression/MethodCallExpression.php
@@ -17,6 +17,8 @@ class MethodCallExpression extends AbstractExpression
{
public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno)
{
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class);
+
parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno);
if ($node instanceof NameExpression) {
@@ -39,23 +41,13 @@ class MethodCallExpression extends AbstractExpression
}
$compiler
- ->raw('twig_call_macro($macros[')
+ ->raw('CoreExtension::callMacro($macros[')
->repr($this->getNode('node')->getAttribute('name'))
->raw('], ')
->repr($this->getAttribute('method'))
- ->raw(', [')
- ;
- $first = true;
- foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) {
- if (!$first) {
- $compiler->raw(', ');
- }
- $first = false;
-
- $compiler->subcompile($pair['value']);
- }
- $compiler
- ->raw('], ')
+ ->raw(', ')
+ ->subcompile($this->getNode('arguments'))
+ ->raw(', ')
->repr($this->getTemplateLine())
->raw(', $context, $this->getSourceContext())');
}
diff --git a/lib/twig/twig/src/Node/Expression/NameExpression.php b/lib/twig/twig/src/Node/Expression/NameExpression.php
index c3563f012..78ae51f03 100644
--- a/lib/twig/twig/src/Node/Expression/NameExpression.php
+++ b/lib/twig/twig/src/Node/Expression/NameExpression.php
@@ -13,6 +13,7 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
+use Twig\Node\Expression\Variable\ContextVariable;
class NameExpression extends AbstractExpression
{
@@ -24,6 +25,10 @@ class NameExpression extends AbstractExpression
public function __construct(string $name, int $lineno)
{
+ if (self::class === static::class) {
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class);
+ }
+
parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno);
}
@@ -34,7 +39,7 @@ class NameExpression extends AbstractExpression
$compiler->addDebugInfo($this);
if ($this->getAttribute('is_defined_test')) {
- if ($this->isSpecial()) {
+ if (isset($this->specialVars[$name])) {
$compiler->repr(true);
} elseif (\PHP_VERSION_ID >= 70400) {
$compiler
@@ -51,7 +56,7 @@ class NameExpression extends AbstractExpression
->raw(', $context))')
;
}
- } elseif ($this->isSpecial()) {
+ } elseif (isset($this->specialVars[$name])) {
$compiler->raw($this->specialVars[$name]);
} elseif ($this->getAttribute('always_defined')) {
$compiler
@@ -85,13 +90,23 @@ class NameExpression extends AbstractExpression
}
}
+ /**
+ * @deprecated since Twig 3.11 (to be removed in 4.0)
+ */
public function isSpecial()
{
+ trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__);
+
return isset($this->specialVars[$this->getAttribute('name')]);
}
+ /**
+ * @deprecated since Twig 3.11 (to be removed in 4.0)
+ */
public function isSimple()
{
+ trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__);
+
return !$this->isSpecial() && !$this->getAttribute('is_defined_test');
}
}
diff --git a/lib/twig/twig/src/Node/Expression/NullCoalesceExpression.php b/lib/twig/twig/src/Node/Expression/NullCoalesceExpression.php
index a72bc4fc6..1a5d90286 100644
--- a/lib/twig/twig/src/Node/Expression/NullCoalesceExpression.php
+++ b/lib/twig/twig/src/Node/Expression/NullCoalesceExpression.php
@@ -12,22 +12,35 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
+use Twig\Node\EmptyNode;
use Twig\Node\Expression\Binary\AndBinary;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Test\NullTest;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Node;
+use Twig\TwigTest;
class NullCoalesceExpression extends ConditionalExpression
{
+ /**
+ * @param AbstractExpression $left
+ * @param AbstractExpression $right
+ */
public function __construct(Node $left, Node $right, int $lineno)
{
- $test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine());
+ if (!$left instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left));
+ }
+ if (!$right instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right));
+ }
+
+ $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
// for "block()", we don't need the null test as the return value is always a string
if (!$left instanceof BlockReferenceExpression) {
$test = new AndBinary(
$test,
- new NotUnary(new NullTest($left, 'null', new Node(), $left->getTemplateLine()), $left->getTemplateLine()),
+ new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
$left->getTemplateLine()
);
}
diff --git a/lib/twig/twig/src/Node/Expression/ParentExpression.php b/lib/twig/twig/src/Node/Expression/ParentExpression.php
index 254919718..22fe38f6a 100644
--- a/lib/twig/twig/src/Node/Expression/ParentExpression.php
+++ b/lib/twig/twig/src/Node/Expression/ParentExpression.php
@@ -21,9 +21,9 @@ use Twig\Compiler;
*/
class ParentExpression extends AbstractExpression
{
- public function __construct(string $name, int $lineno, string $tag = null)
+ public function __construct(string $name, int $lineno)
{
- parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag);
+ parent::__construct([], ['output' => false, 'name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
@@ -31,7 +31,7 @@ class ParentExpression extends AbstractExpression
if ($this->getAttribute('output')) {
$compiler
->addDebugInfo($this)
- ->write('$this->displayParentBlock(')
+ ->write('yield from $this->yieldParentBlock(')
->string($this->getAttribute('name'))
->raw(", \$context, \$blocks);\n")
;
diff --git a/lib/twig/twig/src/Node/Expression/TempNameExpression.php b/lib/twig/twig/src/Node/Expression/TempNameExpression.php
index 004c704a5..925b0e7b3 100644
--- a/lib/twig/twig/src/Node/Expression/TempNameExpression.php
+++ b/lib/twig/twig/src/Node/Expression/TempNameExpression.php
@@ -12,20 +12,38 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
+use Twig\Error\SyntaxError;
class TempNameExpression extends AbstractExpression
{
- public function __construct(string $name, int $lineno)
+ public const RESERVED_NAMES = ['varargs', 'context', 'macros', 'blocks', 'this'];
+
+ public function __construct(string|int|null $name, int $lineno)
{
+ // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded
+ if ($name && \in_array(strtolower($name), ['true', 'false', 'none', 'null'])) {
+ throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno);
+ }
+
+ if (self::class === static::class) {
+ trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', self::class);
+ }
+
+ if (null !== $name && (is_int($name) || ctype_digit($name))) {
+ $name = (int) $name;
+ } elseif (in_array($name, self::RESERVED_NAMES)) {
+ $name = "\u{035C}".$name;
+ }
+
parent::__construct([], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
- $compiler
- ->raw('$_')
- ->raw($this->getAttribute('name'))
- ->raw('_')
- ;
+ if (null === $this->getAttribute('name')) {
+ $this->setAttribute('name', $compiler->getVarName());
+ }
+
+ $compiler->raw('$'.$this->getAttribute('name'));
}
}
diff --git a/lib/twig/twig/src/Node/Expression/Test/ConstantTest.php b/lib/twig/twig/src/Node/Expression/Test/ConstantTest.php
index 57e9319d5..867fd0951 100644
--- a/lib/twig/twig/src/Node/Expression/Test/ConstantTest.php
+++ b/lib/twig/twig/src/Node/Expression/Test/ConstantTest.php
@@ -33,16 +33,16 @@ class ConstantTest extends TestExpression
->raw(' === constant(')
;
- if ($this->getNode('arguments')->hasNode(1)) {
+ if ($this->getNode('arguments')->hasNode('1')) {
$compiler
->raw('get_class(')
- ->subcompile($this->getNode('arguments')->getNode(1))
+ ->subcompile($this->getNode('arguments')->getNode('1'))
->raw(')."::".')
;
}
$compiler
- ->subcompile($this->getNode('arguments')->getNode(0))
+ ->subcompile($this->getNode('arguments')->getNode('0'))
->raw('))')
;
}
diff --git a/lib/twig/twig/src/Node/Expression/Test/DefinedTest.php b/lib/twig/twig/src/Node/Expression/Test/DefinedTest.php
index 3953bbbe2..b6c3ff6f9 100644
--- a/lib/twig/twig/src/Node/Expression/Test/DefinedTest.php
+++ b/lib/twig/twig/src/Node/Expression/Test/DefinedTest.php
@@ -11,17 +11,21 @@
namespace Twig\Node\Expression\Test;
+use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Error\SyntaxError;
+use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
+use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Node;
+use Twig\TwigTest;
/**
* Checks if a variable is defined in the current context.
@@ -35,8 +39,16 @@ use Twig\Node\Node;
*/
class DefinedTest extends TestExpression
{
- public function __construct(Node $node, string $name, ?Node $arguments, int $lineno)
+ /**
+ * @param AbstractExpression $node
+ */
+ #[FirstClassTwigCallableReady]
+ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno)
{
+ if (!$node instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
+ }
+
if ($node instanceof NameExpression) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof GetAttrExpression) {
@@ -44,6 +56,8 @@ class DefinedTest extends TestExpression
$this->changeIgnoreStrictCheck($node);
} elseif ($node instanceof BlockReferenceExpression) {
$node->setAttribute('is_defined_test', true);
+ } elseif ($node instanceof MacroReferenceExpression) {
+ $node->setAttribute('is_defined_test', true);
} elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) {
@@ -54,6 +68,10 @@ class DefinedTest extends TestExpression
throw new SyntaxError('The "defined" test only works with simple variables.', $lineno);
}
+ if (\is_string($name) && 'defined' !== $name) {
+ trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.');
+ }
+
parent::__construct($node, $name, $arguments, $lineno);
}
diff --git a/lib/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php b/lib/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php
index 4cb3ee096..90d58a49a 100644
--- a/lib/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php
+++ b/lib/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php
@@ -29,7 +29,7 @@ class DivisiblebyTest extends TestExpression
->raw('(0 == ')
->subcompile($this->getNode('node'))
->raw(' % ')
- ->subcompile($this->getNode('arguments')->getNode(0))
+ ->subcompile($this->getNode('arguments')->getNode('0'))
->raw(')')
;
}
diff --git a/lib/twig/twig/src/Node/Expression/Test/SameasTest.php b/lib/twig/twig/src/Node/Expression/Test/SameasTest.php
index c96d2bc01..f1e24db6f 100644
--- a/lib/twig/twig/src/Node/Expression/Test/SameasTest.php
+++ b/lib/twig/twig/src/Node/Expression/Test/SameasTest.php
@@ -27,7 +27,7 @@ class SameasTest extends TestExpression
->raw('(')
->subcompile($this->getNode('node'))
->raw(' === ')
- ->subcompile($this->getNode('arguments')->getNode(0))
+ ->subcompile($this->getNode('arguments')->getNode('0'))
->raw(')')
;
}
diff --git a/lib/twig/twig/src/Node/Expression/TestExpression.php b/lib/twig/twig/src/Node/Expression/TestExpression.php
index e518bd8f1..3ad6ac557 100644
--- a/lib/twig/twig/src/Node/Expression/TestExpression.php
+++ b/lib/twig/twig/src/Node/Expression/TestExpression.php
@@ -11,31 +11,62 @@
namespace Twig\Node\Expression;
+use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
+use Twig\Node\NameDeprecation;
use Twig\Node\Node;
+use Twig\TwigTest;
class TestExpression extends CallExpression
{
- public function __construct(Node $node, string $name, ?Node $arguments, int $lineno)
+ #[FirstClassTwigCallableReady]
+ /**
+ * @param AbstractExpression $node
+ */
+ public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno)
{
+ if (!$node instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
+ }
+
$nodes = ['node' => $node];
if (null !== $arguments) {
$nodes['arguments'] = $arguments;
}
- parent::__construct($nodes, ['name' => $name], $lineno);
+ if ($test instanceof TwigTest) {
+ $name = $test->getName();
+ } else {
+ $name = $test;
+ trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigTest" when creating a "%s" test of type "%s" is deprecated.', $name, static::class);
+ }
+
+ parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno);
+
+ if ($test instanceof TwigTest) {
+ $this->setAttribute('twig_callable', $test);
+ }
+
+ $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->getAttribute('name');
- $test = $compiler->getEnvironment()->getTest($name);
+ 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 "test" node in a NodeVisitor class is not supported anymore.');
+ $this->removeAttribute('twig_callable');
+ }
+ }
- $this->setAttribute('name', $name);
- $this->setAttribute('type', 'test');
- $this->setAttribute('arguments', $test->getArguments());
- $this->setAttribute('callable', $test->getCallable());
- $this->setAttribute('is_variadic', $test->isVariadic());
+ if (!$this->hasAttribute('twig_callable')) {
+ $this->setAttribute('twig_callable', $compiler->getEnvironment()->getTest($this->getAttribute('name')));
+ }
$this->compileCallable($compiler);
}
diff --git a/lib/twig/twig/src/Node/Expression/Unary/AbstractUnary.php b/lib/twig/twig/src/Node/Expression/Unary/AbstractUnary.php
index e31e3f84b..96739e2b8 100644
--- a/lib/twig/twig/src/Node/Expression/Unary/AbstractUnary.php
+++ b/lib/twig/twig/src/Node/Expression/Unary/AbstractUnary.php
@@ -18,16 +18,30 @@ use Twig\Node\Node;
abstract class AbstractUnary extends AbstractExpression
{
+ /**
+ * @param AbstractExpression $node
+ */
public function __construct(Node $node, int $lineno)
{
- parent::__construct(['node' => $node], [], $lineno);
+ if (!$node instanceof AbstractExpression) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
+ }
+
+ parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno);
}
public function compile(Compiler $compiler): void
{
- $compiler->raw(' ');
+ if ($this->hasExplicitParentheses()) {
+ $compiler->raw('(');
+ } else {
+ $compiler->raw(' ');
+ }
$this->operator($compiler);
$compiler->subcompile($this->getNode('node'));
+ if ($this->hasExplicitParentheses()) {
+ $compiler->raw(')');
+ }
}
abstract public function operator(Compiler $compiler): Compiler;
diff --git a/lib/twig/twig/src/Node/Expression/Unary/SpreadUnary.php b/lib/twig/twig/src/Node/Expression/Unary/SpreadUnary.php
new file mode 100644
index 000000000..f99072c25
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/Unary/SpreadUnary.php
@@ -0,0 +1,22 @@
+raw('...');
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/Unary/StringCastUnary.php b/lib/twig/twig/src/Node/Expression/Unary/StringCastUnary.php
new file mode 100644
index 000000000..87ea17ca8
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/Unary/StringCastUnary.php
@@ -0,0 +1,22 @@
+raw('(string)');
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php b/lib/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php
new file mode 100644
index 000000000..30d810675
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php
@@ -0,0 +1,18 @@
+ $var], ['global' => $global], $var->getTemplateLine());
+ }
+
+ public function compile(Compiler $compiler): void
+ {
+ /** @var TemplateVariable $var */
+ $var = $this->nodes['var'];
+
+ $compiler
+ ->addDebugInfo($this)
+ ->write('$macros[')
+ ->string($var->getName($compiler))
+ ->raw('] = ')
+ ;
+
+ if ($this->getAttribute('global')) {
+ $compiler
+ ->raw('$this->macros[')
+ ->string($var->getName($compiler))
+ ->raw('] = ')
+ ;
+ }
+ }
+}
diff --git a/lib/twig/twig/src/Node/Expression/Variable/ContextVariable.php b/lib/twig/twig/src/Node/Expression/Variable/ContextVariable.php
new file mode 100644
index 000000000..cabc16a43
--- /dev/null
+++ b/lib/twig/twig/src/Node/Expression/Variable/ContextVariable.php
@@ -0,0 +1,18 @@
+getAttribute('name')) {
+ $this->setAttribute('name', $compiler->getVarName());
+ }
+
+ return $this->getAttribute('name');
+ }
+
+ public function compile(Compiler $compiler): void
+ {
+ $name = $this->getName($compiler);
+
+ if ('_self' === $name) {
+ $compiler->raw('$this');
+ } else {
+ $compiler
+ ->raw('$macros[')
+ ->string($name)
+ ->raw(']')
+ ;
+ }
+ }
+}
diff --git a/lib/twig/twig/src/Node/FlushNode.php b/lib/twig/twig/src/Node/FlushNode.php
index fa50a88ee..ff3bd1cf1 100644
--- a/lib/twig/twig/src/Node/FlushNode.php
+++ b/lib/twig/twig/src/Node/FlushNode.php
@@ -11,6 +11,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,18 +19,22 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class FlushNode extends Node
{
- public function __construct(int $lineno, string $tag)
+ public function __construct(int $lineno)
{
- parent::__construct([], [], $lineno, $tag);
+ parent::__construct([], [], $lineno);
}
public function compile(Compiler $compiler): void
{
- $compiler
- ->addDebugInfo($this)
- ->write("flush();\n")
- ;
+ $compiler->addDebugInfo($this);
+
+ if ($compiler->getEnvironment()->useYield()) {
+ $compiler->write("yield '';\n");
+ }
+
+ $compiler->write("flush();\n");
}
}
diff --git a/lib/twig/twig/src/Node/ForLoopNode.php b/lib/twig/twig/src/Node/ForLoopNode.php
index d5ce845a7..1f0a4f321 100644
--- a/lib/twig/twig/src/Node/ForLoopNode.php
+++ b/lib/twig/twig/src/Node/ForLoopNode.php
@@ -11,6 +11,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,11 +19,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class ForLoopNode extends Node
{
- public function __construct(int $lineno, string $tag = null)
+ public function __construct(int $lineno)
{
- parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag);
+ parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno);
}
public function compile(Compiler $compiler): void
@@ -36,7 +38,7 @@ class ForLoopNode extends Node
->write("++\$context['loop']['index0'];\n")
->write("++\$context['loop']['index'];\n")
->write("\$context['loop']['first'] = false;\n")
- ->write("if (isset(\$context['loop']['length'])) {\n")
+ ->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n")
->indent()
->write("--\$context['loop']['revindex0'];\n")
->write("--\$context['loop']['revindex'];\n")
diff --git a/lib/twig/twig/src/Node/ForNode.php b/lib/twig/twig/src/Node/ForNode.php
index 04addfbfe..53a950a90 100644
--- a/lib/twig/twig/src/Node/ForNode.php
+++ b/lib/twig/twig/src/Node/ForNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\AssignNameExpression;
@@ -21,20 +22,21 @@ use Twig\Node\Expression\AssignNameExpression;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class ForNode extends Node
{
private $loop;
- public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, string $tag = null)
+ public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno)
{
- $body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]);
+ $body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]);
$nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body];
if (null !== $else) {
$nodes['else'] = $else;
}
- parent::__construct($nodes, ['with_loop' => true], $lineno, $tag);
+ parent::__construct($nodes, ['with_loop' => true], $lineno);
}
public function compile(Compiler $compiler): void
@@ -42,7 +44,7 @@ class ForNode extends Node
$compiler
->addDebugInfo($this)
->write("\$context['_parent'] = \$context;\n")
- ->write("\$context['_seq'] = twig_ensure_traversable(")
+ ->write("\$context['_seq'] = CoreExtension::ensureTraversable(")
->subcompile($this->getNode('seq'))
->raw(");\n")
;
@@ -99,7 +101,14 @@ class ForNode extends Node
$compiler->write("\$_parent = \$context['_parent'];\n");
// remove some "private" loop variables (needed for nested loops)
- $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n");
+ $compiler->write('unset($context[\'_seq\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']');
+ if ($this->hasNode('else')) {
+ $compiler->raw(', $context[\'_iterated\']');
+ }
+ if ($this->getAttribute('with_loop')) {
+ $compiler->raw(', $context[\'loop\']');
+ }
+ $compiler->raw(");\n");
// keep the values set in the inner context for variables defined in the outer context
$compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n");
diff --git a/lib/twig/twig/src/Node/IfNode.php b/lib/twig/twig/src/Node/IfNode.php
index 569ab7950..2af48fa81 100644
--- a/lib/twig/twig/src/Node/IfNode.php
+++ b/lib/twig/twig/src/Node/IfNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,16 +20,17 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class IfNode extends Node
{
- public function __construct(Node $tests, ?Node $else, int $lineno, string $tag = null)
+ public function __construct(Node $tests, ?Node $else, int $lineno)
{
$nodes = ['tests' => $tests];
if (null !== $else) {
$nodes['else'] = $else;
}
- parent::__construct($nodes, [], $lineno, $tag);
+ parent::__construct($nodes, [], $lineno);
}
public function compile(Compiler $compiler): void
@@ -47,13 +49,13 @@ class IfNode extends Node
}
$compiler
- ->subcompile($this->getNode('tests')->getNode($i))
+ ->subcompile($this->getNode('tests')->getNode((string) $i))
->raw(") {\n")
->indent()
;
// The node might not exists if the content is empty
- if ($this->getNode('tests')->hasNode($i + 1)) {
- $compiler->subcompile($this->getNode('tests')->getNode($i + 1));
+ if ($this->getNode('tests')->hasNode((string) ($i + 1))) {
+ $compiler->subcompile($this->getNode('tests')->getNode((string) ($i + 1)));
}
}
diff --git a/lib/twig/twig/src/Node/ImportNode.php b/lib/twig/twig/src/Node/ImportNode.php
index 5378d799e..124c41ba9 100644
--- a/lib/twig/twig/src/Node/ImportNode.php
+++ b/lib/twig/twig/src/Node/ImportNode.php
@@ -11,38 +11,38 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\NameExpression;
+use Twig\Node\Expression\Variable\AssignTemplateVariable;
/**
* Represents an import node.
*
* @author Fabien Potencier
*/
+#[YieldReady]
class ImportNode extends Node
{
- public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, string $tag = null, bool $global = true)
+ public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno)
{
- parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag);
+ if (\func_num_args() > 3) {
+ trigger_deprecation('twig/twig', '3.15', \sprintf('Passing more than 3 arguments to "%s()" is deprecated.', __METHOD__));
+ }
+
+ if (!$var instanceof AssignTemplateVariable) {
+ trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, AssignTemplateVariable::class));
+
+ $var = new AssignTemplateVariable($var->getAttribute('name'), $lineno);
+ }
+
+ parent::__construct(['expr' => $expr, 'var' => $var], [], $lineno);
}
public function compile(Compiler $compiler): void
{
- $compiler
- ->addDebugInfo($this)
- ->write('$macros[')
- ->repr($this->getNode('var')->getAttribute('name'))
- ->raw('] = ')
- ;
-
- if ($this->getAttribute('global')) {
- $compiler
- ->raw('$this->macros[')
- ->repr($this->getNode('var')->getAttribute('name'))
- ->raw('] = ')
- ;
- }
+ $compiler->subcompile($this->getNode('var'));
if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) {
$compiler->raw('$this');
diff --git a/lib/twig/twig/src/Node/IncludeNode.php b/lib/twig/twig/src/Node/IncludeNode.php
index d540d6b23..5e0c6deb0 100644
--- a/lib/twig/twig/src/Node/IncludeNode.php
+++ b/lib/twig/twig/src/Node/IncludeNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@@ -20,16 +21,17 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class IncludeNode extends Node implements NodeOutputInterface
{
- public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
+ public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
{
$nodes = ['expr' => $expr];
if (null !== $variables) {
$nodes['variables'] = $variables;
}
- parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno, $tag);
+ parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno);
}
public function compile(Compiler $compiler): void
@@ -40,13 +42,12 @@ class IncludeNode extends Node implements NodeOutputInterface
$template = $compiler->getVarName();
$compiler
- ->write(sprintf("$%s = null;\n", $template))
->write("try {\n")
->indent()
- ->write(sprintf('$%s = ', $template))
+ ->write(\sprintf('$%s = ', $template))
;
- $this->addGetTemplate($compiler);
+ $this->addGetTemplate($compiler, $template);
$compiler
->raw(";\n")
@@ -54,12 +55,14 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("} catch (LoaderError \$e) {\n")
->indent()
->write("// ignore missing template\n")
+ ->write(\sprintf("\$$template = null;\n", $template))
->outdent()
->write("}\n")
- ->write(sprintf("if ($%s) {\n", $template))
+ ->write(\sprintf("if ($%s) {\n", $template))
->indent()
- ->write(sprintf('$%s->display(', $template))
+ ->write(\sprintf('yield from $%s->unwrap()->yield(', $template))
;
+
$this->addTemplateArguments($compiler);
$compiler
->raw(");\n")
@@ -67,17 +70,18 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("}\n")
;
} else {
+ $compiler->write('yield from ');
$this->addGetTemplate($compiler);
- $compiler->raw('->display(');
+ $compiler->raw('->unwrap()->yield(');
$this->addTemplateArguments($compiler);
$compiler->raw(");\n");
}
}
- protected function addGetTemplate(Compiler $compiler)
+ protected function addGetTemplate(Compiler $compiler/* , string $template = '' */)
{
$compiler
- ->write('$this->loadTemplate(')
+ ->raw('$this->loadTemplate(')
->subcompile($this->getNode('expr'))
->raw(', ')
->repr($this->getTemplateName())
@@ -93,12 +97,12 @@ class IncludeNode extends Node implements NodeOutputInterface
$compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]');
} elseif (false === $this->getAttribute('only')) {
$compiler
- ->raw('twig_array_merge($context, ')
+ ->raw('CoreExtension::merge($context, ')
->subcompile($this->getNode('variables'))
->raw(')')
;
} else {
- $compiler->raw('twig_to_array(');
+ $compiler->raw('CoreExtension::toArray(');
$compiler->subcompile($this->getNode('variables'));
$compiler->raw(')');
}
diff --git a/lib/twig/twig/src/Node/MacroNode.php b/lib/twig/twig/src/Node/MacroNode.php
index 7f1b24d53..ab37d85a5 100644
--- a/lib/twig/twig/src/Node/MacroNode.php
+++ b/lib/twig/twig/src/Node/MacroNode.php
@@ -11,101 +11,109 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Error\SyntaxError;
+use Twig\Node\Expression\ArrayExpression;
+use Twig\Node\Expression\Variable\LocalVariable;
/**
* Represents a macro node.
*
* @author Fabien Potencier
*/
+#[YieldReady]
class MacroNode extends Node
{
public const VARARGS_NAME = 'varargs';
- public function __construct(string $name, Node $body, Node $arguments, int $lineno, string $tag = null)
+ /**
+ * @param BodyNode $body
+ * @param ArrayExpression $arguments
+ */
+ public function __construct(string $name, Node $body, Node $arguments, int $lineno)
{
- foreach ($arguments as $argumentName => $argument) {
- if (self::VARARGS_NAME === $argumentName) {
- throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext());
+ if (!$body instanceof BodyNode) {
+ trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class));
+ }
+
+ if (!$arguments instanceof ArrayExpression) {
+ trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
+
+ $args = new ArrayExpression([], $arguments->getTemplateLine());
+ foreach ($arguments as $name => $default) {
+ $args->addElement($default, new LocalVariable($name, $default->getTemplateLine()));
+ }
+ $arguments = $args;
+ }
+
+ foreach ($arguments->getKeyValuePairs() as $pair) {
+ if ("\u{035C}".self::VARARGS_NAME === $pair['key']->getAttribute('name')) {
+ throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext());
}
}
- parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag);
+ parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
- ->write(sprintf('public function macro_%s(', $this->getAttribute('name')))
+ ->write(\sprintf('public function macro_%s(', $this->getAttribute('name')))
;
- $count = \count($this->getNode('arguments'));
- $pos = 0;
- foreach ($this->getNode('arguments') as $name => $default) {
+ /** @var ArrayExpression $arguments */
+ $arguments = $this->getNode('arguments');
+ foreach ($arguments->getKeyValuePairs() as $pair) {
+ $name = $pair['key'];
+ $default = $pair['value'];
$compiler
- ->raw('$__'.$name.'__ = ')
+ ->subcompile($name)
+ ->raw(' = ')
->subcompile($default)
+ ->raw(', ')
;
-
- if (++$pos < $count) {
- $compiler->raw(', ');
- }
- }
-
- if ($count) {
- $compiler->raw(', ');
}
$compiler
- ->raw('...$__varargs__')
- ->raw(")\n")
+ ->raw('...$varargs')
+ ->raw("): string|Markup\n")
->write("{\n")
->indent()
->write("\$macros = \$this->macros;\n")
- ->write("\$context = \$this->env->mergeGlobals([\n")
+ ->write("\$context = [\n")
->indent()
;
- foreach ($this->getNode('arguments') as $name => $default) {
+ foreach ($arguments->getKeyValuePairs() as $pair) {
+ $name = $pair['key'];
+ $var = $name->getAttribute('name');
+ if (str_starts_with($var, "\u{035C}")) {
+ $var = substr($var, \strlen("\u{035C}"));
+ }
$compiler
->write('')
- ->string($name)
- ->raw(' => $__'.$name.'__')
+ ->string($var)
+ ->raw(' => ')
+ ->subcompile($name)
->raw(",\n")
;
}
+ $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno);
+
$compiler
->write('')
->string(self::VARARGS_NAME)
->raw(' => ')
- ;
-
- $compiler
- ->raw("\$__varargs__,\n")
+ ->raw("\$varargs,\n")
->outdent()
- ->write("]);\n\n")
+ ->write("] + \$this->env->getGlobals();\n\n")
->write("\$blocks = [];\n\n")
- ;
- if ($compiler->getEnvironment()->isDebug()) {
- $compiler->write("ob_start();\n");
- } else {
- $compiler->write("ob_start(function () { return ''; });\n");
- }
- $compiler
- ->write("try {\n")
- ->indent()
- ->subcompile($this->getNode('body'))
+ ->write('return ')
+ ->subcompile($node)
->raw("\n")
- ->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n")
- ->outdent()
- ->write("} finally {\n")
- ->indent()
- ->write("ob_end_clean();\n")
- ->outdent()
- ->write("}\n")
->outdent()
->write("}\n\n")
;
diff --git a/lib/twig/twig/src/Node/ModuleNode.php b/lib/twig/twig/src/Node/ModuleNode.php
index 9b485eeaf..97c2089fd 100644
--- a/lib/twig/twig/src/Node/ModuleNode.php
+++ b/lib/twig/twig/src/Node/ModuleNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@@ -20,26 +21,34 @@ use Twig\Source;
/**
* Represents a module node.
*
- * Consider this class as being final. If you need to customize the behavior of
- * the generated class, consider adding nodes to the following nodes: display_start,
- * display_end, constructor_start, constructor_end, and class_end.
+ * If you need to customize the behavior of the generated class, add nodes to
+ * the following nodes: display_start, display_end, constructor_start,
+ * constructor_end, and class_end.
*
* @author Fabien Potencier
*/
+#[YieldReady]
final class ModuleNode extends Node
{
+ /**
+ * @param BodyNode $body
+ */
public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source)
{
+ if (!$body instanceof BodyNode) {
+ trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class));
+ }
+
$nodes = [
'body' => $body,
'blocks' => $blocks,
'macros' => $macros,
'traits' => $traits,
- 'display_start' => new Node(),
- 'display_end' => new Node(),
- 'constructor_start' => new Node(),
- 'constructor_end' => new Node(),
- 'class_end' => new Node(),
+ 'display_start' => new Nodes(),
+ 'display_end' => new Nodes(),
+ 'constructor_start' => new Nodes(),
+ 'constructor_end' => new Nodes(),
+ 'class_end' => new Nodes(),
];
if (null !== $parent) {
$nodes['parent'] = $parent;
@@ -106,7 +115,7 @@ final class ModuleNode extends Node
$parent = $this->getNode('parent');
$compiler
- ->write("protected function doGetParent(array \$context)\n", "{\n")
+ ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n")
->indent()
->addDebugInfo($parent)
->write('return ')
@@ -143,6 +152,7 @@ final class ModuleNode extends Node
->write("use Twig\Environment;\n")
->write("use Twig\Error\LoaderError;\n")
->write("use Twig\Error\RuntimeError;\n")
+ ->write("use Twig\Extension\CoreExtension;\n")
->write("use Twig\Extension\SandboxExtension;\n")
->write("use Twig\Markup;\n")
->write("use Twig\Sandbox\SecurityError;\n")
@@ -150,7 +160,9 @@ final class ModuleNode extends Node
->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n")
->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n")
->write("use Twig\Source;\n")
- ->write("use Twig\Template;\n\n")
+ ->write("use Twig\Template;\n")
+ ->write("use Twig\TemplateWrapper;\n")
+ ->write("\n")
;
}
$compiler
@@ -160,8 +172,11 @@ final class ModuleNode extends Node
->raw(" extends Template\n")
->write("{\n")
->indent()
- ->write("private \$source;\n")
- ->write("private \$macros = [];\n\n")
+ ->write("private Source \$source;\n")
+ ->write("/**\n")
+ ->write(" * @var array\n")
+ ->write(" */\n")
+ ->write("private array \$macros = [];\n\n")
;
}
@@ -188,14 +203,14 @@ final class ModuleNode extends Node
$compiler
->addDebugInfo($node)
- ->write(sprintf('$_trait_%s = $this->loadTemplate(', $i))
+ ->write(\sprintf('$_trait_%s = $this->loadTemplate(', $i))
->subcompile($node)
->raw(', ')
->repr($node->getTemplateName())
->raw(', ')
->repr($node->getTemplateLine())
->raw(");\n")
- ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i))
+ ->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i))
->indent()
->write("throw new RuntimeError('Template \"'.")
->subcompile($trait->getNode('template'))
@@ -204,12 +219,12 @@ final class ModuleNode extends Node
->raw(", \$this->source);\n")
->outdent()
->write("}\n")
- ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i))
+ ->write(\sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i))
;
foreach ($trait->getNode('targets') as $key => $value) {
$compiler
- ->write(sprintf('if (!isset($_trait_%s_blocks[', $i))
+ ->write(\sprintf('if (!isset($_trait_%s_blocks[', $i))
->string($key)
->raw("])) {\n")
->indent()
@@ -223,13 +238,17 @@ final class ModuleNode extends Node
->outdent()
->write("}\n\n")
- ->write(sprintf('$_trait_%s_blocks[', $i))
+ ->write(\sprintf('$_trait_%s_blocks[', $i))
->subcompile($value)
- ->raw(sprintf('] = $_trait_%s_blocks[', $i))
+ ->raw(\sprintf('] = $_trait_%s_blocks[', $i))
->string($key)
- ->raw(sprintf(']; unset($_trait_%s_blocks[', $i))
+ ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i))
->string($key)
- ->raw("]);\n\n")
+ ->raw("]); \$this->traitAliases[")
+ ->subcompile($value)
+ ->raw("] = ")
+ ->string($key)
+ ->raw(";\n\n")
;
}
}
@@ -242,7 +261,7 @@ final class ModuleNode extends Node
for ($i = 0; $i < $countTraits; ++$i) {
$compiler
- ->write(sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i))
+ ->write(\sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i))
;
}
@@ -275,7 +294,7 @@ final class ModuleNode extends Node
foreach ($this->getNode('blocks') as $name => $node) {
$compiler
- ->write(sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name))
+ ->write(\sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name))
;
}
@@ -303,7 +322,7 @@ final class ModuleNode extends Node
protected function compileDisplay(Compiler $compiler)
{
$compiler
- ->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n")
+ ->write("protected function doDisplay(array \$context, array \$blocks = []): iterable\n", "{\n")
->indent()
->write("\$macros = \$this->macros;\n")
->subcompile($this->getNode('display_start'))
@@ -324,15 +343,24 @@ final class ModuleNode extends Node
->repr($parent->getTemplateLine())
->raw(");\n")
;
- $compiler->write('$this->parent');
- } else {
- $compiler->write('$this->getParent($context)');
}
- $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n");
+ $compiler->write('yield from ');
+
+ if ($parent instanceof ConstantExpression) {
+ $compiler->raw('$this->parent');
+ } else {
+ $compiler->raw('$this->getParent($context)');
+ }
+ $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n");
+ }
+
+ $compiler->subcompile($this->getNode('display_end'));
+
+ if (!$this->hasNode('parent')) {
+ $compiler->write("yield from [];\n");
}
$compiler
- ->subcompile($this->getNode('display_end'))
->outdent()
->write("}\n\n")
;
@@ -358,7 +386,7 @@ final class ModuleNode extends Node
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
- ->write("public function getTemplateName()\n", "{\n")
+ ->write("public function getTemplateName(): string\n", "{\n")
->indent()
->write('return ')
->repr($this->getSourceContext()->getName())
@@ -380,13 +408,13 @@ final class ModuleNode extends Node
$traitable = !$this->hasNode('parent') && 0 === \count($this->getNode('macros'));
if ($traitable) {
if ($this->getNode('body') instanceof BodyNode) {
- $nodes = $this->getNode('body')->getNode(0);
+ $nodes = $this->getNode('body')->getNode('0');
} else {
$nodes = $this->getNode('body');
}
if (!\count($nodes)) {
- $nodes = new Node([$nodes]);
+ $nodes = new Nodes([$nodes]);
}
foreach ($nodes as $node) {
@@ -394,14 +422,6 @@ final class ModuleNode extends Node
continue;
}
- if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
- continue;
- }
-
- if ($node instanceof BlockReferenceNode) {
- continue;
- }
-
$traitable = false;
break;
}
@@ -415,9 +435,9 @@ final class ModuleNode extends Node
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
- ->write("public function isTraitable()\n", "{\n")
+ ->write("public function isTraitable(): bool\n", "{\n")
->indent()
- ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false'))
+ ->write("return false;\n")
->outdent()
->write("}\n\n")
;
@@ -429,9 +449,9 @@ final class ModuleNode extends Node
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
- ->write("public function getDebugInfo()\n", "{\n")
+ ->write("public function getDebugInfo(): array\n", "{\n")
->indent()
- ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true))))
+ ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true))))
->outdent()
->write("}\n\n")
;
@@ -440,7 +460,7 @@ final class ModuleNode extends Node
protected function compileGetSourceContext(Compiler $compiler)
{
$compiler
- ->write("public function getSourceContext()\n", "{\n")
+ ->write("public function getSourceContext(): Source\n", "{\n")
->indent()
->write('return new Source(')
->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '')
@@ -458,7 +478,7 @@ final class ModuleNode extends Node
{
if ($node instanceof ConstantExpression) {
$compiler
- ->write(sprintf('%s = $this->loadTemplate(', $var))
+ ->write(\sprintf('%s = $this->loadTemplate(', $var))
->subcompile($node)
->raw(', ')
->repr($node->getTemplateName())
diff --git a/lib/twig/twig/src/Node/NameDeprecation.php b/lib/twig/twig/src/Node/NameDeprecation.php
new file mode 100644
index 000000000..63ab28576
--- /dev/null
+++ b/lib/twig/twig/src/Node/NameDeprecation.php
@@ -0,0 +1,46 @@
+
+ */
+class NameDeprecation
+{
+ private $package;
+ private $version;
+ private $newName;
+
+ public function __construct(string $package = '', string $version = '', string $newName = '')
+ {
+ $this->package = $package;
+ $this->version = $version;
+ $this->newName = $newName;
+ }
+
+ public function getPackage(): string
+ {
+ return $this->package;
+ }
+
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ public function getNewName(): string
+ {
+ return $this->newName;
+ }
+}
diff --git a/lib/twig/twig/src/Node/Node.php b/lib/twig/twig/src/Node/Node.php
index 30659ae0f..7b4044c3f 100644
--- a/lib/twig/twig/src/Node/Node.php
+++ b/lib/twig/twig/src/Node/Node.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Source;
@@ -19,61 +20,89 @@ use Twig\Source;
* Represents a node in the AST.
*
* @author Fabien Potencier
+ *
+ * @implements \IteratorAggregate
*/
+#[YieldReady]
class Node implements \Countable, \IteratorAggregate
{
+ /**
+ * @var array
+ */
protected $nodes;
protected $attributes;
protected $lineno;
protected $tag;
private $sourceContext;
+ /** @var array */
+ private $nodeNameDeprecations = [];
+ /** @var array */
+ private $attributeNameDeprecations = [];
/**
- * @param array $nodes An array of named nodes
- * @param array $attributes An array of attributes (should not be nodes)
- * @param int $lineno The line number
- * @param string $tag The tag name associated with the Node
+ * @param array $nodes An array of named nodes
+ * @param array $attributes An array of attributes (should not be nodes)
+ * @param int $lineno The line number
*/
- public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, string $tag = null)
+ public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0)
{
+ if (self::class === static::class) {
+ trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class));
+ }
+
foreach ($nodes as $name => $node) {
if (!$node instanceof self) {
- throw new \InvalidArgumentException(sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class));
+ throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', get_debug_type($node), $name, static::class));
}
}
$this->nodes = $nodes;
$this->attributes = $attributes;
$this->lineno = $lineno;
- $this->tag = $tag;
+
+ if (\func_num_args() > 3) {
+ trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null'));
+ }
}
public function __toString()
{
- $attributes = [];
- foreach ($this->attributes as $name => $value) {
- $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
+ $repr = static::class;
+
+ if ($this->tag) {
+ $repr .= \sprintf("\n tag: %s", $this->tag);
}
- $repr = [static::class.'('.implode(', ', $attributes)];
+ $attributes = [];
+ foreach ($this->attributes as $name => $value) {
+ if (\is_callable($value)) {
+ $v = '\Closure';
+ } elseif ($value instanceof \Stringable) {
+ $v = (string) $value;
+ } else {
+ $v = str_replace("\n", '', var_export($value, true));
+ }
+ $attributes[] = \sprintf('%s: %s', $name, $v);
+ }
+
+ if ($attributes) {
+ $repr .= \sprintf("\n attributes:\n %s", implode("\n ", $attributes));
+ }
if (\count($this->nodes)) {
+ $repr .= "\n nodes:";
foreach ($this->nodes as $name => $node) {
- $len = \strlen($name) + 4;
+ $len = \strlen($name) + 6;
$noderepr = [];
foreach (explode("\n", (string) $node) as $line) {
$noderepr[] = str_repeat(' ', $len).$line;
}
- $repr[] = sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr)));
+ $repr .= \sprintf("\n %s: %s", $name, ltrim(implode("\n", $noderepr)));
}
-
- $repr[] = ')';
- } else {
- $repr[0] .= ')';
}
- return implode("\n", $repr);
+ return $repr;
}
/**
@@ -82,7 +111,7 @@ class Node implements \Countable, \IteratorAggregate
public function compile(Compiler $compiler)
{
foreach ($this->nodes as $node) {
- $node->compile($compiler);
+ $compiler->subcompile($node);
}
}
@@ -96,6 +125,18 @@ class Node implements \Countable, \IteratorAggregate
return $this->tag;
}
+ /**
+ * @internal
+ */
+ public function setNodeTag(string $tag): void
+ {
+ if ($this->tag) {
+ throw new \LogicException('The tag of a node can only be set once.');
+ }
+
+ $this->tag = $tag;
+ }
+
public function hasAttribute(string $name): bool
{
return \array_key_exists($name, $this->attributes);
@@ -104,7 +145,17 @@ class Node implements \Countable, \IteratorAggregate
public function getAttribute(string $name)
{
if (!\array_key_exists($name, $this->attributes)) {
- throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
+ throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
+ }
+
+ $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
+ if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {
+ $dep = $this->attributeNameDeprecations[$name];
+ if ($dep->getNewName()) {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName());
+ } else {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class);
+ }
}
return $this->attributes[$name];
@@ -112,38 +163,96 @@ class Node implements \Countable, \IteratorAggregate
public function setAttribute(string $name, $value): void
{
+ $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true;
+ if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {
+ $dep = $this->attributeNameDeprecations[$name];
+ if ($dep->getNewName()) {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName());
+ } else {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class);
+ }
+ }
+
$this->attributes[$name] = $value;
}
+ public function deprecateAttribute(string $name, NameDeprecation $dep): void
+ {
+ $this->attributeNameDeprecations[$name] = $dep;
+ }
+
public function removeAttribute(string $name): void
{
unset($this->attributes[$name]);
}
+ /**
+ * @param string|int $name
+ */
public function hasNode(string $name): bool
{
return isset($this->nodes[$name]);
}
+ /**
+ * @param string|int $name
+ */
public function getNode(string $name): self
{
if (!isset($this->nodes[$name])) {
- throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, static::class));
+ throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class));
+ }
+
+ $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
+ if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) {
+ $dep = $this->nodeNameDeprecations[$name];
+ if ($dep->getNewName()) {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName());
+ } else {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class);
+ }
}
return $this->nodes[$name];
}
+ /**
+ * @param string|int $name
+ */
public function setNode(string $name, self $node): void
{
+ $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true;
+ if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) {
+ $dep = $this->nodeNameDeprecations[$name];
+ if ($dep->getNewName()) {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName());
+ } else {
+ trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class);
+ }
+ }
+
+ if (null !== $this->sourceContext) {
+ $node->setSourceContext($this->sourceContext);
+ }
$this->nodes[$name] = $node;
}
+ /**
+ * @param string|int $name
+ */
public function removeNode(string $name): void
{
unset($this->nodes[$name]);
}
+ /**
+ * @param string|int $name
+ */
+ public function deprecateNode(string $name, NameDeprecation $dep): void
+ {
+ $this->nodeNameDeprecations[$name] = $dep;
+ }
+
/**
* @return int
*/
diff --git a/lib/twig/twig/src/Node/Nodes.php b/lib/twig/twig/src/Node/Nodes.php
new file mode 100644
index 000000000..bd67053ab
--- /dev/null
+++ b/lib/twig/twig/src/Node/Nodes.php
@@ -0,0 +1,28 @@
+
+ */
+#[YieldReady]
+final class Nodes extends Node
+{
+ public function __construct(array $nodes = [], int $lineno = 0)
+ {
+ parent::__construct($nodes, [], $lineno);
+ }
+}
diff --git a/lib/twig/twig/src/Node/PrintNode.php b/lib/twig/twig/src/Node/PrintNode.php
index 60386d299..e3c23bbfa 100644
--- a/lib/twig/twig/src/Node/PrintNode.php
+++ b/lib/twig/twig/src/Node/PrintNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@@ -20,19 +21,23 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class PrintNode extends Node implements NodeOutputInterface
{
- 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
{
+ /** @var AbstractExpression */
+ $expr = $this->getNode('expr');
+
$compiler
->addDebugInfo($this)
- ->write('echo ')
- ->subcompile($this->getNode('expr'))
+ ->write($expr->isGenerator() ? 'yield from ' : 'yield ')
+ ->subcompile($expr)
->raw(";\n")
;
}
diff --git a/lib/twig/twig/src/Node/SandboxNode.php b/lib/twig/twig/src/Node/SandboxNode.php
index 4d5666bff..d51cea44b 100644
--- a/lib/twig/twig/src/Node/SandboxNode.php
+++ b/lib/twig/twig/src/Node/SandboxNode.php
@@ -11,6 +11,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,11 +19,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class SandboxNode extends Node
{
- public function __construct(Node $body, int $lineno, string $tag = null)
+ public function __construct(Node $body, int $lineno)
{
- parent::__construct(['body' => $body], [], $lineno, $tag);
+ parent::__construct(['body' => $body], [], $lineno);
}
public function compile(Compiler $compiler): void
diff --git a/lib/twig/twig/src/Node/SetNode.php b/lib/twig/twig/src/Node/SetNode.php
index 96b6bd8bf..6e0661edb 100644
--- a/lib/twig/twig/src/Node/SetNode.php
+++ b/lib/twig/twig/src/Node/SetNode.php
@@ -11,6 +11,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\ConstantExpression;
@@ -19,26 +20,35 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class SetNode extends Node implements NodeCaptureInterface
{
- public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null)
+ public function __construct(bool $capture, Node $names, Node $values, int $lineno)
{
- parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag);
-
/*
* Optimizes the node when capture is used for a large block of text.
*
* {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo");
*/
- if ($this->getAttribute('capture')) {
- $this->setAttribute('safe', true);
-
- $values = $this->getNode('values');
- if ($values instanceof TextNode) {
- $this->setNode('values', new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()));
- $this->setAttribute('capture', false);
+ $safe = false;
+ if ($capture) {
+ $safe = true;
+ // Node::class === get_class($values) should be removed in Twig 4.0
+ if (($values instanceof Nodes || Node::class === get_class($values)) && !count($values)) {
+ $values = new ConstantExpression('', $values->getTemplateLine());
+ $capture = false;
+ } elseif ($values instanceof TextNode) {
+ $values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine());
+ $capture = false;
+ } elseif ($values instanceof PrintNode && $values->getNode('expr') instanceof ConstantExpression) {
+ $values = $values->getNode('expr');
+ $capture = false;
+ } else {
+ $values = new CaptureNode($values, $values->getTemplateLine());
}
}
+
+ parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno);
}
public function compile(Compiler $compiler): void
@@ -46,7 +56,7 @@ class SetNode extends Node implements NodeCaptureInterface
$compiler->addDebugInfo($this);
if (\count($this->getNode('names')) > 1) {
- $compiler->write('list(');
+ $compiler->write('[');
foreach ($this->getNode('names') as $idx => $node) {
if ($idx) {
$compiler->raw(', ');
@@ -54,29 +64,15 @@ class SetNode extends Node implements NodeCaptureInterface
$compiler->subcompile($node);
}
- $compiler->raw(')');
+ $compiler->raw(']');
} else {
- if ($this->getAttribute('capture')) {
- if ($compiler->getEnvironment()->isDebug()) {
- $compiler->write("ob_start();\n");
- } else {
- $compiler->write("ob_start(function () { return ''; });\n");
- }
- $compiler
- ->subcompile($this->getNode('values'))
- ;
- }
-
$compiler->subcompile($this->getNode('names'), false);
-
- if ($this->getAttribute('capture')) {
- $compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset())");
- }
}
+ $compiler->raw(' = ');
- if (!$this->getAttribute('capture')) {
- $compiler->raw(' = ');
-
+ if ($this->getAttribute('capture')) {
+ $compiler->subcompile($this->getNode('values'));
+ } else {
if (\count($this->getNode('names')) > 1) {
$compiler->write('[');
foreach ($this->getNode('values') as $idx => $value) {
@@ -89,17 +85,31 @@ class SetNode extends Node implements NodeCaptureInterface
$compiler->raw(']');
} else {
if ($this->getAttribute('safe')) {
- $compiler
- ->raw("('' === \$tmp = ")
- ->subcompile($this->getNode('values'))
- ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())")
- ;
+ if ($this->getNode('values') instanceof ConstantExpression) {
+ if ('' === $this->getNode('values')->getAttribute('value')) {
+ $compiler->raw('""');
+ } else {
+ $compiler
+ ->raw('new Markup(')
+ ->subcompile($this->getNode('values'))
+ ->raw(', $this->env->getCharset())')
+ ;
+ }
+ } else {
+ $compiler
+ ->raw("('' === \$tmp = ")
+ ->subcompile($this->getNode('values'))
+ ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())")
+ ;
+ }
} else {
$compiler->subcompile($this->getNode('values'));
}
}
+
+ $compiler->raw(';');
}
- $compiler->raw(";\n");
+ $compiler->raw("\n");
}
}
diff --git a/lib/twig/twig/src/Node/TextNode.php b/lib/twig/twig/src/Node/TextNode.php
index d74ebe630..fae65fb2c 100644
--- a/lib/twig/twig/src/Node/TextNode.php
+++ b/lib/twig/twig/src/Node/TextNode.php
@@ -12,6 +12,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,6 +20,7 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class TextNode extends Node implements NodeOutputInterface
{
public function __construct(string $data, int $lineno)
@@ -28,9 +30,10 @@ class TextNode extends Node implements NodeOutputInterface
public function compile(Compiler $compiler): void
{
+ $compiler->addDebugInfo($this);
+
$compiler
- ->addDebugInfo($this)
- ->write('echo ')
+ ->write('yield ')
->string($this->getAttribute('data'))
->raw(";\n")
;
diff --git a/lib/twig/twig/src/Node/TypesNode.php b/lib/twig/twig/src/Node/TypesNode.php
new file mode 100644
index 000000000..ebb304d49
--- /dev/null
+++ b/lib/twig/twig/src/Node/TypesNode.php
@@ -0,0 +1,28 @@
+
+ */
+#[YieldReady]
+class TypesNode extends Node
+{
+ /**
+ * @param array $types
+ */
+ public function __construct(array $types, int $lineno)
+ {
+ parent::__construct([], ['mapping' => $types], $lineno);
+ }
+
+ public function compile(Compiler $compiler)
+ {
+ // Don't compile anything.
+ }
+}
diff --git a/lib/twig/twig/src/Node/WithNode.php b/lib/twig/twig/src/Node/WithNode.php
index 2ac9123d0..487e2800b 100644
--- a/lib/twig/twig/src/Node/WithNode.php
+++ b/lib/twig/twig/src/Node/WithNode.php
@@ -11,6 +11,7 @@
namespace Twig\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,16 +19,17 @@ use Twig\Compiler;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class WithNode extends Node
{
- public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, string $tag = null)
+ public function __construct(Node $body, ?Node $variables, bool $only, int $lineno)
{
$nodes = ['body' => $body];
if (null !== $variables) {
$nodes['variables'] = $variables;
}
- parent::__construct($nodes, ['only' => $only], $lineno, $tag);
+ parent::__construct($nodes, ['only' => $only], $lineno);
}
public function compile(Compiler $compiler): void
@@ -36,35 +38,35 @@ class WithNode extends Node
$parentContextName = $compiler->getVarName();
- $compiler->write(sprintf("\$%s = \$context;\n", $parentContextName));
+ $compiler->write(\sprintf("\$%s = \$context;\n", $parentContextName));
if ($this->hasNode('variables')) {
$node = $this->getNode('variables');
$varsName = $compiler->getVarName();
$compiler
- ->write(sprintf('$%s = ', $varsName))
+ ->write(\sprintf('$%s = ', $varsName))
->subcompile($node)
->raw(";\n")
- ->write(sprintf("if (!is_iterable(\$%s)) {\n", $varsName))
+ ->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName))
->indent()
- ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ")
+ ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ")
->repr($node->getTemplateLine())
->raw(", \$this->getSourceContext());\n")
->outdent()
->write("}\n")
- ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName))
+ ->write(\sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName))
;
if ($this->getAttribute('only')) {
$compiler->write("\$context = [];\n");
}
- $compiler->write(sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName));
+ $compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName));
}
$compiler
->subcompile($this->getNode('body'))
- ->write(sprintf("\$context = \$%s;\n", $parentContextName))
+ ->write(\sprintf("\$context = \$%s;\n", $parentContextName))
;
}
}
diff --git a/lib/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php
index d7036ae55..5de35fd09 100644
--- a/lib/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php
+++ b/lib/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php
@@ -17,9 +17,9 @@ use Twig\Node\Node;
/**
* Used to make node visitors compatible with Twig 1.x and 2.x.
*
- * To be removed in Twig 3.1.
- *
* @author Fabien Potencier
+ *
+ * @deprecated since 3.9 (to be removed in 4.0)
*/
abstract class AbstractNodeVisitor implements NodeVisitorInterface
{
diff --git a/lib/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php
index c390d7cc7..9640c541b 100644
--- a/lib/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php
+++ b/lib/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php
@@ -16,14 +16,14 @@ use Twig\Extension\EscaperExtension;
use Twig\Node\AutoEscapeNode;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
-use Twig\Node\DoNode;
+use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
-use Twig\Node\Expression\InlinePrint;
use Twig\Node\ImportNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\NodeTraverser;
@@ -59,7 +59,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
} elseif ($node instanceof BlockNode) {
$this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping();
} elseif ($node instanceof ImportNode) {
- $this->safeVars[] = $node->getNode('var')->getAttribute('name');
+ $this->safeVars[] = $node->getNode('var')->getNode('var')->getAttribute('name');
}
return $node;
@@ -75,11 +75,13 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
return $this->preEscapeFilterNode($node, $env);
} elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
$expression = $node->getNode('expr');
- if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) {
- return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine());
+ if ($expression instanceof ConditionalExpression) {
+ $this->escapeConditional($expression, $env, $type);
+ } else {
+ $node->setNode('expr', $this->escapeExpression($expression, $env, $type));
}
- return $this->escapePrintNode($node, $env, $type);
+ return $node;
}
if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) {
@@ -91,85 +93,60 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
return $node;
}
- private function shouldUnwrapConditional(ConditionalExpression $expression, Environment $env, string $type): bool
+ private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): void
{
- $expr2Safe = $this->isSafeFor($type, $expression->getNode('expr2'), $env);
- $expr3Safe = $this->isSafeFor($type, $expression->getNode('expr3'), $env);
-
- return $expr2Safe !== $expr3Safe;
- }
-
- private function unwrapConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression
- {
- // convert "echo a ? b : c" to "a ? echo b : echo c" recursively
+ /** @var AbstractExpression $expr2 */
$expr2 = $expression->getNode('expr2');
- if ($expr2 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr2, $env, $type)) {
- $expr2 = $this->unwrapConditional($expr2, $env, $type);
+ if ($expr2 instanceof ConditionalExpression) {
+ $this->escapeConditional($expr2, $env, $type);
} else {
- $expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type);
+ $expression->setNode('expr2', $this->escapeExpression($expr2, $env, $type));
}
+
+ /** @var AbstractExpression $expr3 */
$expr3 = $expression->getNode('expr3');
- if ($expr3 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr3, $env, $type)) {
- $expr3 = $this->unwrapConditional($expr3, $env, $type);
+ if ($expr3 instanceof ConditionalExpression) {
+ $this->escapeConditional($expr3, $env, $type);
} else {
- $expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type);
+ $expression->setNode('expr3', $this->escapeExpression($expr3, $env, $type));
}
-
- return new ConditionalExpression($expression->getNode('expr1'), $expr2, $expr3, $expression->getTemplateLine());
}
- private function escapeInlinePrintNode(InlinePrint $node, Environment $env, string $type): Node
+ private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression
{
- $expression = $node->getNode('node');
-
- if ($this->isSafeFor($type, $expression, $env)) {
- return $node;
- }
-
- return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
- }
-
- private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node
- {
- if (false === $type) {
- return $node;
- }
-
- $expression = $node->getNode('expr');
-
- if ($this->isSafeFor($type, $expression, $env)) {
- return $node;
- }
-
- $class = \get_class($node);
-
- return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
+ return $this->isSafeFor($type, $expression, $env) ? $expression : $this->getEscaperFilter($env, $type, $expression);
}
private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression
{
- $name = $filter->getNode('filter')->getAttribute('value');
+ if ($filter->hasAttribute('twig_callable')) {
+ $type = $filter->getAttribute('twig_callable')->getPreEscape();
+ } else {
+ // legacy
+ $name = $filter->getNode('filter', false)->getAttribute('value');
+ $type = $env->getFilter($name)->getPreEscape();
+ }
- $type = $env->getFilter($name)->getPreEscape();
if (null === $type) {
return $filter;
}
+ /** @var AbstractExpression $node */
$node = $filter->getNode('node');
if ($this->isSafeFor($type, $node, $env)) {
return $filter;
}
- $filter->setNode('node', $this->getEscaperFilter($type, $node));
+ $filter->setNode('node', $this->getEscaperFilter($env, $type, $node));
return $filter;
}
- private function isSafeFor(string $type, Node $expression, Environment $env): bool
+ private function isSafeFor(string $type, AbstractExpression $expression, Environment $env): bool
{
$safe = $this->safeAnalysis->getSafe($expression);
- if (null === $safe) {
+ if (!$safe) {
if (null === $this->traverser) {
$this->traverser = new NodeTraverser($env, [$this->safeAnalysis]);
}
@@ -192,13 +169,13 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
return $this->defaultStrategy ?: false;
}
- private function getEscaperFilter(string $type, Node $node): FilterExpression
+ private function getEscaperFilter(Environment $env, string $type, AbstractExpression $node): FilterExpression
{
$line = $node->getTemplateLine();
- $name = new ConstantExpression('escape', $line);
- $args = new Node([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
+ $filter = $env->getFilter('escape');
+ $args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
- return new FilterExpression($node, $name, $args, $line);
+ return new FilterExpression($node, $filter, $args, $line);
}
public function getPriority(): int
diff --git a/lib/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php
deleted file mode 100644
index d6a7781ba..000000000
--- a/lib/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php
+++ /dev/null
@@ -1,74 +0,0 @@
-
- *
- * @internal
- */
-final class MacroAutoImportNodeVisitor implements NodeVisitorInterface
-{
- private $inAModule = false;
- private $hasMacroCalls = false;
-
- public function enterNode(Node $node, Environment $env): Node
- {
- if ($node instanceof ModuleNode) {
- $this->inAModule = true;
- $this->hasMacroCalls = false;
- }
-
- return $node;
- }
-
- public function leaveNode(Node $node, Environment $env): Node
- {
- if ($node instanceof ModuleNode) {
- $this->inAModule = false;
- if ($this->hasMacroCalls) {
- $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, 'import', true));
- }
- } elseif ($this->inAModule) {
- if (
- $node instanceof GetAttrExpression
- && $node->getNode('node') instanceof NameExpression
- && '_self' === $node->getNode('node')->getAttribute('name')
- && $node->getNode('attribute') instanceof ConstantExpression
- ) {
- $this->hasMacroCalls = true;
-
- $name = $node->getNode('attribute')->getAttribute('value');
- $node = new MethodCallExpression($node->getNode('node'), 'macro_'.$name, $node->getNode('arguments'), $node->getTemplateLine());
- $node->setAttribute('safe', true);
- }
- }
-
- return $node;
- }
-
- public function getPriority(): int
- {
- // we must be ran before auto-escaping
- return -10;
- }
-}
diff --git a/lib/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php
index 6b39f0094..a943f45c3 100644
--- a/lib/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php
+++ b/lib/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php
@@ -15,7 +15,6 @@ use Twig\Environment;
use Twig\Node\BlockReferenceNode;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression;
-use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
@@ -24,6 +23,7 @@ use Twig\Node\ForNode;
use Twig\Node\IncludeNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
+use Twig\Node\TextNode;
/**
* Tries to optimize the AST.
@@ -43,21 +43,28 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
public const OPTIMIZE_NONE = 0;
public const OPTIMIZE_FOR = 2;
public const OPTIMIZE_RAW_FILTER = 4;
+ public const OPTIMIZE_TEXT_NODES = 8;
private $loops = [];
private $loopsTargets = [];
- private $optimizers;
/**
* @param int $optimizers The optimizer mode
*/
- public function __construct(int $optimizers = -1)
- {
- if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) {
- throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers));
+ public function __construct(
+ private int $optimizers = -1,
+ ) {
+ if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) {
+ throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers));
}
- $this->optimizers = $optimizers;
+ if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) {
+ trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.');
+ }
+
+ if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) {
+ trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.');
+ }
}
public function enterNode(Node $node, Environment $env): Node
@@ -75,10 +82,6 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
$this->leaveOptimizeFor($node);
}
- if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) {
- $node = $this->optimizeRawFilter($node);
- }
-
$node = $this->optimizePrintNode($node);
return $node;
@@ -98,6 +101,11 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
}
$exprNode = $node->getNode('expr');
+
+ if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) {
+ return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine());
+ }
+
if (
$exprNode instanceof BlockReferenceExpression
|| $exprNode instanceof ParentExpression
@@ -110,18 +118,6 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
return $node;
}
- /**
- * Removes "raw" filters.
- */
- private function optimizeRawFilter(Node $node): Node
- {
- if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) {
- return $node->getNode('node');
- }
-
- return $node;
- }
-
/**
* Optimizes "for" tag by removing the "loop" variable creation whenever possible.
*/
diff --git a/lib/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php
index 90d6f2e0f..9eda8c8e1 100644
--- a/lib/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php
+++ b/lib/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php
@@ -18,6 +18,7 @@ use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
+use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression;
@@ -36,11 +37,14 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
$this->safeVars = $safeVars;
}
+ /**
+ * @return array
+ */
public function getSafe(Node $node)
{
$hash = spl_object_hash($node);
if (!isset($this->data[$hash])) {
- return;
+ return [];
}
foreach ($this->data[$hash] as $bucket) {
@@ -54,6 +58,8 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
return $bucket['value'];
}
+
+ return [];
}
private function setSafe(Node $node, array $safe): void
@@ -96,49 +102,58 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
$this->setSafe($node, $safe);
} elseif ($node instanceof FilterExpression) {
// filter expression is safe when the filter is safe
- $name = $node->getNode('filter')->getAttribute('value');
- $args = $node->getNode('arguments');
- if ($filter = $env->getFilter($name)) {
- $safe = $filter->getSafe($args);
+ if ($node->hasAttribute('twig_callable')) {
+ $filter = $node->getAttribute('twig_callable');
+ } else {
+ // legacy
+ $filter = $env->getFilter($node->getAttribute('name'));
+ }
+
+ if ($filter) {
+ $safe = $filter->getSafe($node->getNode('arguments'));
if (null === $safe) {
+ trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class);
+ $safe = [];
+ }
+
+ if (!$safe) {
$safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety());
}
$this->setSafe($node, $safe);
- } else {
- $this->setSafe($node, []);
}
} elseif ($node instanceof FunctionExpression) {
// function expression is safe when the function is safe
- $name = $node->getAttribute('name');
- $args = $node->getNode('arguments');
- if ($function = $env->getFunction($name)) {
- $this->setSafe($node, $function->getSafe($args));
+ if ($node->hasAttribute('twig_callable')) {
+ $function = $node->getAttribute('twig_callable');
} else {
- $this->setSafe($node, []);
+ // legacy
+ $function = $env->getFunction($node->getAttribute('name'));
}
- } elseif ($node instanceof MethodCallExpression) {
- if ($node->getAttribute('safe')) {
- $this->setSafe($node, ['all']);
- } else {
- $this->setSafe($node, []);
+
+ if ($function) {
+ $safe = $function->getSafe($node->getNode('arguments'));
+ if (null === $safe) {
+ trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $function::class);
+ $safe = [];
+ }
+ $this->setSafe($node, $safe);
}
+ } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) {
+ // all macro calls are safe
+ $this->setSafe($node, ['all']);
} elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) {
$name = $node->getNode('node')->getAttribute('name');
if (\in_array($name, $this->safeVars)) {
$this->setSafe($node, ['all']);
- } else {
- $this->setSafe($node, []);
}
- } else {
- $this->setSafe($node, []);
}
return $node;
}
- private function intersectSafe(array $a = null, array $b = null): array
+ private function intersectSafe(array $a, array $b): array
{
- if (null === $a || null === $b) {
+ if (!$a || !$b) {
return [];
}
diff --git a/lib/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php
index 1446cee6b..74b686f6e 100644
--- a/lib/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php
+++ b/lib/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php
@@ -15,14 +15,17 @@ use Twig\Environment;
use Twig\Node\CheckSecurityCallNode;
use Twig\Node\CheckSecurityNode;
use Twig\Node\CheckToStringNode;
+use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\Binary\RangeBinary;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
+use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
@@ -34,8 +37,11 @@ use Twig\Node\SetNode;
final class SandboxNodeVisitor implements NodeVisitorInterface
{
private $inAModule = false;
+ /** @var array */
private $tags;
+ /** @var array */
private $filters;
+ /** @var array */
private $functions;
private $needsToStringWrap = false;
@@ -51,22 +57,22 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
} elseif ($this->inAModule) {
// look for tags
if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) {
- $this->tags[$node->getNodeTag()] = $node;
+ $this->tags[$node->getNodeTag()] = $node->getTemplateLine();
}
// look for filters
- if ($node instanceof FilterExpression && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) {
- $this->filters[$node->getNode('filter')->getAttribute('value')] = $node;
+ if ($node instanceof FilterExpression && !isset($this->filters[$node->getAttribute('name')])) {
+ $this->filters[$node->getAttribute('name')] = $node->getTemplateLine();
}
// look for functions
if ($node instanceof FunctionExpression && !isset($this->functions[$node->getAttribute('name')])) {
- $this->functions[$node->getAttribute('name')] = $node;
+ $this->functions[$node->getAttribute('name')] = $node->getTemplateLine();
}
// the .. operator is equivalent to the range() function
if ($node instanceof RangeBinary && !isset($this->functions['range'])) {
- $this->functions['range'] = $node;
+ $this->functions['range'] = $node->getTemplateLine();
}
if ($node instanceof PrintNode) {
@@ -102,8 +108,8 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
if ($node instanceof ModuleNode) {
$this->inAModule = false;
- $node->setNode('constructor_end', new Node([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
- $node->setNode('class_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
+ $node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
+ $node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
} elseif ($this->inAModule) {
if ($node instanceof PrintNode || $node instanceof SetNode) {
$this->needsToStringWrap = false;
@@ -116,8 +122,19 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
private function wrapNode(Node $node, string $name): void
{
$expr = $node->getNode($name);
- if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) {
- $node->setNode($name, new CheckToStringNode($expr));
+ if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
+ // Simplify in 4.0 as the spread attribute has been removed there
+ $new = new CheckToStringNode($expr);
+ if ($expr->hasAttribute('spread')) {
+ $new->setAttribute('spread', $expr->getAttribute('spread'));
+ }
+ $node->setNode($name, $new);
+ } elseif ($expr instanceof SpreadUnary) {
+ $this->wrapNode($expr, 'node');
+ } elseif ($expr instanceof ArrayExpression) {
+ foreach ($expr as $name => $_) {
+ $this->wrapNode($expr, $name);
+ }
}
}
diff --git a/lib/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/lib/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php
new file mode 100644
index 000000000..3c9786275
--- /dev/null
+++ b/lib/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php
@@ -0,0 +1,59 @@
+yieldReadyNodes[$class])) {
+ return $node;
+ }
+
+ if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) {
+ if ($this->useYield) {
+ throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class));
+ }
+
+ trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class);
+ }
+
+ return $node;
+ }
+
+ public function leaveNode(Node $node, Environment $env): ?Node
+ {
+ return $node;
+ }
+
+ public function getPriority(): int
+ {
+ return 255;
+ }
+}
diff --git a/lib/twig/twig/src/OperatorPrecedenceChange.php b/lib/twig/twig/src/OperatorPrecedenceChange.php
new file mode 100644
index 000000000..1d9edefd1
--- /dev/null
+++ b/lib/twig/twig/src/OperatorPrecedenceChange.php
@@ -0,0 +1,42 @@
+
+ */
+class OperatorPrecedenceChange
+{
+ public function __construct(
+ private string $package,
+ private string $version,
+ private int $newPrecedence,
+ ) {
+ }
+
+ public function getPackage(): string
+ {
+ return $this->package;
+ }
+
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ public function getNewPrecedence(): int
+ {
+ return $this->newPrecedence;
+ }
+}
diff --git a/lib/twig/twig/src/Parser.php b/lib/twig/twig/src/Parser.php
index 4016a5f39..7bf51b73c 100644
--- a/lib/twig/twig/src/Parser.php
+++ b/lib/twig/twig/src/Parser.php
@@ -16,15 +16,20 @@ use Twig\Error\SyntaxError;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
use Twig\Node\BodyNode;
+use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
+use Twig\Node\Expression\Variable\AssignTemplateVariable;
+use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\MacroNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\NodeCaptureInterface;
use Twig\Node\NodeOutputInterface;
+use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\TextNode;
use Twig\TokenParser\TokenParserInterface;
+use Twig\Util\ReflectionCallable;
/**
* @author Fabien Potencier
@@ -39,20 +44,27 @@ class Parser
private $blocks;
private $blockStack;
private $macros;
- private $env;
private $importedSymbols;
private $traits;
private $embeddedTemplates = [];
private $varNameSalt = 0;
+ private $ignoreUnknownTwigCallables = false;
- public function __construct(Environment $env)
+ public function __construct(
+ private Environment $env,
+ ) {
+ }
+
+ public function getEnvironment(): Environment
{
- $this->env = $env;
+ return $this->env;
}
public function getVarName(): string
{
- return sprintf('__internal_parse_%d', $this->varNameSalt++);
+ trigger_deprecation('twig/twig', '3.15', 'The "%s()" method is deprecated.', __METHOD__);
+
+ return \sprintf('__internal_parse_%d', $this->varNameSalt++);
}
public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
@@ -83,7 +95,7 @@ class Parser
$body = $this->subparse($test, $dropNeedle);
if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
- $body = new Node();
+ $body = new EmptyNode();
}
} catch (SyntaxError $e) {
if (!$e->getSourceContext()) {
@@ -91,16 +103,19 @@ class Parser
}
if (!$e->getTemplateLine()) {
- $e->setTemplateLine($this->stream->getCurrent()->getLine());
+ $e->setTemplateLine($this->getCurrentToken()->getLine());
}
throw $e;
}
- $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
+ $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Nodes($this->blocks), new Nodes($this->macros), new Nodes($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
$traverser = new NodeTraverser($this->env, $this->visitors);
+ /**
+ * @var ModuleNode $node
+ */
$node = $traverser->traverse($node);
// restore previous stack so previous parse() call can resume working
@@ -111,29 +126,45 @@ class Parser
return $node;
}
+ public function shouldIgnoreUnknownTwigCallables(): bool
+ {
+ return $this->ignoreUnknownTwigCallables;
+ }
+
+ public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void
+ {
+ $previous = $this->ignoreUnknownTwigCallables;
+ $this->ignoreUnknownTwigCallables = true;
+ try {
+ $this->subparse($test, $dropNeedle);
+ } finally {
+ $this->ignoreUnknownTwigCallables = $previous;
+ }
+ }
+
public function subparse($test, bool $dropNeedle = false): Node
{
$lineno = $this->getCurrentToken()->getLine();
$rv = [];
while (!$this->stream->isEOF()) {
switch ($this->getCurrentToken()->getType()) {
- case /* Token::TEXT_TYPE */ 0:
+ case Token::TEXT_TYPE:
$token = $this->stream->next();
$rv[] = new TextNode($token->getValue(), $token->getLine());
break;
- case /* Token::VAR_START_TYPE */ 2:
+ case Token::VAR_START_TYPE:
$token = $this->stream->next();
$expr = $this->expressionParser->parseExpression();
- $this->stream->expect(/* Token::VAR_END_TYPE */ 4);
+ $this->stream->expect(Token::VAR_END_TYPE);
$rv[] = new PrintNode($expr, $token->getLine());
break;
- case /* Token::BLOCK_START_TYPE */ 1:
+ case Token::BLOCK_START_TYPE:
$this->stream->next();
$token = $this->getCurrentToken();
- if (/* Token::NAME_TYPE */ 5 !== $token->getType()) {
+ if (Token::NAME_TYPE !== $token->getType()) {
throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
}
@@ -146,18 +177,19 @@ class Parser
return $rv[0];
}
- return new Node($rv, [], $lineno);
+ return new Nodes($rv, $lineno);
}
if (!$subparser = $this->env->getTokenParser($token->getValue())) {
if (null !== $test) {
- $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
+ $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
- if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) {
- $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
+ $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable();
+ if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) {
+ $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno));
}
} else {
- $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
+ $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
$e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers()));
}
@@ -168,13 +200,16 @@ class Parser
$subparser->setParser($this);
$node = $subparser->parse($token);
- if (null !== $node) {
+ if (!$node) {
+ trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class);
+ } else {
+ $node->setNodeTag($subparser->getTag());
$rv[] = $node;
}
break;
default:
- throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
+ throw new SyntaxError('The lexer or the parser ended up in an unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
}
}
@@ -182,11 +217,13 @@ class Parser
return $rv[0];
}
- return new Node($rv, [], $lineno);
+ return new Nodes($rv, $lineno);
}
public function getBlockStack(): array
{
+ trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
+
return $this->blockStack;
}
@@ -207,21 +244,31 @@ class Parser
public function hasBlock(string $name): bool
{
+ trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
+
return isset($this->blocks[$name]);
}
public function getBlock(string $name): Node
{
+ trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
+
return $this->blocks[$name];
}
public function setBlock(string $name, BlockNode $value): void
{
+ if (isset($this->blocks[$name])) {
+ throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext());
+ }
+
$this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
}
public function hasMacro(string $name): bool
{
+ trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
+
return isset($this->macros[$name]);
}
@@ -237,6 +284,8 @@ class Parser
public function hasTraits(): bool
{
+ trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
+
return \count($this->traits) > 0;
}
@@ -247,9 +296,15 @@ class Parser
$this->embeddedTemplates[] = $template;
}
- public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void
+ public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|AssignTemplateVariable|null $internalRef = null): void
{
- $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
+ if ($internalRef && !$internalRef instanceof AssignTemplateVariable) {
+ trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, AssignTemplateVariable::class, $internalRef::class);
+
+ $internalRef = new AssignTemplateVariable(new TemplateVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()), $internalRef->getAttribute('global'));
+ }
+
+ $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef];
}
public function getImportedSymbol(string $type, string $alias)
@@ -280,11 +335,26 @@ class Parser
public function getParent(): ?Node
{
+ trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
+
return $this->parent;
}
+ public function hasInheritance()
+ {
+ return $this->parent || 0 < \count($this->traits);
+ }
+
public function setParent(?Node $parent): void
{
+ if (null === $parent) {
+ trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__);
+ }
+
+ if (null !== $this->parent) {
+ throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext());
+ }
+
$this->parent = $parent;
}
@@ -335,7 +405,8 @@ class Parser
// here, $nested means "being at the root level of a child template"
// we need to discard the wrapping "Node" for the "body" node
- $nested = $nested || Node::class !== \get_class($node);
+ // Node::class !== \get_class($node) should be removed in Twig 4.0
+ $nested = $nested || (Node::class !== \get_class($node) && !$node instanceof Nodes);
foreach ($node as $k => $n) {
if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
$node->removeNode($k);
diff --git a/lib/twig/twig/src/Profiler/Dumper/BaseDumper.php b/lib/twig/twig/src/Profiler/Dumper/BaseDumper.php
index 4da43e475..267718c1f 100644
--- a/lib/twig/twig/src/Profiler/Dumper/BaseDumper.php
+++ b/lib/twig/twig/src/Profiler/Dumper/BaseDumper.php
@@ -50,7 +50,7 @@ abstract class BaseDumper
if ($profile->getDuration() * 1000 < 1) {
$str = $start."\n";
} else {
- $str = sprintf("%s %s\n", $start, $this->formatTime($profile, $percent));
+ $str = \sprintf("%s %s\n", $start, $this->formatTime($profile, $percent));
}
$nCount = \count($profile->getProfiles());
diff --git a/lib/twig/twig/src/Profiler/Dumper/BlackfireDumper.php b/lib/twig/twig/src/Profiler/Dumper/BlackfireDumper.php
index 03abe0fa0..bb3fbb52a 100644
--- a/lib/twig/twig/src/Profiler/Dumper/BlackfireDumper.php
+++ b/lib/twig/twig/src/Profiler/Dumper/BlackfireDumper.php
@@ -24,7 +24,7 @@ final class BlackfireDumper
$this->dumpProfile('main()', $profile, $data);
$this->dumpChildren('main()', $profile, $data);
- $start = sprintf('%f', microtime(true));
+ $start = \sprintf('%f', microtime(true));
$str = <<isTemplate()) {
$name = $p->getTemplate();
} else {
- $name = sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName());
+ $name = \sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName());
}
- $this->dumpProfile(sprintf('%s==>%s', $parent, $name), $p, $data);
+ $this->dumpProfile(\sprintf('%s==>%s', $parent, $name), $p, $data);
$this->dumpChildren($name, $p, $data);
}
}
diff --git a/lib/twig/twig/src/Profiler/Dumper/HtmlDumper.php b/lib/twig/twig/src/Profiler/Dumper/HtmlDumper.php
index 3c0daf1c8..cdab2de59 100644
--- a/lib/twig/twig/src/Profiler/Dumper/HtmlDumper.php
+++ b/lib/twig/twig/src/Profiler/Dumper/HtmlDumper.php
@@ -32,16 +32,16 @@ final class HtmlDumper extends BaseDumper
protected function formatTemplate(Profile $profile, $prefix): string
{
- return sprintf('%s└ %s', $prefix, self::$colors['template'], $profile->getTemplate());
+ return \sprintf('%s└ %s', $prefix, self::$colors['template'], $profile->getTemplate());
}
protected function formatNonTemplate(Profile $profile, $prefix): string
{
- return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName());
+ return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName());
}
protected function formatTime(Profile $profile, $percent): string
{
- return sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent);
+ return \sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent);
}
}
diff --git a/lib/twig/twig/src/Profiler/Dumper/TextDumper.php b/lib/twig/twig/src/Profiler/Dumper/TextDumper.php
index 31561c466..1c1f77e94 100644
--- a/lib/twig/twig/src/Profiler/Dumper/TextDumper.php
+++ b/lib/twig/twig/src/Profiler/Dumper/TextDumper.php
@@ -20,16 +20,16 @@ final class TextDumper extends BaseDumper
{
protected function formatTemplate(Profile $profile, $prefix): string
{
- return sprintf('%s└ %s', $prefix, $profile->getTemplate());
+ return \sprintf('%s└ %s', $prefix, $profile->getTemplate());
}
protected function formatNonTemplate(Profile $profile, $prefix): string
{
- return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName());
+ return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName());
}
protected function formatTime(Profile $profile, $percent): string
{
- return sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent);
+ return \sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent);
}
}
diff --git a/lib/twig/twig/src/Profiler/Node/EnterProfileNode.php b/lib/twig/twig/src/Profiler/Node/EnterProfileNode.php
index 1494baf44..4d8e504d1 100644
--- a/lib/twig/twig/src/Profiler/Node/EnterProfileNode.php
+++ b/lib/twig/twig/src/Profiler/Node/EnterProfileNode.php
@@ -11,6 +11,7 @@
namespace Twig\Profiler\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Node;
@@ -19,6 +20,7 @@ use Twig\Node\Node;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class EnterProfileNode extends Node
{
public function __construct(string $extensionName, string $type, string $name, string $varName)
@@ -29,10 +31,10 @@ class EnterProfileNode extends Node
public function compile(Compiler $compiler): void
{
$compiler
- ->write(sprintf('$%s = $this->extensions[', $this->getAttribute('var_name')))
+ ->write(\sprintf('$%s = $this->extensions[', $this->getAttribute('var_name')))
->repr($this->getAttribute('extension_name'))
->raw("];\n")
- ->write(sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof'))
+ ->write(\sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof'))
->repr($this->getAttribute('type'))
->raw(', ')
->repr($this->getAttribute('name'))
diff --git a/lib/twig/twig/src/Profiler/Node/LeaveProfileNode.php b/lib/twig/twig/src/Profiler/Node/LeaveProfileNode.php
index 94cebbaa8..bd9227e52 100644
--- a/lib/twig/twig/src/Profiler/Node/LeaveProfileNode.php
+++ b/lib/twig/twig/src/Profiler/Node/LeaveProfileNode.php
@@ -11,6 +11,7 @@
namespace Twig\Profiler\Node;
+use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Node;
@@ -19,6 +20,7 @@ use Twig\Node\Node;
*
* @author Fabien Potencier
*/
+#[YieldReady]
class LeaveProfileNode extends Node
{
public function __construct(string $varName)
@@ -30,7 +32,7 @@ class LeaveProfileNode extends Node
{
$compiler
->write("\n")
- ->write(sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof'))
+ ->write(\sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof'))
;
}
}
diff --git a/lib/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/lib/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php
index 91abee807..4c5c2005d 100644
--- a/lib/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php
+++ b/lib/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php
@@ -17,6 +17,7 @@ use Twig\Node\BodyNode;
use Twig\Node\MacroNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\Profiler\Node\EnterProfileNode;
use Twig\Profiler\Node\LeaveProfileNode;
@@ -27,13 +28,12 @@ use Twig\Profiler\Profile;
*/
final class ProfilerNodeVisitor implements NodeVisitorInterface
{
- private $extensionName;
private $varName;
- public function __construct(string $extensionName)
- {
- $this->extensionName = $extensionName;
- $this->varName = sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName));
+ public function __construct(
+ private string $extensionName,
+ ) {
+ $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName));
}
public function enterNode(Node $node, Environment $env): Node
@@ -44,8 +44,8 @@ final class ProfilerNodeVisitor implements NodeVisitorInterface
public function leaveNode(Node $node, Environment $env): ?Node
{
if ($node instanceof ModuleNode) {
- $node->setNode('display_start', new Node([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')]));
- $node->setNode('display_end', new Node([new LeaveProfileNode($this->varName), $node->getNode('display_end')]));
+ $node->setNode('display_start', new Nodes([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')]));
+ $node->setNode('display_end', new Nodes([new LeaveProfileNode($this->varName), $node->getNode('display_end')]));
} elseif ($node instanceof BlockNode) {
$node->setNode('body', new BodyNode([
new EnterProfileNode($this->extensionName, Profile::BLOCK, $node->getAttribute('name'), $this->varName),
diff --git a/lib/twig/twig/src/Profiler/Profile.php b/lib/twig/twig/src/Profiler/Profile.php
index 7979a23c6..a3c6ee02e 100644
--- a/lib/twig/twig/src/Profiler/Profile.php
+++ b/lib/twig/twig/src/Profiler/Profile.php
@@ -20,18 +20,15 @@ final class Profile implements \IteratorAggregate, \Serializable
public const BLOCK = 'block';
public const TEMPLATE = 'template';
public const MACRO = 'macro';
-
- private $template;
- private $name;
- private $type;
private $starts = [];
private $ends = [];
private $profiles = [];
- public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main')
- {
- $this->template = $template;
- $this->type = $type;
+ public function __construct(
+ private string $template = 'main',
+ private string $type = self::ROOT,
+ private string $name = 'main',
+ ) {
$this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name;
$this->enter();
}
@@ -102,6 +99,22 @@ final class Profile implements \IteratorAggregate, \Serializable
return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0;
}
+ /**
+ * Returns the start time in microseconds.
+ */
+ public function getStartTime(): float
+ {
+ return $this->starts['wt'] ?? 0.0;
+ }
+
+ /**
+ * Returns the end time in microseconds.
+ */
+ public function getEndTime(): float
+ {
+ return $this->ends['wt'] ?? 0.0;
+ }
+
/**
* Returns the memory usage in bytes.
*/
@@ -176,6 +189,6 @@ final class Profile implements \IteratorAggregate, \Serializable
*/
public function __unserialize(array $data): void
{
- list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data;
+ [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data;
}
}
diff --git a/lib/twig/twig/src/Resources/core.php b/lib/twig/twig/src/Resources/core.php
new file mode 100644
index 000000000..bc0b27104
--- /dev/null
+++ b/lib/twig/twig/src/Resources/core.php
@@ -0,0 +1,541 @@
+getCharset(), $values, $max);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_date_modify_filter(Environment $env, $date, $modifier)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_sprintf($format, ...$values)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::sprintf($format, ...$values);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_date_converter(Environment $env, $date = null, $timezone = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_replace_filter($str, $from)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::replace($str, $from);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_round($value, $precision = 0, $method = 'common')
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::round($value, $precision, $method);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_urlencode_filter($url)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::urlencode($url);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_merge(...$arrays)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::merge(...$arrays);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_first(Environment $env, $item)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::first($env->getCharset(), $item);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_last(Environment $env, $item)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::last($env->getCharset(), $item);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_join_filter($value, $glue = '', $and = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::join($value, $glue, $and);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_get_array_keys_filter($array)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::keys($array);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_sort_filter(Environment $env, $array, $arrow = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::sort($env, $array, $arrow);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_matches(string $regexp, ?string $str)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::matches($regexp, $str);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_trim_filter($string, $characterMask = null, $side = 'both')
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::trim($string, $characterMask, $side);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_nl2br($string)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::nl2br($string);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_spaceless($content)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::spaceless($content);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_convert_encoding($string, $to, $from)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::convertEncoding($string, $to, $from);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_length_filter(Environment $env, $thing)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::length($env->getCharset(), $thing);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_upper_filter(Environment $env, $string)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::upper($env->getCharset(), $string);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_lower_filter(Environment $env, $string)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::lower($env->getCharset(), $string);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_striptags($string, $allowable_tags = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::striptags($string, $allowable_tags);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_title_string_filter(Environment $env, $string)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::titleCase($env->getCharset(), $string);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_capitalize_string_filter(Environment $env, $string)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::capitalize($env->getCharset(), $string);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_test_empty($value)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::testEmpty($value);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_test_iterable($value)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return is_iterable($value);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_source(Environment $env, $name, $ignoreMissing = false)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::source($env, $name, $ignoreMissing);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_constant($constant, $object = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::constant($constant, $object);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_constant_is_defined($constant, $object = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::constant($constant, $object, true);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::batch($items, $size, $fill, $preserveKeys);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_column($array, $name, $index = null): array
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::column($array, $name, $index);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_filter(Environment $env, $array, $arrow)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::filter($env, $array, $arrow);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_map(Environment $env, $array, $arrow)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::map($env, $array, $arrow);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::reduce($env, $array, $arrow, $initial);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_some(Environment $env, $array, $arrow)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::arraySome($env, $array, $arrow);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_array_every(Environment $env, $array, $arrow)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return CoreExtension::arrayEvery($env, $array, $arrow);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ CoreExtension::checkArrow($env, $arrow, $thing, $type);
+}
diff --git a/lib/twig/twig/src/Resources/debug.php b/lib/twig/twig/src/Resources/debug.php
new file mode 100644
index 000000000..104b4f4e0
--- /dev/null
+++ b/lib/twig/twig/src/Resources/debug.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Twig\Environment;
+use Twig\Extension\DebugExtension;
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_var_dump(Environment $env, $context, ...$vars)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ DebugExtension::dump($env, $context, ...$vars);
+}
diff --git a/lib/twig/twig/src/Resources/escaper.php b/lib/twig/twig/src/Resources/escaper.php
new file mode 100644
index 000000000..a2ee8e7aa
--- /dev/null
+++ b/lib/twig/twig/src/Resources/escaper.php
@@ -0,0 +1,51 @@
+getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape);
+}
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_escape_filter_is_safe(Node $filterArgs)
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return EscaperExtension::escapeFilterIsSafe($filterArgs);
+}
diff --git a/lib/twig/twig/src/Resources/string_loader.php b/lib/twig/twig/src/Resources/string_loader.php
new file mode 100644
index 000000000..8f0e6492a
--- /dev/null
+++ b/lib/twig/twig/src/Resources/string_loader.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Twig\Environment;
+use Twig\Extension\StringLoaderExtension;
+use Twig\TemplateWrapper;
+
+/**
+ * @internal
+ *
+ * @deprecated since Twig 3.9
+ */
+function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper
+{
+ trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
+
+ return StringLoaderExtension::templateFromString($env, $template, $name);
+}
diff --git a/lib/twig/twig/src/Runtime/EscaperRuntime.php b/lib/twig/twig/src/Runtime/EscaperRuntime.php
new file mode 100644
index 000000000..ce41e0a8b
--- /dev/null
+++ b/lib/twig/twig/src/Runtime/EscaperRuntime.php
@@ -0,0 +1,334 @@
+ */
+ private $escapers = [];
+
+ /** @internal */
+ public $safeClasses = [];
+
+ /** @internal */
+ public $safeLookup = [];
+
+ public function __construct(
+ private $charset = 'UTF-8',
+ ) {
+ }
+
+ /**
+ * 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(string $string, string $charset): string $callable A valid PHP callable
+ */
+ public function setEscaper($strategy, callable $callable)
+ {
+ $this->escapers[$strategy] = $callable;
+ }
+
+ /**
+ * Gets all defined escapers.
+ *
+ * @return array An array of escapers
+ */
+ public function getEscapers()
+ {
+ return $this->escapers;
+ }
+
+ /**
+ * @param array, string[]> $safeClasses
+ */
+ public function setSafeClasses(array $safeClasses = [])
+ {
+ $this->safeClasses = [];
+ $this->safeLookup = [];
+ foreach ($safeClasses as $class => $strategies) {
+ $this->addSafeClass($class, $strategies);
+ }
+ }
+
+ /**
+ * @param class-string<\Stringable> $class
+ * @param string[] $strategies
+ */
+ 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);
+
+ foreach ($strategies as $strategy) {
+ $this->safeLookup[$strategy][$class] = true;
+ }
+ }
+
+ /**
+ * Escapes a string.
+ *
+ * @param mixed $string The value to be escaped
+ * @param string $strategy The escaping strategy
+ * @param string|null $charset The charset
+ * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
+ *
+ * @throws RuntimeError
+ */
+ public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false)
+ {
+ if ($autoescape && $string instanceof Markup) {
+ return $string;
+ }
+
+ if (!\is_string($string)) {
+ if ($string instanceof \Stringable) {
+ if ($autoescape) {
+ $c = \get_class($string);
+ if (!isset($this->safeClasses[$c])) {
+ $this->safeClasses[$c] = [];
+ foreach (class_parents($string) + class_implements($string) as $class) {
+ if (isset($this->safeClasses[$class])) {
+ $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class]));
+ foreach ($this->safeClasses[$class] as $s) {
+ $this->safeLookup[$s][$c] = true;
+ }
+ }
+ }
+ }
+ if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) {
+ return (string) $string;
+ }
+ }
+
+ $string = (string) $string;
+ } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
+ // we return the input as is (which can be of any type)
+ return $string;
+ }
+ }
+
+ if ('' === $string) {
+ return '';
+ }
+
+ $charset = $charset ?: $this->charset;
+
+ 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 = $this->convertEncoding($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 = $this->convertEncoding($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 = $this->convertEncoding($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 = $this->convertEncoding($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 '�';
+ }
+
+ /*
+ * 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 => '"', /* quotation mark */
+ 38 => '&', /* ampersand */
+ 60 => '<', /* less-than sign */
+ 62 => '>', /* greater-than sign */
+ ];
+
+ if (isset($entityMap[$ord])) {
+ return $entityMap[$ord];
+ }
+
+ return \sprintf('%02X;', $ord);
+ }
+
+ /*
+ * Per OWASP recommendations, we'll use hex entities for any other
+ * characters where a named entity does not exist.
+ */
+ return \sprintf('%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:
+ if (\array_key_exists($strategy, $this->escapers)) {
+ return $this->escapers[$strategy]($string, $charset);
+ }
+
+ $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
+
+ throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
+ }
+ }
+
+ private function convertEncoding(string $string, string $to, string $from)
+ {
+ if (!\function_exists('iconv')) {
+ throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
+ }
+
+ return iconv($from, $to, $string);
+ }
+}
diff --git a/lib/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php b/lib/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php
index b360d7bea..05106680c 100644
--- a/lib/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php
+++ b/lib/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php
@@ -23,11 +23,9 @@ use Psr\Container\ContainerInterface;
*/
class ContainerRuntimeLoader implements RuntimeLoaderInterface
{
- private $container;
-
- public function __construct(ContainerInterface $container)
- {
- $this->container = $container;
+ public function __construct(
+ private ContainerInterface $container,
+ ) {
}
public function load(string $class)
diff --git a/lib/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php b/lib/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php
index 130648392..5d4e70b92 100644
--- a/lib/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php
+++ b/lib/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php
@@ -18,14 +18,12 @@ namespace Twig\RuntimeLoader;
*/
class FactoryRuntimeLoader implements RuntimeLoaderInterface
{
- private $map;
-
/**
* @param array $map An array where keys are class names and values factory callables
*/
- public function __construct(array $map = [])
- {
- $this->map = $map;
+ public function __construct(
+ private array $map = [],
+ ) {
}
public function load(string $class)
diff --git a/lib/twig/twig/src/Sandbox/SecurityPolicy.php b/lib/twig/twig/src/Sandbox/SecurityPolicy.php
index a725aa4f1..b0d054260 100644
--- a/lib/twig/twig/src/Sandbox/SecurityPolicy.php
+++ b/lib/twig/twig/src/Sandbox/SecurityPolicy.php
@@ -50,7 +50,7 @@ final class SecurityPolicy implements SecurityPolicyInterface
{
$this->allowedMethods = [];
foreach ($methods as $class => $m) {
- $this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
+ $this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]);
}
}
@@ -68,19 +68,25 @@ final class SecurityPolicy implements SecurityPolicyInterface
{
foreach ($tags as $tag) {
if (!\in_array($tag, $this->allowedTags)) {
- throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
+ if ('extends' === $tag) {
+ trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
+ } elseif ('use' === $tag) {
+ trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
+ } else {
+ throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
+ }
}
}
foreach ($filters as $filter) {
if (!\in_array($filter, $this->allowedFilters)) {
- throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
+ throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
}
}
foreach ($functions as $function) {
if (!\in_array($function, $this->allowedFunctions)) {
- throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
+ throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
}
}
}
@@ -92,7 +98,7 @@ final class SecurityPolicy implements SecurityPolicyInterface
}
$allowed = false;
- $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+ $method = strtolower($method);
foreach ($this->allowedMethods as $class => $methods) {
if ($obj instanceof $class && \in_array($method, $methods)) {
$allowed = true;
@@ -102,7 +108,7 @@ final class SecurityPolicy implements SecurityPolicyInterface
if (!$allowed) {
$class = \get_class($obj);
- throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
+ throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
}
}
@@ -118,7 +124,7 @@ final class SecurityPolicy implements SecurityPolicyInterface
if (!$allowed) {
$class = \get_class($obj);
- throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
+ throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
}
}
}
diff --git a/lib/twig/twig/src/Sandbox/SourcePolicyInterface.php b/lib/twig/twig/src/Sandbox/SourcePolicyInterface.php
new file mode 100644
index 000000000..b952f1ea6
--- /dev/null
+++ b/lib/twig/twig/src/Sandbox/SourcePolicyInterface.php
@@ -0,0 +1,24 @@
+code = $code;
- $this->name = $name;
- $this->path = $path;
+ public function __construct(
+ private string $code,
+ private string $name,
+ private string $path = '',
+ ) {
}
public function getCode(): string
diff --git a/lib/twig/twig/src/Template.php b/lib/twig/twig/src/Template.php
index ffbaae1ea..26f5b5d81 100644
--- a/lib/twig/twig/src/Template.php
+++ b/lib/twig/twig/src/Template.php
@@ -13,7 +13,6 @@
namespace Twig;
use Twig\Error\Error;
-use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
/**
@@ -35,38 +34,37 @@ abstract class Template
protected $parent;
protected $parents = [];
- protected $env;
protected $blocks = [];
protected $traits = [];
+ protected $traitAliases = [];
protected $extensions = [];
protected $sandbox;
- public function __construct(Environment $env)
- {
- $this->env = $env;
+ private $useYield;
+
+ public function __construct(
+ protected Environment $env,
+ ) {
+ $this->useYield = $env->useYield();
$this->extensions = $env->getExtensions();
}
/**
* Returns the template name.
- *
- * @return string The template name
*/
- abstract public function getTemplateName();
+ abstract public function getTemplateName(): string;
/**
* Returns debug information about the template.
*
- * @return array Debug information
+ * @return array Debug information
*/
- abstract public function getDebugInfo();
+ abstract public function getDebugInfo(): array;
/**
* Returns information about the original template source code.
- *
- * @return Source
*/
- abstract public function getSourceContext();
+ abstract public function getSourceContext(): Source;
/**
* Returns the parent template.
@@ -74,44 +72,35 @@ abstract class Template
* This method is for internal use only and should never be called
* directly.
*
- * @return Template|TemplateWrapper|false The parent template or false if there is no parent
+ * @return self|TemplateWrapper|false The parent template or false if there is no parent
*/
- public function getParent(array $context)
+ public function getParent(array $context): self|TemplateWrapper|false
{
if (null !== $this->parent) {
return $this->parent;
}
- try {
- $parent = $this->doGetParent($context);
+ if (!$parent = $this->doGetParent($context)) {
+ return false;
+ }
- if (false === $parent) {
- return false;
- }
+ if ($parent instanceof self || $parent instanceof TemplateWrapper) {
+ return $this->parents[$parent->getSourceContext()->getName()] = $parent;
+ }
- if ($parent instanceof self || $parent instanceof TemplateWrapper) {
- return $this->parents[$parent->getSourceContext()->getName()] = $parent;
- }
-
- if (!isset($this->parents[$parent])) {
- $this->parents[$parent] = $this->loadTemplate($parent);
- }
- } catch (LoaderError $e) {
- $e->setSourceContext(null);
- $e->guess();
-
- throw $e;
+ if (!isset($this->parents[$parent])) {
+ $this->parents[$parent] = $this->loadTemplate($parent);
}
return $this->parents[$parent];
}
- protected function doGetParent(array $context)
+ protected function doGetParent(array $context): bool|string|self|TemplateWrapper
{
return false;
}
- public function isTraitable()
+ public function isTraitable(): bool
{
return true;
}
@@ -126,14 +115,10 @@ abstract class Template
* @param array $context The context
* @param array $blocks The current set of blocks
*/
- public function displayParentBlock($name, array $context, array $blocks = [])
+ public function displayParentBlock($name, array $context, array $blocks = []): void
{
- if (isset($this->traits[$name])) {
- $this->traits[$name][0]->displayBlock($name, $context, $blocks, false);
- } elseif (false !== $parent = $this->getParent($context)) {
- $parent->displayBlock($name, $context, $blocks, false);
- } else {
- throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
+ foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
+ echo $data;
}
}
@@ -148,51 +133,10 @@ abstract class Template
* @param array $blocks The current set of blocks
* @param bool $useBlocks Whether to use the current set of blocks
*/
- public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, self $templateContext = null)
+ public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void
{
- if ($useBlocks && isset($blocks[$name])) {
- $template = $blocks[$name][0];
- $block = $blocks[$name][1];
- } elseif (isset($this->blocks[$name])) {
- $template = $this->blocks[$name][0];
- $block = $this->blocks[$name][1];
- } else {
- $template = null;
- $block = null;
- }
-
- // avoid RCEs when sandbox is enabled
- if (null !== $template && !$template instanceof self) {
- throw new \LogicException('A block must be a method on a \Twig\Template instance.');
- }
-
- if (null !== $template) {
- try {
- $template->$block($context, $blocks);
- } catch (Error $e) {
- if (!$e->getSourceContext()) {
- $e->setSourceContext($template->getSourceContext());
- }
-
- // this is mostly useful for \Twig\Error\LoaderError exceptions
- // see \Twig\Error\LoaderError
- if (-1 === $e->getTemplateLine()) {
- $e->guess();
- }
-
- throw $e;
- } catch (\Throwable $e) {
- $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
- $e->guess();
-
- throw $e;
- }
- } elseif (false !== $parent = $this->getParent($context)) {
- $parent->displayBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
- } elseif (isset($blocks[$name])) {
- throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
- } else {
- throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
+ foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) {
+ echo $data;
}
}
@@ -208,16 +152,25 @@ abstract class Template
*
* @return string The rendered block
*/
- public function renderParentBlock($name, array $context, array $blocks = [])
+ public function renderParentBlock($name, array $context, array $blocks = []): string
{
- if ($this->env->isDebug()) {
- ob_start();
- } else {
- ob_start(function () { return ''; });
- }
- $this->displayParentBlock($name, $context, $blocks);
+ if (!$this->useYield) {
+ if ($this->env->isDebug()) {
+ ob_start();
+ } else {
+ ob_start(function () { return ''; });
+ }
+ $this->displayParentBlock($name, $context, $blocks);
- return ob_get_clean();
+ return ob_get_clean();
+ }
+
+ $content = '';
+ foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
+ $content .= $data;
+ }
+
+ return $content;
}
/**
@@ -233,16 +186,34 @@ abstract class Template
*
* @return string The rendered block
*/
- public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true)
+ public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string
{
- if ($this->env->isDebug()) {
- ob_start();
- } else {
- ob_start(function () { return ''; });
- }
- $this->displayBlock($name, $context, $blocks, $useBlocks);
+ if (!$this->useYield) {
+ $level = ob_get_level();
+ if ($this->env->isDebug()) {
+ ob_start();
+ } else {
+ ob_start(function () { return ''; });
+ }
+ try {
+ $this->displayBlock($name, $context, $blocks, $useBlocks);
+ } catch (\Throwable $e) {
+ while (ob_get_level() > $level) {
+ ob_end_clean();
+ }
- return ob_get_clean();
+ throw $e;
+ }
+
+ return ob_get_clean();
+ }
+
+ $content = '';
+ foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) {
+ $content .= $data;
+ }
+
+ return $content;
}
/**
@@ -257,7 +228,7 @@ abstract class Template
*
* @return bool true if the block exists, false otherwise
*/
- public function hasBlock($name, array $context, array $blocks = [])
+ public function hasBlock($name, array $context, array $blocks = []): bool
{
if (isset($blocks[$name])) {
return $blocks[$name][0] instanceof self;
@@ -267,7 +238,7 @@ abstract class Template
return true;
}
- if (false !== $parent = $this->getParent($context)) {
+ if ($parent = $this->getParent($context)) {
return $parent->hasBlock($name, $context);
}
@@ -283,13 +254,13 @@ abstract class Template
* @param array $context The context
* @param array $blocks The current set of blocks
*
- * @return array An array of block names
+ * @return array An array of block names
*/
- public function getBlockNames(array $context, array $blocks = [])
+ public function getBlockNames(array $context, array $blocks = []): array
{
$names = array_merge(array_keys($blocks), array_keys($this->blocks));
- if (false !== $parent = $this->getParent($context)) {
+ if ($parent = $this->getParent($context)) {
$names = array_merge($names, $parent->getBlockNames($context));
}
@@ -297,16 +268,22 @@ abstract class Template
}
/**
- * @return Template|TemplateWrapper
+ * @param string|TemplateWrapper|array $template
*/
- protected function loadTemplate($template, $templateName = null, $line = null, $index = null)
+ protected function loadTemplate($template, $templateName = null, $line = null, $index = null): self|TemplateWrapper
{
try {
if (\is_array($template)) {
return $this->env->resolveTemplate($template);
}
- if ($template instanceof self || $template instanceof TemplateWrapper) {
+ if ($template instanceof TemplateWrapper) {
+ return $template;
+ }
+
+ if ($template instanceof self) {
+ trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__);
+
return $template;
}
@@ -341,10 +318,9 @@ abstract class Template
/**
* @internal
- *
- * @return Template
+ * @return $this
*/
- public function unwrap()
+ public function unwrap(): self
{
return $this;
}
@@ -357,41 +333,58 @@ abstract class Template
*
* @return array An array of blocks
*/
- public function getBlocks()
+ public function getBlocks(): array
{
return $this->blocks;
}
- public function display(array $context, array $blocks = [])
+ public function display(array $context, array $blocks = []): void
{
- $this->displayWithErrorHandling($this->env->mergeGlobals($context), array_merge($this->blocks, $blocks));
+ foreach ($this->yield($context, $blocks) as $data) {
+ echo $data;
+ }
}
- public function render(array $context)
+ public function render(array $context): string
{
- $level = ob_get_level();
- if ($this->env->isDebug()) {
- ob_start();
- } else {
- ob_start(function () { return ''; });
- }
- try {
- $this->display($context);
- } catch (\Throwable $e) {
- while (ob_get_level() > $level) {
- ob_end_clean();
+ if (!$this->useYield) {
+ $level = ob_get_level();
+ if ($this->env->isDebug()) {
+ ob_start();
+ } else {
+ ob_start(function () { return ''; });
+ }
+ try {
+ $this->display($context);
+ } catch (\Throwable $e) {
+ while (ob_get_level() > $level) {
+ ob_end_clean();
+ }
+
+ throw $e;
}
- throw $e;
+ return ob_get_clean();
}
- return ob_get_clean();
+ $content = '';
+ foreach ($this->yield($context) as $data) {
+ $content .= $data;
+ }
+
+ return $content;
}
- protected function displayWithErrorHandling(array $context, array $blocks = [])
+ /**
+ * @return iterable
+ */
+ public function yield(array $context, array $blocks = []): iterable
{
+ $context += $this->env->getGlobals();
+ $blocks = array_merge($this->blocks, $blocks);
+
try {
- $this->doDisplay($context, $blocks);
+ yield from $this->doDisplay($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($this->getSourceContext());
@@ -405,18 +398,123 @@ abstract class Template
throw $e;
} catch (\Throwable $e) {
- $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
+ $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
$e->guess();
throw $e;
}
}
+ /**
+ * @return iterable
+ */
+ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable
+ {
+ if ($useBlocks && isset($blocks[$name])) {
+ $template = $blocks[$name][0];
+ $block = $blocks[$name][1];
+ } elseif (isset($this->blocks[$name])) {
+ $template = $this->blocks[$name][0];
+ $block = $this->blocks[$name][1];
+ } else {
+ $template = null;
+ $block = null;
+ }
+
+ // avoid RCEs when sandbox is enabled
+ if (null !== $template && !$template instanceof self) {
+ throw new \LogicException('A block must be a method on a \Twig\Template instance.');
+ }
+
+ if (null !== $template) {
+ try {
+ yield from $template->$block($context, $blocks);
+ } catch (Error $e) {
+ if (!$e->getSourceContext()) {
+ $e->setSourceContext($template->getSourceContext());
+ }
+
+ // this is mostly useful for \Twig\Error\LoaderError exceptions
+ // see \Twig\Error\LoaderError
+ if (-1 === $e->getTemplateLine()) {
+ $e->guess();
+ }
+
+ throw $e;
+ } catch (\Throwable $e) {
+ $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
+ $e->guess();
+
+ throw $e;
+ }
+ } elseif ($parent = $this->getParent($context)) {
+ yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
+ } elseif (isset($blocks[$name])) {
+ throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
+ } else {
+ throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
+ }
+ }
+
+ /**
+ * Yields a parent block.
+ *
+ * This method is for internal use only and should never be called
+ * directly.
+ *
+ * @param string $name The block name to display from the parent
+ * @param array $context The context
+ * @param array $blocks The current set of blocks
+ *
+ * @return iterable
+ */
+ public function yieldParentBlock($name, array $context, array $blocks = []): iterable
+ {
+ if (isset($this->traits[$name])) {
+ yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false);
+ } elseif ($parent = $this->getParent($context)) {
+ yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false);
+ } else {
+ throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
+ }
+ }
+
+ protected function hasMacro(string $name, array $context): bool
+ {
+ if (method_exists($this, $name)) {
+ return true;
+ }
+
+ if (!$parent = $this->getParent($context)) {
+ return false;
+ }
+
+ return $parent->hasMacro($name, $context);
+ }
+
+ protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): Template
+ {
+ if (method_exists($this, $name)) {
+ return $this;
+ }
+
+ $parent = $this;
+ while ($parent = $parent->getParent($context)) {
+ if (method_exists($parent, $name)) {
+ return $parent;
+ }
+ }
+
+ throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source);
+ }
+
/**
* Auto-generated method to display the template with the given context.
*
* @param array $context An array of parameters to pass to the template
* @param array $blocks An array of blocks to pass to the template
+ *
+ * @return iterable
*/
- abstract protected function doDisplay(array $context, array $blocks = []);
+ abstract protected function doDisplay(array $context, array $blocks = []): iterable;
}
diff --git a/lib/twig/twig/src/TemplateWrapper.php b/lib/twig/twig/src/TemplateWrapper.php
index 1ecd82251..135c59188 100644
--- a/lib/twig/twig/src/TemplateWrapper.php
+++ b/lib/twig/twig/src/TemplateWrapper.php
@@ -18,19 +18,16 @@ namespace Twig;
*/
final class TemplateWrapper
{
- private $env;
- private $template;
-
/**
* This method is for internal use only and should never be called
* directly (use Twig\Environment::load() instead).
*
* @internal
*/
- public function __construct(Environment $env, Template $template)
- {
- $this->env = $env;
- $this->template = $template;
+ public function __construct(
+ private Environment $env,
+ private Template $template,
+ ) {
}
public function render(array $context = []): string
@@ -60,29 +57,15 @@ final class TemplateWrapper
public function renderBlock(string $name, array $context = []): string
{
- $context = $this->env->mergeGlobals($context);
- $level = ob_get_level();
- if ($this->env->isDebug()) {
- ob_start();
- } else {
- ob_start(function () { return ''; });
- }
- try {
- $this->template->displayBlock($name, $context);
- } catch (\Throwable $e) {
- while (ob_get_level() > $level) {
- ob_end_clean();
- }
-
- throw $e;
- }
-
- return ob_get_clean();
+ return $this->template->renderBlock($name, $context + $this->env->getGlobals());
}
public function displayBlock(string $name, array $context = [])
{
- $this->template->displayBlock($name, $this->env->mergeGlobals($context));
+ $context += $this->env->getGlobals();
+ foreach ($this->template->yieldBlock($name, $context) as $data) {
+ echo $data;
+ }
}
public function getSourceContext(): Source
diff --git a/lib/twig/twig/src/Token.php b/lib/twig/twig/src/Token.php
index 59279b8fe..237634ad1 100644
--- a/lib/twig/twig/src/Token.php
+++ b/lib/twig/twig/src/Token.php
@@ -17,10 +17,6 @@ namespace Twig;
*/
final class Token
{
- private $value;
- private $type;
- private $lineno;
-
public const EOF_TYPE = -1;
public const TEXT_TYPE = 0;
public const BLOCK_START_TYPE = 1;
@@ -37,16 +33,16 @@ final class Token
public const ARROW_TYPE = 12;
public const SPREAD_TYPE = 13;
- public function __construct(int $type, $value, int $lineno)
- {
- $this->type = $type;
- $this->value = $value;
- $this->lineno = $lineno;
+ public function __construct(
+ private int $type,
+ private $value,
+ private int $lineno,
+ ) {
}
public function __toString()
{
- return sprintf('%s(%s)', self::typeToString($this->type, true), $this->value);
+ return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value);
}
/**
@@ -138,7 +134,7 @@ final class Token
$name = 'SPREAD_TYPE';
break;
default:
- throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type));
+ throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type));
}
return $short ? $name : 'Twig\Token::'.$name;
@@ -178,7 +174,7 @@ final class Token
case self::SPREAD_TYPE:
return 'spread operator';
default:
- throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type));
+ throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type));
}
}
}
diff --git a/lib/twig/twig/src/TokenParser/ApplyTokenParser.php b/lib/twig/twig/src/TokenParser/ApplyTokenParser.php
index 4dbf30406..0c9507482 100644
--- a/lib/twig/twig/src/TokenParser/ApplyTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/ApplyTokenParser.php
@@ -11,8 +11,9 @@
namespace Twig\TokenParser;
-use Twig\Node\Expression\TempNameExpression;
+use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Token;
@@ -31,21 +32,17 @@ final class ApplyTokenParser extends AbstractTokenParser
public function parse(Token $token): Node
{
$lineno = $token->getLine();
- $name = $this->parser->getVarName();
-
- $ref = new TempNameExpression($name, $lineno);
- $ref->setAttribute('always_defined', true);
-
- $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref, $this->getTag());
+ $ref = new LocalVariable(null, $lineno);
+ $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref);
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideApplyEnd'], true);
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
- return new Node([
- new SetNode(true, $ref, $body, $lineno, $this->getTag()),
- new PrintNode($filter, $lineno, $this->getTag()),
- ]);
+ return new Nodes([
+ new SetNode(true, $ref, $body, $lineno),
+ new PrintNode($filter, $lineno),
+ ], $lineno);
}
public function decideApplyEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/AutoEscapeTokenParser.php b/lib/twig/twig/src/TokenParser/AutoEscapeTokenParser.php
index b674bea4a..b50b29e65 100644
--- a/lib/twig/twig/src/TokenParser/AutoEscapeTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/AutoEscapeTokenParser.php
@@ -29,7 +29,7 @@ final class AutoEscapeTokenParser extends AbstractTokenParser
$lineno = $token->getLine();
$stream = $this->parser->getStream();
- if ($stream->test(/* Token::BLOCK_END_TYPE */ 3)) {
+ if ($stream->test(Token::BLOCK_END_TYPE)) {
$value = 'html';
} else {
$expr = $this->parser->getExpressionParser()->parseExpression();
@@ -39,11 +39,11 @@ final class AutoEscapeTokenParser extends AbstractTokenParser
$value = $expr->getAttribute('value');
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- return new AutoEscapeNode($value, $body, $lineno, $this->getTag());
+ return new AutoEscapeNode($value, $body, $lineno);
}
public function decideBlockEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/BlockTokenParser.php b/lib/twig/twig/src/TokenParser/BlockTokenParser.php
index 5878131be..3561b99cd 100644
--- a/lib/twig/twig/src/TokenParser/BlockTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/BlockTokenParser.php
@@ -15,7 +15,9 @@ namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
+use Twig\Node\EmptyNode;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Token;
@@ -35,35 +37,32 @@ final class BlockTokenParser extends AbstractTokenParser
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
- $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
- if ($this->parser->hasBlock($name)) {
- throw new SyntaxError(sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext());
- }
- $this->parser->setBlock($name, $block = new BlockNode($name, new Node([]), $lineno));
+ $name = $stream->expect(Token::NAME_TYPE)->getValue();
+ $this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno));
$this->parser->pushLocalScope();
$this->parser->pushBlockStack($name);
- if ($stream->nextIf(/* Token::BLOCK_END_TYPE */ 3)) {
+ if ($stream->nextIf(Token::BLOCK_END_TYPE)) {
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
- if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) {
+ if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
- throw new SyntaxError(sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
+ throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
} else {
- $body = new Node([
+ $body = new Nodes([
new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno),
]);
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$block->setNode('body', $body);
$this->parser->popBlockStack();
$this->parser->popLocalScope();
- return new BlockReferenceNode($name, $lineno, $this->getTag());
+ return new BlockReferenceNode($name, $lineno);
}
public function decideBlockEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/DeprecatedTokenParser.php b/lib/twig/twig/src/TokenParser/DeprecatedTokenParser.php
index 31416c79c..164ef26ee 100644
--- a/lib/twig/twig/src/TokenParser/DeprecatedTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/DeprecatedTokenParser.php
@@ -11,6 +11,7 @@
namespace Twig\TokenParser;
+use Twig\Error\SyntaxError;
use Twig\Node\DeprecatedNode;
use Twig\Node\Node;
use Twig\Token;
@@ -21,6 +22,8 @@ use Twig\Token;
* {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %}
* {% extends 'layout.html.twig' %}
*
+ * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package="foo/bar" version="1.1" %}
+ *
* @author Yonel Ceruto
*
* @internal
@@ -29,11 +32,31 @@ final class DeprecatedTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
- $expr = $this->parser->getExpressionParser()->parseExpression();
+ $stream = $this->parser->getStream();
+ $expressionParser = $this->parser->getExpressionParser();
+ $expr = $expressionParser->parseExpression();
+ $node = new DeprecatedNode($expr, $token->getLine());
- $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
+ while ($stream->test(Token::NAME_TYPE)) {
+ $k = $stream->getCurrent()->getValue();
+ $stream->next();
+ $stream->expect(Token::OPERATOR_TYPE, '=');
- return new DeprecatedNode($expr, $token->getLine(), $this->getTag());
+ switch ($k) {
+ case 'package':
+ $node->setNode('package', $expressionParser->parseExpression());
+ break;
+ case 'version':
+ $node->setNode('version', $expressionParser->parseExpression());
+ break;
+ default:
+ throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext());
+ }
+ }
+
+ $stream->expect(Token::BLOCK_END_TYPE);
+
+ return $node;
}
public function getTag(): string
diff --git a/lib/twig/twig/src/TokenParser/DoTokenParser.php b/lib/twig/twig/src/TokenParser/DoTokenParser.php
index 32c8f12ff..8afd48559 100644
--- a/lib/twig/twig/src/TokenParser/DoTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/DoTokenParser.php
@@ -26,9 +26,9 @@ final class DoTokenParser extends AbstractTokenParser
{
$expr = $this->parser->getExpressionParser()->parseExpression();
- $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
- return new DoNode($expr, $token->getLine(), $this->getTag());
+ return new DoNode($expr, $token->getLine());
}
public function getTag(): string
diff --git a/lib/twig/twig/src/TokenParser/EmbedTokenParser.php b/lib/twig/twig/src/TokenParser/EmbedTokenParser.php
index 64b4f296f..7bf3233e2 100644
--- a/lib/twig/twig/src/TokenParser/EmbedTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/EmbedTokenParser.php
@@ -30,21 +30,21 @@ final class EmbedTokenParser extends IncludeTokenParser
$parent = $this->parser->getExpressionParser()->parseExpression();
- list($variables, $only, $ignoreMissing) = $this->parseArguments();
+ [$variables, $only, $ignoreMissing] = $this->parseArguments();
- $parentToken = $fakeParentToken = new Token(/* Token::STRING_TYPE */ 7, '__parent__', $token->getLine());
+ $parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine());
if ($parent instanceof ConstantExpression) {
- $parentToken = new Token(/* Token::STRING_TYPE */ 7, $parent->getAttribute('value'), $token->getLine());
+ $parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine());
} elseif ($parent instanceof NameExpression) {
- $parentToken = new Token(/* Token::NAME_TYPE */ 5, $parent->getAttribute('name'), $token->getLine());
+ $parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine());
}
// inject a fake parent to make the parent() function work
$stream->injectTokens([
- new Token(/* Token::BLOCK_START_TYPE */ 1, '', $token->getLine()),
- new Token(/* Token::NAME_TYPE */ 5, 'extends', $token->getLine()),
+ new Token(Token::BLOCK_START_TYPE, '', $token->getLine()),
+ new Token(Token::NAME_TYPE, 'extends', $token->getLine()),
$parentToken,
- new Token(/* Token::BLOCK_END_TYPE */ 3, '', $token->getLine()),
+ new Token(Token::BLOCK_END_TYPE, '', $token->getLine()),
]);
$module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true);
@@ -56,9 +56,9 @@ final class EmbedTokenParser extends IncludeTokenParser
$this->parser->embedTemplate($module);
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag());
+ return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine());
}
public function decideBlockEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/ExtendsTokenParser.php b/lib/twig/twig/src/TokenParser/ExtendsTokenParser.php
index 0ca46dd29..a93afe8cd 100644
--- a/lib/twig/twig/src/TokenParser/ExtendsTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/ExtendsTokenParser.php
@@ -13,6 +13,7 @@
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
+use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Token;
@@ -35,14 +36,11 @@ final class ExtendsTokenParser extends AbstractTokenParser
throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
}
- if (null !== $this->parser->getParent()) {
- throw new SyntaxError('Multiple extends tags are forbidden.', $token->getLine(), $stream->getSourceContext());
- }
$this->parser->setParent($this->parser->getExpressionParser()->parseExpression());
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- return new Node();
+ return new EmptyNode($token->getLine());
}
public function getTag(): string
diff --git a/lib/twig/twig/src/TokenParser/FlushTokenParser.php b/lib/twig/twig/src/TokenParser/FlushTokenParser.php
index 02c74aa13..0d2388745 100644
--- a/lib/twig/twig/src/TokenParser/FlushTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/FlushTokenParser.php
@@ -26,9 +26,9 @@ final class FlushTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
- $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
- return new FlushNode($token->getLine(), $this->getTag());
+ return new FlushNode($token->getLine());
}
public function getTag(): string
diff --git a/lib/twig/twig/src/TokenParser/ForTokenParser.php b/lib/twig/twig/src/TokenParser/ForTokenParser.php
index bac8ba2da..c0a0e3c29 100644
--- a/lib/twig/twig/src/TokenParser/ForTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/ForTokenParser.php
@@ -12,7 +12,7 @@
namespace Twig\TokenParser;
-use Twig\Node\Expression\AssignNameExpression;
+use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\ForNode;
use Twig\Node\Node;
use Twig\Token;
@@ -35,30 +35,30 @@ final class ForTokenParser extends AbstractTokenParser
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$targets = $this->parser->getExpressionParser()->parseAssignmentExpression();
- $stream->expect(/* Token::OPERATOR_TYPE */ 8, 'in');
+ $stream->expect(Token::OPERATOR_TYPE, 'in');
$seq = $this->parser->getExpressionParser()->parseExpression();
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideForFork']);
if ('else' == $stream->next()->getValue()) {
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$else = $this->parser->subparse([$this, 'decideForEnd'], true);
} else {
$else = null;
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
if (\count($targets) > 1) {
- $keyTarget = $targets->getNode(0);
- $keyTarget = new AssignNameExpression($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine());
- $valueTarget = $targets->getNode(1);
+ $keyTarget = $targets->getNode('0');
+ $keyTarget = new AssignContextVariable($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine());
+ $valueTarget = $targets->getNode('1');
} else {
- $keyTarget = new AssignNameExpression('_key', $lineno);
- $valueTarget = $targets->getNode(0);
+ $keyTarget = new AssignContextVariable('_key', $lineno);
+ $valueTarget = $targets->getNode('0');
}
- $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine());
+ $valueTarget = new AssignContextVariable($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine());
- return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno, $this->getTag());
+ return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno);
}
public function decideForFork(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/FromTokenParser.php b/lib/twig/twig/src/TokenParser/FromTokenParser.php
index 31b6cde41..3bb4201a3 100644
--- a/lib/twig/twig/src/TokenParser/FromTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/FromTokenParser.php
@@ -11,7 +11,9 @@
namespace Twig\TokenParser;
-use Twig\Node\Expression\AssignNameExpression;
+use Twig\Node\Expression\Variable\AssignContextVariable;
+use Twig\Node\Expression\Variable\AssignTemplateVariable;
+use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\ImportNode;
use Twig\Node\Node;
use Twig\Token;
@@ -29,31 +31,32 @@ final class FromTokenParser extends AbstractTokenParser
{
$macro = $this->parser->getExpressionParser()->parseExpression();
$stream = $this->parser->getStream();
- $stream->expect(/* Token::NAME_TYPE */ 5, 'import');
+ $stream->expect(Token::NAME_TYPE, 'import');
$targets = [];
while (true) {
- $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+ $name = $stream->expect(Token::NAME_TYPE)->getValue();
- $alias = $name;
if ($stream->nextIf('as')) {
- $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+ $alias = new AssignContextVariable($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine());
+ } else {
+ $alias = new AssignContextVariable($name, $token->getLine());
}
$targets[$name] = $alias;
- if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+ if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine());
- $node = new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope());
+ $internalRef = new AssignTemplateVariable(new TemplateVariable(null, $token->getLine()), $this->parser->isMainScope());
+ $node = new ImportNode($macro, $internalRef, $token->getLine());
foreach ($targets as $name => $alias) {
- $this->parser->addImportedSymbol('function', $alias, 'macro_'.$name, $var);
+ $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef);
}
return $node;
diff --git a/lib/twig/twig/src/TokenParser/GuardTokenParser.php b/lib/twig/twig/src/TokenParser/GuardTokenParser.php
new file mode 100644
index 000000000..17b221d4c
--- /dev/null
+++ b/lib/twig/twig/src/TokenParser/GuardTokenParser.php
@@ -0,0 +1,69 @@
+parser->getStream();
+ $typeToken = $stream->expect(Token::NAME_TYPE);
+ if (!in_array($typeToken->getValue(), ['function', 'filter', 'test'])) {
+ throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext());
+ }
+ $method = 'get'.$typeToken->getValue();
+
+ $nameToken = $stream->expect(Token::NAME_TYPE);
+
+ $exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue());
+
+ $stream->expect(Token::BLOCK_END_TYPE);
+ if ($exists) {
+ $body = $this->parser->subparse([$this, 'decideGuardFork']);
+ } else {
+ $body = new EmptyNode();
+ $this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']);
+ }
+ $else = new EmptyNode();
+ if ('else' === $stream->next()->getValue()) {
+ $stream->expect(Token::BLOCK_END_TYPE);
+ $else = $this->parser->subparse([$this, 'decideGuardEnd'], true);
+ }
+ $stream->expect(Token::BLOCK_END_TYPE);
+
+ return new Nodes([$exists ? $body : $else]);
+ }
+
+ public function decideGuardFork(Token $token): bool
+ {
+ return $token->test(['else', 'endguard']);
+ }
+
+ public function decideGuardEnd(Token $token): bool
+ {
+ return $token->test(['endguard']);
+ }
+
+ public function getTag(): string
+ {
+ return 'guard';
+ }
+}
diff --git a/lib/twig/twig/src/TokenParser/IfTokenParser.php b/lib/twig/twig/src/TokenParser/IfTokenParser.php
index c0fe6df0d..6b9010563 100644
--- a/lib/twig/twig/src/TokenParser/IfTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/IfTokenParser.php
@@ -15,6 +15,7 @@ namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\IfNode;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\Token;
/**
@@ -37,7 +38,7 @@ final class IfTokenParser extends AbstractTokenParser
$lineno = $token->getLine();
$expr = $this->parser->getExpressionParser()->parseExpression();
$stream = $this->parser->getStream();
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfFork']);
$tests = [$expr, $body];
$else = null;
@@ -46,13 +47,13 @@ final class IfTokenParser extends AbstractTokenParser
while (!$end) {
switch ($stream->next()->getValue()) {
case 'else':
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$else = $this->parser->subparse([$this, 'decideIfEnd']);
break;
case 'elseif':
$expr = $this->parser->getExpressionParser()->parseExpression();
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfFork']);
$tests[] = $expr;
$tests[] = $body;
@@ -63,13 +64,13 @@ final class IfTokenParser extends AbstractTokenParser
break;
default:
- throw new SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext());
+ throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- return new IfNode(new Node($tests), $else, $lineno, $this->getTag());
+ return new IfNode(new Nodes($tests), $else, $lineno);
}
public function decideIfFork(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/ImportTokenParser.php b/lib/twig/twig/src/TokenParser/ImportTokenParser.php
index 44cb4dad7..5b3a5f2b8 100644
--- a/lib/twig/twig/src/TokenParser/ImportTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/ImportTokenParser.php
@@ -11,7 +11,8 @@
namespace Twig\TokenParser;
-use Twig\Node\Expression\AssignNameExpression;
+use Twig\Node\Expression\Variable\AssignTemplateVariable;
+use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\ImportNode;
use Twig\Node\Node;
use Twig\Token;
@@ -28,13 +29,13 @@ final class ImportTokenParser extends AbstractTokenParser
public function parse(Token $token): Node
{
$macro = $this->parser->getExpressionParser()->parseExpression();
- $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5, 'as');
- $var = new AssignNameExpression($this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5)->getValue(), $token->getLine());
- $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $this->parser->getStream()->expect(Token::NAME_TYPE, 'as');
+ $name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue();
+ $var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope());
+ $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
+ $this->parser->addImportedSymbol('template', $name);
- $this->parser->addImportedSymbol('template', $var->getAttribute('name'));
-
- return new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope());
+ return new ImportNode($macro, $var, $token->getLine());
}
public function getTag(): string
diff --git a/lib/twig/twig/src/TokenParser/IncludeTokenParser.php b/lib/twig/twig/src/TokenParser/IncludeTokenParser.php
index 28beb8ae4..466f2288c 100644
--- a/lib/twig/twig/src/TokenParser/IncludeTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/IncludeTokenParser.php
@@ -31,9 +31,9 @@ class IncludeTokenParser extends AbstractTokenParser
{
$expr = $this->parser->getExpressionParser()->parseExpression();
- list($variables, $only, $ignoreMissing) = $this->parseArguments();
+ [$variables, $only, $ignoreMissing] = $this->parseArguments();
- return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag());
+ return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine());
}
protected function parseArguments()
@@ -41,23 +41,23 @@ class IncludeTokenParser extends AbstractTokenParser
$stream = $this->parser->getStream();
$ignoreMissing = false;
- if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'ignore')) {
- $stream->expect(/* Token::NAME_TYPE */ 5, 'missing');
+ if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) {
+ $stream->expect(Token::NAME_TYPE, 'missing');
$ignoreMissing = true;
}
$variables = null;
- if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'with')) {
+ if ($stream->nextIf(Token::NAME_TYPE, 'with')) {
$variables = $this->parser->getExpressionParser()->parseExpression();
}
$only = false;
- if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'only')) {
+ if ($stream->nextIf(Token::NAME_TYPE, 'only')) {
$only = true;
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
return [$variables, $only, $ignoreMissing];
}
diff --git a/lib/twig/twig/src/TokenParser/MacroTokenParser.php b/lib/twig/twig/src/TokenParser/MacroTokenParser.php
index f584927e9..7d47821b2 100644
--- a/lib/twig/twig/src/TokenParser/MacroTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/MacroTokenParser.php
@@ -13,6 +13,12 @@ namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\BodyNode;
+use Twig\Node\EmptyNode;
+use Twig\Node\Expression\ArrayExpression;
+use Twig\Node\Expression\ConstantExpression;
+use Twig\Node\Expression\Unary\NegUnary;
+use Twig\Node\Expression\Unary\PosUnary;
+use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\MacroNode;
use Twig\Node\Node;
use Twig\Token;
@@ -32,26 +38,25 @@ final class MacroTokenParser extends AbstractTokenParser
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
- $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+ $name = $stream->expect(Token::NAME_TYPE)->getValue();
+ $arguments = $this->parseDefinition();
- $arguments = $this->parser->getExpressionParser()->parseArguments(true, true);
-
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$this->parser->pushLocalScope();
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
- if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) {
+ if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
- throw new SyntaxError(sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
+ throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$this->parser->popLocalScope();
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $this->getTag()));
+ $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno));
- return new Node();
+ return new EmptyNode($lineno);
}
public function decideBlockEnd(Token $token): bool
@@ -63,4 +68,56 @@ final class MacroTokenParser extends AbstractTokenParser
{
return 'macro';
}
+
+ private function parseDefinition(): ArrayExpression
+ {
+ $arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine());
+ $stream = $this->parser->getStream();
+ $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
+ while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
+ if (count($arguments)) {
+ $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, ')')) {
+ break;
+ }
+ }
+
+ $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
+ $name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
+ if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) {
+ $default = $this->parser->getExpressionParser()->parseExpression();
+ } else {
+ $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
+ $default->setAttribute('is_implicit', true);
+ }
+
+ if (!$this->checkConstantExpression($default)) {
+ 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());
+ }
+ $arguments->addElement($default, $name);
+ }
+ $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
+
+ return $arguments;
+ }
+
+ // checks that the node only contains "constant" elements
+ private function checkConstantExpression(Node $node): bool
+ {
+ if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
+ || $node instanceof NegUnary || $node instanceof PosUnary
+ )) {
+ return false;
+ }
+
+ foreach ($node as $n) {
+ if (!$this->checkConstantExpression($n)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/lib/twig/twig/src/TokenParser/SandboxTokenParser.php b/lib/twig/twig/src/TokenParser/SandboxTokenParser.php
index c919556ec..a7260ac46 100644
--- a/lib/twig/twig/src/TokenParser/SandboxTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/SandboxTokenParser.php
@@ -34,9 +34,11 @@ final class SandboxTokenParser extends AbstractTokenParser
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ trigger_deprecation('twig/twig', '3.15', \sprintf('The "sandbox" tag is deprecated in "%s" at line %d.', $stream->getSourceContext()->getName(), $token->getLine()));
+
+ $stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
// in a sandbox tag, only include tags are allowed
if (!$body instanceof IncludeNode) {
@@ -51,7 +53,7 @@ final class SandboxTokenParser extends AbstractTokenParser
}
}
- return new SandboxNode($body, $token->getLine(), $this->getTag());
+ return new SandboxNode($body, $token->getLine());
}
public function decideBlockEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/SetTokenParser.php b/lib/twig/twig/src/TokenParser/SetTokenParser.php
index 2fbdfe090..bb43907bd 100644
--- a/lib/twig/twig/src/TokenParser/SetTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/SetTokenParser.php
@@ -37,10 +37,10 @@ final class SetTokenParser extends AbstractTokenParser
$names = $this->parser->getExpressionParser()->parseAssignmentExpression();
$capture = false;
- if ($stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
+ if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) {
$values = $this->parser->getExpressionParser()->parseMultitargetExpression();
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
if (\count($names) !== \count($values)) {
throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
@@ -52,13 +52,13 @@ final class SetTokenParser extends AbstractTokenParser
throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$values = $this->parser->subparse([$this, 'decideBlockEnd'], true);
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
}
- return new SetNode($capture, $names, $values, $lineno, $this->getTag());
+ return new SetNode($capture, $names, $values, $lineno);
}
public function decideBlockEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenParser/TypesTokenParser.php b/lib/twig/twig/src/TokenParser/TypesTokenParser.php
new file mode 100644
index 000000000..b97eb3b2e
--- /dev/null
+++ b/lib/twig/twig/src/TokenParser/TypesTokenParser.php
@@ -0,0 +1,86 @@
+
+ *
+ * @internal
+ */
+final class TypesTokenParser extends AbstractTokenParser
+{
+ public function parse(Token $token): Node
+ {
+ $stream = $this->parser->getStream();
+
+ $types = $this->parseSimpleMappingExpression($stream);
+
+ $stream->expect(Token::BLOCK_END_TYPE);
+
+ return new TypesNode($types, $token->getLine());
+ }
+
+ /**
+ * @return array
+ *
+ * @throws SyntaxError
+ */
+ private function parseSimpleMappingExpression(TokenStream $stream): array
+ {
+ $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
+
+ $types = [];
+
+ $first = true;
+ while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
+ if (!$first) {
+ $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma');
+
+ // trailing ,?
+ if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
+ break;
+ }
+ }
+ $first = false;
+
+ $nameToken = $stream->expect(Token::NAME_TYPE);
+ $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?');
+
+ $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)');
+
+ $valueToken = $stream->expect(Token::STRING_TYPE);
+
+ $types[$nameToken->getValue()] = [
+ 'type' => $valueToken->getValue(),
+ 'optional' => $isOptional,
+ ];
+ }
+ $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
+
+ return $types;
+ }
+
+ public function getTag(): string
+ {
+ return 'types';
+ }
+}
diff --git a/lib/twig/twig/src/TokenParser/UseTokenParser.php b/lib/twig/twig/src/TokenParser/UseTokenParser.php
index 3cdbb98ad..ebd95aa31 100644
--- a/lib/twig/twig/src/TokenParser/UseTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/UseTokenParser.php
@@ -12,8 +12,10 @@
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
+use Twig\Node\EmptyNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
+use Twig\Node\Nodes;
use Twig\Token;
/**
@@ -44,26 +46,26 @@ final class UseTokenParser extends AbstractTokenParser
$targets = [];
if ($stream->nextIf('with')) {
while (true) {
- $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+ $name = $stream->expect(Token::NAME_TYPE)->getValue();
$alias = $name;
if ($stream->nextIf('as')) {
- $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+ $alias = $stream->expect(Token::NAME_TYPE)->getValue();
}
$targets[$name] = new ConstantExpression($alias, -1);
- if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+ if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- $this->parser->addTrait(new Node(['template' => $template, 'targets' => new Node($targets)]));
+ $this->parser->addTrait(new Nodes(['template' => $template, 'targets' => new Nodes($targets)]));
- return new Node();
+ return new EmptyNode($token->getLine());
}
public function getTag(): string
diff --git a/lib/twig/twig/src/TokenParser/WithTokenParser.php b/lib/twig/twig/src/TokenParser/WithTokenParser.php
index 7d8cbe261..8ce4f02b2 100644
--- a/lib/twig/twig/src/TokenParser/WithTokenParser.php
+++ b/lib/twig/twig/src/TokenParser/WithTokenParser.php
@@ -30,18 +30,18 @@ final class WithTokenParser extends AbstractTokenParser
$variables = null;
$only = false;
- if (!$stream->test(/* Token::BLOCK_END_TYPE */ 3)) {
+ if (!$stream->test(Token::BLOCK_END_TYPE)) {
$variables = $this->parser->getExpressionParser()->parseExpression();
- $only = (bool) $stream->nextIf(/* Token::NAME_TYPE */ 5, 'only');
+ $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only');
}
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideWithEnd'], true);
- $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+ $stream->expect(Token::BLOCK_END_TYPE);
- return new WithNode($body, $variables, $only, $token->getLine(), $this->getTag());
+ return new WithNode($body, $variables, $only, $token->getLine());
}
public function decideWithEnd(Token $token): bool
diff --git a/lib/twig/twig/src/TokenStream.php b/lib/twig/twig/src/TokenStream.php
index 1eac11a02..35aa9714f 100644
--- a/lib/twig/twig/src/TokenStream.php
+++ b/lib/twig/twig/src/TokenStream.php
@@ -21,14 +21,17 @@ use Twig\Error\SyntaxError;
*/
final class TokenStream
{
- private $tokens;
private $current = 0;
- private $source;
- public function __construct(array $tokens, Source $source = null)
- {
- $this->tokens = $tokens;
- $this->source = $source ?: new Source('', '');
+ public function __construct(
+ private array $tokens,
+ private ?Source $source = null,
+ ) {
+ if (null === $this->source) {
+ trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__));
+
+ $this->source = new Source('', '');
+ }
}
public function __toString()
@@ -60,24 +63,22 @@ final class TokenStream
*/
public function nextIf($primary, $secondary = null)
{
- if ($this->tokens[$this->current]->test($primary, $secondary)) {
- return $this->next();
- }
+ return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null;
}
/**
* Tests a token and returns it or throws a syntax error.
*/
- public function expect($type, $value = null, string $message = null): Token
+ public function expect($type, $value = null, ?string $message = null): Token
{
$token = $this->tokens[$this->current];
if (!$token->test($type, $value)) {
$line = $token->getLine();
- throw new SyntaxError(sprintf('%sUnexpected token "%s"%s ("%s" expected%s).',
+ throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).',
$message ? $message.'. ' : '',
Token::typeToEnglish($token->getType()),
- $token->getValue() ? sprintf(' of value "%s"', $token->getValue()) : '',
- Token::typeToEnglish($type), $value ? sprintf(' with value "%s"', $value) : ''),
+ $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '',
+ Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''),
$line,
$this->source
);
@@ -112,7 +113,7 @@ final class TokenStream
*/
public function isEOF(): bool
{
- return /* Token::EOF_TYPE */ -1 === $this->tokens[$this->current]->getType();
+ return Token::EOF_TYPE === $this->tokens[$this->current]->getType();
}
public function getCurrent(): Token
@@ -120,11 +121,6 @@ final class TokenStream
return $this->tokens[$this->current];
}
- /**
- * Gets the source associated with this stream.
- *
- * @internal
- */
public function getSourceContext(): Source
{
return $this->source;
diff --git a/lib/twig/twig/src/TwigCallableInterface.php b/lib/twig/twig/src/TwigCallableInterface.php
new file mode 100644
index 000000000..2a8ff6116
--- /dev/null
+++ b/lib/twig/twig/src/TwigCallableInterface.php
@@ -0,0 +1,53 @@
+
+ */
+interface TwigCallableInterface extends \Stringable
+{
+ public function getName(): string;
+
+ public function getType(): string;
+
+ public function getDynamicName(): string;
+
+ /**
+ * @return callable|array{class-string, string}|null
+ */
+ public function getCallable();
+
+ public function getNodeClass(): string;
+
+ public function needsCharset(): bool;
+
+ public function needsEnvironment(): bool;
+
+ public function needsContext(): bool;
+
+ public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self;
+
+ public function getArguments(): array;
+
+ public function isVariadic(): bool;
+
+ public function isDeprecated(): bool;
+
+ public function getDeprecatingPackage(): string;
+
+ public function getDeprecatedVersion(): string;
+
+ public function getAlternative(): ?string;
+
+ public function getMinimalNumberOfRequiredArguments(): int;
+}
diff --git a/lib/twig/twig/src/TwigFilter.php b/lib/twig/twig/src/TwigFilter.php
index 8993026c8..dece51843 100644
--- a/lib/twig/twig/src/TwigFilter.php
+++ b/lib/twig/twig/src/TwigFilter.php
@@ -21,72 +21,27 @@ use Twig\Node\Node;
*
* @see https://twig.symfony.com/doc/templates.html#filters
*/
-final class TwigFilter
+final class TwigFilter extends AbstractTwigCallable
{
- private $name;
- private $callable;
- private $options;
- private $arguments = [];
-
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
- $this->name = $name;
- $this->callable = $callable;
+ parent::__construct($name, $callable, $options);
+
$this->options = array_merge([
- 'needs_environment' => false,
- 'needs_context' => false,
- 'is_variadic' => false,
'is_safe' => null,
'is_safe_callback' => null,
'pre_escape' => null,
'preserves_safety' => null,
'node_class' => FilterExpression::class,
- 'deprecated' => false,
- 'alternative' => null,
- ], $options);
+ ], $this->options);
}
- public function getName(): string
+ public function getType(): string
{
- return $this->name;
- }
-
- /**
- * Returns the callable to execute for this filter.
- *
- * @return callable|array{class-string, string}|null
- */
- public function getCallable()
- {
- return $this->callable;
- }
-
- public function getNodeClass(): string
- {
- return $this->options['node_class'];
- }
-
- public function setArguments(array $arguments): void
- {
- $this->arguments = $arguments;
- }
-
- public function getArguments(): array
- {
- return $this->arguments;
- }
-
- public function needsEnvironment(): bool
- {
- return $this->options['needs_environment'];
- }
-
- public function needsContext(): bool
- {
- return $this->options['needs_context'];
+ return 'filter';
}
public function getSafe(Node $filterArgs): ?array
@@ -99,12 +54,12 @@ final class TwigFilter
return $this->options['is_safe_callback']($filterArgs);
}
- return null;
+ return [];
}
- public function getPreservesSafety(): ?array
+ public function getPreservesSafety(): array
{
- return $this->options['preserves_safety'];
+ return $this->options['preserves_safety'] ?? [];
}
public function getPreEscape(): ?string
@@ -112,23 +67,8 @@ final class TwigFilter
return $this->options['pre_escape'];
}
- public function isVariadic(): bool
+ public function getMinimalNumberOfRequiredArguments(): int
{
- return $this->options['is_variadic'];
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->options['deprecated'];
- }
-
- public function getDeprecatedVersion(): string
- {
- return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
- }
-
- public function getAlternative(): ?string
- {
- return $this->options['alternative'];
+ return parent::getMinimalNumberOfRequiredArguments() + 1;
}
}
diff --git a/lib/twig/twig/src/TwigFunction.php b/lib/twig/twig/src/TwigFunction.php
index d910d1fd5..4a10df95e 100644
--- a/lib/twig/twig/src/TwigFunction.php
+++ b/lib/twig/twig/src/TwigFunction.php
@@ -21,70 +21,31 @@ use Twig\Node\Node;
*
* @see https://twig.symfony.com/doc/templates.html#functions
*/
-final class TwigFunction
+final class TwigFunction extends AbstractTwigCallable
{
- private $name;
- private $callable;
- private $options;
- private $arguments = [];
-
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
- $this->name = $name;
- $this->callable = $callable;
+ parent::__construct($name, $callable, $options);
+
$this->options = array_merge([
- 'needs_environment' => false,
- 'needs_context' => false,
- 'is_variadic' => false,
'is_safe' => null,
'is_safe_callback' => null,
'node_class' => FunctionExpression::class,
- 'deprecated' => false,
- 'alternative' => null,
- ], $options);
+ 'parser_callable' => null,
+ ], $this->options);
}
- public function getName(): string
+ public function getType(): string
{
- return $this->name;
+ return 'function';
}
- /**
- * Returns the callable to execute for this function.
- *
- * @return callable|array{class-string, string}|null
- */
- public function getCallable()
+ public function getParserCallable(): ?callable
{
- return $this->callable;
- }
-
- public function getNodeClass(): string
- {
- return $this->options['node_class'];
- }
-
- public function setArguments(array $arguments): void
- {
- $this->arguments = $arguments;
- }
-
- public function getArguments(): array
- {
- return $this->arguments;
- }
-
- public function needsEnvironment(): bool
- {
- return $this->options['needs_environment'];
- }
-
- public function needsContext(): bool
- {
- return $this->options['needs_context'];
+ return $this->options['parser_callable'];
}
public function getSafe(Node $functionArgs): ?array
@@ -99,24 +60,4 @@ final class TwigFunction
return [];
}
-
- public function isVariadic(): bool
- {
- return (bool) $this->options['is_variadic'];
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->options['deprecated'];
- }
-
- public function getDeprecatedVersion(): string
- {
- return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
- }
-
- public function getAlternative(): ?string
- {
- return $this->options['alternative'];
- }
}
diff --git a/lib/twig/twig/src/TwigTest.php b/lib/twig/twig/src/TwigTest.php
index 3769ec162..5e58ad8b0 100644
--- a/lib/twig/twig/src/TwigTest.php
+++ b/lib/twig/twig/src/TwigTest.php
@@ -20,81 +20,48 @@ use Twig\Node\Expression\TestExpression;
*
* @see https://twig.symfony.com/doc/templates.html#test-operator
*/
-final class TwigTest
+final class TwigTest extends AbstractTwigCallable
{
- private $name;
- private $callable;
- private $options;
- private $arguments = [];
-
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
- $this->name = $name;
- $this->callable = $callable;
+ parent::__construct($name, $callable, $options);
+
$this->options = array_merge([
- 'is_variadic' => false,
'node_class' => TestExpression::class,
- 'deprecated' => false,
- 'alternative' => null,
'one_mandatory_argument' => false,
- ], $options);
+ ], $this->options);
}
- public function getName(): string
+ public function getType(): string
{
- return $this->name;
+ return 'test';
}
- /**
- * Returns the callable to execute for this test.
- *
- * @return callable|array{class-string, string}|null
- */
- public function getCallable()
+ public function needsCharset(): bool
{
- return $this->callable;
+ return false;
}
- public function getNodeClass(): string
+ public function needsEnvironment(): bool
{
- return $this->options['node_class'];
+ return false;
}
- public function setArguments(array $arguments): void
+ public function needsContext(): bool
{
- $this->arguments = $arguments;
- }
-
- public function getArguments(): array
- {
- return $this->arguments;
- }
-
- public function isVariadic(): bool
- {
- return (bool) $this->options['is_variadic'];
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->options['deprecated'];
- }
-
- public function getDeprecatedVersion(): string
- {
- return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
- }
-
- public function getAlternative(): ?string
- {
- return $this->options['alternative'];
+ return false;
}
public function hasOneMandatoryArgument(): bool
{
return (bool) $this->options['one_mandatory_argument'];
}
+
+ public function getMinimalNumberOfRequiredArguments(): int
+ {
+ return parent::getMinimalNumberOfRequiredArguments() + 1;
+ }
}
diff --git a/lib/twig/twig/src/Util/CallableArgumentsExtractor.php b/lib/twig/twig/src/Util/CallableArgumentsExtractor.php
new file mode 100644
index 000000000..d8625169d
--- /dev/null
+++ b/lib/twig/twig/src/Util/CallableArgumentsExtractor.php
@@ -0,0 +1,219 @@
+
+ *
+ * @internal
+ */
+final class CallableArgumentsExtractor
+{
+ private ReflectionCallable $rc;
+
+ public function __construct(
+ private Node $node,
+ private TwigCallableInterface $twigCallable,
+ ) {
+ $this->rc = new ReflectionCallable($twigCallable);
+ }
+
+ /**
+ * @return array
+ */
+ public function extractArguments(Node $arguments): array
+ {
+ $extractedArguments = [];
+ $extractedArgumentNameMap = [];
+ $named = false;
+ foreach ($arguments as $name => $node) {
+ if (!\is_int($name)) {
+ $named = true;
+ } elseif ($named) {
+ throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
+ }
+
+ $extractedArguments[$normalizedName = $this->normalizeName($name)] = $node;
+ $extractedArgumentNameMap[$normalizedName] = $name;
+ }
+
+ if (!$named && !$this->twigCallable->isVariadic()) {
+ $min = $this->twigCallable->getMinimalNumberOfRequiredArguments();
+ if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) {
+ $argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName());
+
+ throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
+ }
+
+ return $extractedArguments;
+ }
+
+ if (!$callable = $this->twigCallable->getCallable()) {
+ if ($named) {
+ throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()));
+ }
+
+ throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()));
+ }
+
+ [$callableParameters, $isPhpVariadic] = $this->getCallableParameters();
+ $arguments = [];
+ $callableParameterNames = [];
+ $missingArguments = [];
+ $optionalArguments = [];
+ $pos = 0;
+ foreach ($callableParameters as $callableParameter) {
+ $callableParameterName = $callableParameter->name;
+ if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) {
+ if ('start' === $callableParameterName) {
+ $callableParameterName = 'low';
+ } elseif ('end' === $callableParameterName) {
+ $callableParameterName = 'high';
+ }
+ }
+
+ $callableParameterNames[] = $callableParameterName;
+ $normalizedCallableParameterName = $this->normalizeName($callableParameterName);
+
+ if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) {
+ if (\array_key_exists($pos, $extractedArguments)) {
+ throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
+ }
+
+ if (\count($missingArguments)) {
+ 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".',
+ $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
+ ), $this->node->getTemplateLine(), $this->node->getSourceContext());
+ }
+
+ $arguments = array_merge($arguments, $optionalArguments);
+ $arguments[] = $extractedArguments[$normalizedCallableParameterName];
+ unset($extractedArguments[$normalizedCallableParameterName]);
+ $optionalArguments = [];
+ } elseif (\array_key_exists($pos, $extractedArguments)) {
+ $arguments = array_merge($arguments, $optionalArguments);
+ $arguments[] = $extractedArguments[$pos];
+ unset($extractedArguments[$pos]);
+ $optionalArguments = [];
+ ++$pos;
+ } elseif ($callableParameter->isDefaultValueAvailable()) {
+ $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine());
+ } elseif ($callableParameter->isOptional()) {
+ if (!$extractedArguments) {
+ break;
+ }
+
+ $missingArguments[] = $callableParameterName;
+ } else {
+ throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
+ }
+ }
+
+ if ($this->twigCallable->isVariadic()) {
+ $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine());
+ foreach ($extractedArguments as $key => $value) {
+ if (\is_int($key)) {
+ $arbitraryArguments->addElement($value);
+ } else {
+ $originalKey = $extractedArgumentNameMap[$key];
+ if ($originalKey !== $this->toSnakeCase($originalKey)) {
+ trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine()));
+ }
+ $arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine()));
+ // I Twig 4.0, don't convert the key:
+ // $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine()));
+ }
+ unset($extractedArguments[$key]);
+ }
+
+ if ($arbitraryArguments->count()) {
+ $arguments = array_merge($arguments, $optionalArguments);
+ $arguments[] = $arbitraryArguments;
+ }
+ }
+
+ if ($extractedArguments) {
+ $unknownArgument = null;
+ foreach ($extractedArguments as $extractedArgument) {
+ if ($extractedArgument instanceof Node) {
+ $unknownArgument = $extractedArgument;
+ break;
+ }
+ }
+
+ throw new SyntaxError(
+ \sprintf(
+ 'Unknown argument%s "%s" for %s "%s(%s)".',
+ \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames))
+ ),
+ $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(),
+ $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext()
+ );
+ }
+
+ return $arguments;
+ }
+
+ private function normalizeName(string $name): string
+ {
+ return strtolower(str_replace('_', '', $name));
+ }
+
+ private function toSnakeCase(string $name): string
+ {
+ return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name));
+ }
+
+ private function getCallableParameters(): array
+ {
+ $parameters = $this->rc->getReflector()->getParameters();
+ if ($this->node->hasNode('node')) {
+ array_shift($parameters);
+ }
+ if ($this->twigCallable->needsCharset()) {
+ array_shift($parameters);
+ }
+ if ($this->twigCallable->needsEnvironment()) {
+ array_shift($parameters);
+ }
+ if ($this->twigCallable->needsContext()) {
+ array_shift($parameters);
+ }
+ foreach ($this->twigCallable->getArguments() as $argument) {
+ array_shift($parameters);
+ }
+
+ $isPhpVariadic = false;
+ if ($this->twigCallable->isVariadic()) {
+ $argument = end($parameters);
+ $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 SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()));
+ }
+ }
+
+ return [$parameters, $isPhpVariadic];
+ }
+}
diff --git a/lib/twig/twig/src/Util/DeprecationCollector.php b/lib/twig/twig/src/Util/DeprecationCollector.php
index 378b666bd..0ea26ed4b 100644
--- a/lib/twig/twig/src/Util/DeprecationCollector.php
+++ b/lib/twig/twig/src/Util/DeprecationCollector.php
@@ -20,11 +20,9 @@ use Twig\Source;
*/
final class DeprecationCollector
{
- private $twig;
-
- public function __construct(Environment $twig)
- {
- $this->twig = $twig;
+ public function __construct(
+ private Environment $twig,
+ ) {
}
/**
@@ -60,6 +58,8 @@ final class DeprecationCollector
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
+
+ return false;
});
foreach ($iterator as $name => $contents) {
diff --git a/lib/twig/twig/src/Util/ReflectionCallable.php b/lib/twig/twig/src/Util/ReflectionCallable.php
new file mode 100644
index 000000000..16734d9df
--- /dev/null
+++ b/lib/twig/twig/src/Util/ReflectionCallable.php
@@ -0,0 +1,92 @@
+
+ *
+ * @internal
+ */
+final class ReflectionCallable
+{
+ private $reflector;
+ private $callable;
+ private $name;
+
+ public function __construct(
+ TwigCallableInterface $twigCallable,
+ ) {
+ $callable = $twigCallable->getCallable();
+ if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
+ $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
+ }
+
+ if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
+ $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]);
+ $this->callable = $callable;
+ $this->name = $r->class.'::'.$r->name;
+
+ return;
+ }
+
+ $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.', $twigCallable->getType(), $twigCallable->getName()), 0, $e);
+ }
+ $this->reflector = $r = new \ReflectionFunction($closure);
+
+ if (str_contains($r->name, '{closure')) {
+ $this->callable = $callable;
+ $this->name = 'Closure';
+
+ return;
+ }
+
+ if ($object = $r->getClosureThis()) {
+ $callable = [$object, $r->name];
+ $this->name = get_debug_type($object).'::'.$r->name;
+ } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
+ $callable = [$class->name, $r->name];
+ $this->name = $class->name.'::'.$r->name;
+ } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
+ $callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name];
+ $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
+ } else {
+ $callable = $this->name = $r->name;
+ }
+
+ if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
+ $callable = $r->getClosure();
+ }
+
+ $this->callable = $callable;
+ }
+
+ public function getReflector(): \ReflectionFunctionAbstract
+ {
+ return $this->reflector;
+ }
+
+ public function getCallable()
+ {
+ return $this->callable;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+}
diff --git a/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php b/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php
new file mode 100644
index 000000000..056f163bb
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php
@@ -0,0 +1,94 @@
+ "GABUZOMEU",
+ 'org_id' => $this->CreateOrganization(uniqid())->GetKey(),
+ ];
+ $this->oUR = $this->CreateUserRequest(666, $aUserRequestCustomParams);
+ }
+
+ public function RenderOQLSearchProvider()
+ {
+ $aUseCases = [];
+ $aValues = [false, true];
+ foreach ($aValues as $bSearchPane) {
+ foreach ($aValues as $bSearchOpen) {
+ foreach ($aValues as $bAutoreload) {
+ $aUseCases[] = [
+ 'bSearchPane' => $bSearchPane,
+ 'bSearchOpen' => $bSearchOpen,
+ "bAutoreload" => $bAutoreload,
+ ];
+ }
+
+ }
+
+ return $aUseCases;
+ }
+ }
+
+ /**
+ * @dataProvider RenderOQLSearchProvider
+ */
+ public function testRenderOQLSearch($bSearchPane, $bSearchOpen, $bAutoreload)
+ {
+ $sOql = <<CallRenderOQLSearch($bSearchPane, $bSearchOpen, $bAutoreload, $sOql);
+
+ $this->assertTrue(false !== strpos($sContent, $this->oUR->Get('title')), $sContent);
+ }
+
+ /**
+ * @covers N°7750 - Bug with OQL set as Shortcut
+ */
+ public function testRenderOQLSearchOqlWithDateFormatOnDeadline()
+ {
+ $sOql = <<CallRenderOQLSearch(true, true, true, $sOql);
+ $this->assertTrue(true);
+ } catch(\Exception $e){
+ echo($e->getMessage());
+ $this->fail('Without N°7750 fix Exception raised => TypeError : date(): Argument #2 ($timestamp) must be of type ?int, string given');
+ }
+ }
+
+ public function CallRenderOQLSearch(bool $bSearchPane, bool $bSearchOpen, bool $bAutoreload, string $sOql) : string
+ {
+ $sTitle = 'title';
+ $oPage = new WebPage($sTitle);
+
+ if ($bAutoreload) {
+ $aExtraParams = [
+ 'auto_reload' => "5", //value in seconds
+ ];
+ } else {
+ $aExtraParams = [];
+ }
+
+ \OQLMenuNode::RenderOQLSearch($sOql, $sTitle, 'shortcut_XXX', $bSearchPane, $bSearchOpen, $oPage, $aExtraParams, true);
+
+ $oResponse = $oPage->GenerateResponse();
+ return $oResponse->getContent();
+ }
+}
+