Skip to content

Commit 3c3f369

Browse files
committed
[Symfony 7.3] Replace AuthorizationChecker with AccessDecisionManager in voters
1 parent d013008 commit 3c3f369

5 files changed

Lines changed: 298 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\Fixture;
4+
5+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
6+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
7+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
8+
9+
final class AuthorizationCheckerVoter extends Voter
10+
{
11+
public function __construct(
12+
private AuthorizationCheckerInterface $authorizationChecker
13+
) {}
14+
15+
protected function supports(string $attribute, mixed $subject): bool
16+
{
17+
return true;
18+
}
19+
20+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
21+
{
22+
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
23+
}
24+
}
25+
-----
26+
<?php
27+
28+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\Fixture;
29+
30+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
31+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
32+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
33+
34+
final class AuthorizationCheckerVoter extends Voter
35+
{
36+
public function __construct(
37+
private \Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface $accessDecisionManager
38+
) {}
39+
40+
protected function supports(string $attribute, mixed $subject): bool
41+
{
42+
return true;
43+
}
44+
45+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
46+
{
47+
return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']);
48+
}
49+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector;
7+
8+
return RectorConfig::configure()
9+
->withRules([
10+
AuthorizationCheckerToAccessDecisionManagerInVoterRector::class,
11+
]);
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Symfony73\Rector\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\ArrayItem;
10+
use PhpParser\Node\Expr\Array_;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\PropertyFetch;
13+
use PhpParser\Node\Expr\Variable;
14+
use PhpParser\Node\Identifier;
15+
use PhpParser\Node\Name\FullyQualified;
16+
use PhpParser\Node\Stmt\Class_;
17+
use PhpParser\Node\Stmt\ClassMethod;
18+
use PHPStan\Type\ObjectType;
19+
use Rector\Rector\AbstractRector;
20+
use Rector\Symfony\Enum\SymfonyClass;
21+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
22+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
23+
24+
/**
25+
* @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest
26+
*/
27+
final class AuthorizationCheckerToAccessDecisionManagerInVoterRector extends AbstractRector
28+
{
29+
private const AUTHORIZATION_CHECKER_PROPERTY = 'authorizationChecker';
30+
private const ACCESS_DECISION_MANAGER_PROPERTY = 'accessDecisionManager';
31+
32+
public function getRuleDefinition(): RuleDefinition
33+
{
34+
return new RuleDefinition(
35+
'Replaces AuthorizationCheckerInterface with AccessDecisionManagerInterface inside Symfony Voters',
36+
[
37+
new CodeSample(
38+
<<<'CODE_SAMPLE'
39+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
40+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
41+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
42+
43+
final class AuthorizationCheckerVoter extends Voter
44+
{
45+
public function __construct(
46+
private AuthorizationCheckerInterface $authorizationChecker
47+
) {}
48+
49+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
50+
{
51+
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
52+
}
53+
}
54+
CODE_SAMPLE
55+
,
56+
<<<'CODE_SAMPLE'
57+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
58+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
59+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
60+
61+
final class AuthorizationCheckerVoter extends Voter
62+
{
63+
public function __construct(
64+
private AccessDecisionManagerInterface $accessDecisionManager
65+
) {}
66+
67+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
68+
{
69+
return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']);
70+
}
71+
}
72+
CODE_SAMPLE
73+
),
74+
]
75+
);
76+
}
77+
78+
/**
79+
* @return array<class-string<Node>>
80+
*/
81+
public function getNodeTypes(): array
82+
{
83+
return [Class_::class];
84+
}
85+
86+
/**
87+
* @param Class_ $node
88+
*/
89+
public function refactor(Node $node): ?Node
90+
{
91+
if ($node->extends === null || ! $this->isName($node->extends, SymfonyClass::VOTER_CLASS)) {
92+
return null;
93+
}
94+
95+
$hasChanged = false;
96+
$renamedProperties = [];
97+
98+
$authorizationCheckerType = new ObjectType(
99+
SymfonyClass::AUTHORIZATION_CHECKER_INTERFACE
100+
);
101+
102+
// 1) Regular properties
103+
foreach ($node->getProperties() as $property) {
104+
if (! $this->isObjectType($property, $authorizationCheckerType)) {
105+
continue;
106+
}
107+
108+
$property->type = new FullyQualified(
109+
SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE
110+
);
111+
112+
foreach ($property->props as $prop) {
113+
if ($this->getName($prop) === self::AUTHORIZATION_CHECKER_PROPERTY) {
114+
$prop->name = new Identifier(self::ACCESS_DECISION_MANAGER_PROPERTY);
115+
$renamedProperties[self::AUTHORIZATION_CHECKER_PROPERTY]
116+
= self::ACCESS_DECISION_MANAGER_PROPERTY;
117+
}
118+
}
119+
120+
$hasChanged = true;
121+
}
122+
123+
// 2) Promoted properties (constructor)
124+
$constructor = $node->getMethod('__construct');
125+
if ($constructor instanceof ClassMethod) {
126+
foreach ($constructor->params as $param) {
127+
if (
128+
$param->type === null
129+
|| ! $this->isName($param->type, SymfonyClass::AUTHORIZATION_CHECKER_INTERFACE)
130+
) {
131+
continue;
132+
}
133+
134+
$param->type = new FullyQualified(
135+
SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE
136+
);
137+
138+
if (
139+
$param->var instanceof Variable
140+
&& $this->getName($param->var) === self::AUTHORIZATION_CHECKER_PROPERTY
141+
) {
142+
$param->var->name = self::ACCESS_DECISION_MANAGER_PROPERTY;
143+
$renamedProperties[self::AUTHORIZATION_CHECKER_PROPERTY]
144+
= self::ACCESS_DECISION_MANAGER_PROPERTY;
145+
}
146+
147+
$hasChanged = true;
148+
}
149+
}
150+
151+
// 3) Replace isGranted() with decide()
152+
$voteMethod = $node->getMethod('voteOnAttribute');
153+
if ($voteMethod instanceof ClassMethod) {
154+
$this->traverseNodesWithCallable(
155+
$voteMethod,
156+
function (Node $node) use (&$hasChanged, $voteMethod, $renamedProperties) {
157+
if (
158+
! $node instanceof MethodCall
159+
|| ! $this->isName($node->name, 'isGranted')
160+
|| ! $node->var instanceof PropertyFetch
161+
) {
162+
return null;
163+
}
164+
165+
$propertyName = $this->getName($node->var->name);
166+
if ($propertyName === null || ! isset($renamedProperties[$propertyName])) {
167+
return null;
168+
}
169+
170+
$node->var->name = new Identifier($renamedProperties[$propertyName]);
171+
$node->name = new Identifier('decide');
172+
173+
$tokenVariable = $voteMethod->params[2]->var ?? null;
174+
if (! $tokenVariable instanceof Variable) {
175+
return null;
176+
}
177+
178+
$attributeArg = $node->args[0] ?? null;
179+
if (! $attributeArg instanceof Arg) {
180+
return null;
181+
}
182+
183+
$attributeExpr = $attributeArg->value;
184+
185+
$node->args = [
186+
new Arg($tokenVariable),
187+
new Arg(new Array_([
188+
new ArrayItem($attributeExpr),
189+
])),
190+
];
191+
192+
$hasChanged = true;
193+
return $node;
194+
}
195+
);
196+
}
197+
198+
return $hasChanged ? $node : null;
199+
}
200+
}

src/Enum/SymfonyClass.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ final class SymfonyClass
206206
*/
207207
public const VOTER_CLASS = 'Symfony\Component\Security\Core\Authorization\Voter\Voter';
208208

209+
/**
210+
* @var string
211+
*/
212+
public const AUTHORIZATION_CHECKER_INTERFACE = 'Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface';
213+
214+
/**
215+
* @var string
216+
*/
217+
public const ACCESS_DECISION_MANAGER_INTERFACE = 'Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface';
218+
209219
/**
210220
* @var string
211221
*/

0 commit comments

Comments
 (0)