Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\GetFunctionsToAsTwigFunctionAttributeRector\Fixture;

use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class KeepBuiltinConvertCustom extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('include', $this->includeWithEvent(...), ['needs_environment' => true]),
new TwigFunction('custom_function', [$this, 'customFunction']),
];
}

public function includeWithEvent(Environment $env, $template)
{
return $template;
}

public function customFunction($value)
{
return $value;
}
}

?>
-----
<?php

namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\GetFunctionsToAsTwigFunctionAttributeRector\Fixture;

use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class KeepBuiltinConvertCustom extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('include', $this->includeWithEvent(...), ['needs_environment' => true]),
];
}

public function includeWithEvent(Environment $env, $template)
{
return $template;
}

#[\Twig\Attribute\AsTwigFunction(name: 'custom_function')]
public function customFunction($value)
{
return $value;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\GetFunctionsToAsTwigFunctionAttributeRector\Fixture;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class SkipBuiltinFunctionOverride extends AbstractExtension
{
public function getFunctions(): array
{
return [
// Override the built-in include function with higher priority
new TwigFunction('include', $this->includeWithEvent(...), [
'needs_environment' => true,
'needs_context' => true,
'is_safe' => ['html'],
]),
];
}

public function includeWithEvent(\Twig\Environment $env, array $context, $template)
{
return $template;
}
}
103 changes: 102 additions & 1 deletion rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,75 @@
'getGlobals',
];

/**
* Built-in Twig function names. Overriding one only works while the class stays a classic
* "extends AbstractExtension", so such an item must remain in the array and not become an attribute.
*
* @var string[]
*/
private const array CORE_TWIG_FUNCTION_NAMES = [
'attribute',
'block',
'constant',
'cycle',
'date',
'dump',
'enum_cases',
'include',
'max',
'min',
'parent',
'random',
'range',
'source',
'template_from_string',
];

/**
* Built-in Twig filter names, see self::CORE_TWIG_FUNCTION_NAMES for the reasoning.
*
* @var string[]
*/
private const array CORE_TWIG_FILTER_NAMES = [
'abs',
'batch',
'capitalize',
'column',
'convert_encoding',
'date',
'date_modify',
'default',
'e',
'escape',
'filter',
'first',
'format',
'join',
'json_encode',
'keys',
'last',
'length',
'lower',
'map',
'merge',
'nl2br',
'number_format',
'raw',
'reduce',
'replace',
'reverse',
'round',
'slice',
'sort',
'spaceless',
'split',
'striptags',
'title',
'trim',
'upper',
'url_encode',
];

public function __construct(
private LocalArrayMethodCallableMatcher $localArrayMethodCallableMatcher,
private ReturnEmptyArrayMethodRemover $returnEmptyArrayMethodRemover,
Expand Down Expand Up @@ -104,6 +173,12 @@ public function transformClassGetMethodToAttributeMarker(
return false;
}

// items that override a built-in Twig function/filter must stay registered the classic way,
// so keep them in the array and let the class keep "extends AbstractExtension"
if ($this->isBuiltinTwigNameOverride($arrayItem, $methodName)) {
continue;
}

$conversion = $this->matchArrayItemConversion(
$key,
$arrayItem,
Expand All @@ -118,6 +193,11 @@ public function transformClassGetMethodToAttributeMarker(
$conversions[] = $conversion;
}

// only built-in overrides (or none) left to convert, nothing to do
if ($conversions === []) {
return false;
}

// attribute-based extensions and "extends AbstractExtension" are incompatible, so the class
// must not keep relying on the parent class for other registrations (e.g. getTests(), globals)
if ($this->stillRequiresAbstractExtension($class, $methodName)) {
Expand All @@ -141,7 +221,10 @@ public function transformClassGetMethodToAttributeMarker(

$this->returnEmptyArrayMethodRemover->removeClassMethodIfArrayEmpty($class, $returnArray, $methodName);

if ($class->extends instanceof FullyQualified && $class->extends->toString() === TwigClass::TWIG_EXTENSION) {
// a kept built-in override leaves the array non-empty, so the class still needs the parent extension
if ($returnArray->items === []
&& $class->extends instanceof FullyQualified
&& $class->extends->toString() === TwigClass::TWIG_EXTENSION) {
$class->extends = null;
}

Expand Down Expand Up @@ -227,6 +310,24 @@ private function matchArrayItemConversion(
return new AsTwigAttributeConversion($key, $localMethod, $nameArg, $optionArguments);
}

private function isBuiltinTwigNameOverride(ArrayItem $arrayItem, string $methodName): bool
{
if (! $arrayItem->value instanceof New_) {
return false;
}

$firstArg = $arrayItem->value->getArgs()[0] ?? null;
if (! $firstArg?->value instanceof String_) {
return false;
}

$builtinNames = $methodName === 'getFilters'
? self::CORE_TWIG_FILTER_NAMES
: self::CORE_TWIG_FUNCTION_NAMES;

return in_array($firstArg->value->value, $builtinNames, true);
}

private function stillRequiresAbstractExtension(Class_ $class, string $convertedMethodName): bool
{
foreach ($class->getMethods() as $classMethod) {
Expand Down
Loading