Skip to content

Commit e64d0b2

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

File tree

5 files changed

+372
-0
lines changed

5 files changed

+372
-0
lines changed
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\AccessDecisionManagerInterface;
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 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: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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;
16+
use PhpParser\Node\Stmt\Class_;
17+
use PhpParser\Node\Stmt\ClassMethod;
18+
use PhpParser\Node\Stmt\Namespace_;
19+
use PhpParser\Node\Stmt\Use_;
20+
use PHPStan\Type\ObjectType;
21+
use Rector\Rector\AbstractRector;
22+
use Rector\Symfony\Enum\SymfonyClass;
23+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
24+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
25+
26+
/**
27+
* @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest
28+
*/
29+
final class AuthorizationCheckerToAccessDecisionManagerInVoterRector extends AbstractRector
30+
{
31+
private const AUTHORIZATION_CHECKER_PROPERTY = 'authorizationChecker';
32+
private const ACCESS_DECISION_MANAGER_PROPERTY = 'accessDecisionManager';
33+
34+
private const ACCESS_DECISION_MANAGER_SHORT_NAME = 'AccessDecisionManagerInterface';
35+
36+
public function getRuleDefinition(): RuleDefinition
37+
{
38+
return new RuleDefinition(
39+
'Replaces AuthorizationCheckerInterface with AccessDecisionManagerInterface inside Symfony Voters',
40+
[
41+
new CodeSample(
42+
<<<'CODE_SAMPLE'
43+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
44+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
45+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
46+
47+
final class PostVoter extends Voter
48+
{
49+
public function __construct(
50+
private AuthorizationCheckerInterface $authorizationChecker
51+
) {}
52+
53+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
54+
{
55+
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
56+
}
57+
}
58+
CODE_SAMPLE
59+
,
60+
<<<'CODE_SAMPLE'
61+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
62+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
63+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
64+
65+
final class PostVoter extends Voter
66+
{
67+
public function __construct(
68+
private AccessDecisionManagerInterface $accessDecisionManager
69+
) {}
70+
71+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
72+
{
73+
return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']);
74+
}
75+
}
76+
CODE_SAMPLE
77+
),
78+
]
79+
);
80+
}
81+
82+
public function getNodeTypes(): array
83+
{
84+
return [Namespace_::class];
85+
}
86+
87+
public function refactor(Node $node): ?Node
88+
{
89+
$hasChanged = false;
90+
91+
foreach ($node->stmts as $stmt) {
92+
if (! $stmt instanceof Class_) {
93+
continue;
94+
}
95+
96+
if ($this->refactorVoterClass($stmt)) {
97+
$hasChanged = true;
98+
}
99+
}
100+
101+
if (! $hasChanged) {
102+
return null;
103+
}
104+
105+
$this->replaceUseImport(
106+
$node,
107+
SymfonyClass::AUTHORIZATION_CHECKER_INTERFACE,
108+
SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE
109+
);
110+
111+
return $node;
112+
}
113+
114+
private function refactorVoterClass(Class_ $class): bool
115+
{
116+
if ($class->extends === null || ! $this->isName($class->extends, SymfonyClass::VOTER_CLASS)) {
117+
return false;
118+
}
119+
120+
$hasChanged = false;
121+
$renamedPropertyByOldName = [];
122+
123+
$authorizationCheckerType = new ObjectType(SymfonyClass::AUTHORIZATION_CHECKER_INTERFACE);
124+
125+
// properties
126+
foreach ($class->getProperties() as $property) {
127+
if (! $this->isObjectType($property, $authorizationCheckerType)) {
128+
continue;
129+
}
130+
131+
$property->type = new Name(self::ACCESS_DECISION_MANAGER_SHORT_NAME);
132+
133+
foreach ($property->props as $propertyProperty) {
134+
if ($this->getName($propertyProperty) === self::AUTHORIZATION_CHECKER_PROPERTY) {
135+
$propertyProperty->name = new Identifier(self::ACCESS_DECISION_MANAGER_PROPERTY);
136+
$renamedPropertyByOldName[self::AUTHORIZATION_CHECKER_PROPERTY]
137+
= self::ACCESS_DECISION_MANAGER_PROPERTY;
138+
}
139+
}
140+
141+
$hasChanged = true;
142+
}
143+
144+
// promoted properties
145+
$constructor = $class->getMethod('__construct');
146+
if ($constructor instanceof ClassMethod) {
147+
foreach ($constructor->params as $param) {
148+
if (
149+
$param->type === null
150+
|| ! $this->isName($param->type, SymfonyClass::AUTHORIZATION_CHECKER_INTERFACE)
151+
) {
152+
continue;
153+
}
154+
155+
$param->type = new Name(self::ACCESS_DECISION_MANAGER_SHORT_NAME);
156+
157+
if (
158+
$param->var instanceof Variable
159+
&& $this->getName($param->var) === self::AUTHORIZATION_CHECKER_PROPERTY
160+
) {
161+
$param->var->name = self::ACCESS_DECISION_MANAGER_PROPERTY;
162+
$renamedPropertyByOldName[self::AUTHORIZATION_CHECKER_PROPERTY]
163+
= self::ACCESS_DECISION_MANAGER_PROPERTY;
164+
}
165+
166+
$hasChanged = true;
167+
}
168+
}
169+
170+
// isGranted → decide
171+
$voteMethod = $class->getMethod('voteOnAttribute');
172+
if ($voteMethod instanceof ClassMethod) {
173+
$this->traverseNodesWithCallable(
174+
$voteMethod,
175+
function (Node $node) use (
176+
&$hasChanged,
177+
$voteMethod,
178+
$renamedPropertyByOldName
179+
) {
180+
if (
181+
! $node instanceof MethodCall
182+
|| ! $this->isName($node->name, 'isGranted')
183+
|| ! $node->var instanceof PropertyFetch
184+
) {
185+
return null;
186+
}
187+
188+
$propertyName = $this->getName($node->var->name);
189+
if ($propertyName === null || ! isset($renamedPropertyByOldName[$propertyName])) {
190+
return null;
191+
}
192+
193+
$node->var->name = new Identifier($renamedPropertyByOldName[$propertyName]);
194+
$node->name = new Identifier('decide');
195+
196+
$tokenVariable = $voteMethod->params[2]->var ?? null;
197+
if (! $tokenVariable instanceof Variable) {
198+
return null;
199+
}
200+
201+
$attributeArgument = $node->args[0] ?? null;
202+
if ($attributeArgument === null) {
203+
return null;
204+
}
205+
206+
$node->args = [
207+
new Arg($tokenVariable),
208+
new Arg(new Array_([new ArrayItem($attributeArgument->value)])),
209+
];
210+
211+
$hasChanged = true;
212+
return $node;
213+
}
214+
);
215+
}
216+
217+
return $hasChanged;
218+
}
219+
220+
private function replaceUseImport(Namespace_ $namespace, string $oldFqn, string $newFqn): void
221+
{
222+
$hasNewImport = false;
223+
224+
foreach ($namespace->stmts as $stmt) {
225+
if (! $stmt instanceof Use_) {
226+
continue;
227+
}
228+
229+
foreach ($stmt->uses as $useUse) {
230+
if ($this->isName($useUse->name, $newFqn)) {
231+
$hasNewImport = true;
232+
break 2;
233+
}
234+
}
235+
}
236+
237+
foreach ($namespace->stmts as $index => $stmt) {
238+
if (! $stmt instanceof Use_) {
239+
continue;
240+
}
241+
242+
$newUses = [];
243+
$hasChanged = false;
244+
245+
foreach ($stmt->uses as $useUse) {
246+
if ($this->isName($useUse->name, $oldFqn)) {
247+
$hasChanged = true;
248+
249+
if (! $hasNewImport) {
250+
$useUse->name = new Name($newFqn);
251+
$newUses[] = $useUse;
252+
$hasNewImport = true;
253+
}
254+
255+
continue;
256+
}
257+
258+
$newUses[] = $useUse;
259+
}
260+
261+
if (! $hasChanged) {
262+
continue;
263+
}
264+
265+
if ($newUses === []) {
266+
unset($namespace->stmts[$index]);
267+
$namespace->stmts = array_values($namespace->stmts);
268+
continue;
269+
}
270+
271+
$stmt->uses = $newUses;
272+
}
273+
}
274+
}

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)