mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-29 20:53:30 +00:00
[Web] Update composer libraries
This commit is contained in:
1297 files changed
+71585
-24460
No files matched your search
+314
@@ -1,3 +1,306 @@
|
||||
# 3.30.0 (2026-09-25)
|
||||
|
||||
* Fix `split`, `random`, and `shuffle` merging a trailing newline into the last character of a string
|
||||
* Speed up splitting a string into characters in `split`, `random`, and `shuffle`
|
||||
* Speed up escaping by fetching the escaper runtime once per template
|
||||
* Speed up adding extensions to an environment
|
||||
* Fix the escaping safe analysis retaining every compiled template node for the lifetime of the environment
|
||||
* Speed up loading a template that the environment has already loaded
|
||||
* Speed up rendering by compiling a cheaper generator guard into templates
|
||||
* Speed up compiling filter, function, and test calls
|
||||
* Fix `IntlExtension` letting the pattern derived from a date formatter prototype override an explicit locale
|
||||
* Fix `IntlExtension` not honoring the locale of a date formatter prototype configured with no date and time styles
|
||||
* Speed up macro calls
|
||||
* Fix `TemplateWrapper::unwrap()` failing when called without arguments, which is now deprecated
|
||||
* Add `TemplateWrapper::getDefaultEscapeStrategy()` to know the escaping strategy a template was compiled with
|
||||
* Report a clear error when `random`, `reverse`, `shuffle`, and `split` receive a string that is not valid UTF-8
|
||||
* Fix the deprecation about omitting parentheses when calling a macro being reported twice for the same call
|
||||
* Add the macro name to the deprecation about omitting parentheses when calling a macro
|
||||
* Deprecate cloning a `Twig\Environment` instance
|
||||
* Fix array access with a `Stringable` key on subclasses of `ArrayObject` and `ArrayIterator`
|
||||
|
||||
# 3.29.0 (2026-09-18)
|
||||
|
||||
* Fix the PHP warning and cryptic error when a block or a macro rendered on its own uses macros imported in the template body
|
||||
* Fix `{% cache %}` always missing in the Symfony bundle when `framework.cache.app` uses a natively tag aware adapter
|
||||
* Fix the sandbox resolving `use` trait templates before checking that the `use` tag is allowed
|
||||
* Fix `html_attr` dropping `style` declarations whose value is `0`, `0.0` or `'0'`
|
||||
* Fix the `default` filter fallback emitting an undefined variable warning when it uses the null-safe operator
|
||||
* Fix the `matches` operator silently treating PCRE execution errors as non-matches; it now throws a `RuntimeError`
|
||||
* Fix `TemplateWrapper::streamBlock()`, `TemplateWrapper::hasBlock()`, and `TemplateWrapper::getBlockNames()` omitting environment globals
|
||||
* Fix exceptions from dynamic parent expressions escaping without template context
|
||||
* Add the `BlockChain` class to compose blocks from multiple templates without using template internals
|
||||
* Fix `TemplateWrapper::hasBlock()` and `TemplateWrapper::getBlockNames()` losing the `extends` line when the parent template does not exist
|
||||
* Fix an output buffer leak when a parent block rendered in an expression throws in non-yield mode
|
||||
* Add the `HtmlExtension::htmlAttrValue()` method to resolve a single HTML attribute value the way the `html_attr` function renders it
|
||||
* Fix `html_attr` JSON encoding a `Stringable` value in a `data-*` attribute instead of using its string representation
|
||||
* Add documentation comments to attach metadata to nodes (experimental)
|
||||
* Fix destructuring patterns containing no variables (empty patterns or sequences with only empty slots) triggering a PHP fatal error instead of a `SyntaxError`
|
||||
* Fix object and mapping destructuring evaluating the right-hand expression more than once
|
||||
* Fix sequence destructuring of iterators throwing a `TypeError`
|
||||
* Fix `MissingExtensionSuggestor` not suggesting the `twig/*-extra` package to install for some `html-extra`, `intl-extra`, and `string-extra` filters and functions
|
||||
* Add `TempestMarkdown` to use `tempest/markdown` as the `markdown_to_html` converter
|
||||
* Add the `include_only` function to render a template without giving it access to the current context
|
||||
* Add the `Twig\Sandbox\SandboxInterface` interface and `Twig\Sandbox\Sandbox` class to render untrusted templates through a dedicated, always-sandboxed environment crafted for it
|
||||
* Reject `TemplateWrapper` instances created by another `Environment`
|
||||
* Add the `Twig\Extension\SandboxBridgeExtension` to render sandboxed templates from trusted templates with an explicit output escaping strategy
|
||||
* Extract the sandbox runtime enforcement into a new internal `Twig\Sandbox\SecurityChecker` class used by compiled templates and `CoreExtension`
|
||||
* Mark `SandboxExtension` as internal, use `Twig\Sandbox\Sandbox` instead
|
||||
* Deprecate the `sandboxed` argument of the `include` function, use `Twig\Sandbox\Sandbox` instead
|
||||
* Deprecate `SandboxExtension::enableSandbox()`, `disableSandbox()`, and `isSandboxedGlobally()`
|
||||
* Normalize destructuring variable AST nodes as assignment targets
|
||||
* Fix `IntlExtension` ignoring explicit date/time formats and configured calendars when using a date formatter prototype
|
||||
* Add a `format_list` filter to `IntlExtension` to format a list of strings using PHP 8.5's `IntlListFormatter`
|
||||
* Fix array access with a `Stringable` key for `ArrayObject` and `ArrayIterator` while preserving object keys for `SplObjectStorage`
|
||||
* Fix duplicated macro argument names triggering a PHP fatal error instead of a `SyntaxError`
|
||||
* Deprecate defining a macro more than once in the same template
|
||||
* Deprecate `TemplateVariable` and `AssignTemplateVariable`; use `MacroVariable` and `AssignMacroVariable` instead
|
||||
* Deprecate calling or testing a macro with a name whose case differs from its definition; macro names will be case-sensitive in 4.0
|
||||
* Deprecate omitting parentheses when calling a macro; it will throw a `SyntaxError` in 4.0
|
||||
* Deprecate using parentheses when testing a macro with the `defined` test; it will throw a `SyntaxError` in 4.0
|
||||
* Deprecate calling a macro without a value for an argument that has no default value; the argument will be required in 4.0
|
||||
* Deprecate passing extra or unknown arguments to a macro that does not declare a variadic argument; it will throw in 4.0
|
||||
* Add support for declaring an explicit variadic macro argument (`{% macro foo(a, ...rest) %}`)
|
||||
* Compile macros as closures stored in a per-template registry instead of `macro_`-prefixed PHP methods
|
||||
* Represent imported macro namespaces with a narrow capability object instead of template instances
|
||||
* Mark `Twig\Node\MacroNode` as `@final`; it will be final in Twig 4.0
|
||||
* Deprecate not passing a `MacrosNode` instance as the macros of a `ModuleNode` constructor
|
||||
* Change `MacroReferenceExpression` to take the bare macro name instead of a `macro_`-prefixed method name
|
||||
* Deprecate resolving a macro through a `macro_`-prefixed name; pass the bare macro name to `MacroReferenceExpression`
|
||||
|
||||
# 3.28.0 (2026-07-03)
|
||||
|
||||
* Render backed enums using their backing value in the `html_attr` function
|
||||
* Fix empty Markup values being treated as truthy in and, or, xor, not, ternary, and elvis expressions
|
||||
* Fix a PHP 8.5 `chr()` deprecation when decoding an octal string escape sequence larger than `\377` (such as `"\777"`)
|
||||
* Mark `Twig\Markup` as `@final`; it will be final in Twig 4.0
|
||||
* Reduce memory usage and speed up the context restoration compiled at the end of `for` loops
|
||||
* Allow calling a macro with a dynamic name via the dot operator (`macros.(name)(args)`)
|
||||
* Report the column number in syntax errors and expose it via `Error::getTemplateColumn()`
|
||||
* Track the source offset of each token and expose it via `Token::getOffset()`
|
||||
* Fix nested `block()` calls to resolve against the overriding template when a block rendered through `block(name, template)` calls `parent()`
|
||||
* Deprecate the possibility to use a `block` tag within a capture node (like `set`)
|
||||
* Deprecate the possibility to use a `block` tag within a capture node (like `set`) in child templates
|
||||
* Deprecate using a `macro`, `extends`, or `use` tag outside the root of a template
|
||||
* Stop reporting a skipped test in `IntegrationTestCase` when there is no legacy test to run
|
||||
* Fix `markdown_to_html` to strip the indentation shared by all lines instead of mangling content that starts with a blank line
|
||||
* Make the `IntegrationTestCase` and `NodeTestCase` test helpers compatible with PHPUnit 11
|
||||
* Cast printed expressions to string so values that cannot be converted to a string (arrays, non-`Stringable` objects, ...) report a usable stack trace at the print location
|
||||
* Make the `include()` function return a `Markup` object so an assigned result is not re-escaped when printed
|
||||
* Skip the sandbox `__toString` check on arguments whose PHP parameter type cannot implicitly coerce to string
|
||||
* Document the criteria for marking a callable or tag as always allowed in a sandbox, and the list of built-in tags, filters, and functions that will be always allowed in Twig 4.0
|
||||
* Add an `always_allowed_in_sandbox` option for filters, functions, and tests, and an `isAlwaysAllowedInSandbox()` method for token parsers, to let authors mark callables and tags that are always allowed in sandbox mode without explicit allow-listing
|
||||
* Add an allow-list for tests to `Twig\Sandbox\SecurityPolicy`, with the safe built-in tests flagged as always allowed so they keep working without allow-listing
|
||||
|
||||
# 3.27.1 (2026-05-30)
|
||||
|
||||
* Fix array access with a `Stringable` key to coerce the key to string consistently instead of throwing in the optimized path
|
||||
* Fix sandbox replacing `IteratorAggregate` arguments (e.g. Symfony's `FormView`) by a plain array
|
||||
|
||||
# 3.27.0 (2026-05-27)
|
||||
|
||||
* Add a strict mode to `Twig\Sandbox\SecurityPolicy` to opt-in to the 4.0 behavior for the `extends`/`use` tags and the `parent`/`block`/`attribute` functions, which are otherwise still implicitly allowed in a sandbox
|
||||
* Deprecate the fact that the `parent`, `block`, and `attribute` functions are always allowed in a sandboxed template
|
||||
* Fix sandbox filter/tag/function allow-list bypass when the sandbox state changed between renders of a cached `Template` instance
|
||||
* Fix PHP 8.1+ implicit float-to-int deprecation triggered by sandboxed `ArrayAccess` attribute access with a float key
|
||||
* Restrict allowed classes in `Twig\Profiler\Profile::unserialize()` to prevent arbitrary class instantiation
|
||||
* Escape root profile name in `HtmlDumper`
|
||||
* Fix sandbox bypass in deprecated internal wrappers `twig_array_some()`, `twig_array_every()`, and `twig_check_arrow_in_sandbox()` (`src/Resources/core.php`)
|
||||
* Deprecate the `Twig\Sandbox\SourcePolicyInterface` interface with no replacement
|
||||
* Fix sandbox bypass in the "column" filter when sandboxing is enabled via `SourcePolicyInterface`
|
||||
* Fix sandbox `__toString` bypass via `Traversable` arguments to the `join` and `replace` filters (also covers containers that implement both `Stringable` and `Traversable`)
|
||||
* Fix sandbox `__toString` bypass via the `in` and `not in` operators
|
||||
* Prevent a stack overflow in `SandboxExtension::ensureToStringAllowed()` when a self-referencing iterable is passed to a sandboxed template
|
||||
* Add support for any expression as a dynamic mapping key (attribute access, filters, ...)
|
||||
* Fix sandbox `__toString` policy bypass via dynamic mapping keys
|
||||
|
||||
# 3.26.0 (2026-05-20)
|
||||
|
||||
* Document that the sandbox doesn't protect against resource exhaustion
|
||||
* Document `template_from_string` caveats when used in a sandboxed environment
|
||||
* Add docs on `Markup` about the goal of this class in the context of a sandbox
|
||||
* Pre-escape HTML input on the `spaceless` filter
|
||||
* Pre-escape HTML input on `inline_css` and `inky_to_html` filters
|
||||
* Fix XSS by adjusting `is_safe` annotation on HTML-emitting filters
|
||||
* [Profiler] Escape template and profile names in `HtmlDumper`
|
||||
* Fix unbounded memoisation of `IntlDateFormatter` / `NumberFormatter`
|
||||
* Fix sandbox bypass in the "column" filter
|
||||
* Fix sandbox bypass in the `{% sandbox %}` tag when including a preloaded template
|
||||
* Fix sandbox bypass: PHP code injection via `{% use %}` template name
|
||||
* Fix sandbox bypass: PHP code injection via `_self` / import macro reference
|
||||
* Fix sandbox bypass in object destructuring assignment
|
||||
* Fix sandbox bypass: propagate `Source` to `checkArrow` for source-policy sandboxing
|
||||
* Encode single quotes as `\x27` in `Compiler::string()` as a defense-in-depth measure
|
||||
* Fix sandbox `__toString` bypasses
|
||||
* Add `Twig\Node\CoercesChildrenToStringInterface` to let nodes declare which of their child nodes will be string-coerced at runtime so the sandbox wraps them with a `__toString` check
|
||||
|
||||
# 3.25.0 (2026-05-17)
|
||||
|
||||
* Add a `needs_is_sandboxed` option for filters, functions, and tests
|
||||
* Use deterministic suffixes for generated embed classes
|
||||
* Lazy-load `EscaperRuntime` in `EscaperExtension`
|
||||
|
||||
# 3.24.0 (2026-03-17)
|
||||
|
||||
* Deprecate not implementing the `getOperatorTokens()` method in `ExpressionParserInterface` implementations
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Twig\Node\Expression\Binary\MatchesBinary` constructor
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Parser::setParent()`
|
||||
* Add support for renaming variables in object destructuring (`{name: userName} = user`)
|
||||
* Add `html_attr_relaxed` escaping strategy that preserves :, @, [, and ] for front-end framework attribute names
|
||||
* Add support for short-circuiting in null-safe operator chains
|
||||
* Add the `html_attr` function and `html_attr_merge` as well as `html_attr_type` filters
|
||||
|
||||
# 3.23.0 (2026-01-23)
|
||||
|
||||
* Add `=` assignment operator (allows to set variables in expression or to replace the short-form of the set tag)
|
||||
* Add sequence, mapping, and object destructuring
|
||||
* Add `?.` null-safe operator
|
||||
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
|
||||
* Fix opcache preload warning for unlinked anonymous class
|
||||
* Fix spread operator behavior
|
||||
|
||||
# 3.22.2 (2025-12-14)
|
||||
|
||||
* Fix "cycle" with non-countable ArrayAccess + Traversable objects
|
||||
* Use "getShareDir" as an indicator of Symfony version in Symfony bundle
|
||||
* Fix escaper compatibility with PHP 8.5
|
||||
|
||||
# 3.22.1 (2025-11-16)
|
||||
|
||||
* Add support for Symfony 8
|
||||
|
||||
# 3.22.0 (2025-10-29)
|
||||
|
||||
* Add support for two words test in guard tag
|
||||
* Add `Environment::registerUndefinedTestCallback()`
|
||||
* Fix compatibility with Symfony 8
|
||||
* Fix accessing arrays with stringable objects as key
|
||||
* Avoid errors when failing to guess the template info for an error
|
||||
* Fix expression parser compatibility layer
|
||||
* Fix compiling 'index' with repr (not string) in EmbedNode
|
||||
* Update configuration keys + allow extra keys for CommonMark extensions
|
||||
* Allow usage of other Markdown converters than CommonMark in LeagueMarkdown
|
||||
|
||||
# 3.21.1 (2025-05-03)
|
||||
|
||||
* Fix ExtensionSet usage of BinaryOperatorExpressionParser
|
||||
|
||||
# 3.21.0 (2025-05-02)
|
||||
|
||||
* Fix wrong array index
|
||||
* Deprecate `Template::loadTemplate()`
|
||||
* Fix testing and expression when it evaluates to an instance of `Markup`
|
||||
* Add `ReturnPrimitiveTypeInterface` (and sub-interfaces for number, boolean, string, and array)
|
||||
* Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test
|
||||
* Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence
|
||||
* Deprecate operator precedence outside of the [0, 512] range
|
||||
* Introduce expression parser classes to describe operators and operands provided by extensions
|
||||
instead of arrays (it comes with many deprecations that are documented in
|
||||
the ``deprecated`` documentation chapter)
|
||||
* Deprecate the `Twig\ExpressionParser`, and `Twig\OperatorPrecedenceChange` classes
|
||||
* Add attributes `AsTwigFilter`, `AsTwigFunction`, and `AsTwigTest` to ease extension development
|
||||
|
||||
# 3.20.0 (2025-02-13)
|
||||
|
||||
* Fix support for ignoring syntax errors in an undefined handler in guard
|
||||
* Add configuration for Commonmark
|
||||
* Fix wrong array index
|
||||
* Bump minimum PHP version to 8.1
|
||||
* Add support for registering callbacks for undefined functions, filters or token parsers in the IntegrationTestCase
|
||||
* Use correct line number for `ForElseNode`
|
||||
* Fix timezone conversion on strings
|
||||
|
||||
# 3.19.0 (2025-01-28)
|
||||
|
||||
* Fix a security issue where escaping was missing when using `??`
|
||||
* Deprecate `Token::getType()`, use `Token::test()` instead
|
||||
* Add `Token::toEnglish()`
|
||||
* Add `ForElseNode`
|
||||
* Deprecate `Twig\ExpressionParser::parseOnlyArguments()` and
|
||||
`Twig\ExpressionParser::parseArguments()` (use
|
||||
`Twig\ExpressionParser::parseNamedArguments()` instead)
|
||||
* Fix `constant()` behavior when used with `??`
|
||||
* Add the `invoke` filter
|
||||
* Make `{}` optional for the `types` tag
|
||||
* Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes
|
||||
* Ignore static properties when using the dot operator
|
||||
|
||||
# 3.18.0 (2024-12-29)
|
||||
|
||||
* Support for invoking closures
|
||||
* Fix unary operator precedence change
|
||||
* Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag
|
||||
* Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()`)
|
||||
|
||||
# 3.17.1 (2024-12-12)
|
||||
|
||||
* Fix the null coalescing operator when the test returns null
|
||||
* Fix the Elvis operator when used as '? :' instead of '?:'
|
||||
|
||||
# 3.17.0 (2024-12-10)
|
||||
|
||||
* Fix ArrayAccess with objects as keys
|
||||
* Support underscores in number literals
|
||||
* Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead)
|
||||
|
||||
# 3.16.0 (2024-11-29)
|
||||
|
||||
* 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
|
||||
@@ -44,6 +347,17 @@
|
||||
* 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`
|
||||
|
||||
+5
-4
@@ -24,15 +24,16 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.0.2",
|
||||
"php": ">=8.1.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "^1.3",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-php81": "^1.29"
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
|
||||
"psr/container": "^1.0|^2.0"
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"phpstan/phpstan": "^2.0@stable",
|
||||
"php-cs-fixer/shim": "^3.0@stable"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
||||
@@ -32,11 +32,38 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
'needs_environment' => false,
|
||||
'needs_context' => false,
|
||||
'needs_charset' => false,
|
||||
'needs_is_sandboxed' => false,
|
||||
'is_variadic' => false,
|
||||
'always_allowed_in_sandbox' => 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
|
||||
@@ -54,6 +81,9 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
return $this->dynamicName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable|array{class-string, string}|null
|
||||
*/
|
||||
public function getCallable()
|
||||
{
|
||||
return $this->callable;
|
||||
@@ -79,6 +109,19 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
return $this->options['needs_context'];
|
||||
}
|
||||
|
||||
public function needsIsSandboxed(): bool
|
||||
{
|
||||
return $this->options['needs_is_sandboxed'];
|
||||
}
|
||||
|
||||
public function isAlwaysAllowedInSandbox(): bool
|
||||
{
|
||||
return $this->options['always_allowed_in_sandbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self
|
||||
{
|
||||
$new = clone $this;
|
||||
@@ -111,26 +154,46 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
|
||||
public function isDeprecated(): bool
|
||||
{
|
||||
return (bool) $this->options['deprecated'];
|
||||
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);
|
||||
return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + ($this->options['needs_is_sandboxed'] ? 1 : 0) + \count($this->arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Attribute;
|
||||
|
||||
use Twig\DeprecatedCallableInfo;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* Registers a method as template filter.
|
||||
*
|
||||
* If the first argument of the method has Twig\Environment type-hint, the filter will receive the current environment.
|
||||
* Additional arguments of the method come from the filter call.
|
||||
*
|
||||
* #[AsTwigFilter(name: 'foo')]
|
||||
* function fooFilter(Environment $env, $string, $arg1 = null, ...) { ... }
|
||||
*
|
||||
* {{ 'string'|foo(arg1) }}
|
||||
*
|
||||
* @see TwigFilter
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
final class AsTwigFilter
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $name The name of the filter in Twig
|
||||
* @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument
|
||||
* @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset
|
||||
* @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment
|
||||
* @param bool|null $needsIsSandboxed Whether the filter needs the current sandbox state (a boolean) passed as the first argument, or after the charset, the environment, and the context
|
||||
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped
|
||||
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe
|
||||
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe
|
||||
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to
|
||||
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
|
||||
* @param bool|null $alwaysAllowedInSandbox Whether the filter is always allowed in sandbox mode, even when not explicitly allow-listed
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?bool $needsCharset = null,
|
||||
public ?bool $needsEnvironment = null,
|
||||
public ?bool $needsContext = null,
|
||||
public ?bool $needsIsSandboxed = null,
|
||||
public ?array $isSafe = null,
|
||||
public string|array|null $isSafeCallback = null,
|
||||
public ?string $preEscape = null,
|
||||
public ?array $preservesSafety = null,
|
||||
public ?DeprecatedCallableInfo $deprecationInfo = null,
|
||||
public ?bool $alwaysAllowedInSandbox = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Attribute;
|
||||
|
||||
use Twig\DeprecatedCallableInfo;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* Registers a method as template function.
|
||||
*
|
||||
* If the first argument of the method has Twig\Environment type-hint, the function will receive the current environment.
|
||||
* Additional arguments of the method come from the function call.
|
||||
*
|
||||
* #[AsTwigFunction(name: 'foo')]
|
||||
* function fooFunction(Environment $env, string $string, $arg1 = null, ...) { ... }
|
||||
*
|
||||
* {{ foo('string', arg1) }}
|
||||
*
|
||||
* @see TwigFunction
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
final class AsTwigFunction
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $name The name of the function in Twig
|
||||
* @param bool|null $needsCharset Whether the function needs the charset passed as the first argument
|
||||
* @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset
|
||||
* @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment
|
||||
* @param bool|null $needsIsSandboxed Whether the function needs the current sandbox state (a boolean) passed as the first argument, or after the charset, the environment, and the context
|
||||
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped
|
||||
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe
|
||||
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
|
||||
* @param bool|null $alwaysAllowedInSandbox Whether the function is always allowed in sandbox mode, even when not explicitly allow-listed
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?bool $needsCharset = null,
|
||||
public ?bool $needsEnvironment = null,
|
||||
public ?bool $needsContext = null,
|
||||
public ?bool $needsIsSandboxed = null,
|
||||
public ?array $isSafe = null,
|
||||
public string|array|null $isSafeCallback = null,
|
||||
public ?DeprecatedCallableInfo $deprecationInfo = null,
|
||||
public ?bool $alwaysAllowedInSandbox = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Attribute;
|
||||
|
||||
use Twig\DeprecatedCallableInfo;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Registers a method as template test.
|
||||
*
|
||||
* The first argument is the value to test and the other arguments are the
|
||||
* arguments passed to the test in the template.
|
||||
*
|
||||
* #[AsTwigTest(name: 'foo')]
|
||||
* public function fooTest($value, $arg1 = null) { ... }
|
||||
*
|
||||
* {% if value is foo(arg1) %}
|
||||
*
|
||||
* @see TwigTest
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
final class AsTwigTest
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $name The name of the test in Twig
|
||||
* @param bool|null $needsCharset Whether the test needs the charset passed as the first argument
|
||||
* @param bool|null $needsEnvironment Whether the test needs the environment passed as the first argument, or after the charset
|
||||
* @param bool|null $needsContext Whether the test needs the context array passed as the first argument, or after the charset and the environment
|
||||
* @param bool|null $needsIsSandboxed Whether the test needs the current sandbox state (a boolean) passed as the first argument, or after the charset, the environment, and the context
|
||||
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
|
||||
* @param bool|null $alwaysAllowedInSandbox Whether the test is always allowed in sandbox mode, even when not explicitly allow-listed
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?bool $needsCharset = null,
|
||||
public ?bool $needsEnvironment = null,
|
||||
public ?bool $needsContext = null,
|
||||
public ?bool $needsIsSandboxed = null,
|
||||
public ?DeprecatedCallableInfo $deprecationInfo = null,
|
||||
public ?bool $alwaysAllowedInSandbox = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\RuntimeError;
|
||||
|
||||
/**
|
||||
* Composes blocks from several templates without composing their bodies or macros.
|
||||
*/
|
||||
final class BlockChain
|
||||
{
|
||||
/** @var list<Template|self> */
|
||||
private array $templates = [];
|
||||
|
||||
/** @var list<Template|self> */
|
||||
private array $lineage = [];
|
||||
|
||||
/** @var array<string, array{Template, string}> */
|
||||
private array $blocks = [];
|
||||
|
||||
/**
|
||||
* Whether the lineage can no longer move, making every further resolution pointless.
|
||||
*/
|
||||
private bool $fixed = false;
|
||||
|
||||
/**
|
||||
* @param iterable<string|TemplateWrapper|self> $templates Templates and nested chains ordered from highest to lowest precedence
|
||||
* @param array<string, mixed> $context Default variables used to resolve dynamic parent expressions
|
||||
*/
|
||||
public function __construct(
|
||||
private Environment $env,
|
||||
iterable $templates,
|
||||
private array $context = [],
|
||||
) {
|
||||
foreach ($templates as $template) {
|
||||
if ($template instanceof self) {
|
||||
if ($env !== $template->env) {
|
||||
throw new \LogicException('A block chain cannot contain a block chain from a different Twig environment.');
|
||||
}
|
||||
|
||||
$this->templates[] = $template;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_string($template)) {
|
||||
$template = $env->load($template);
|
||||
}
|
||||
if (!$template instanceof TemplateWrapper) {
|
||||
throw new \TypeError(\sprintf('Block chain templates must be strings, "%s" or "%s" instances, "%s" given.', TemplateWrapper::class, self::class, get_debug_type($template)));
|
||||
}
|
||||
|
||||
if (!$template->isOwnedBy($env)) {
|
||||
throw new \LogicException('A block chain cannot contain templates from different Twig environments.');
|
||||
}
|
||||
|
||||
$this->templates[] = $template->unwrap($env);
|
||||
}
|
||||
|
||||
if (!$this->templates) {
|
||||
throw new \InvalidArgumentException('A block chain requires at least one template.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function hasBlock(string $name, array $context = []): bool
|
||||
{
|
||||
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context + $this->context + $this->env->getGlobals());
|
||||
|
||||
return isset($blocks[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getBlockNames(array $context = []): array
|
||||
{
|
||||
return array_keys($this->fixed ? $this->blocks : $this->resolveBlocks($context + $this->context + $this->env->getGlobals()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*
|
||||
* @return iterable<scalar|\Stringable|null>
|
||||
*/
|
||||
public function streamBlock(string $name, array $context = []): iterable
|
||||
{
|
||||
$context += $this->context + $this->env->getGlobals();
|
||||
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context);
|
||||
if (!isset($blocks[$name])) {
|
||||
$this->throwUnknownBlock($name);
|
||||
}
|
||||
|
||||
yield from $this->entryTemplate()->yieldBlock($name, $context, $blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function renderBlock(string $name, array $context = []): string
|
||||
{
|
||||
$context += $this->context + $this->env->getGlobals();
|
||||
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context);
|
||||
if (!isset($blocks[$name])) {
|
||||
$this->throwUnknownBlock($name);
|
||||
}
|
||||
|
||||
return $this->entryTemplate()->renderBlock($name, $context, $blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function displayBlock(string $name, array $context = []): void
|
||||
{
|
||||
$context += $this->context + $this->env->getGlobals();
|
||||
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context);
|
||||
if (!isset($blocks[$name])) {
|
||||
$this->throwUnknownBlock($name);
|
||||
}
|
||||
|
||||
$this->entryTemplate()->displayBlock($name, $context, $blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*
|
||||
* @return array<string, array{Template, string}>
|
||||
*/
|
||||
private function resolveBlocks(array $context): array
|
||||
{
|
||||
[$lineage, $fixed] = $this->resolveLineage($context);
|
||||
|
||||
$stale = $lineage !== $this->lineage;
|
||||
|
||||
foreach ($lineage as $template) {
|
||||
if (!$template instanceof self) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$template->fixed) {
|
||||
// a nested chain that can still move may resolve to other blocks on every call
|
||||
$template->resolveBlocks($context + $template->context);
|
||||
$stale = true;
|
||||
}
|
||||
|
||||
$fixed = $fixed && $template->fixed;
|
||||
}
|
||||
|
||||
$this->fixed = $fixed;
|
||||
|
||||
if (!$stale) {
|
||||
return $this->blocks;
|
||||
}
|
||||
|
||||
$blocks = [];
|
||||
foreach ($lineage as $template) {
|
||||
if ($template instanceof self) {
|
||||
$blocks += $template->blocks;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($template->getBlocks() as $name => $block) {
|
||||
$blocks[$name] ??= $block;
|
||||
}
|
||||
}
|
||||
|
||||
$this->lineage = $lineage;
|
||||
|
||||
return $this->blocks = $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*
|
||||
* @return array{list<Template|self>, bool}
|
||||
*/
|
||||
private function resolveLineage(array $context): array
|
||||
{
|
||||
$lineage = [];
|
||||
$fixed = true;
|
||||
|
||||
foreach ($this->templates as $template) {
|
||||
if ($template instanceof self) {
|
||||
$lineage[] = $template;
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen = [];
|
||||
do {
|
||||
if (isset($seen[$id = spl_object_id($template)])) {
|
||||
throw new \LogicException(\sprintf('Circular template inheritance detected while building a block chain from "%s".', $template->getTemplateName()));
|
||||
}
|
||||
$seen[$id] = true;
|
||||
$lineage[] = $template;
|
||||
|
||||
$parent = $template->getParent($context);
|
||||
$fixed = $fixed && $template->hasFixedParent();
|
||||
|
||||
$template = $parent;
|
||||
} while (false !== $template);
|
||||
}
|
||||
|
||||
return [$lineage, $fixed];
|
||||
}
|
||||
|
||||
private function entryTemplate(): Template
|
||||
{
|
||||
return ($template = $this->templates[0]) instanceof self ? $template->entryTemplate() : $template;
|
||||
}
|
||||
|
||||
private function throwUnknownBlock(string $name): never
|
||||
{
|
||||
throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->entryTemplate()->getTemplateName()), -1, $this->entryTemplate()->getSourceContext());
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -19,7 +19,7 @@ namespace Twig\Cache;
|
||||
*
|
||||
* @author Quentin Devos <quentin@devos.pm>
|
||||
*/
|
||||
final class ChainCache implements CacheInterface
|
||||
final class ChainCache implements CacheInterface, RemovableCacheInterface
|
||||
{
|
||||
/**
|
||||
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
|
||||
@@ -69,6 +69,15 @@ final class ChainCache implements CacheInterface
|
||||
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[]
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Twig\Cache;
|
||||
*
|
||||
* @author Andrew Tch <andrew@noop.lv>
|
||||
*/
|
||||
class FilesystemCache implements CacheInterface
|
||||
class FilesystemCache implements CacheInterface, RemovableCacheInterface
|
||||
{
|
||||
public const FORCE_BYTECODE_INVALIDATION = 1;
|
||||
|
||||
@@ -76,6 +76,14 @@ class FilesystemCache implements CacheInterface
|
||||
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
|
||||
{
|
||||
if (!is_file($key)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Twig\Cache;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Cache;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface RemovableCacheInterface
|
||||
{
|
||||
public function remove(string $name, string $cls): void;
|
||||
}
|
||||
+12
-6
@@ -74,7 +74,7 @@ class Compiler
|
||||
$node->compile($this);
|
||||
|
||||
if ($this->didUseEcho) {
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
|
||||
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, $node::class);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -99,7 +99,7 @@ class Compiler
|
||||
$node->compile($this);
|
||||
|
||||
if ($this->didUseEcho) {
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
|
||||
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, $node::class);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -143,7 +143,13 @@ class Compiler
|
||||
*/
|
||||
public function string(string $value)
|
||||
{
|
||||
$this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
|
||||
// Single quotes are encoded as \x27 (not \') as a defense-in-depth measure:
|
||||
// it guarantees that the compiled output never contains a literal "'" derived
|
||||
// from user input, which prevents breaking out of a surrounding single-quoted
|
||||
// PHP context if a caller mistakenly concatenates the result into one.
|
||||
// \' is not a recognized escape sequence in PHP double-quoted strings (the
|
||||
// backslash would be kept literally), so \x27 is used instead.
|
||||
$this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\")));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -170,7 +176,7 @@ class Compiler
|
||||
} elseif (\is_bool($value)) {
|
||||
$this->raw($value ? 'true' : 'false');
|
||||
} elseif (\is_array($value)) {
|
||||
$this->raw('array(');
|
||||
$this->raw('[');
|
||||
$first = true;
|
||||
foreach ($value as $key => $v) {
|
||||
if (!$first) {
|
||||
@@ -181,7 +187,7 @@ class Compiler
|
||||
$this->raw(' => ');
|
||||
$this->repr($v);
|
||||
}
|
||||
$this->raw(')');
|
||||
$this->raw(']');
|
||||
} else {
|
||||
$this->string($value);
|
||||
}
|
||||
@@ -243,7 +249,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
+120
-34
@@ -14,10 +14,12 @@ 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;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\ExpressionParsers;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\EscaperExtension;
|
||||
use Twig\Extension\ExtensionInterface;
|
||||
@@ -26,8 +28,6 @@ use Twig\Extension\YieldNotReadyExtension;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Loader\ChainLoader;
|
||||
use Twig\Loader\LoaderInterface;
|
||||
use Twig\Node\Expression\Binary\AbstractBinary;
|
||||
use Twig\Node\Expression\Unary\AbstractUnary;
|
||||
use Twig\Node\ModuleNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||
@@ -43,10 +43,10 @@ use Twig\TokenParser\TokenParserInterface;
|
||||
*/
|
||||
class Environment
|
||||
{
|
||||
public const VERSION = '3.14.0';
|
||||
public const VERSION_ID = 31400;
|
||||
public const VERSION = '3.30.0';
|
||||
public const VERSION_ID = 33000;
|
||||
public const MAJOR_VERSION = 3;
|
||||
public const MINOR_VERSION = 14;
|
||||
public const MINOR_VERSION = 30;
|
||||
public const RELEASE_VERSION = 0;
|
||||
public const EXTRA_VERSION = '';
|
||||
|
||||
@@ -71,6 +71,11 @@ class Environment
|
||||
/** @var bool */
|
||||
private $useYield;
|
||||
private $defaultRuntimeLoader;
|
||||
private array $hotCache = [];
|
||||
/**
|
||||
* @var array<string, TemplateWrapper>
|
||||
*/
|
||||
private array $loadedWrappers = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -143,6 +148,11 @@ class Environment
|
||||
$this->addExtension(new OptimizerExtension($options['optimizations']));
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.30', 'Cloning a "%s" instance is deprecated and will throw in Twig 4.0; build a new environment instead.', self::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -153,20 +163,24 @@ class Environment
|
||||
|
||||
/**
|
||||
* Enables debugging mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableDebug()
|
||||
{
|
||||
$this->debug = true;
|
||||
$this->updateOptionsHash();
|
||||
$this->optionsHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables debugging mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableDebug()
|
||||
{
|
||||
$this->debug = false;
|
||||
$this->updateOptionsHash();
|
||||
$this->optionsHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +195,8 @@ class Environment
|
||||
|
||||
/**
|
||||
* Enables the auto_reload option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableAutoReload()
|
||||
{
|
||||
@@ -189,6 +205,8 @@ class Environment
|
||||
|
||||
/**
|
||||
* Disables the auto_reload option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableAutoReload()
|
||||
{
|
||||
@@ -207,20 +225,24 @@ class Environment
|
||||
|
||||
/**
|
||||
* Enables the strict_variables option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableStrictVariables()
|
||||
{
|
||||
$this->strictVariables = true;
|
||||
$this->updateOptionsHash();
|
||||
$this->optionsHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the strict_variables option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableStrictVariables()
|
||||
{
|
||||
$this->strictVariables = false;
|
||||
$this->updateOptionsHash();
|
||||
$this->optionsHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,6 +255,19 @@ class Environment
|
||||
return $this->strictVariables;
|
||||
}
|
||||
|
||||
public function removeCache(string $name): void
|
||||
{
|
||||
$cls = $this->getTemplateClass($name);
|
||||
$this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16));
|
||||
unset($this->loadedWrappers[$cls]);
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -253,6 +288,8 @@ class Environment
|
||||
* @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation,
|
||||
* an absolute path to the compiled templates,
|
||||
* or false to disable cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCache($cache)
|
||||
{
|
||||
@@ -287,7 +324,7 @@ class Environment
|
||||
*/
|
||||
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 ??= $this->getOptionsHash());
|
||||
|
||||
return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
|
||||
}
|
||||
@@ -332,6 +369,8 @@ class Environment
|
||||
public function load($name): TemplateWrapper
|
||||
{
|
||||
if ($name instanceof TemplateWrapper) {
|
||||
$name->unwrap($this);
|
||||
|
||||
return $name;
|
||||
}
|
||||
if ($name instanceof Template) {
|
||||
@@ -340,7 +379,9 @@ class Environment
|
||||
return $name;
|
||||
}
|
||||
|
||||
return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
|
||||
$cls = $this->getTemplateClass($name);
|
||||
|
||||
return $this->loadedWrappers[$cls] ??= new TemplateWrapper($this, $this->loadTemplate($cls, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,8 +420,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,
|
||||
@@ -474,7 +517,7 @@ class Environment
|
||||
return new TemplateWrapper($this, $name);
|
||||
}
|
||||
if ($name instanceof TemplateWrapper) {
|
||||
return $name;
|
||||
return $this->load($name);
|
||||
}
|
||||
|
||||
if (1 !== $count && !$this->getLoader()->exists($name)) {
|
||||
@@ -487,6 +530,9 @@ class Environment
|
||||
throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setLexer(Lexer $lexer)
|
||||
{
|
||||
$this->lexer = $lexer;
|
||||
@@ -504,6 +550,9 @@ class Environment
|
||||
return $this->lexer->tokenize($source);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setParser(Parser $parser)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
@@ -523,6 +572,9 @@ class Environment
|
||||
return $this->parser->parse($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setCompiler(Compiler $compiler)
|
||||
{
|
||||
$this->compiler = $compiler;
|
||||
@@ -557,6 +609,9 @@ class Environment
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setLoader(LoaderInterface $loader)
|
||||
{
|
||||
$this->loader = $loader;
|
||||
@@ -567,6 +622,9 @@ class Environment
|
||||
return $this->loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setCharset(string $charset)
|
||||
{
|
||||
if ('UTF8' === $charset = strtoupper($charset ?: '')) {
|
||||
@@ -587,6 +645,9 @@ class Environment
|
||||
return $this->extensionSet->hasExtension($class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addRuntimeLoader(RuntimeLoaderInterface $loader)
|
||||
{
|
||||
$this->runtimeLoaders[] = $loader;
|
||||
@@ -634,19 +695,24 @@ class Environment
|
||||
throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addExtension(ExtensionInterface $extension)
|
||||
{
|
||||
$this->extensionSet->addExtension($extension);
|
||||
$this->updateOptionsHash();
|
||||
$this->optionsHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExtensionInterface[] $extensions An array of extensions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setExtensions(array $extensions)
|
||||
{
|
||||
$this->extensionSet->setExtensions($extensions);
|
||||
$this->updateOptionsHash();
|
||||
$this->optionsHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,6 +723,9 @@ class Environment
|
||||
return $this->extensionSet->getExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addTokenParser(TokenParserInterface $parser)
|
||||
{
|
||||
$this->extensionSet->addTokenParser($parser);
|
||||
@@ -680,11 +749,17 @@ class Environment
|
||||
return $this->extensionSet->getTokenParser($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TokenParserInterface|false) $callable
|
||||
*/
|
||||
public function registerUndefinedTokenParserCallback(callable $callable): void
|
||||
{
|
||||
$this->extensionSet->registerUndefinedTokenParserCallback($callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addNodeVisitor(NodeVisitorInterface $visitor)
|
||||
{
|
||||
$this->extensionSet->addNodeVisitor($visitor);
|
||||
@@ -700,6 +775,9 @@ class Environment
|
||||
return $this->extensionSet->getNodeVisitors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addFilter(TwigFilter $filter)
|
||||
{
|
||||
$this->extensionSet->addFilter($filter);
|
||||
@@ -713,6 +791,9 @@ class Environment
|
||||
return $this->extensionSet->getFilter($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TwigFilter|false) $callable
|
||||
*/
|
||||
public function registerUndefinedFilterCallback(callable $callable): void
|
||||
{
|
||||
$this->extensionSet->registerUndefinedFilterCallback($callable);
|
||||
@@ -734,6 +815,9 @@ class Environment
|
||||
return $this->extensionSet->getFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addTest(TwigTest $test)
|
||||
{
|
||||
$this->extensionSet->addTest($test);
|
||||
@@ -757,6 +841,17 @@ class Environment
|
||||
return $this->extensionSet->getTest($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TwigTest|false) $callable
|
||||
*/
|
||||
public function registerUndefinedTestCallback(callable $callable): void
|
||||
{
|
||||
$this->extensionSet->registerUndefinedTestCallback($callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addFunction(TwigFunction $function)
|
||||
{
|
||||
$this->extensionSet->addFunction($function);
|
||||
@@ -770,6 +865,9 @@ class Environment
|
||||
return $this->extensionSet->getFunction($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TwigFunction|false) $callable
|
||||
*/
|
||||
public function registerUndefinedFunctionCallback(callable $callable): void
|
||||
{
|
||||
$this->extensionSet->registerUndefinedFunctionCallback($callable);
|
||||
@@ -798,6 +896,8 @@ class Environment
|
||||
* but after, you can only update existing globals.
|
||||
*
|
||||
* @param mixed $value The global value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addGlobal(string $name, $value)
|
||||
{
|
||||
@@ -813,8 +913,6 @@ class Environment
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getGlobals(): array
|
||||
@@ -848,27 +946,15 @@ class Environment
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
|
||||
*/
|
||||
public function getUnaryOperators(): array
|
||||
public function getExpressionParsers(): ExpressionParsers
|
||||
{
|
||||
return $this->extensionSet->getUnaryOperators();
|
||||
return $this->extensionSet->getExpressionParsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
|
||||
*/
|
||||
public function getBinaryOperators(): array
|
||||
private function getOptionsHash(): string
|
||||
{
|
||||
return $this->extensionSet->getBinaryOperators();
|
||||
}
|
||||
|
||||
private function updateOptionsHash(): void
|
||||
{
|
||||
$this->optionsHash = implode(':', [
|
||||
return implode(':', [
|
||||
$this->extensionSet->getSignature(),
|
||||
\PHP_MAJOR_VERSION,
|
||||
\PHP_MINOR_VERSION,
|
||||
|
||||
+69
-88
@@ -29,44 +29,39 @@ use Twig\Template;
|
||||
* Whenever possible, you must set these information (original template name
|
||||
* and line number) yourself by passing them to the constructor. If some or all
|
||||
* these information are not available from where you throw the exception, then
|
||||
* this class will guess them automatically (when the line number is set to -1
|
||||
* and/or the name is set to null). As this is a costly operation, this
|
||||
* can be disabled by passing false for both the name and the line number
|
||||
* when creating a new instance of this class.
|
||||
* this class will guess them automatically.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Error extends \Exception
|
||||
{
|
||||
private $lineno;
|
||||
private $name;
|
||||
/** @var positive-int|null */
|
||||
private ?int $columnno;
|
||||
private $rawMessage;
|
||||
private $sourcePath;
|
||||
private $sourceCode;
|
||||
private ?Source $source;
|
||||
private string $phpFile;
|
||||
private int $phpLine;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* By default, automatic guessing is enabled.
|
||||
*
|
||||
* @param string $message The error message
|
||||
* @param int $lineno The template line where the error occurred
|
||||
* @param Source|null $source The source context where the error occurred
|
||||
* @param string $message The error message
|
||||
* @param int $lineno The template line where the error occurred
|
||||
* @param Source|null $source The source context where the error occurred
|
||||
* @param positive-int|null $columnno The template column 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, ?int $columnno = null)
|
||||
{
|
||||
parent::__construct('', 0, $previous);
|
||||
|
||||
if (null === $source) {
|
||||
$name = null;
|
||||
} else {
|
||||
$name = $source->getName();
|
||||
$this->sourceCode = $source->getCode();
|
||||
$this->sourcePath = $source->getPath();
|
||||
}
|
||||
|
||||
$this->phpFile = $this->getFile();
|
||||
$this->phpLine = $this->getLine();
|
||||
$this->lineno = $lineno;
|
||||
$this->name = $name;
|
||||
$this->columnno = $columnno;
|
||||
$this->source = $source;
|
||||
$this->rawMessage = $message;
|
||||
$this->updateRepr();
|
||||
}
|
||||
@@ -84,30 +79,45 @@ class Error extends \Exception
|
||||
public function setTemplateLine(int $lineno): void
|
||||
{
|
||||
$this->lineno = $lineno;
|
||||
$this->updateRepr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 1-based column where the error occurred, or null if unknown.
|
||||
*
|
||||
* @return positive-int|null
|
||||
*/
|
||||
public function getTemplateColumn(): ?int
|
||||
{
|
||||
return $this->columnno;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param positive-int|null $columnno
|
||||
*/
|
||||
public function setTemplateColumn(?int $columnno): void
|
||||
{
|
||||
$this->columnno = $columnno;
|
||||
$this->updateRepr();
|
||||
}
|
||||
|
||||
public function getSourceContext(): ?Source
|
||||
{
|
||||
return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function setSourceContext(?Source $source = null): void
|
||||
{
|
||||
if (null === $source) {
|
||||
$this->sourceCode = $this->name = $this->sourcePath = null;
|
||||
} else {
|
||||
$this->sourceCode = $source->getCode();
|
||||
$this->name = $source->getName();
|
||||
$this->sourcePath = $source->getPath();
|
||||
}
|
||||
|
||||
$this->source = $source;
|
||||
$this->updateRepr();
|
||||
}
|
||||
|
||||
public function guess(): void
|
||||
{
|
||||
if ($this->lineno > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->guessTemplateInfo();
|
||||
$this->updateRepr();
|
||||
}
|
||||
@@ -120,80 +130,52 @@ class Error extends \Exception
|
||||
|
||||
private function updateRepr(): void
|
||||
{
|
||||
$this->message = $this->rawMessage;
|
||||
|
||||
if ($this->sourcePath && $this->lineno > 0) {
|
||||
$this->file = $this->sourcePath;
|
||||
$this->line = $this->lineno;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dot = false;
|
||||
if (str_ends_with($this->message, '.')) {
|
||||
$this->message = substr($this->message, 0, -1);
|
||||
$dot = true;
|
||||
}
|
||||
|
||||
$questionMark = false;
|
||||
if (str_ends_with($this->message, '?')) {
|
||||
$this->message = substr($this->message, 0, -1);
|
||||
$questionMark = true;
|
||||
}
|
||||
|
||||
if ($this->name) {
|
||||
if (\is_string($this->name) || $this->name instanceof \Stringable) {
|
||||
$name = \sprintf('"%s"', $this->name);
|
||||
if ($this->source && $this->source->getPath()) {
|
||||
// we only update the file and the line together
|
||||
$this->file = $this->source->getPath();
|
||||
if ($this->lineno > 0) {
|
||||
$this->line = $this->lineno;
|
||||
} else {
|
||||
$name = json_encode($this->name);
|
||||
$this->line = -1;
|
||||
}
|
||||
$this->message .= \sprintf(' in %s', $name);
|
||||
}
|
||||
|
||||
if ($this->lineno && $this->lineno >= 0) {
|
||||
$this->message = $this->rawMessage;
|
||||
$last = substr($this->message, -1);
|
||||
if ($punctuation = '.' === $last || '?' === $last ? $last : '') {
|
||||
$this->message = substr($this->message, 0, -1);
|
||||
}
|
||||
if ($this->source && $this->source->getName()) {
|
||||
$this->message .= \sprintf(' in "%s"', $this->source->getName());
|
||||
}
|
||||
if ($this->lineno > 0) {
|
||||
$this->message .= \sprintf(' at line %d', $this->lineno);
|
||||
if (null !== $this->columnno) {
|
||||
$this->message .= \sprintf(' column %d', $this->columnno);
|
||||
}
|
||||
}
|
||||
|
||||
if ($dot) {
|
||||
$this->message .= '.';
|
||||
}
|
||||
|
||||
if ($questionMark) {
|
||||
$this->message .= '?';
|
||||
if ($punctuation) {
|
||||
$this->message .= $punctuation;
|
||||
}
|
||||
}
|
||||
|
||||
private function guessTemplateInfo(): void
|
||||
{
|
||||
$template = null;
|
||||
$templateClass = null;
|
||||
// $this->source is never null here (see guess() usage in Template)
|
||||
|
||||
$this->lineno = 0;
|
||||
$template = null;
|
||||
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT);
|
||||
foreach ($backtrace as $trace) {
|
||||
if (isset($trace['object']) && $trace['object'] instanceof Template) {
|
||||
$currentClass = \get_class($trace['object']);
|
||||
$isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass);
|
||||
if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
|
||||
$template = $trace['object'];
|
||||
$templateClass = \get_class($trace['object']);
|
||||
}
|
||||
if (isset($trace['object']) && $trace['object'] instanceof Template && $this->source->getName() === $trace['object']->getTemplateName()) {
|
||||
$template = $trace['object'];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update template name
|
||||
if (null !== $template && null === $this->name) {
|
||||
$this->name = $template->getTemplateName();
|
||||
}
|
||||
|
||||
// update template path if any
|
||||
if (null !== $template && null === $this->sourcePath) {
|
||||
$src = $template->getSourceContext();
|
||||
$this->sourceCode = $src->getCode();
|
||||
$this->sourcePath = $src->getPath();
|
||||
}
|
||||
|
||||
if (null === $template || $this->lineno > -1) {
|
||||
return;
|
||||
if (null === $template) {
|
||||
return; // Impossible to guess the info as the template was not found in the backtrace
|
||||
}
|
||||
|
||||
$r = new \ReflectionObject($template);
|
||||
@@ -206,8 +188,7 @@ class Error extends \Exception
|
||||
|
||||
while ($e = array_pop($exceptions)) {
|
||||
$traces = $e->getTrace();
|
||||
array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]);
|
||||
|
||||
array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]);
|
||||
while ($trace = array_shift($traces)) {
|
||||
if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
|
||||
continue;
|
||||
|
||||
+142
-626
@@ -12,25 +12,19 @@
|
||||
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\ExpressionParser\Infix\DotExpressionParser;
|
||||
use Twig\ExpressionParser\Infix\FilterExpressionParser;
|
||||
use Twig\ExpressionParser\Infix\SquareBracketExpressionParser;
|
||||
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\ConditionalExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\GetAttrExpression;
|
||||
use Twig\Node\Expression\MethodCallExpression;
|
||||
use Twig\Node\Expression\NameExpression;
|
||||
use Twig\Node\Expression\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\Node;
|
||||
use Twig\Node\Nodes;
|
||||
|
||||
/**
|
||||
* Parses expressions.
|
||||
@@ -41,400 +35,108 @@ use Twig\Node\Node;
|
||||
* @see https://en.wikipedia.org/wiki/Operator-precedence_parser
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
class ExpressionParser
|
||||
{
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public const OPERATOR_LEFT = 1;
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public const OPERATOR_RIGHT = 2;
|
||||
|
||||
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
|
||||
private $unaryOperators;
|
||||
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
|
||||
private $binaryOperators;
|
||||
private $readyNodes = [];
|
||||
|
||||
public function __construct(
|
||||
private Parser $parser,
|
||||
private Environment $env,
|
||||
) {
|
||||
$this->unaryOperators = $env->getUnaryOperators();
|
||||
$this->binaryOperators = $env->getBinaryOperators();
|
||||
trigger_deprecation('twig/twig', '3.21', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__);
|
||||
}
|
||||
|
||||
public function parseExpression($precedence = 0, $allowArrow = false)
|
||||
public function parseExpression($precedence = 0)
|
||||
{
|
||||
if ($allowArrow && $arrow = $this->parseArrow()) {
|
||||
return $arrow;
|
||||
if (\func_num_args() > 1) {
|
||||
trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__);
|
||||
}
|
||||
|
||||
$expr = $this->getPrimary();
|
||||
$token = $this->parser->getCurrentToken();
|
||||
while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
|
||||
$op = $this->binaryOperators[$token->getValue()];
|
||||
$this->parser->getStream()->next();
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__);
|
||||
|
||||
if ('is not' === $token->getValue()) {
|
||||
$expr = $this->parseNotTestExpression($expr);
|
||||
} elseif ('is' === $token->getValue()) {
|
||||
$expr = $this->parseTestExpression($expr);
|
||||
} 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);
|
||||
$class = $op['class'];
|
||||
$expr = new $class($expr, $expr1, $token->getLine());
|
||||
}
|
||||
|
||||
$token = $this->parser->getCurrentToken();
|
||||
}
|
||||
|
||||
if (0 === $precedence) {
|
||||
return $this->parseConditionalExpression($expr);
|
||||
}
|
||||
|
||||
return $expr;
|
||||
return $this->parser->parseExpression((int) $precedence);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrowFunctionExpression|null
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
private function parseArrow()
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
|
||||
// short array syntax (one argument, no parentheses)?
|
||||
if ($stream->look(1)->test(Token::ARROW_TYPE)) {
|
||||
$line = $stream->getCurrent()->getLine();
|
||||
$token = $stream->expect(Token::NAME_TYPE);
|
||||
$names = [new AssignNameExpression($token->getValue(), $token->getLine())];
|
||||
$stream->expect(Token::ARROW_TYPE);
|
||||
|
||||
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
|
||||
}
|
||||
|
||||
// first, determine if we are parsing an arrow function by finding => (long form)
|
||||
$i = 0;
|
||||
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
return null;
|
||||
}
|
||||
++$i;
|
||||
while (true) {
|
||||
// variable name
|
||||
++$i;
|
||||
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
|
||||
break;
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
|
||||
return null;
|
||||
}
|
||||
++$i;
|
||||
if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// yes, let's parse it properly
|
||||
$token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
|
||||
$line = $token->getLine();
|
||||
|
||||
$names = [];
|
||||
while (true) {
|
||||
$token = $stream->expect(Token::NAME_TYPE);
|
||||
$names[] = new AssignNameExpression($token->getValue(), $token->getLine());
|
||||
|
||||
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ')');
|
||||
$stream->expect(Token::ARROW_TYPE);
|
||||
|
||||
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
|
||||
}
|
||||
|
||||
private function getPrimary(): AbstractExpression
|
||||
{
|
||||
$token = $this->parser->getCurrentToken();
|
||||
|
||||
if ($this->isUnary($token)) {
|
||||
$operator = $this->unaryOperators[$token->getValue()];
|
||||
$this->parser->getStream()->next();
|
||||
$expr = $this->parseExpression($operator['precedence']);
|
||||
$class = $operator['class'];
|
||||
|
||||
return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
|
||||
} elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
$this->parser->getStream()->next();
|
||||
$expr = $this->parseExpression();
|
||||
$this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
|
||||
|
||||
return $this->parsePostfixExpression($expr);
|
||||
}
|
||||
|
||||
return $this->parsePrimaryExpression();
|
||||
}
|
||||
|
||||
private function parseConditionalExpression($expr): AbstractExpression
|
||||
{
|
||||
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, ':')) {
|
||||
// Ternary operator (expr ? expr2 : expr3)
|
||||
$expr3 = $this->parseExpression();
|
||||
} else {
|
||||
// Ternary without else (expr ? expr2)
|
||||
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
|
||||
}
|
||||
} else {
|
||||
// Ternary without then (expr ?: expr3)
|
||||
$expr2 = $expr;
|
||||
$expr3 = $this->parseExpression();
|
||||
}
|
||||
|
||||
$expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
|
||||
}
|
||||
|
||||
return $expr;
|
||||
}
|
||||
|
||||
private function isUnary(Token $token): bool
|
||||
{
|
||||
return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
|
||||
}
|
||||
|
||||
private function isBinary(Token $token): bool
|
||||
{
|
||||
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:
|
||||
$this->parser->getStream()->next();
|
||||
switch ($token->getValue()) {
|
||||
case 'true':
|
||||
case 'TRUE':
|
||||
$node = new ConstantExpression(true, $token->getLine());
|
||||
break;
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
case 'false':
|
||||
case 'FALSE':
|
||||
$node = new ConstantExpression(false, $token->getLine());
|
||||
break;
|
||||
|
||||
case 'none':
|
||||
case 'NONE':
|
||||
case 'null':
|
||||
case 'NULL':
|
||||
$node = new ConstantExpression(null, $token->getLine());
|
||||
break;
|
||||
|
||||
default:
|
||||
if ('(' === $this->parser->getCurrentToken()->getValue()) {
|
||||
$node = $this->getFunctionNode($token->getValue(), $token->getLine());
|
||||
} else {
|
||||
$node = new NameExpression($token->getValue(), $token->getLine());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Token::NUMBER_TYPE:
|
||||
$this->parser->getStream()->next();
|
||||
$node = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
break;
|
||||
|
||||
case Token::STRING_TYPE:
|
||||
case Token::INTERPOLATION_START_TYPE:
|
||||
$node = $this->parseStringExpression();
|
||||
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());
|
||||
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;
|
||||
}
|
||||
|
||||
// no break
|
||||
default:
|
||||
if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
|
||||
$node = $this->parseSequenceExpression();
|
||||
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
|
||||
$node = $this->parseMappingExpression();
|
||||
} elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
|
||||
throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
|
||||
} else {
|
||||
throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parsePostfixExpression($node);
|
||||
}
|
||||
|
||||
public function parseStringExpression()
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
|
||||
$nodes = [];
|
||||
// a string cannot be followed by another string in a single expression
|
||||
$nextCanBeString = true;
|
||||
while (true) {
|
||||
if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
|
||||
$nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
$nextCanBeString = false;
|
||||
} elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
|
||||
$nodes[] = $this->parseExpression();
|
||||
$stream->expect(Token::INTERPOLATION_END_TYPE);
|
||||
$nextCanBeString = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$expr = array_shift($nodes);
|
||||
foreach ($nodes as $node) {
|
||||
$expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
|
||||
}
|
||||
|
||||
return $expr;
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 3.11, use parseSequenceExpression() instead
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parseStringExpression()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.11, use parseExpression() instead
|
||||
*/
|
||||
public function parseArrayExpression()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__);
|
||||
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__);
|
||||
|
||||
return $this->parseSequenceExpression();
|
||||
}
|
||||
|
||||
public function parseSequenceExpression()
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected');
|
||||
|
||||
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
|
||||
$first = true;
|
||||
while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
|
||||
if (!$first) {
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
|
||||
|
||||
// trailing ,?
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($stream->test(Token::SPREAD_TYPE)) {
|
||||
$stream->next();
|
||||
$expr = $this->parseExpression();
|
||||
$expr->setAttribute('spread', true);
|
||||
$node->addElement($expr);
|
||||
} else {
|
||||
$node->addElement($this->parseExpression());
|
||||
}
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
|
||||
|
||||
return $node;
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 3.11, use parseMappingExpression() instead
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parseSequenceExpression()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.11, use parseExpression() instead
|
||||
*/
|
||||
public function parseHashExpression()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__);
|
||||
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__);
|
||||
|
||||
return $this->parseMappingExpression();
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parseMappingExpression()
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
|
||||
$first = true;
|
||||
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
|
||||
if (!$first) {
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
|
||||
|
||||
// trailing ,?
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($stream->test(Token::SPREAD_TYPE)) {
|
||||
$stream->next();
|
||||
$value = $this->parseExpression();
|
||||
$value->setAttribute('spread', true);
|
||||
$node->addElement($value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// a mapping key can be:
|
||||
//
|
||||
// * a number -- 12
|
||||
// * a string -- 'a'
|
||||
// * a name, which is equivalent to a string -- a
|
||||
// * an expression, which must be enclosed in parentheses -- (1 + 2)
|
||||
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
|
||||
$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());
|
||||
$node->addElement($value, $key);
|
||||
continue;
|
||||
}
|
||||
} 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, '(')) {
|
||||
$key = $this->parseExpression();
|
||||
} else {
|
||||
$current = $stream->getCurrent();
|
||||
|
||||
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, ':', 'A mapping key must be followed by a colon (:)');
|
||||
$value = $this->parseExpression();
|
||||
|
||||
$node->addElement($value, $key);
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
|
||||
|
||||
return $node;
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parsePostfixExpression($node)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
while (true) {
|
||||
$token = $this->parser->getCurrentToken();
|
||||
if (Token::PUNCTUATION_TYPE == $token->getType()) {
|
||||
if ($token->test(Token::PUNCTUATION_TYPE)) {
|
||||
if ('.' == $token->getValue() || '[' == $token->getValue()) {
|
||||
$node = $this->parseSubscriptExpression($node);
|
||||
} elseif ('|' == $token->getValue()) {
|
||||
@@ -450,154 +152,49 @@ class ExpressionParser
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function getFunctionNode($name, $line)
|
||||
{
|
||||
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);
|
||||
$function = $this->getFunction($name, $line);
|
||||
|
||||
if ($function->getParserCallable()) {
|
||||
$fakeNode = new Node(lineno: $line);
|
||||
$fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
|
||||
|
||||
return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
|
||||
}
|
||||
|
||||
if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
|
||||
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
|
||||
}
|
||||
|
||||
if (!$ready = $this->readyNodes[$class]) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
|
||||
}
|
||||
|
||||
return new $class($ready ? $function : $function->getName(), $args, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
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 == $token->getType()
|
||||
||
|
||||
Token::NUMBER_TYPE == $token->getType()
|
||||
||
|
||||
(Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
|
||||
) {
|
||||
$arg = new ConstantExpression($token->getValue(), $lineno);
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
$type = Template::METHOD_CALL;
|
||||
foreach ($this->parseArguments() as $n) {
|
||||
$arguments->addElement($n);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
|
||||
}
|
||||
$parsers = new \ReflectionProperty($this->parser, 'parsers');
|
||||
|
||||
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, ':')) {
|
||||
$slice = true;
|
||||
$arg = new ConstantExpression(0, $token->getLine());
|
||||
} else {
|
||||
$arg = $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 Node([$arg, $length]);
|
||||
$filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
|
||||
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']');
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']');
|
||||
if ('.' === $this->parser->getStream()->next()->getValue()) {
|
||||
return $parsers->getValue($this->parser)->getByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken());
|
||||
}
|
||||
|
||||
return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
|
||||
return $parsers->getValue($this->parser)->getByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parseFilterExpression($node)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
$this->parser->getStream()->next();
|
||||
|
||||
return $this->parseFilterExpressionRaw($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parseFilterExpressionRaw($node)
|
||||
{
|
||||
if (\func_num_args() > 1) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
|
||||
}
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
$parsers = new \ReflectionProperty($this->parser, 'parsers');
|
||||
|
||||
$op = $parsers->getValue($this->parser)->getByClass(FilterExpressionParser::class);
|
||||
while (true) {
|
||||
$token = $this->parser->getStream()->expect(Token::NAME_TYPE);
|
||||
|
||||
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
$arguments = new Node();
|
||||
} else {
|
||||
$arguments = $this->parseArguments(true, false, true);
|
||||
}
|
||||
|
||||
$filter = $this->getFilter($token->getValue(), $token->getLine());
|
||||
|
||||
$ready = true;
|
||||
if (!isset($this->readyNodes[$class = $filter->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 "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, '|')) {
|
||||
$node = $op->parse($this->parser, $node, $this->parser->getCurrentToken());
|
||||
if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->parser->getStream()->next();
|
||||
}
|
||||
|
||||
@@ -607,21 +204,35 @@ 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 (or macro) definition
|
||||
*
|
||||
* @return Node
|
||||
*
|
||||
* @throws SyntaxError
|
||||
*
|
||||
* @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead
|
||||
*/
|
||||
public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
|
||||
public function parseArguments()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__));
|
||||
|
||||
$parsePrimaryExpression = new \ReflectionMethod($this->parser, 'parsePrimaryExpression');
|
||||
|
||||
$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, '(', 'A list of arguments must begin with an opening parenthesis');
|
||||
$stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
|
||||
$hasSpread = false;
|
||||
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
|
||||
if (!empty($args)) {
|
||||
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
|
||||
@@ -632,26 +243,33 @@ class ExpressionParser
|
||||
|
||||
if ($definition) {
|
||||
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
|
||||
$value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
|
||||
$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, '=')) || ($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());
|
||||
if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) {
|
||||
if (!$value instanceof ContextVariable) {
|
||||
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
$name = $value->getAttribute('name');
|
||||
|
||||
if ($definition) {
|
||||
$value = $this->parsePrimaryExpression();
|
||||
$value = $parsePrimaryExpression->invoke($this->parser);
|
||||
|
||||
if (!$this->checkConstantExpression($value)) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,11 +290,16 @@ class ExpressionParser
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
|
||||
|
||||
return new Node($args);
|
||||
return new Nodes($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21, use "AbstractTokenParser::parseAssignmentExpression()" instead
|
||||
*/
|
||||
public function parseAssignmentExpression()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__);
|
||||
|
||||
$stream = $this->parser->getStream();
|
||||
$targets = [];
|
||||
while (true) {
|
||||
@@ -687,22 +310,23 @@ class ExpressionParser
|
||||
} else {
|
||||
$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, ',')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Node($targets);
|
||||
return new Nodes($targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.21
|
||||
*/
|
||||
public function parseMultitargetExpression()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
|
||||
|
||||
$targets = [];
|
||||
while (true) {
|
||||
$targets[] = $this->parseExpression();
|
||||
@@ -711,129 +335,11 @@ class ExpressionParser
|
||||
}
|
||||
}
|
||||
|
||||
return new Node($targets);
|
||||
}
|
||||
|
||||
private function parseNotTestExpression(Node $node): NotUnary
|
||||
{
|
||||
return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
|
||||
}
|
||||
|
||||
private function parseTestExpression(Node $node): TestExpression
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
$test = $this->getTest($node->getTemplateLine());
|
||||
|
||||
$arguments = null;
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
$arguments = $this->parseArguments(true);
|
||||
} elseif ($test->hasOneMandatoryArgument()) {
|
||||
$arguments = new Node([0 => $this->parsePrimaryExpression()]);
|
||||
}
|
||||
|
||||
if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
|
||||
$node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
|
||||
$node->setAttribute('safe', true);
|
||||
}
|
||||
|
||||
$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): TwigTest
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
$name = $stream->expect(Token::NAME_TYPE)->getValue();
|
||||
|
||||
if ($stream->test(Token::NAME_TYPE)) {
|
||||
// try 2-words tests
|
||||
$name = $name.' '.$this->parser->getCurrentToken()->getValue();
|
||||
|
||||
if ($test = $this->env->getTest($name)) {
|
||||
$stream->next();
|
||||
}
|
||||
} else {
|
||||
$test = $this->env->getTest($name);
|
||||
}
|
||||
|
||||
if (!$test) {
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getTests()));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($test->isDeprecated()) {
|
||||
$stream = $this->parser->getStream();
|
||||
$message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
|
||||
|
||||
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_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
|
||||
}
|
||||
|
||||
return $test;
|
||||
}
|
||||
|
||||
private function getFunction(string $name, int $line): TwigFunction
|
||||
{
|
||||
if (!$function = $this->env->getFunction($name)) {
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFunctions()));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($function->isDeprecated()) {
|
||||
$message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
|
||||
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_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
|
||||
}
|
||||
|
||||
return $function;
|
||||
}
|
||||
|
||||
private function getFilter(string $name, int $line): TwigFilter
|
||||
{
|
||||
if (!$filter = $this->env->getFilter($name)) {
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFilters()));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($filter->isDeprecated()) {
|
||||
$message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
|
||||
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_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
|
||||
}
|
||||
|
||||
return $filter;
|
||||
return new Nodes($targets);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -850,4 +356,14 @@ class ExpressionParser
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead
|
||||
*/
|
||||
public function parseOnlyArguments()
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__));
|
||||
|
||||
return $this->parseArguments();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
abstract class AbstractExpressionParser implements ExpressionParserInterface
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return \sprintf('%s(%s)', ExpressionParserType::getType($this)->value, $this->getName());
|
||||
}
|
||||
|
||||
public function getPrecedenceChange(): ?PrecedenceChange
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAliases(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [$this->getName(), ...$this->getAliases()];
|
||||
}
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
interface ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function getDescription(): string;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* @method list<string> getOperatorTokens() Returns the operator token strings that this expression parser handles.
|
||||
* These are the strings that should be recognized as operator tokens by the Lexer,
|
||||
* and used to look up the parser in the registry.
|
||||
* For most parsers, this returns the name and aliases. Parsers that don't handle
|
||||
* operator tokens (like LiteralExpressionParser) should return an empty array.
|
||||
* This method will be added to the interface in Twig 4.0.
|
||||
*/
|
||||
interface ExpressionParserInterface
|
||||
{
|
||||
public function __toString(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
public function getPrecedence(): int;
|
||||
|
||||
public function getPrecedenceChange(): ?PrecedenceChange;
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getAliases(): array;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
enum ExpressionParserType: string
|
||||
{
|
||||
case Prefix = 'prefix';
|
||||
case Infix = 'infix';
|
||||
|
||||
public static function getType(object $object): ExpressionParserType
|
||||
{
|
||||
if ($object instanceof PrefixExpressionParserInterface) {
|
||||
return self::Prefix;
|
||||
}
|
||||
if ($object instanceof InfixExpressionParserInterface) {
|
||||
return self::Infix;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(\sprintf('Unsupported expression parser type: %s', $object::class));
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* @template-implements \IteratorAggregate<ExpressionParserInterface>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ExpressionParsers implements \IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var array<class-string<ExpressionParserInterface>, array<string, ExpressionParserInterface>>
|
||||
*/
|
||||
private array $parsersByName = [];
|
||||
|
||||
/**
|
||||
* @var array<class-string<ExpressionParserInterface>, ExpressionParserInterface>
|
||||
*/
|
||||
private array $parsersByClass = [];
|
||||
|
||||
/**
|
||||
* @var \WeakMap<ExpressionParserInterface, array<ExpressionParserInterface>>|null
|
||||
*/
|
||||
private ?\WeakMap $precedenceChanges = null;
|
||||
|
||||
/**
|
||||
* @param array<ExpressionParserInterface> $parsers
|
||||
*/
|
||||
public function __construct(array $parsers = [])
|
||||
{
|
||||
$this->add($parsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<ExpressionParserInterface> $parsers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function add(array $parsers): static
|
||||
{
|
||||
foreach ($parsers as $parser) {
|
||||
if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence());
|
||||
// throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()));
|
||||
}
|
||||
$interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class;
|
||||
$this->parsersByClass[$parser::class] = $parser;
|
||||
foreach (self::getOperatorTokensFor($parser) as $token) {
|
||||
$this->parsersByName[$interface][$token] = $parser;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of ExpressionParserInterface
|
||||
*
|
||||
* @param class-string<T> $class
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function getByClass(string $class): ?ExpressionParserInterface
|
||||
{
|
||||
return $this->parsersByClass[$class] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of ExpressionParserInterface
|
||||
*
|
||||
* @param class-string<T> $interface
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function getByName(string $interface, string $name): ?ExpressionParserInterface
|
||||
{
|
||||
return $this->parsersByName[$interface][$name] ?? null;
|
||||
}
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
$seen = [];
|
||||
foreach ($this->parsersByName as $parsers) {
|
||||
foreach ($parsers as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($this->parsersByClass as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return \WeakMap<ExpressionParserInterface, array<ExpressionParserInterface>>
|
||||
*/
|
||||
public function getPrecedenceChanges(): \WeakMap
|
||||
{
|
||||
if (null === $this->precedenceChanges) {
|
||||
$this->precedenceChanges = new \WeakMap();
|
||||
foreach ($this as $ep) {
|
||||
if (!$ep->getPrecedenceChange()) {
|
||||
continue;
|
||||
}
|
||||
$min = min($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence());
|
||||
$max = max($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence());
|
||||
foreach ($this as $e) {
|
||||
if ($e->getPrecedence() > $min && $e->getPrecedence() < $max) {
|
||||
if (!isset($this->precedenceChanges[$e])) {
|
||||
$this->precedenceChanges[$e] = [];
|
||||
}
|
||||
$this->precedenceChanges[$e][] = $ep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->precedenceChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getOperatorTokensFor(ExpressionParserInterface $parser): array
|
||||
{
|
||||
if (method_exists($parser, 'getOperatorTokens')) {
|
||||
return $parser->getOperatorTokens();
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.24', 'Not implementing the "getOperatorTokens()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $parser::class, ExpressionParserInterface::class);
|
||||
|
||||
return [$parser->getName(), ...$parser->getAliases()];
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\SetBinary;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\Expression\Variable\LocalVariable;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
trait ArgumentsTrait
|
||||
{
|
||||
private function parseCallableArguments(Parser $parser, int $line, bool $parseOpenParenthesis = true, bool $preserveNames = false): ArrayExpression
|
||||
{
|
||||
$arguments = new ArrayExpression([], $line);
|
||||
foreach ($this->parseNamedArguments($parser, $parseOpenParenthesis) as $k => $n) {
|
||||
$arguments->addElement($n, \is_int($k) || !$preserveNames ? new LocalVariable($k, $line) : new ConstantExpression($k, $line));
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis = true): Nodes
|
||||
{
|
||||
$args = [];
|
||||
$stream = $parser->getStream();
|
||||
if ($parseOpenParenthesis) {
|
||||
$stream->expect(Token::OPERATOR_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;
|
||||
}
|
||||
}
|
||||
|
||||
$value = $parser->parseExpression();
|
||||
if ($value instanceof SpreadUnary) {
|
||||
$hasSpread = true;
|
||||
} elseif ($hasSpread) {
|
||||
throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
|
||||
$name = null;
|
||||
if ($value instanceof SetBinary) {
|
||||
$name = $value->getNode('left')->getAttribute('name');
|
||||
$value = $value->getNode('right');
|
||||
} elseif (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
|
||||
if (!$value instanceof ContextVariable) {
|
||||
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
$name = $value->getAttribute('name');
|
||||
$value = $parser->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);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrowFunctionExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
// As the expression of the arrow function is independent from the current precedence, we want a precedence of 0
|
||||
return new ArrowFunctionExpression($parser->parseExpression(), $expr, $token->getLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '=>';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Arrow function (x => expr)';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 250;
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\AbstractBinary;
|
||||
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
|
||||
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
|
||||
use Twig\Node\Expression\Binary\SetBinary;
|
||||
use Twig\Node\Expression\EmptyExpression;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AssignmentExpressionParser extends BinaryOperatorExpressionParser
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
) {
|
||||
parent::__construct(SetBinary::class, $name, 0, InfixAssociativity::Right);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractBinary
|
||||
*/
|
||||
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
|
||||
{
|
||||
if (!$left instanceof ContextVariable && !$left instanceof ArrayExpression) {
|
||||
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned.', $left::class), $token->getLine(), $parser->getStream()->getSourceContext());
|
||||
}
|
||||
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
|
||||
$right = match ($this->getName()) {
|
||||
'=' => $right,
|
||||
default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())),
|
||||
};
|
||||
|
||||
if ($left instanceof ArrayExpression) {
|
||||
$pairs = $left->getKeyValuePairs();
|
||||
$isSequence = $left->isSequence();
|
||||
if (!$pairs || ($isSequence && !array_filter($pairs, static fn (array $pair): bool => !$pair['value'] instanceof EmptyExpression))) {
|
||||
throw new SyntaxError('Cannot destructure to an empty list of variables.', $token->getLine(), $parser->getStream()->getSourceContext());
|
||||
}
|
||||
|
||||
foreach ($pairs as $i => $pair) {
|
||||
if ($pair['value'] instanceof ContextVariable && !$pair['value'] instanceof AssignContextVariable) {
|
||||
$left->setNode(2 * $i + 1, new AssignContextVariable($pair['value']->getAttribute('name'), $pair['value']->getTemplateLine()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($isSequence) {
|
||||
return new SequenceDestructuringSetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
return new ObjectDestructuringSetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
return new SetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Assignment operator';
|
||||
}
|
||||
}
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\Binary\AbstractBinary;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function __construct(
|
||||
/** @var class-string<AbstractBinary> */
|
||||
private string $nodeClass,
|
||||
private string $name,
|
||||
private int $precedence,
|
||||
private InfixAssociativity $associativity = InfixAssociativity::Left,
|
||||
private ?PrecedenceChange $precedenceChange = null,
|
||||
private ?string $description = null,
|
||||
private array $aliases = [],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractBinary
|
||||
*/
|
||||
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
|
||||
{
|
||||
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
|
||||
|
||||
return new ($this->nodeClass)($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return $this->associativity;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description ?? '';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return $this->precedence;
|
||||
}
|
||||
|
||||
public function getPrecedenceChange(): ?PrecedenceChange
|
||||
{
|
||||
return $this->precedenceChange;
|
||||
}
|
||||
|
||||
public function getAliases(): array
|
||||
{
|
||||
return $this->aliases;
|
||||
}
|
||||
}
|
||||
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\Ternary\ConditionalTernary;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
|
||||
{
|
||||
$then = $parser->parseExpression($this->getPrecedence());
|
||||
if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
|
||||
// Ternary operator (expr ? expr2 : expr3)
|
||||
$else = $parser->parseExpression($this->getPrecedence());
|
||||
} else {
|
||||
// Ternary without else (expr ? expr2)
|
||||
$else = new ConstantExpression('', $token->getLine());
|
||||
}
|
||||
|
||||
return new ConditionalTernary($left, $then, $else, $token->getLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '?';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Conditional operator (a ? b : c)';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\Lexer;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\GetAttrExpression;
|
||||
use Twig\Node\Expression\MacroReferenceExpression;
|
||||
use Twig\Node\Expression\NameExpression;
|
||||
use Twig\Node\Expression\Variable\MacroVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Template;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
use ArgumentsTrait;
|
||||
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
$nullSafe = '?.' === $token->getValue();
|
||||
$stream = $parser->getStream();
|
||||
$token = $stream->getCurrent();
|
||||
$lineno = $token->getLine();
|
||||
$arguments = new ArrayExpression([], $lineno);
|
||||
$type = Template::ANY_CALL;
|
||||
|
||||
if ($stream->nextIf(Token::OPERATOR_TYPE, '(')) {
|
||||
$attribute = $parser->parseExpression();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ')');
|
||||
} else {
|
||||
$token = $stream->next();
|
||||
if (
|
||||
$token->test(Token::NAME_TYPE)
|
||||
|| $token->test(Token::NUMBER_TYPE)
|
||||
|| ($token->test(Token::OPERATOR_TYPE) && 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->toEnglish()), $token->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
}
|
||||
|
||||
$isMacroTarget = $expr instanceof NameExpression
|
||||
&& (
|
||||
null !== $parser->getImportedSymbol('template', $expr->getAttribute('name'))
|
||||
|| '_self' === $expr->getAttribute('name')
|
||||
);
|
||||
|
||||
if ($stream->test(Token::OPERATOR_TYPE, '(')) {
|
||||
$type = Template::METHOD_CALL;
|
||||
$arguments = $this->parseCallableArguments($parser, $token->getLine(), preserveNames: $isMacroTarget);
|
||||
}
|
||||
|
||||
if ($isMacroTarget) {
|
||||
$name = $attribute instanceof ConstantExpression ? (string) $attribute->getAttribute('value') : $attribute;
|
||||
$node = new MacroReferenceExpression(new MacroVariable($expr->getAttribute('name'), $expr->getTemplateLine()), $name, $arguments, $expr->getTemplateLine());
|
||||
$node->setHasCallParentheses(Template::METHOD_CALL === $type);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '.';
|
||||
}
|
||||
|
||||
public function getAliases(): array
|
||||
{
|
||||
return ['?.'];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Get an attribute on a variable';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 512;
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
use ArgumentsTrait;
|
||||
|
||||
private $readyNodes = [];
|
||||
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
$token = $stream->expect(Token::NAME_TYPE);
|
||||
$line = $token->getLine();
|
||||
|
||||
if (!$stream->test(Token::OPERATOR_TYPE, '(')) {
|
||||
$arguments = new EmptyNode();
|
||||
} else {
|
||||
$arguments = $this->parseNamedArguments($parser);
|
||||
}
|
||||
|
||||
$filter = $parser->getFilter($token->getValue(), $line);
|
||||
|
||||
$ready = true;
|
||||
if (!isset($this->readyNodes[$class = $filter->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 "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
|
||||
}
|
||||
|
||||
return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '|';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Twig filter call';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 512;
|
||||
}
|
||||
|
||||
public function getPrecedenceChange(): ?PrecedenceChange
|
||||
{
|
||||
return new PrecedenceChange('twig/twig', '3.21', 300);
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\MacroReferenceExpression;
|
||||
use Twig\Node\Expression\NameExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
use ArgumentsTrait;
|
||||
|
||||
private $readyNodes = [];
|
||||
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
$line = $token->getLine();
|
||||
if (!$expr instanceof NameExpression) {
|
||||
throw new SyntaxError('Function name must be an identifier.', $line, $parser->getStream()->getSourceContext());
|
||||
}
|
||||
|
||||
$name = $expr->getAttribute('name');
|
||||
|
||||
// A bare call to a macro imported via "from" is syntactically a function call;
|
||||
// it is resolved through the "function" imported symbol registered by
|
||||
// FromTokenParser, which maps the local alias to the macro name and the
|
||||
// template it comes from.
|
||||
if (null !== $alias = $parser->getImportedSymbol('function', $name)) {
|
||||
$arguments = $this->parseCallableArguments($parser, $line, false, true);
|
||||
$node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $arguments, $line);
|
||||
$node->setHasCallParentheses(true);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
$args = $this->parseNamedArguments($parser, false);
|
||||
|
||||
$function = $parser->getFunction($name, $line);
|
||||
|
||||
if ($function->getParserCallable()) {
|
||||
$fakeNode = new EmptyNode($line);
|
||||
$fakeNode->setSourceContext($parser->getStream()->getSourceContext());
|
||||
|
||||
$node = ($function->getParserCallable())($parser, $fakeNode, $args, $line);
|
||||
// remember the original function name so the sandbox can enforce
|
||||
// the `allowedFunctions` allow-list even though the parser callable
|
||||
// returned a specialized node (e.g. `parent`, `block`, `attribute`).
|
||||
$node->setAttribute('sandboxed_function_name', $name);
|
||||
$node->setAttribute('sandboxed_function', $function);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
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 getName(): string
|
||||
{
|
||||
return '(';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Twig function call';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 512;
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\MacroReferenceExpression;
|
||||
use Twig\Node\Expression\NameExpression;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
use ArgumentsTrait;
|
||||
|
||||
private $readyNodes = [];
|
||||
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
$test = $parser->getTest($token->getLine());
|
||||
|
||||
$arguments = null;
|
||||
if ($stream->test(Token::OPERATOR_TYPE, '(')) {
|
||||
$arguments = $this->parseNamedArguments($parser);
|
||||
} elseif ($test->hasOneMandatoryArgument()) {
|
||||
$arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]);
|
||||
}
|
||||
|
||||
if ('defined' === $test->getName()) {
|
||||
if ($expr instanceof NameExpression && null !== $alias = $parser->getImportedSymbol('function', $expr->getAttribute('name'))) {
|
||||
$expr = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $expr->getTemplateLine()), $expr->getTemplateLine());
|
||||
$expr->setHasCallParentheses(false);
|
||||
}
|
||||
|
||||
if ($expr instanceof MacroReferenceExpression && $expr->hasCallParentheses()) {
|
||||
trigger_deprecation('twig/twig', '3.29', 'Using parentheses when testing a macro with the "defined" test is deprecated and will throw a SyntaxError in Twig 4.0; remove the parentheses after the macro name in "%s" at line %d.', $stream->getSourceContext()->getName(), $expr->getTemplateLine());
|
||||
}
|
||||
}
|
||||
|
||||
$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($expr, $ready ? $test : $test->getName(), $arguments, $stream->getCurrent()->getLine());
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'is';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Twig tests';
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\Unary\NotUnary;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class IsNotExpressionParser extends IsExpressionParser
|
||||
{
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
return new NotUnary(parent::parse($parser, $expr, $token), $token->getLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'is not';
|
||||
}
|
||||
}
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Infix;
|
||||
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\GetAttrExpression;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Parser;
|
||||
use Twig\Template;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
$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 = $parser->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 = $parser->parseExpression();
|
||||
}
|
||||
|
||||
$filter = $parser->getFilter('slice', $token->getLine());
|
||||
$arguments = new Nodes([$attribute, $length]);
|
||||
$filter = new ($filter->getNodeClass())($expr, $filter, $arguments, $token->getLine());
|
||||
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']');
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']');
|
||||
|
||||
return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '[';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Array access';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 512;
|
||||
}
|
||||
|
||||
public function getAssociativity(): InfixAssociativity
|
||||
{
|
||||
return InfixAssociativity::Left;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
enum InfixAssociativity
|
||||
{
|
||||
case Left;
|
||||
case Right;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
interface InfixExpressionParserInterface extends ExpressionParserInterface
|
||||
{
|
||||
/**
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression;
|
||||
|
||||
public function getAssociativity(): InfixAssociativity;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* Represents a precedence change.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class PrecedenceChange
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Prefix;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ListExpression;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
$expr = $parser->parseExpression($this->getPrecedence());
|
||||
|
||||
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) {
|
||||
if (!$stream->test(Token::OPERATOR_TYPE, '=>')) {
|
||||
return $expr->setExplicitParentheses();
|
||||
}
|
||||
|
||||
return new ListExpression([self::toAssignContextVariable($expr)], $token->getLine());
|
||||
}
|
||||
|
||||
// determine if we are parsing an arrow function arguments
|
||||
if (!$stream->test(Token::PUNCTUATION_TYPE, ',')) {
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
|
||||
}
|
||||
|
||||
$names = [$expr];
|
||||
while (true) {
|
||||
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) {
|
||||
break;
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ',');
|
||||
$token = $stream->expect(Token::NAME_TYPE);
|
||||
$names[] = new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
|
||||
if (!$stream->test(Token::OPERATOR_TYPE, '=>')) {
|
||||
throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
|
||||
return new ListExpression(array_map(self::toAssignContextVariable(...), $names), $token->getLine());
|
||||
}
|
||||
|
||||
private static function toAssignContextVariable(AbstractExpression $expr): AssignContextVariable
|
||||
{
|
||||
if (!$expr instanceof ContextVariable) {
|
||||
throw new SyntaxError('A list must only contain variables.', $expr->getTemplateLine(), $expr->getSourceContext());
|
||||
}
|
||||
|
||||
return $expr instanceof AssignContextVariable ? $expr : new AssignContextVariable($expr->getAttribute('name'), $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '(';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Explicit group expression (a)';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Prefix;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Lexer;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\ConcatBinary;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\EmptyExpression;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
switch (true) {
|
||||
case $token->test(Token::NAME_TYPE):
|
||||
$stream->next();
|
||||
switch ($token->getValue()) {
|
||||
case 'true':
|
||||
case 'TRUE':
|
||||
return new ConstantExpression(true, $token->getLine());
|
||||
|
||||
case 'false':
|
||||
case 'FALSE':
|
||||
return new ConstantExpression(false, $token->getLine());
|
||||
|
||||
case 'none':
|
||||
case 'NONE':
|
||||
case 'null':
|
||||
case 'NULL':
|
||||
return new ConstantExpression(null, $token->getLine());
|
||||
|
||||
default:
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
|
||||
// no break
|
||||
case $token->test(Token::NUMBER_TYPE):
|
||||
$stream->next();
|
||||
|
||||
return new ConstantExpression($token->getValue(), $token->getLine());
|
||||
|
||||
case $token->test(Token::STRING_TYPE):
|
||||
case $token->test(Token::INTERPOLATION_START_TYPE):
|
||||
return $this->parseStringExpression($parser);
|
||||
|
||||
case $token->test(Token::PUNCTUATION_TYPE):
|
||||
// In 4.0, we should always return the node or throw an error for default
|
||||
if ($node = match ($token->getValue()) {
|
||||
'{' => $this->parseMappingExpression($parser),
|
||||
default => null,
|
||||
}) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
// no break
|
||||
case $token->test(Token::OPERATOR_TYPE):
|
||||
if ('[' === $token->getValue()) {
|
||||
return $this->parseSequenceExpression($parser);
|
||||
}
|
||||
|
||||
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
|
||||
// in this context, string operators are variable names
|
||||
$stream->next();
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
|
||||
// no break
|
||||
default:
|
||||
throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'literal';
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'A literal value (boolean, string, number, sequence, mapping, ...)';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
// not used
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function parseStringExpression(Parser $parser)
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
|
||||
$nodes = [];
|
||||
// a string cannot be followed by another string in a single expression
|
||||
$nextCanBeString = true;
|
||||
while (true) {
|
||||
if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
|
||||
$nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
$nextCanBeString = false;
|
||||
} elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
|
||||
$nodes[] = $parser->parseExpression();
|
||||
$stream->expect(Token::INTERPOLATION_END_TYPE);
|
||||
$nextCanBeString = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$expr = array_shift($nodes);
|
||||
foreach ($nodes as $node) {
|
||||
$expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
|
||||
}
|
||||
|
||||
return $expr;
|
||||
}
|
||||
|
||||
private function parseSequenceExpression(Parser $parser)
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected');
|
||||
|
||||
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
|
||||
$first = true;
|
||||
while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
|
||||
if (!$first) {
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
|
||||
|
||||
// trailing ,?
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$first = false;
|
||||
|
||||
// Check for empty slots (comma with no expression)
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, ',')) {
|
||||
$node->addElement(new EmptyExpression($stream->getCurrent()->getLine()));
|
||||
} else {
|
||||
$node->addElement($parser->parseExpression());
|
||||
}
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function parseMappingExpression(Parser $parser)
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
|
||||
|
||||
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
|
||||
$first = true;
|
||||
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
|
||||
if (!$first) {
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
|
||||
|
||||
// trailing ,?
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($stream->test(Token::OPERATOR_TYPE, '...')) {
|
||||
$node->addElement($parser->parseExpression());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// a mapping key can be:
|
||||
//
|
||||
// * a number -- 12
|
||||
// * a string -- 'a'
|
||||
// * a name, which is equivalent to a string -- a
|
||||
// * an expression, which must be enclosed in parentheses -- (1 + 2)
|
||||
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
|
||||
$key = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
|
||||
// {a} is a shortcut for {a:a}
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
|
||||
$value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine());
|
||||
$node->addElement($value, $key);
|
||||
continue;
|
||||
}
|
||||
} elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
|
||||
$key = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
} elseif ($stream->test(Token::OPERATOR_TYPE, '(')) {
|
||||
$key = $parser->parseExpression();
|
||||
} else {
|
||||
$current = $stream->getCurrent();
|
||||
|
||||
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".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)');
|
||||
$value = $parser->parseExpression();
|
||||
|
||||
$node->addElement($value, $key);
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser\Prefix;
|
||||
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\Unary\AbstractUnary;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class UnaryOperatorExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function __construct(
|
||||
/** @var class-string<AbstractUnary> */
|
||||
private string $nodeClass,
|
||||
private string $name,
|
||||
private int $precedence,
|
||||
private ?PrecedenceChange $precedenceChange = null,
|
||||
private ?string $description = null,
|
||||
private array $aliases = [],
|
||||
private ?int $operandPrecedence = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractUnary
|
||||
*/
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
return new ($this->nodeClass)($parser->parseExpression($this->operandPrecedence ?? $this->precedence), $token->getLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description ?? '';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return $this->precedence;
|
||||
}
|
||||
|
||||
public function getPrecedenceChange(): ?PrecedenceChange
|
||||
{
|
||||
return $this->precedenceChange;
|
||||
}
|
||||
|
||||
public function getAliases(): array
|
||||
{
|
||||
return $this->aliases;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
interface PrefixExpressionParserInterface extends ExpressionParserInterface
|
||||
{
|
||||
/**
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression;
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace Twig\Extension;
|
||||
|
||||
abstract class AbstractExtension implements ExtensionInterface
|
||||
abstract class AbstractExtension implements LastModifiedExtensionInterface
|
||||
{
|
||||
public function getTokenParsers()
|
||||
{
|
||||
@@ -42,4 +42,26 @@ abstract class AbstractExtension implements ExtensionInterface
|
||||
{
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
public function getExpressionParsers(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLastModified(): int
|
||||
{
|
||||
$filename = (new \ReflectionClass($this))->getFileName();
|
||||
if (!is_file($filename)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$lastModified = filemtime($filename);
|
||||
|
||||
// Track modifications of the runtime class if it exists and follows the naming convention
|
||||
if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13).'Runtime.php')) {
|
||||
$lastModified = max($lastModified, filemtime($filename));
|
||||
}
|
||||
|
||||
return $lastModified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Extension;
|
||||
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
use Twig\Attribute\AsTwigFunction;
|
||||
use Twig\Attribute\AsTwigTest;
|
||||
use Twig\Environment;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Define Twig filters, functions, and tests with PHP attributes.
|
||||
*
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
final class AttributeExtension extends AbstractExtension
|
||||
{
|
||||
private array $filters;
|
||||
private array $functions;
|
||||
private array $tests;
|
||||
|
||||
/**
|
||||
* Use a runtime class using PHP attributes to define filters, functions, and tests.
|
||||
*
|
||||
* @param class-string $class
|
||||
*/
|
||||
public function __construct(private string $class)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
public function getClass(): string
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
public function getFilters(): array
|
||||
{
|
||||
if (!isset($this->filters)) {
|
||||
$this->initFromAttributes();
|
||||
}
|
||||
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
if (!isset($this->functions)) {
|
||||
$this->initFromAttributes();
|
||||
}
|
||||
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
public function getTests(): array
|
||||
{
|
||||
if (!isset($this->tests)) {
|
||||
$this->initFromAttributes();
|
||||
}
|
||||
|
||||
return $this->tests;
|
||||
}
|
||||
|
||||
public function getLastModified(): int
|
||||
{
|
||||
return max(
|
||||
filemtime(__FILE__),
|
||||
is_file($filename = (new \ReflectionClass($this->getClass()))->getFileName()) ? filemtime($filename) : 0,
|
||||
);
|
||||
}
|
||||
|
||||
private function initFromAttributes(): void
|
||||
{
|
||||
$filters = $functions = $tests = [];
|
||||
$reflectionClass = new \ReflectionClass($this->getClass());
|
||||
foreach ($reflectionClass->getMethods() as $method) {
|
||||
foreach ($method->getAttributes(AsTwigFilter::class) as $reflectionAttribute) {
|
||||
/** @var AsTwigFilter $attribute */
|
||||
$attribute = $reflectionAttribute->newInstance();
|
||||
|
||||
$callable = new TwigFilter($attribute->name, [$reflectionClass->name, $method->getName()], [
|
||||
'needs_context' => $attribute->needsContext ?? false,
|
||||
'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method),
|
||||
'needs_charset' => $attribute->needsCharset ?? false,
|
||||
'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false,
|
||||
'is_variadic' => $method->isVariadic(),
|
||||
'is_safe' => $attribute->isSafe,
|
||||
'is_safe_callback' => $attribute->isSafeCallback,
|
||||
'pre_escape' => $attribute->preEscape,
|
||||
'preserves_safety' => $attribute->preservesSafety,
|
||||
'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
|
||||
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
|
||||
}
|
||||
|
||||
$filters[$attribute->name] = $callable;
|
||||
}
|
||||
|
||||
foreach ($method->getAttributes(AsTwigFunction::class) as $reflectionAttribute) {
|
||||
/** @var AsTwigFunction $attribute */
|
||||
$attribute = $reflectionAttribute->newInstance();
|
||||
|
||||
$callable = new TwigFunction($attribute->name, [$reflectionClass->name, $method->getName()], [
|
||||
'needs_context' => $attribute->needsContext ?? false,
|
||||
'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method),
|
||||
'needs_charset' => $attribute->needsCharset ?? false,
|
||||
'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false,
|
||||
'is_variadic' => $method->isVariadic(),
|
||||
'is_safe' => $attribute->isSafe,
|
||||
'is_safe_callback' => $attribute->isSafeCallback,
|
||||
'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
|
||||
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
|
||||
}
|
||||
|
||||
$functions[$attribute->name] = $callable;
|
||||
}
|
||||
|
||||
foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) {
|
||||
/** @var AsTwigTest $attribute */
|
||||
$attribute = $reflectionAttribute->newInstance();
|
||||
|
||||
$callable = new TwigTest($attribute->name, [$reflectionClass->name, $method->getName()], [
|
||||
'needs_context' => $attribute->needsContext ?? false,
|
||||
'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method),
|
||||
'needs_charset' => $attribute->needsCharset ?? false,
|
||||
'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false,
|
||||
'is_variadic' => $method->isVariadic(),
|
||||
'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
|
||||
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
|
||||
}
|
||||
|
||||
$tests[$attribute->name] = $callable;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign all at the end to avoid inconsistent state in case of exception
|
||||
$this->filters = array_values($filters);
|
||||
$this->functions = array_values($functions);
|
||||
$this->tests = array_values($tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the first argument of the method is the environment.
|
||||
*/
|
||||
private function needsEnvironment(\ReflectionFunctionAbstract $function): bool
|
||||
{
|
||||
if (!$parameters = $function->getParameters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $parameters[0]->getType() instanceof \ReflectionNamedType
|
||||
&& Environment::class === $parameters[0]->getType()->getName()
|
||||
&& !$parameters[0]->isVariadic();
|
||||
}
|
||||
}
|
||||
+531
-210
File diff suppressed because it is too large.
Load diff
@@ -14,6 +14,7 @@ namespace Twig\Extension;
|
||||
use Twig\Environment;
|
||||
use Twig\FileExtensionEscapingStrategy;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\Filter\EscapeFilter;
|
||||
use Twig\Node\Expression\Filter\RawFilter;
|
||||
use Twig\Node\Node;
|
||||
use Twig\NodeVisitor\EscaperNodeVisitor;
|
||||
@@ -51,12 +52,20 @@ final class EscaperExtension extends AbstractExtension
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
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('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe'], 'node_class' => EscapeFilter::class]),
|
||||
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe'], 'node_class' => EscapeFilter::class]),
|
||||
new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getLastModified(): int
|
||||
{
|
||||
return max(
|
||||
parent::getLastModified(),
|
||||
filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.10
|
||||
*/
|
||||
@@ -68,13 +77,13 @@ final class EscaperExtension extends AbstractExtension
|
||||
}
|
||||
|
||||
$this->environment = $environment;
|
||||
$this->escaper = $environment->getRuntime(EscaperRuntime::class);
|
||||
$this->escaper = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.10
|
||||
*/
|
||||
public function setEscaperRuntime(EscaperRuntime $escaper)
|
||||
public function setEscaperRuntime(EscaperRuntime $escaper): void
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
|
||||
|
||||
@@ -124,20 +133,18 @@ final class EscaperExtension extends AbstractExtension
|
||||
*
|
||||
* @deprecated since Twig 3.10
|
||||
*/
|
||||
public function setEscaper($strategy, callable $callable)
|
||||
public function setEscaper($strategy, callable $callable): void
|
||||
{
|
||||
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__));
|
||||
}
|
||||
$escaper = $this->getEscaper(__METHOD__);
|
||||
|
||||
$this->escapers[$strategy] = $callable;
|
||||
$callable = function ($string, $charset) use ($callable) {
|
||||
return $callable($this->environment, $string, $charset);
|
||||
};
|
||||
|
||||
$this->escaper->setEscaper($strategy, $callable);
|
||||
$escaper->setEscaper($strategy, $callable);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,33 +164,40 @@ final class EscaperExtension extends AbstractExtension
|
||||
/**
|
||||
* @deprecated since Twig 3.10
|
||||
*/
|
||||
public function setSafeClasses(array $safeClasses = [])
|
||||
public function setSafeClasses(array $safeClasses = []): void
|
||||
{
|
||||
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);
|
||||
$this->getEscaper(__METHOD__)->setSafeClasses($safeClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.10
|
||||
*/
|
||||
public function addSafeClass(string $class, array $strategies)
|
||||
public function addSafeClass(string $class, array $strategies): void
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__);
|
||||
|
||||
if (!isset($this->escaper)) {
|
||||
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
|
||||
$this->getEscaper(__METHOD__)->addSafeClass($class, $strategies);
|
||||
}
|
||||
|
||||
private function getEscaper(string $fromMethod): EscaperRuntime
|
||||
{
|
||||
if (isset($this->escaper)) {
|
||||
return $this->escaper;
|
||||
}
|
||||
|
||||
$this->escaper->addSafeClass($class, $strategies);
|
||||
if (isset($this->environment)) {
|
||||
return $this->escaper = $this->environment->getRuntime(EscaperRuntime::class);
|
||||
}
|
||||
|
||||
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', $fromMethod));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function escapeFilterIsSafe(Node $filterArgs)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
namespace Twig\Extension;
|
||||
|
||||
use Twig\ExpressionParser;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\ExpressionParser\ExpressionParserInterface;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\Node\Expression\Binary\AbstractBinary;
|
||||
use Twig\Node\Expression\Unary\AbstractUnary;
|
||||
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||
use Twig\TokenParser\TokenParserInterface;
|
||||
use Twig\TwigFilter;
|
||||
@@ -23,6 +26,8 @@ use Twig\TwigTest;
|
||||
* Interface implemented by extension classes.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @method array<ExpressionParserInterface> getExpressionParsers()
|
||||
*/
|
||||
interface ExtensionInterface
|
||||
{
|
||||
@@ -64,11 +69,11 @@ interface ExtensionInterface
|
||||
/**
|
||||
* Returns a list of operators to add to the existing list.
|
||||
*
|
||||
* @return array<array> First array of unary operators, second array of binary operators
|
||||
* @return array<array>
|
||||
*
|
||||
* @psalm-return array{
|
||||
* array<string, array{precedence: int, class: class-string<AbstractExpression>}>,
|
||||
* array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
|
||||
* array<string, array{precedence: int, precedence_change?: PrecedenceChange, class: class-string<AbstractUnary>}>,
|
||||
* array<string, array{precedence: int, precedence_change?: PrecedenceChange, class?: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
|
||||
* }
|
||||
*/
|
||||
public function getOperators();
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Extension;
|
||||
|
||||
interface LastModifiedExtensionInterface extends ExtensionInterface
|
||||
{
|
||||
/**
|
||||
* Returns the last modification time of the extension for cache invalidation.
|
||||
*
|
||||
* This timestamp should be the last time the source code of the extension class
|
||||
* and all its dependencies were modified (including the Runtime class).
|
||||
*/
|
||||
public function getLastModified(): int;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Extension;
|
||||
|
||||
use Twig\Node\Expression\FunctionNode\RenderSandboxedFunction;
|
||||
use Twig\Runtime\SandboxBridgeRuntime;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* Exposes a sandbox to trusted templates.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class SandboxBridgeExtension extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('render_sandboxed', [SandboxBridgeRuntime::class, 'render'], ['is_safe_callback' => [RenderSandboxedFunction::class, 'getSafe'], 'node_class' => RenderSandboxedFunction::class]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getLastModified(): int
|
||||
{
|
||||
return max(
|
||||
parent::getLastModified(),
|
||||
filemtime((new \ReflectionClass(SandboxBridgeRuntime::class))->getFileName()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,25 +12,31 @@
|
||||
namespace Twig\Extension;
|
||||
|
||||
use Twig\NodeVisitor\SandboxNodeVisitor;
|
||||
use Twig\Sandbox\Sandbox;
|
||||
use Twig\Sandbox\SecurityChecker;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Sandbox\SourcePolicyInterface;
|
||||
use Twig\Source;
|
||||
use Twig\TokenParser\SandboxTokenParser;
|
||||
|
||||
/**
|
||||
* This extension is the wiring behind "Twig\Sandbox\Sandbox" and should not
|
||||
* be used directly, use a "Sandbox" to render untrusted templates instead.
|
||||
*
|
||||
* @internal since Twig 3.29
|
||||
*/
|
||||
final class SandboxExtension extends AbstractExtension
|
||||
{
|
||||
private $sandboxedGlobally;
|
||||
private $sandboxed;
|
||||
private $policy;
|
||||
private $sourcePolicy;
|
||||
private SecurityChecker $checker;
|
||||
|
||||
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null)
|
||||
{
|
||||
$this->policy = $policy;
|
||||
$this->sandboxedGlobally = $sandboxed;
|
||||
$this->sourcePolicy = $sourcePolicy;
|
||||
if (null !== $sourcePolicy) {
|
||||
trigger_deprecation('twig/twig', '3.27.0', 'The "%s" interface is deprecated with no replacement, do not pass an instance to "%s".', SourcePolicyInterface::class, self::class);
|
||||
}
|
||||
|
||||
$this->checker = new SecurityChecker($policy, (bool) $sandboxed, $sourcePolicy);
|
||||
}
|
||||
|
||||
public function getTokenParsers(): array
|
||||
@@ -43,93 +49,99 @@ final class SandboxExtension extends AbstractExtension
|
||||
return [new SandboxNodeVisitor()];
|
||||
}
|
||||
|
||||
public function enableSandbox(): void
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getChecker(): SecurityChecker
|
||||
{
|
||||
$this->sandboxed = true;
|
||||
return $this->checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.29, use "Twig\Sandbox\Sandbox" to render untrusted templates instead
|
||||
*/
|
||||
public function enableSandbox(): void
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.29', 'The "%s()" method is deprecated, use "%s" to render untrusted templates instead.', __METHOD__, Sandbox::class);
|
||||
|
||||
$this->checker->setSandboxed(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.29, use "Twig\Sandbox\Sandbox" to render untrusted templates instead
|
||||
*/
|
||||
public function disableSandbox(): void
|
||||
{
|
||||
$this->sandboxed = false;
|
||||
trigger_deprecation('twig/twig', '3.29', 'The "%s()" method is deprecated, use "%s" to render untrusted templates instead.', __METHOD__, Sandbox::class);
|
||||
|
||||
$this->checker->setSandboxed(false);
|
||||
}
|
||||
|
||||
public function isSandboxed(?Source $source = null): bool
|
||||
{
|
||||
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
|
||||
return $this->checker->isSandboxed($source);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.29, use "Twig\Sandbox\Sandbox" to render untrusted templates instead
|
||||
*/
|
||||
public function isSandboxedGlobally(): bool
|
||||
{
|
||||
return $this->sandboxedGlobally;
|
||||
trigger_deprecation('twig/twig', '3.29', 'The "%s()" method is deprecated, use "%s" to render untrusted templates instead.', __METHOD__, Sandbox::class);
|
||||
|
||||
return $this->checker->isSandboxedGlobally();
|
||||
}
|
||||
|
||||
private function isSourceSandboxed(?Source $source): bool
|
||||
public function setSecurityPolicy(SecurityPolicyInterface $policy): void
|
||||
{
|
||||
if (null === $source || null === $this->sourcePolicy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->sourcePolicy->enableSandbox($source);
|
||||
}
|
||||
|
||||
public function setSecurityPolicy(SecurityPolicyInterface $policy)
|
||||
{
|
||||
$this->policy = $policy;
|
||||
$this->checker->setSecurityPolicy($policy);
|
||||
}
|
||||
|
||||
public function getSecurityPolicy(): SecurityPolicyInterface
|
||||
{
|
||||
return $this->policy;
|
||||
return $this->checker->getSecurityPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void
|
||||
public function checkSecurity($tags, $filters, $functions, $tests = [], $source = null): void
|
||||
{
|
||||
if ($this->isSandboxed($source)) {
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
// BC: previous signature was checkSecurity($tags, $filters, $functions, ?Source $source = null);
|
||||
// detect a legacy call where the 4th positional argument was the Source.
|
||||
if ($tests instanceof Source || (null === $tests && \func_num_args() < 5)) {
|
||||
trigger_deprecation('twig/twig', '3.28', 'Passing a "Twig\Source" as the 4th argument of "%s()" is deprecated; pass an array of tests instead.', __METHOD__);
|
||||
$source = $tests;
|
||||
$tests = [];
|
||||
}
|
||||
|
||||
$this->checker->checkSecurity($tags, $filters, $functions, $tests, $source);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void
|
||||
{
|
||||
if ($this->isSandboxed($source)) {
|
||||
try {
|
||||
$this->policy->checkMethodAllowed($obj, $method);
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$e->setSourceContext($source);
|
||||
$e->setTemplateLine($lineno);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->checker->checkMethodAllowed($obj, $method, $lineno, $source);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void
|
||||
{
|
||||
if ($this->isSandboxed($source)) {
|
||||
try {
|
||||
$this->policy->checkPropertyAllowed($obj, $property);
|
||||
} catch (SecurityNotAllowedPropertyError $e) {
|
||||
$e->setSourceContext($source);
|
||||
$e->setTemplateLine($lineno);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->checker->checkPropertyAllowed($obj, $property, $lineno, $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
|
||||
{
|
||||
if ($this->isSandboxed($source) && $obj instanceof \Stringable) {
|
||||
try {
|
||||
$this->policy->checkMethodAllowed($obj, '__toString');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$e->setSourceContext($source);
|
||||
$e->setTemplateLine($lineno);
|
||||
return $this->checker->ensureToStringAllowed($obj, $lineno, $source);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $obj;
|
||||
/**
|
||||
* Materialises a spread operand and runs the policy on every element.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array
|
||||
{
|
||||
return $this->checker->ensureSpreadAllowed($obj, $lineno, $source);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ final class StringLoaderExtension extends AbstractExtension
|
||||
*
|
||||
* {{ include(template_from_string("Hello {{ name }}")) }}
|
||||
*
|
||||
* Never expose `template_from_string` to untrusted template
|
||||
* authors (like in a sandboxed environment). See the docs for more details.
|
||||
*
|
||||
* @param string|null $name An optional name of the template to be used in error messages
|
||||
*
|
||||
* @internal
|
||||
|
||||
+113
-37
@@ -12,15 +12,25 @@
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\ExpressionParser\ExpressionParsers;
|
||||
use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
|
||||
use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
|
||||
use Twig\Extension\AttributeExtension;
|
||||
use Twig\Extension\ExtensionInterface;
|
||||
use Twig\Extension\GlobalsInterface;
|
||||
use Twig\Extension\LastModifiedExtensionInterface;
|
||||
use Twig\Extension\StagingExtension;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\Binary\AbstractBinary;
|
||||
use Twig\Node\Expression\Unary\AbstractUnary;
|
||||
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||
use Twig\TokenParser\TokenParserInterface;
|
||||
|
||||
// Help opcache.preload discover always-needed symbols
|
||||
// @see https://github.com/php/php-src/issues/10131
|
||||
class_exists(BinaryOperatorExpressionParser::class);
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
@@ -46,14 +56,16 @@ final class ExtensionSet
|
||||
private $functions;
|
||||
/** @var array<string, TwigFunction> */
|
||||
private $dynamicFunctions;
|
||||
/** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
|
||||
private $unaryOperators;
|
||||
/** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
|
||||
private $binaryOperators;
|
||||
/** @var array<string, mixed> */
|
||||
private ExpressionParsers $expressionParsers;
|
||||
/** @var array<string, mixed>|null */
|
||||
private $globals;
|
||||
/** @var array<callable(string): (TwigFunction|false)> */
|
||||
private $functionCallbacks = [];
|
||||
/** @var array<callable(string): (TwigFilter|false)> */
|
||||
private $filterCallbacks = [];
|
||||
/** @var array<callable(string): (TwigTest|false)> */
|
||||
private $testCallbacks = [];
|
||||
/** @var array<callable(string): (TokenParserInterface|false)> */
|
||||
private $parserCallbacks = [];
|
||||
private $lastModified = 0;
|
||||
|
||||
@@ -62,7 +74,7 @@ final class ExtensionSet
|
||||
$this->staging = new StagingExtension();
|
||||
}
|
||||
|
||||
public function initRuntime()
|
||||
public function initRuntime(): void
|
||||
{
|
||||
$this->runtimeInitialized = true;
|
||||
}
|
||||
@@ -117,19 +129,28 @@ final class ExtensionSet
|
||||
return $this->lastModified;
|
||||
}
|
||||
|
||||
$lastModified = 0;
|
||||
foreach ($this->extensions as $extension) {
|
||||
$r = new \ReflectionObject($extension);
|
||||
if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) {
|
||||
$this->lastModified = $extensionTime;
|
||||
if ($extension instanceof LastModifiedExtensionInterface) {
|
||||
$lastModified = max($extension->getLastModified(), $lastModified);
|
||||
} else {
|
||||
$r = new \ReflectionObject($extension);
|
||||
if (is_file($r->getFileName())) {
|
||||
$lastModified = max(filemtime($r->getFileName()), $lastModified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lastModified;
|
||||
return $this->lastModified = $lastModified;
|
||||
}
|
||||
|
||||
public function addExtension(ExtensionInterface $extension): void
|
||||
{
|
||||
$class = \get_class($extension);
|
||||
if ($extension instanceof AttributeExtension) {
|
||||
$class = $extension->getClass();
|
||||
} else {
|
||||
$class = $extension::class;
|
||||
}
|
||||
|
||||
if ($this->initialized) {
|
||||
throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
|
||||
@@ -190,6 +211,9 @@ final class ExtensionSet
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TwigFunction|false) $callable
|
||||
*/
|
||||
public function registerUndefinedFunctionCallback(callable $callable): void
|
||||
{
|
||||
$this->functionCallbacks[] = $callable;
|
||||
@@ -243,6 +267,9 @@ final class ExtensionSet
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TwigFilter|false) $callable
|
||||
*/
|
||||
public function registerUndefinedFilterCallback(callable $callable): void
|
||||
{
|
||||
$this->filterCallbacks[] = $callable;
|
||||
@@ -309,6 +336,9 @@ final class ExtensionSet
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string): (TokenParserInterface|false) $callable
|
||||
*/
|
||||
public function registerUndefinedTokenParserCallback(callable $callable): void
|
||||
{
|
||||
$this->parserCallbacks[] = $callable;
|
||||
@@ -383,31 +413,30 @@ final class ExtensionSet
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->testCallbacks as $callback) {
|
||||
if (false !== $test = $callback($name)) {
|
||||
return $test;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
|
||||
* @param callable(string): (TwigTest|false) $callable
|
||||
*/
|
||||
public function getUnaryOperators(): array
|
||||
public function registerUndefinedTestCallback(callable $callable): void
|
||||
{
|
||||
if (!$this->initialized) {
|
||||
$this->initExtensions();
|
||||
}
|
||||
|
||||
return $this->unaryOperators;
|
||||
$this->testCallbacks[] = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
|
||||
*/
|
||||
public function getBinaryOperators(): array
|
||||
public function getExpressionParsers(): ExpressionParsers
|
||||
{
|
||||
if (!$this->initialized) {
|
||||
$this->initExtensions();
|
||||
}
|
||||
|
||||
return $this->binaryOperators;
|
||||
return $this->expressionParsers;
|
||||
}
|
||||
|
||||
private function initExtensions(): void
|
||||
@@ -420,8 +449,7 @@ final class ExtensionSet
|
||||
$this->dynamicFunctions = [];
|
||||
$this->dynamicTests = [];
|
||||
$this->visitors = [];
|
||||
$this->unaryOperators = [];
|
||||
$this->binaryOperators = [];
|
||||
$this->expressionParsers = new ExpressionParsers();
|
||||
|
||||
foreach ($this->extensions as $extension) {
|
||||
$this->initExtension($extension);
|
||||
@@ -471,18 +499,66 @@ final class ExtensionSet
|
||||
$this->visitors[] = $visitor;
|
||||
}
|
||||
|
||||
// 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)));
|
||||
}
|
||||
// expression parsers
|
||||
if (method_exists($extension, 'getExpressionParsers')) {
|
||||
$this->expressionParsers->add($extension->getExpressionParsers());
|
||||
}
|
||||
|
||||
if (2 !== \count($operators)) {
|
||||
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
|
||||
}
|
||||
$operators = $extension->getOperators();
|
||||
if (!\is_array($operators)) {
|
||||
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', $extension::class, get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators)));
|
||||
}
|
||||
|
||||
$this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
|
||||
$this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
|
||||
if (2 !== \count($operators)) {
|
||||
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', $extension::class, \count($operators)));
|
||||
}
|
||||
|
||||
$expressionParsers = [];
|
||||
foreach ($operators[0] as $operator => $op) {
|
||||
$expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
|
||||
}
|
||||
foreach ($operators[1] as $operator => $op) {
|
||||
$op['associativity'] = match ($op['associativity']) {
|
||||
1 => InfixAssociativity::Left,
|
||||
2 => InfixAssociativity::Right,
|
||||
default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)),
|
||||
};
|
||||
|
||||
if (isset($op['callable'])) {
|
||||
$expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']);
|
||||
} else {
|
||||
$expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($expressionParsers)) {
|
||||
trigger_deprecation('twig/twig', '3.21', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class));
|
||||
|
||||
$this->expressionParsers->add($expressionParsers);
|
||||
}
|
||||
}
|
||||
|
||||
private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.21', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator));
|
||||
|
||||
return new class($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases, $callable) extends BinaryOperatorExpressionParser {
|
||||
public function __construct(
|
||||
string $nodeClass,
|
||||
string $operator,
|
||||
int $precedence,
|
||||
InfixAssociativity $associativity = InfixAssociativity::Left,
|
||||
?PrecedenceChange $precedenceChange = null,
|
||||
array $aliases = [],
|
||||
private $callable = null,
|
||||
) {
|
||||
parent::__construct($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases);
|
||||
}
|
||||
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
return ($this->callable)($parser, $expr);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class FileExtensionEscapingStrategy
|
||||
*/
|
||||
public static function guess(string $name)
|
||||
{
|
||||
if (\in_array(substr($name, -1), ['/', '\\'])) {
|
||||
if (\in_array(substr($name, -1), ['/', '\\'], true)) {
|
||||
return 'html'; // return html for directories
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class FileExtensionEscapingStrategy
|
||||
|
||||
switch ($extension) {
|
||||
case 'js':
|
||||
case 'json':
|
||||
return 'js';
|
||||
|
||||
case 'css':
|
||||
|
||||
+176
-81
@@ -13,6 +13,7 @@
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\ExpressionParsers;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
@@ -36,6 +37,10 @@ class Lexer
|
||||
private $position;
|
||||
private $positions;
|
||||
private $currentVarBlockLine;
|
||||
/** @var list<string> */
|
||||
private array $documentation = [];
|
||||
private array $openingBrackets = ['{', '(', '['];
|
||||
private array $closingBrackets = ['}', ')', ']'];
|
||||
|
||||
public const STATE_DATA = 0;
|
||||
public const STATE_BLOCK = 1;
|
||||
@@ -44,12 +49,23 @@ class Lexer
|
||||
public const STATE_INTERPOLATION = 4;
|
||||
|
||||
public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A';
|
||||
public const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A';
|
||||
public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
|
||||
|
||||
public const REGEX_NUMBER = '/(?(DEFINE)
|
||||
(?<LNUM>[0-9]+(_[0-9]+)*) # Integers (with underscores) 123_456
|
||||
(?<FRAC>\.(?&LNUM)) # Fractional part .456
|
||||
(?<EXPONENT>[eE][+-]?(?&LNUM)) # Exponent part E+10
|
||||
(?<DNUM>(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456
|
||||
)(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10
|
||||
/Ax';
|
||||
|
||||
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 REGEX_RAW_INLINE_COMMENT = '/#[^\r\n]*/A';
|
||||
|
||||
private const SPECIAL_CHARS = [
|
||||
'f' => "\f",
|
||||
'n' => "\n",
|
||||
@@ -73,12 +89,16 @@ class Lexer
|
||||
], $options);
|
||||
}
|
||||
|
||||
private function initialize()
|
||||
private function initialize(): void
|
||||
{
|
||||
if ($this->isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when the comment closing tag starts with "#" (as "#}" does), the empty comment ("{##}")
|
||||
// starts like a documentation comment ("{##"); make sure it keeps lexing as a comment
|
||||
$emptyCommentLookahead = str_starts_with($this->options['tag_comment'][1], '#') ? '(?!'.preg_quote(substr($this->options['tag_comment'][1], 1), '#').')' : '';
|
||||
|
||||
// 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 = [
|
||||
// }}
|
||||
@@ -101,7 +121,7 @@ class Lexer
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
|
||||
'|'.
|
||||
preg_quote($this->options['tag_block'][1], '#').'\n?'. // %}\n?
|
||||
preg_quote($this->options['tag_block'][1], '#').'(?:\r\n?|\n)?'. // %}(?:\r\n?|\n)?
|
||||
')
|
||||
}Ax',
|
||||
|
||||
@@ -131,7 +151,22 @@ class Lexer
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]*
|
||||
'|'.
|
||||
preg_quote($this->options['tag_comment'][1], '#').'\n?'. // #}\n?
|
||||
preg_quote($this->options['tag_comment'][1], '#').'(?:\r\n?|\n)?'. // #}(?:\r\n?|\n)?
|
||||
')
|
||||
}sx',
|
||||
|
||||
// #} or ##}
|
||||
'lex_documentation_comment' => '{
|
||||
(?:'.
|
||||
preg_quote($this->options['whitespace_trim'].'#'.$this->options['tag_comment'][1], '#').'\s*\n?'. // -##}\s*\n?
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_line_trim'].'#'.$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~##}[ \t\0\x0B]*
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_trim'].$this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n?
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]*
|
||||
'|'.
|
||||
preg_quote($this->options['tag_comment'][1], '#').'(?:\r\n?|\n)?'. // #}(?:\r\n?|\n)?
|
||||
')
|
||||
}sx',
|
||||
|
||||
@@ -156,6 +191,8 @@ class Lexer
|
||||
'|'.
|
||||
preg_quote($this->options['tag_block'][0], '#'). // {%
|
||||
'|'.
|
||||
preg_quote($this->options['tag_comment'][0].'#', '#').$emptyCommentLookahead. // {## (but not the empty comment {##})
|
||||
'|'.
|
||||
preg_quote($this->options['tag_comment'][0], '#'). // {#
|
||||
')('.
|
||||
preg_quote($this->options['whitespace_trim'], '#'). // -
|
||||
@@ -175,7 +212,7 @@ class Lexer
|
||||
$this->initialize();
|
||||
|
||||
$this->source = $source;
|
||||
$this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
|
||||
$this->code = $source->getCode();
|
||||
$this->cursor = 0;
|
||||
$this->lineno = 1;
|
||||
$this->end = \strlen($this->code);
|
||||
@@ -184,6 +221,7 @@ class Lexer
|
||||
$this->states = [];
|
||||
$this->brackets = [];
|
||||
$this->position = -1;
|
||||
$this->documentation = [];
|
||||
|
||||
// find all token starts in one go
|
||||
preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, \PREG_OFFSET_CAPTURE);
|
||||
@@ -217,7 +255,7 @@ class Lexer
|
||||
|
||||
$this->pushToken(Token::EOF_TYPE);
|
||||
|
||||
if (!empty($this->brackets)) {
|
||||
if ($this->brackets) {
|
||||
[$expect, $lineno] = array_pop($this->brackets);
|
||||
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
|
||||
}
|
||||
@@ -229,8 +267,9 @@ 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, substr($this->code, $this->cursor));
|
||||
$this->cursor = $this->end;
|
||||
$text = substr($this->code, $this->cursor);
|
||||
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text));
|
||||
$this->moveCursor($text);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -258,32 +297,47 @@ class Lexer
|
||||
$text = rtrim($text, " \t\0\x0B");
|
||||
}
|
||||
}
|
||||
$this->pushToken(Token::TEXT_TYPE, $text);
|
||||
$this->moveCursor($textContent.$position[0]);
|
||||
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text));
|
||||
$this->moveCursor($textContent);
|
||||
|
||||
switch ($this->positions[1][$this->position][0]) {
|
||||
case $this->options['tag_comment'][0].'#':
|
||||
$this->moveCursor($position[0]);
|
||||
$this->lexComment(true);
|
||||
break;
|
||||
|
||||
case $this->options['tag_comment'][0]:
|
||||
$this->moveCursor($position[0]);
|
||||
$this->lexComment();
|
||||
break;
|
||||
|
||||
case $this->options['tag_block'][0]:
|
||||
$lineno = $this->lineno;
|
||||
$cursor = $this->cursor;
|
||||
$this->moveCursor($position[0]);
|
||||
|
||||
// raw data?
|
||||
if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->documentation = [];
|
||||
$this->moveCursor($match[0]);
|
||||
$this->lexRawData();
|
||||
// {% line \d+ %}
|
||||
} elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->documentation = [];
|
||||
$this->moveCursor($match[0]);
|
||||
$this->lineno = (int) $match[1];
|
||||
} else {
|
||||
$this->pushToken(Token::BLOCK_START_TYPE);
|
||||
$this->pushToken(Token::BLOCK_START_TYPE, '', $cursor, $lineno);
|
||||
$this->pushState(self::STATE_BLOCK);
|
||||
$this->currentVarBlockLine = $this->lineno;
|
||||
}
|
||||
break;
|
||||
|
||||
case $this->options['tag_variable'][0]:
|
||||
$this->pushToken(Token::VAR_START_TYPE);
|
||||
$lineno = $this->lineno;
|
||||
$cursor = $this->cursor;
|
||||
$this->moveCursor($position[0]);
|
||||
$this->pushToken(Token::VAR_START_TYPE, '', $cursor, $lineno);
|
||||
$this->pushState(self::STATE_VAR);
|
||||
$this->currentVarBlockLine = $this->lineno;
|
||||
break;
|
||||
@@ -292,9 +346,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);
|
||||
$this->moveCursor($match[0]);
|
||||
if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->pushClosingToken(Token::BLOCK_END_TYPE, $match[0]);
|
||||
$this->popState();
|
||||
} else {
|
||||
$this->lexExpression();
|
||||
@@ -303,9 +356,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);
|
||||
$this->moveCursor($match[0]);
|
||||
if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->pushClosingToken(Token::VAR_END_TYPE, $match[0]);
|
||||
$this->popState();
|
||||
} else {
|
||||
$this->lexExpression();
|
||||
@@ -323,19 +375,13 @@ class Lexer
|
||||
}
|
||||
}
|
||||
|
||||
// spread operator
|
||||
if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
|
||||
$this->pushToken(Token::SPREAD_TYPE, '...');
|
||||
$this->moveCursor('...');
|
||||
}
|
||||
// arrow function
|
||||
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, preg_replace('/\s+/', ' ', $match[0]));
|
||||
if (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
|
||||
$operator = preg_replace('/\s+/', ' ', $match[0]);
|
||||
if (\in_array($operator, $this->openingBrackets, true)) {
|
||||
$this->checkBrackets($operator);
|
||||
}
|
||||
$this->pushToken(Token::OPERATOR_TYPE, $operator);
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// names
|
||||
@@ -345,37 +391,18 @@ class Lexer
|
||||
}
|
||||
// numbers
|
||||
elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
|
||||
$number = (float) $match[0]; // floats
|
||||
if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
|
||||
$number = (int) $match[0]; // integers lower than the maximum
|
||||
}
|
||||
$this->pushToken(Token::NUMBER_TYPE, $number);
|
||||
$this->pushToken(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0]));
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// punctuation
|
||||
elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
|
||||
// opening bracket
|
||||
if (str_contains('([{', $this->code[$this->cursor])) {
|
||||
$this->brackets[] = [$this->code[$this->cursor], $this->lineno];
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
[$expect, $lineno] = array_pop($this->brackets);
|
||||
if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
|
||||
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkBrackets($this->code[$this->cursor]);
|
||||
$this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]);
|
||||
++$this->cursor;
|
||||
$this->moveCursor($this->code[$this->cursor]);
|
||||
}
|
||||
// strings
|
||||
elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1)));
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines(substr($match[0], 1, -1)), substr($match[0], 0, 1)));
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// opening double quoted string
|
||||
@@ -384,9 +411,16 @@ class Lexer
|
||||
$this->pushState(self::STATE_STRING);
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// inline comment
|
||||
elseif (preg_match(self::REGEX_RAW_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
|
||||
if (str_starts_with($match[0], '##')) {
|
||||
$this->addDocumentation(substr($match[0], 2));
|
||||
}
|
||||
$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, columnno: $this->source->getColumn($this->cursor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +452,7 @@ class Lexer
|
||||
$result .= $nextChar;
|
||||
} elseif ("'" === $nextChar || '"' === $nextChar) {
|
||||
if ($nextChar !== $quoteType) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
|
||||
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]) {
|
||||
@@ -435,9 +469,9 @@ class Lexer
|
||||
while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) {
|
||||
$octal .= $str[++$i];
|
||||
}
|
||||
$result .= \chr(octdec($octal));
|
||||
$result .= \chr(octdec($octal) % 256);
|
||||
} else {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -453,6 +487,7 @@ class Lexer
|
||||
throw new SyntaxError('Unexpected end of file: Unclosed "verbatim" block.', $this->lineno, $this->source);
|
||||
}
|
||||
|
||||
$offset = $this->cursor;
|
||||
$text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
|
||||
$this->moveCursor($text.$match[0][0]);
|
||||
|
||||
@@ -468,16 +503,27 @@ class Lexer
|
||||
}
|
||||
}
|
||||
|
||||
$this->pushToken(Token::TEXT_TYPE, $text);
|
||||
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text), $offset);
|
||||
}
|
||||
|
||||
private function lexComment(): void
|
||||
private function lexComment(bool $isDocumentation = false): void
|
||||
{
|
||||
if (!preg_match($this->regexes['lex_comment'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
|
||||
$regex = $isDocumentation ? 'lex_documentation_comment' : 'lex_comment';
|
||||
if (!preg_match($this->regexes[$regex], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
|
||||
throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source);
|
||||
}
|
||||
|
||||
$this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]);
|
||||
$comment = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
|
||||
if ($isDocumentation) {
|
||||
$documentation = $comment;
|
||||
// support a symmetric "##}" closing marker
|
||||
if (str_ends_with($documentation, '#')) {
|
||||
$documentation = substr($documentation, 0, -1);
|
||||
}
|
||||
$this->addDocumentation($documentation);
|
||||
}
|
||||
|
||||
$this->moveCursor($comment.$match[0][0]);
|
||||
}
|
||||
|
||||
private function lexString(): void
|
||||
@@ -488,7 +534,7 @@ class Lexer
|
||||
$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, $this->stripcslashes($match[0], '"'));
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines($match[0]), '"'));
|
||||
$this->moveCursor($match[0]);
|
||||
} elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
|
||||
[$expect, $lineno] = array_pop($this->brackets);
|
||||
@@ -497,10 +543,10 @@ class Lexer
|
||||
}
|
||||
|
||||
$this->popState();
|
||||
++$this->cursor;
|
||||
$this->moveCursor($match[0]);
|
||||
} 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, columnno: $this->source->getColumn($this->cursor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,53 +555,84 @@ 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);
|
||||
$this->moveCursor($match[0]);
|
||||
$this->pushClosingToken(Token::INTERPOLATION_END_TYPE, $match[0]);
|
||||
$this->popState();
|
||||
} else {
|
||||
$this->lexExpression();
|
||||
}
|
||||
}
|
||||
|
||||
private function pushToken($type, $value = ''): void
|
||||
private function pushToken($type, $value = '', ?int $offset = null, ?int $lineno = null): void
|
||||
{
|
||||
// do not push empty text tokens
|
||||
if (Token::TEXT_TYPE === $type && '' === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tokens[] = new Token($type, $value, $this->lineno);
|
||||
// by default the token starts at the current cursor; callers that
|
||||
// emit a token after consuming it must pass an explicit offset
|
||||
$documentation = null;
|
||||
if ($this->documentation && (Token::TEXT_TYPE !== $type || '' !== trim($value))) {
|
||||
$documentation = implode("\n", $this->documentation);
|
||||
$this->documentation = [];
|
||||
}
|
||||
|
||||
$this->tokens[] = new Token($type, $value, $lineno ?? $this->lineno, $offset ?? $this->cursor, $documentation);
|
||||
}
|
||||
|
||||
private function addDocumentation(string $documentation): void
|
||||
{
|
||||
if ('' !== $documentation = trim($this->normalizeNewlines($documentation))) {
|
||||
$this->documentation[] = $documentation;
|
||||
}
|
||||
}
|
||||
|
||||
private function moveCursor($text): void
|
||||
{
|
||||
$this->cursor += \strlen($text);
|
||||
$this->lineno += substr_count($text, "\n");
|
||||
// count "\r\n" and "\r" as a single newline without allocating a
|
||||
// normalized copy when the chunk has no carriage return (common case)
|
||||
$this->lineno += str_contains($text, "\r") ? substr_count($this->normalizeNewlines($text), "\n") : substr_count($text, "\n");
|
||||
}
|
||||
|
||||
private function normalizeNewlines(string $text): string
|
||||
{
|
||||
return str_replace(["\r\n", "\r"], "\n", $text);
|
||||
}
|
||||
|
||||
private function pushClosingToken(int $type, string $match): void
|
||||
{
|
||||
$leadingWhitespaceLength = \strlen($match) - \strlen(ltrim($match));
|
||||
if ($leadingWhitespaceLength) {
|
||||
$this->moveCursor(substr($match, 0, $leadingWhitespaceLength));
|
||||
}
|
||||
|
||||
$this->pushToken($type);
|
||||
$this->moveCursor(substr($match, $leadingWhitespaceLength));
|
||||
}
|
||||
|
||||
private function getOperatorRegex(): string
|
||||
{
|
||||
$operators = array_merge(
|
||||
['='],
|
||||
array_keys($this->env->getUnaryOperators()),
|
||||
array_keys($this->env->getBinaryOperators())
|
||||
);
|
||||
$expressionParsers = [];
|
||||
foreach ($this->env->getExpressionParsers() as $expressionParser) {
|
||||
$expressionParsers = array_merge($expressionParsers, ExpressionParsers::getOperatorTokensFor($expressionParser));
|
||||
}
|
||||
|
||||
$operators = array_combine($operators, array_map('strlen', $operators));
|
||||
arsort($operators);
|
||||
$expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers));
|
||||
arsort($expressionParsers);
|
||||
|
||||
$regex = [];
|
||||
foreach ($operators as $operator => $length) {
|
||||
foreach ($expressionParsers as $expressionParser => $length) {
|
||||
// an operator that ends with a character must be followed by
|
||||
// a whitespace, a parenthesis, an opening map [ or sequence {
|
||||
$r = preg_quote($operator, '/');
|
||||
if (ctype_alpha($operator[$length - 1])) {
|
||||
$r = preg_quote($expressionParser, '/');
|
||||
if (ctype_alpha($expressionParser[$length - 1])) {
|
||||
$r .= '(?=[\s()\[{])';
|
||||
}
|
||||
|
||||
// an operator that begins with a character must not have a dot or pipe before
|
||||
if (ctype_alpha($operator[0])) {
|
||||
$r = '(?<![\.\|])'.$r;
|
||||
if (ctype_alpha($expressionParser[0])) {
|
||||
$r = '(?<![\.\|]\s|.[\.\|])'.$r;
|
||||
}
|
||||
|
||||
// an operator with a space can be any amount of whitespaces
|
||||
@@ -581,4 +658,22 @@ class Lexer
|
||||
|
||||
$this->state = array_pop($this->states);
|
||||
}
|
||||
|
||||
private function checkBrackets(string $code): void
|
||||
{
|
||||
// opening bracket
|
||||
if (\in_array($code, $this->openingBrackets, true)) {
|
||||
$this->brackets[] = [$code, $this->lineno];
|
||||
} elseif (\in_array($code, $this->closingBrackets, true)) {
|
||||
// closing bracket
|
||||
if (!$this->brackets) {
|
||||
throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
|
||||
}
|
||||
|
||||
[$expect, $lineno] = array_pop($this->brackets);
|
||||
if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) {
|
||||
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ final class ChainLoader implements LoaderInterface
|
||||
try {
|
||||
return $loader->getCacheKey($name);
|
||||
} catch (LoaderError $e) {
|
||||
$exceptions[] = \get_class($loader).': '.$e->getMessage();
|
||||
$exceptions[] = $loader::class.': '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ final class ChainLoader implements LoaderInterface
|
||||
try {
|
||||
return $loader->isFresh($name, $time);
|
||||
} catch (LoaderError $e) {
|
||||
$exceptions[] = \get_class($loader).': '.$e->getMessage();
|
||||
$exceptions[] = $loader::class.': '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ class FilesystemLoader implements LoaderInterface
|
||||
/** Identifier of the main namespace. */
|
||||
public const MAIN_NAMESPACE = '__main__';
|
||||
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
protected $paths = [];
|
||||
protected $cache = [];
|
||||
protected $errorCache = [];
|
||||
@@ -31,8 +34,8 @@ class FilesystemLoader implements LoaderInterface
|
||||
private $rootPath;
|
||||
|
||||
/**
|
||||
* @param string|array $paths A path or an array of paths where to look for templates
|
||||
* @param string|null $rootPath The root path common to all relative paths (null for getcwd())
|
||||
* @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)
|
||||
{
|
||||
@@ -48,6 +51,8 @@ class FilesystemLoader implements LoaderInterface
|
||||
|
||||
/**
|
||||
* Returns the paths to the templates.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Node\Expression\MacroReferenceExpression;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MacroNamespace
|
||||
{
|
||||
/**
|
||||
* @param array<string, TwigMacro> $macros
|
||||
*/
|
||||
public function __construct(
|
||||
private Template $template,
|
||||
private array $macros,
|
||||
) {
|
||||
}
|
||||
|
||||
public function has(string $name, array $context): bool
|
||||
{
|
||||
if (null !== $declaration = $this->findDeclaredName($name, $context)) {
|
||||
[$declaredName, $templateName] = $declaration;
|
||||
if ($declaredName !== $name) {
|
||||
trigger_deprecation('twig/twig', '3.29', 'Testing whether the macro "%s" (defined in template "%s") is defined as "%s" is deprecated; macro names will be case-sensitive in Twig 4.0 and this test will return false.', $declaredName, $templateName, $name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!str_starts_with($name, 'macro_') || null === $this->findDeclaredName($bareName = substr($name, \strlen('macro_')), $context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.29', 'Testing whether the macro "%s" is defined via the "macro_"-prefixed name "%s" is deprecated; pass the bare macro name to "%s" instead.', $bareName, $name, MacroReferenceExpression::class);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $arguments
|
||||
*/
|
||||
public function call(string $name, array $arguments, array $context, int $line, Source $source): string|Markup
|
||||
{
|
||||
if (null !== $macro = $this->getDeclared($name)) {
|
||||
return $macro->callLegacy($arguments, $source, $line);
|
||||
}
|
||||
|
||||
if (null === $macro = $this->resolve($name, $context)) {
|
||||
if (!str_starts_with($name, 'macro_') || null === $macro = $this->resolve($bareName = substr($name, \strlen('macro_')), $context)) {
|
||||
throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', $name, $this->template->getTemplateName()), $line, $source);
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.29', 'Calling the macro "%s" via the "macro_"-prefixed name "%s" is deprecated; pass the bare macro name to "%s" instead.', $bareName, $name, MacroReferenceExpression::class);
|
||||
}
|
||||
|
||||
return $macro->callLegacy($arguments, $source, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string}|null
|
||||
*/
|
||||
private function findDeclaredName(string $name, array $context): ?array
|
||||
{
|
||||
$namespace = $this;
|
||||
while (true) {
|
||||
if (isset($namespace->macros[$name])) {
|
||||
return [$name, $namespace->template->getTemplateName()];
|
||||
}
|
||||
foreach ($namespace->macros as $declaredName => $macro) {
|
||||
if (0 === strcasecmp($declaredName, $name)) {
|
||||
return [$declaredName, $namespace->template->getTemplateName()];
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $namespace = $namespace->getParent($context)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getDeclared(string $name): ?TwigMacro
|
||||
{
|
||||
if (isset($this->macros[$name])) {
|
||||
$this->template->ensureSecurityChecked();
|
||||
|
||||
return $this->macros[$name];
|
||||
}
|
||||
|
||||
foreach ($this->macros as $declaredName => $macro) {
|
||||
if (0 === strcasecmp($declaredName, $name)) {
|
||||
trigger_deprecation('twig/twig', '3.29', 'Calling the macro "%s" (defined in template "%s") as "%s" is deprecated; macro names will be case-sensitive in Twig 4.0.', $declaredName, $this->template->getTemplateName(), $name);
|
||||
|
||||
$this->template->ensureSecurityChecked();
|
||||
|
||||
return $macro;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolve(string $name, array $context): ?TwigMacro
|
||||
{
|
||||
$namespace = $this;
|
||||
while (true) {
|
||||
if (null !== $macro = $namespace->getDeclared($name)) {
|
||||
return $macro;
|
||||
}
|
||||
|
||||
if (null === $namespace = $namespace->getParent($context)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getParent(array $context): ?self
|
||||
{
|
||||
if (!$parent = $this->template->getParent($context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $parent->unwrap()->getMacroNamespace();
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -14,6 +14,16 @@ namespace Twig;
|
||||
/**
|
||||
* Marks a content as safe.
|
||||
*
|
||||
* Instances of this class (and existing subclasses) are trusted by the Twig
|
||||
* sandbox: method calls and property accesses on a Markup instance bypass the
|
||||
* SecurityPolicy method/property allowlists. This is by design: Markup
|
||||
* represents content that has already been deemed safe to output.
|
||||
*
|
||||
* This class is considered final as of Twig 3.28 and will be final in Twig
|
||||
* 4.0.
|
||||
*
|
||||
* @final since Twig 3.28
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Markup implements \Countable, \JsonSerializable, \Stringable
|
||||
@@ -27,11 +37,16 @@ class Markup implements \Countable, \JsonSerializable, \Stringable
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getCharset(): string
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,7 @@ class BlockNode extends Node
|
||||
|
||||
$compiler
|
||||
->subcompile($this->getNode('body'))
|
||||
->write("yield from [];\n")
|
||||
->write("return; yield;\n")
|
||||
->outdent()
|
||||
->write("}\n\n")
|
||||
;
|
||||
|
||||
@@ -39,7 +39,7 @@ class CaptureNode extends Node
|
||||
->raw("(function () use (&\$context, \$macros, \$blocks) {\n")
|
||||
->indent()
|
||||
->subcompile($this->getNode('body'))
|
||||
->write("yield from [];\n")
|
||||
->write("return; yield;\n")
|
||||
->outdent()
|
||||
->write('})()')
|
||||
;
|
||||
|
||||
@@ -15,16 +15,21 @@ use Twig\Attribute\YieldReady;
|
||||
use Twig\Compiler;
|
||||
|
||||
/**
|
||||
* Wires the security checker at the very top of the template constructor, as
|
||||
* the constructor resolves `use` traits before the sandbox could check them.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class CheckSecurityCallNode extends Node
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function compile(Compiler $compiler)
|
||||
{
|
||||
$compiler
|
||||
->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n")
|
||||
->write("\$this->checkSecurity();\n")
|
||||
->write("\$this->sandbox = \$env->getExtension(SandboxExtension::class)->getChecker();\n")
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -23,45 +23,90 @@ class CheckSecurityNode extends Node
|
||||
private $usedFilters;
|
||||
private $usedTags;
|
||||
private $usedFunctions;
|
||||
private $usedTests;
|
||||
|
||||
/**
|
||||
* @param array<string, int> $usedFilters
|
||||
* @param array<string, int> $usedTags
|
||||
* @param array<string, int> $usedFunctions
|
||||
* @param array<string, int> $usedTests
|
||||
*/
|
||||
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
|
||||
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions, array $usedTests = [])
|
||||
{
|
||||
if (\func_num_args() < 4) {
|
||||
trigger_deprecation('twig/twig', '3.28', 'Not passing the "$usedTests" argument to "%s::__construct()" is deprecated; it will be required in 4.0.', static::class);
|
||||
}
|
||||
|
||||
$this->usedFilters = $usedFilters;
|
||||
$this->usedTags = $usedTags;
|
||||
$this->usedFunctions = $usedFunctions;
|
||||
$this->usedTests = $usedTests;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if (isset($this->usedTags['use'])) {
|
||||
$compiler
|
||||
->write("\n")
|
||||
->write("protected function checkTraitsAllowed(): void\n")
|
||||
->write("{\n")
|
||||
->indent()
|
||||
->write("if (!\$this->sandbox->isSandboxed(\$this->source)) {\n")
|
||||
->indent()
|
||||
->write("return;\n")
|
||||
->outdent()
|
||||
->write("}\n\n")
|
||||
->write("try {\n")
|
||||
->indent()
|
||||
->write("\$this->sandbox->checkSecurity(['use'], [], [], [], \$this->source);\n")
|
||||
->outdent()
|
||||
->write("} catch (SecurityNotAllowedTagError \$e) {\n")
|
||||
->indent()
|
||||
->write('$e->setTemplateLine(')->repr($this->usedTags['use'])->raw(");\n\n")
|
||||
->write("throw \$e;\n")
|
||||
->outdent()
|
||||
->write("}\n")
|
||||
->outdent()
|
||||
->write("}\n")
|
||||
;
|
||||
}
|
||||
|
||||
$compiler
|
||||
->write("\n")
|
||||
->write("public function ensureSecurityChecked(): void\n")
|
||||
->write("{\n")
|
||||
->indent()
|
||||
->write("if (\$this->sandbox->isSandboxed(\$this->source)) {\n")
|
||||
->indent()
|
||||
->write("\$this->checkSecurity();\n")
|
||||
->outdent()
|
||||
->write("}\n")
|
||||
->outdent()
|
||||
->write("}\n")
|
||||
->write("\n")
|
||||
->write("public function checkSecurity()\n")
|
||||
->write("{\n")
|
||||
->indent()
|
||||
->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('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n")
|
||||
->write('static $tests = ')->repr(array_filter($this->usedTests))->raw(";\n\n")
|
||||
->write("try {\n")
|
||||
->indent()
|
||||
->write("\$this->sandbox->checkSecurity(\n")
|
||||
->indent()
|
||||
->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('')->repr(array_keys($this->usedTags))->raw(",\n")
|
||||
->write('')->repr(array_keys($this->usedFilters))->raw(",\n")
|
||||
->write('')->repr(array_keys($this->usedFunctions))->raw(",\n")
|
||||
->write('')->repr(array_keys($this->usedTests))->raw(",\n")
|
||||
->write("\$this->source\n")
|
||||
->outdent()
|
||||
->write(");\n")
|
||||
->outdent()
|
||||
->write("} catch (SecurityError \$e) {\n")
|
||||
->indent()
|
||||
->write("\$e->setSourceContext(\$this->source);\n\n")
|
||||
->write("if (\$e instanceof SecurityNotAllowedTagError && isset(\$tags[\$e->getTagName()])) {\n")
|
||||
->indent()
|
||||
->write("\$e->setTemplateLine(\$tags[\$e->getTagName()]);\n")
|
||||
@@ -74,6 +119,10 @@ class CheckSecurityNode extends Node
|
||||
->indent()
|
||||
->write("\$e->setTemplateLine(\$functions[\$e->getFunctionName()]);\n")
|
||||
->outdent()
|
||||
->write("} elseif (\$e instanceof SecurityNotAllowedTestError && isset(\$tests[\$e->getTestName()])) {\n")
|
||||
->indent()
|
||||
->write("\$e->setTemplateLine(\$tests[\$e->getTestName()]);\n")
|
||||
->outdent()
|
||||
->write("}\n\n")
|
||||
->write("throw \$e;\n")
|
||||
->outdent()
|
||||
|
||||
@@ -28,16 +28,17 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
#[YieldReady]
|
||||
class CheckToStringNode extends AbstractExpression
|
||||
{
|
||||
public function __construct(AbstractExpression $expr)
|
||||
public function __construct(AbstractExpression $expr, bool $spread = false)
|
||||
{
|
||||
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine());
|
||||
parent::__construct(['expr' => $expr], ['spread' => $spread], $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$expr = $this->getNode('expr');
|
||||
$method = $this->getAttribute('spread') ? 'ensureSpreadAllowed' : 'ensureToStringAllowed';
|
||||
$compiler
|
||||
->raw('$this->sandbox->ensureToStringAllowed(')
|
||||
->raw('$this->sandbox->'.$method.'(')
|
||||
->subcompile($expr)
|
||||
->raw(', ')
|
||||
->repr($expr->getTemplateLine())
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node;
|
||||
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
|
||||
/**
|
||||
* Implemented by nodes that implicitly coerce one or more of their child
|
||||
* nodes to string at runtime (PHP string casts, regex matching, comparisons,
|
||||
* range bounds, template-name resolution by the loader, etc.).
|
||||
*
|
||||
* The sandbox node visitor wraps the listed children with a CheckToStringNode
|
||||
* so that an implicit `__toString()` call goes through the sandbox policy
|
||||
* check, independently of where this node's result is used.
|
||||
*
|
||||
* This is distinct from {@see OperatorEscapeInterface}, which describes
|
||||
* operands whose value becomes this expression's value (passthrough operators
|
||||
* like ternaries) and is consumed by the escaper.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* Returns the names of the child nodes that will be coerced to
|
||||
* string when this node is evaluated.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getStringCoercedChildNames(): array;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node;
|
||||
|
||||
use Twig\Attribute\YieldReady;
|
||||
|
||||
/**
|
||||
* Represents a node that has global side effects but does not generate template code.
|
||||
*
|
||||
* Such nodes must be at the root level of the body of a template.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class ConfigNode extends Node
|
||||
{
|
||||
public function __construct(int $lineno)
|
||||
{
|
||||
parent::__construct([], [], $lineno);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use Twig\Node\Expression\ConstantExpression;
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class DeprecatedNode extends Node
|
||||
class DeprecatedNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, int $lineno)
|
||||
{
|
||||
@@ -70,4 +70,18 @@ class DeprecatedNode extends Node
|
||||
->raw(");\n")
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the message is concatenated with `.`, and `package` / `version` are typed `string` on trigger_deprecation()
|
||||
$names = ['expr'];
|
||||
if ($this->hasNode('package')) {
|
||||
$names[] = 'package';
|
||||
}
|
||||
if ($this->hasNode('version')) {
|
||||
$names[] = 'version';
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
@@ -33,18 +33,22 @@ class EmbedNode extends IncludeNode
|
||||
$this->setAttribute('index', $index);
|
||||
}
|
||||
|
||||
protected function addGetTemplate(Compiler $compiler): void
|
||||
protected function addGetTemplate(Compiler $compiler, string $template = ''): void
|
||||
{
|
||||
$compiler
|
||||
->write('$this->loadTemplate(')
|
||||
->raw('$this->load(')
|
||||
->string($this->getAttribute('name'))
|
||||
->raw(', ')
|
||||
->repr($this->getTemplateName())
|
||||
->raw(', ')
|
||||
->repr($this->getTemplateLine())
|
||||
->raw(', ')
|
||||
->string($this->getAttribute('index'))
|
||||
->repr($this->getAttribute('index'))
|
||||
->raw(')')
|
||||
;
|
||||
if ($this->getAttribute('ignore_missing')) {
|
||||
$compiler
|
||||
->raw(";\n")
|
||||
->write(\sprintf("\$%s->getParent(\$context);\n", $template))
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node;
|
||||
|
||||
use Twig\Attribute\YieldReady;
|
||||
|
||||
/**
|
||||
* Represents an empty node.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[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.');
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,19 @@ abstract class AbstractExpression extends Node
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
+87
-57
@@ -12,9 +12,15 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Unary\StringCastUnary;
|
||||
|
||||
class ArrayExpression extends AbstractExpression
|
||||
class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnArrayInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
private $index;
|
||||
|
||||
public function __construct(array $elements, int $lineno)
|
||||
@@ -55,6 +61,31 @@ class ArrayExpression extends AbstractExpression
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the array is a sequence (keys are sequential integers starting from 0).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function isSequence(): bool
|
||||
{
|
||||
foreach ($this->getKeyValuePairs() as $i => $pair) {
|
||||
$key = $pair['key'];
|
||||
if ($key instanceof TempNameExpression) {
|
||||
$keyValue = $key->getAttribute('name');
|
||||
} elseif ($key instanceof ConstantExpression) {
|
||||
$keyValue = $key->getAttribute('value');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($keyValue !== $i) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
|
||||
{
|
||||
if (null === $key) {
|
||||
@@ -64,72 +95,71 @@ class ArrayExpression extends AbstractExpression
|
||||
array_push($this->nodes, $key, $value);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
$keyValuePairs = $this->getKeyValuePairs();
|
||||
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
|
||||
|
||||
if ($needsArrayMergeSpread) {
|
||||
$compiler->raw('CoreExtension::merge(');
|
||||
}
|
||||
$compiler->raw('[');
|
||||
$first = true;
|
||||
$reopenAfterMergeSpread = false;
|
||||
$nextIndex = 0;
|
||||
foreach ($keyValuePairs as $pair) {
|
||||
if ($reopenAfterMergeSpread) {
|
||||
$compiler->raw(', [');
|
||||
$reopenAfterMergeSpread = false;
|
||||
}
|
||||
|
||||
if ($needsArrayMergeSpread && $pair['value']->hasAttribute('spread')) {
|
||||
$compiler->raw('], ')->subcompile($pair['value']);
|
||||
$first = true;
|
||||
$reopenAfterMergeSpread = true;
|
||||
// dynamic mapping keys (computed at runtime) are coerced to string;
|
||||
// static keys (constants or sequence indexes) are emitted as PHP
|
||||
// literals by compile() and never trigger a __toString() call
|
||||
$names = [];
|
||||
foreach (array_chunk($this->nodes, 2) as $i => $pair) {
|
||||
$key = $pair[0];
|
||||
if ($key instanceof ConstantExpression || $key instanceof TempNameExpression) {
|
||||
continue;
|
||||
}
|
||||
if (!$first) {
|
||||
|
||||
$names[] = (string) ($i * 2);
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->definedTest) {
|
||||
$compiler->repr(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for empty expressions which are only allowed in destructuring
|
||||
foreach ($this->getKeyValuePairs() as $pair) {
|
||||
if ($pair['value'] instanceof EmptyExpression) {
|
||||
throw new SyntaxError('Empty array elements are only allowed in destructuring assignments.', $pair['value']->getTemplateLine(), $this->getSourceContext());
|
||||
}
|
||||
}
|
||||
|
||||
$compiler->raw('[');
|
||||
$isSequence = true;
|
||||
foreach ($this->getKeyValuePairs() as $i => $pair) {
|
||||
if (0 !== $i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) {
|
||||
$compiler->raw('...')->subcompile($pair['value']);
|
||||
++$nextIndex;
|
||||
$key = null;
|
||||
if ($pair['key'] instanceof TempNameExpression) {
|
||||
$key = $pair['key']->getAttribute('name');
|
||||
$pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
|
||||
} elseif ($pair['key'] instanceof ConstantExpression) {
|
||||
$key = $pair['key']->getAttribute('value');
|
||||
} else {
|
||||
$key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
|
||||
|
||||
if ($nextIndex !== $key) {
|
||||
if (\is_int($key)) {
|
||||
$nextIndex = $key + 1;
|
||||
}
|
||||
$compiler
|
||||
->subcompile($pair['key'])
|
||||
->raw(' => ')
|
||||
;
|
||||
} else {
|
||||
++$nextIndex;
|
||||
}
|
||||
|
||||
$compiler->subcompile($pair['value']);
|
||||
// dynamic key: cast to string so PHP accepts it as an array offset
|
||||
// (the sandbox visitor has already wrapped it with a __toString policy check)
|
||||
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
|
||||
}
|
||||
}
|
||||
if (!$reopenAfterMergeSpread) {
|
||||
$compiler->raw(']');
|
||||
}
|
||||
if ($needsArrayMergeSpread) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
private function hasSpreadItem(array $pairs): bool
|
||||
{
|
||||
foreach ($pairs as $pair) {
|
||||
if ($pair['value']->hasAttribute('spread')) {
|
||||
return true;
|
||||
if ($key !== $i) {
|
||||
$isSequence = false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!$isSequence && !$pair['value'] instanceof SpreadUnary) {
|
||||
$compiler
|
||||
->subcompile($pair['key'])
|
||||
->raw(' => ')
|
||||
;
|
||||
}
|
||||
|
||||
$compiler->subcompile($pair['value']);
|
||||
}
|
||||
$compiler->raw(']');
|
||||
}
|
||||
}
|
||||
+12
-13
@@ -12,6 +12,9 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\Node;
|
||||
|
||||
/**
|
||||
@@ -23,6 +26,14 @@ class ArrowFunctionExpression extends AbstractExpression
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, Node $names, $lineno)
|
||||
{
|
||||
if ($names instanceof ContextVariable) {
|
||||
$names = new ListExpression([new AssignContextVariable($names->getAttribute('name'), $names->getTemplateLine())], $lineno);
|
||||
}
|
||||
|
||||
if (!$names instanceof ListExpression) {
|
||||
throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext());
|
||||
}
|
||||
|
||||
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno);
|
||||
}
|
||||
|
||||
@@ -31,19 +42,7 @@ class ArrowFunctionExpression extends AbstractExpression
|
||||
$compiler
|
||||
->addDebugInfo($this)
|
||||
->raw('function (')
|
||||
;
|
||||
foreach ($this->getNode('names') as $i => $name) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
|
||||
$compiler
|
||||
->raw('$__')
|
||||
->raw($name->getAttribute('name'))
|
||||
->raw('__')
|
||||
;
|
||||
}
|
||||
$compiler
|
||||
->subcompile($this->getNode('names'))
|
||||
->raw(') use ($context, $macros) { ')
|
||||
;
|
||||
foreach ($this->getNode('names') as $name) {
|
||||
|
||||
+18
-1
@@ -13,9 +13,26 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
|
||||
class AssignNameExpression extends NameExpression
|
||||
class AssignNameExpression extends ContextVariable
|
||||
{
|
||||
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'], true)) {
|
||||
throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno);
|
||||
}
|
||||
|
||||
parent::__construct($name, $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler
|
||||
|
||||
+12
-1
@@ -16,10 +16,21 @@ use Twig\Compiler;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Node;
|
||||
|
||||
abstract class AbstractBinary extends AbstractExpression
|
||||
abstract class AbstractBinary extends AbstractExpression implements BinaryInterface
|
||||
{
|
||||
/**
|
||||
* @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, $left::class);
|
||||
}
|
||||
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, $right::class);
|
||||
}
|
||||
|
||||
parent::__construct(['left' => $left, 'right' => $right], [], $lineno);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class AddBinary extends AbstractBinary
|
||||
class AddBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
@@ -13,9 +13,17 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class AndBinary extends AbstractBinary
|
||||
class AndBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('&&');
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface BinaryInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno);
|
||||
}
|
||||
+2
-1
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class BitwiseAndBinary extends AbstractBinary
|
||||
class BitwiseAndBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
+2
-1
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class BitwiseOrBinary extends AbstractBinary
|
||||
class BitwiseOrBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
+2
-1
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class BitwiseXorBinary extends AbstractBinary
|
||||
class BitwiseXorBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
+8
-1
@@ -13,11 +13,18 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnStringInterface;
|
||||
|
||||
class ConcatBinary extends AbstractBinary
|
||||
class ConcatBinary extends AbstractBinary implements ReturnStringInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('.');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class DivBinary extends AbstractBinary
|
||||
class DivBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterface
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $left
|
||||
* @param AbstractExpression $right
|
||||
*/
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct($left, $right, $lineno);
|
||||
|
||||
$this->setNode('test', TrueTest::wrap(clone $left));
|
||||
$left->setAttribute('always_defined', true);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler
|
||||
->raw('((')
|
||||
->subcompile($this->getNode('test'))
|
||||
->raw(') ? (')
|
||||
->subcompile($this->getNode('left'))
|
||||
->raw(') : (')
|
||||
->subcompile($this->getNode('right'))
|
||||
->raw('))')
|
||||
;
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('?:');
|
||||
}
|
||||
|
||||
public function getOperandNamesToEscape(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -12,8 +12,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class EndsWithBinary extends AbstractBinary
|
||||
class EndsWithBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class EqualBinary extends AbstractBinary
|
||||
class EqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -36,4 +38,9 @@ class EqualBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('==');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -12,8 +12,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class FloorDivBinary extends AbstractBinary
|
||||
class FloorDivBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class GreaterBinary extends AbstractBinary
|
||||
class GreaterBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -36,4 +38,9 @@ class GreaterBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('>');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class GreaterEqualBinary extends AbstractBinary
|
||||
class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -36,4 +38,9 @@ class GreaterEqualBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('>=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -12,8 +12,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class HasEveryBinary extends AbstractBinary
|
||||
class HasEveryBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -22,7 +23,7 @@ class HasEveryBinary extends AbstractBinary
|
||||
->subcompile($this->getNode('left'))
|
||||
->raw(', ')
|
||||
->subcompile($this->getNode('right'))
|
||||
->raw(')')
|
||||
->raw(', $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->getChecker()->isSandboxed($this->source))')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -12,8 +12,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class HasSomeBinary extends AbstractBinary
|
||||
class HasSomeBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -22,7 +23,7 @@ class HasSomeBinary extends AbstractBinary
|
||||
->subcompile($this->getNode('left'))
|
||||
->raw(', ')
|
||||
->subcompile($this->getNode('right'))
|
||||
->raw(')')
|
||||
->raw(', $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->getChecker()->isSandboxed($this->source))')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class InBinary extends AbstractBinary
|
||||
class InBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -30,4 +32,9 @@ class InBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('in');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class LessBinary extends AbstractBinary
|
||||
class LessBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -36,4 +38,9 @@ class LessBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('<');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class LessEqualBinary extends AbstractBinary
|
||||
class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -36,4 +38,9 @@ class LessEqualBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('<=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+34
-1
@@ -12,9 +12,37 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class MatchesBinary extends AbstractBinary
|
||||
class MatchesBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
if (!$left instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" first argument is deprecated, pass an "AbstractExpression" instance instead.', $left::class, __METHOD__);
|
||||
}
|
||||
if (!$right instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" second argument is deprecated, pass an "AbstractExpression" instance instead.', $right::class, __METHOD__);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -30,4 +58,9 @@ class MatchesBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class ModBinary extends AbstractBinary
|
||||
class ModBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class MulBinary extends AbstractBinary
|
||||
class MulBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class NotEqualBinary extends AbstractBinary
|
||||
class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -36,4 +38,9 @@ class NotEqualBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('!=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class NotInBinary extends AbstractBinary
|
||||
class NotInBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -30,4 +32,9 @@ class NotInBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('not in');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class NotSameAsBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('!==');
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\BlockReferenceExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
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;
|
||||
|
||||
final class NullCoalesceBinary extends AbstractBinary implements OperatorEscapeInterface
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $left
|
||||
* @param AbstractExpression $right
|
||||
*/
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct($left, $right, $lineno);
|
||||
|
||||
$test = new DefinedTest(clone $left, new TwigTest('defined', null, ['always_allowed_in_sandbox' => true]), 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, new TwigTest('null', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
|
||||
$left->getTemplateLine(),
|
||||
);
|
||||
}
|
||||
|
||||
$left->setAttribute('always_defined', true);
|
||||
$this->setNode('test', $test);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler
|
||||
->raw('((')
|
||||
->subcompile($this->getNode('test'))
|
||||
->raw(') ? (')
|
||||
->subcompile($this->getNode('left'))
|
||||
->raw(') : (')
|
||||
->subcompile($this->getNode('right'))
|
||||
->raw('))')
|
||||
;
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('??');
|
||||
}
|
||||
|
||||
public function getOperandNamesToEscape(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Node;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
/** @var list<array{property: string, variable: string}> */
|
||||
private array $mappings = [];
|
||||
|
||||
/**
|
||||
* @param ArrayExpression $left The array expression containing object/mapping destructuring properties
|
||||
* @param AbstractExpression $right The expression providing values for assignment
|
||||
*/
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
if (!$left instanceof ArrayExpression) {
|
||||
throw new \LogicException('Left side must be ArrayExpression for object/mapping destructuring.');
|
||||
}
|
||||
foreach ($left->getKeyValuePairs() as $pair) {
|
||||
if (!$pair['value'] instanceof AssignContextVariable) {
|
||||
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
|
||||
}
|
||||
|
||||
$this->mappings[] = [
|
||||
'property' => $pair['key']->getAttribute('value'),
|
||||
'variable' => $pair['value']->getAttribute('name'),
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($left, $right, $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler->addDebugInfo($this);
|
||||
$var = '$'.$compiler->getVarName();
|
||||
$compiler->raw('[[');
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('$context[')->repr($mapping['variable'])->raw(']');
|
||||
}
|
||||
$compiler->raw('] = [');
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||
if (0 === $i) {
|
||||
$compiler->raw('('.$var.' = ')->subcompile($this->getNode('right'))->raw(')');
|
||||
} else {
|
||||
$compiler->raw($var);
|
||||
}
|
||||
$compiler->raw(', ')->repr($mapping['property'])->raw(', [], \\Twig\\Template::ANY_CALL, false, false, ')->repr($compiler->getEnvironment()->hasExtension(SandboxExtension::class))->raw(', ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
|
||||
}
|
||||
$compiler->raw('], '.$var.' = null][0]');
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('=');
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,17 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class OrBinary extends AbstractBinary
|
||||
class OrBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('||');
|
||||
|
||||
+2
-1
@@ -12,8 +12,9 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class PowerBinary extends AbstractBinary
|
||||
class PowerBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
|
||||
+8
-1
@@ -12,8 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnArrayInterface;
|
||||
|
||||
class RangeBinary extends AbstractBinary
|
||||
class RangeBinary extends AbstractBinary implements ReturnArrayInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -30,4 +32,9 @@ class RangeBinary extends AbstractBinary
|
||||
{
|
||||
return $compiler->raw('..');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1297 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user