Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[symfony 7.3] Add GetFiltersToAsTwigFilterAttributeRector to migrate #[TwigFilter] #761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 9, 2025
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
1 change: 1 addition & 0 deletions config/sets/symfony/symfony7/symfony73.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/symfony73/symfony73-console.php');
$rectorConfig->import(__DIR__ . '/symfony73/symfony73-twig-bundle.php');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be registered in Symfony7SetProvider as well below this:

new ComposerTriggeredSet(
SetGroup::SYMFONY,
'symfony/console',
'7.3',
__DIR__ . '/../../../config/sets/symfony/symfony7/symfony73/symfony73-console.php'
),
];

Copy link
Member

@samsonasik samsonasik May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};
10 changes: 10 additions & 0 deletions config/sets/symfony/symfony7/symfony73/symfony73-twig-bundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Symfony\Symfony73\Rector\Class_\GetFiltersToAsTwigFilterAttributeRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rules([GetFiltersToAsTwigFilterAttributeRector::class]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

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

use Twig\Extension\AbstractExtension;

final class SomeGetFilter extends AbstractExtension
{
public function getFilters()
{
return [
new \Twig\TwigFilter('some_filter', [$this, 'someFilter']),
];
}

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

?>
-----
<?php

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

use Twig\Extension\AbstractExtension;

final class SomeGetFilter extends AbstractExtension
{
#[\Twig\Attribute\AsTwigFilter]
public function someFilter($value)
{
return $value;
}
}

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

declare(strict_types=1);

namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\GetFiltersToAsTwigFilterAttributeRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class GetFiltersToAsTwigFilterAttributeRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Symfony\Symfony73\Rector\Class_\GetFiltersToAsTwigFilterAttributeRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(GetFiltersToAsTwigFilterAttributeRector::class);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
use Rector\Symfony\NodeFinder\EmptyReturnNodeFinder;
use Rector\Symfony\TypeAnalyzer\ArrayUnionResponseTypeAnalyzer;
use Rector\Symfony\TypeDeclaration\ReturnTypeDeclarationUpdater;
use Symfony\Component\HttpFoundation\Response;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php

declare(strict_types=1);

namespace Rector\Symfony\Symfony73\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ObjectType;
use Rector\Exception\ShouldNotHappenException;
use Rector\Rector\AbstractRector;
use Rector\Symfony\Enum\TwigClass;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see https://symfony.com/blog/new-in-symfony-7-3-twig-extension-attributes
*
* @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\GetFiltersToAsTwigFilterAttributeRector\GetFiltersToAsTwigFilterAttributeRectorTest
*/
final class GetFiltersToAsTwigFilterAttributeRector extends AbstractRector
{
public function __construct(
private readonly ReflectionProvider $reflectionProvider
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Changes getFilters() in TwigExtension to #[TwigFilter] marker attribute above function',
[
new CodeSample(
<<<'CODE_SAMPLE'
use Twig\Extension\AbstractExtension;

class SomeClass extends AbstractExtension
{
public function getFilters()
{
return [
new \Twig\TwigFilter('filter_name', [$this, 'localMethod']),
];
}

public function localMethod($value)
{
return $value;
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class SomeClass extends AbstractExtension
{
#[TwigFilter('filter_name')]
public function localMethod($value)
{
return $value;
}
}
CODE_SAMPLE
)]
);
}

public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Class_
{
if (! $this->reflectionProvider->hasClass(TwigClass::TWIG_EXTENSION)) {
return null;
}

if ($node->isAbstract() || $node->isAnonymous()) {
return null;
}

if (! $this->isObjectType($node, new ObjectType(TwigClass::TWIG_EXTENSION))) {
return null;
}

$getFilterMethod = $node->getMethod('getFilters');
if (! $getFilterMethod instanceof ClassMethod) {
return null;
}

$hasChanged = false;

foreach ((array) $getFilterMethod->stmts as $stmt) {
// handle return array simple case
if (! $stmt instanceof Return_) {
continue;
}

if (! $stmt->expr instanceof Array_) {
continue;
}

$returnArray = $stmt->expr;
foreach ($returnArray->items as $key => $arrayItem) {
if (! $arrayItem->value instanceof New_) {
continue;
}

$new = $arrayItem->value;
if (! $this->isObjectType($new->class, new ObjectType(TwigClass::TWIG_FILTER))) {
continue;
}

if (count($new->getArgs()) !== 2) {
continue;
}

$secondArg = $new->getArgs()[1];
if ($secondArg->value instanceof MethodCall && $secondArg->value->isFirstClassCallable()) {
throw new ShouldNotHappenException('Not supported yet');
}
Comment on lines +136 to +138
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the syntax seems should be:

new \Twig\TwigFilter('some_filter', $this->localMethod(...)),

I will look into it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if ($secondArg->value instanceof Array_ && count($secondArg->value->items) === 2) {
$localMethodName = $this->matchLocalMethodName($secondArg->value);
if (! is_string($localMethodName)) {
continue;
}

$localMethod = $node->getMethod($localMethodName);
if (! $localMethod instanceof ClassMethod) {
continue;
}

$localMethod->attrGroups[] = new AttributeGroup([
new Attribute(new FullyQualified(TwigClass::AS_TWIG_FILTER_ATTRIBUTE)),
]);

// remove old new fuction instance
unset($returnArray->items[$key]);

$hasChanged = true;
}
}

$this->removeGetFilterMethodIfEmpty($returnArray, $node, $stmt);
}

if ($hasChanged) {
return $node;
}

return null;
}

private function matchLocalMethodName(Array_ $callableArray): ?string
{
$firstItem = $callableArray->items[0];
if (! $firstItem->value instanceof Variable) {
return null;
}

if (! $this->isName($firstItem->value, 'this')) {
return null;
}

$secondItem = $callableArray->items[1];
if (! $secondItem->value instanceof String_) {
return null;
}

return $secondItem->value->value;
}

private function removeGetFilterMethodIfEmpty(Array_ $getFilterReturnArray, Class_ $class, Return_ $return): void
{
if (count($getFilterReturnArray->items) !== 0) {
return;
}

// remove "getFilters()" method
foreach ($class->stmts as $key => $classStmt) {
if (! $classStmt instanceof ClassMethod) {
continue;
}

if (! $this->isName($classStmt, 'getFilters')) {
continue;
}

unset($class->stmts[$key]);
}
}
}
20 changes: 20 additions & 0 deletions src/Enum/TwigClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Rector\Symfony\Enum;

final class TwigClass
{
/**
* @var string
*/
public const TWIG_EXTENSION = 'Twig\Extension\AbstractExtension';

/**
* @var string
*/
public const AS_TWIG_FILTER_ATTRIBUTE = 'Twig\Attribute\AsTwigFilter';

public const TWIG_FILTER = 'Twig\TwigFilter';
}
Loading