Skip to content

Commit 0a162c9

Browse files
committed
[DependencyInjection] Extract GetByTypeMethodCallToConstructorInjectionRector to make migration smoother
1 parent cd07337 commit 0a162c9

File tree

9 files changed

+297
-27
lines changed

9 files changed

+297
-27
lines changed
Lines changed: 28 additions & 0 deletions
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\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ControllerGetByTypeToConstructorInjectionRectorTest 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+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Fixture;
4+
5+
use Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source\SomeService;
6+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7+
8+
final class ControllerGetWithType extends Controller
9+
{
10+
public function configure()
11+
{
12+
$someType = $this->get(SomeService::class);
13+
}
14+
}
15+
16+
?>
17+
-----
18+
<?php
19+
20+
namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Fixture;
21+
22+
use Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source\SomeService;
23+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
24+
25+
final class ControllerGetWithType extends Controller
26+
{
27+
public function __construct(private \Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source\SomeService $someService)
28+
{
29+
}
30+
public function configure()
31+
{
32+
$someType = $this->someService;
33+
}
34+
}
35+
36+
?>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source;
4+
5+
final class SomeService
6+
{
7+
8+
}
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+
7+
return static function (RectorConfig $rectorConfig): void {
8+
$rectorConfig->rule(
9+
\Rector\Symfony\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector::class
10+
);
11+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\DependencyInjection\NodeDecorator;
6+
7+
use PhpParser\Node\Expr\StaticCall;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use PhpParser\Node\Stmt\Expression;
12+
use PHPStan\Type\ObjectType;
13+
use Rector\NodeTypeResolver\NodeTypeResolver;
14+
use Rector\ValueObject\MethodName;
15+
16+
final class CommandConstructorDecorator
17+
{
18+
public function __construct(
19+
private NodeTypeResolver $nodeTypeResolver
20+
) {
21+
}
22+
23+
public function decorate(Class_ $class): void
24+
{
25+
// special case for command to keep parent constructor call
26+
if (! $this->nodeTypeResolver->isObjectType(
27+
$class,
28+
new ObjectType('Symfony\Component\Console\Command\Command')
29+
)) {
30+
return;
31+
}
32+
33+
$constuctClassMethod = $class->getMethod(MethodName::CONSTRUCT);
34+
if (! $constuctClassMethod instanceof ClassMethod) {
35+
return;
36+
}
37+
38+
// empty stmts? add parent::__construct() to setup command
39+
if ((array) $constuctClassMethod->stmts === []) {
40+
$parentConstructStaticCall = new StaticCall(new Name('parent'), '__construct');
41+
$constuctClassMethod->stmts[] = new Expression($parentConstructStaticCall);
42+
}
43+
}
44+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\DependencyInjection\Rector\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ClassConstFetch;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Stmt\Class_;
11+
use PHPStan\Reflection\ClassReflection;
12+
use Rector\Naming\Naming\PropertyNaming;
13+
use Rector\NodeManipulator\ClassDependencyManipulator;
14+
use Rector\PHPStan\ScopeFetcher;
15+
use Rector\PostRector\ValueObject\PropertyMetadata;
16+
use Rector\Rector\AbstractRector;
17+
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
18+
use Rector\Symfony\Enum\SymfonyClass;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
20+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
21+
22+
/**
23+
* @see \Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\ControllerGetByTypeToConstructorInjectionRectorTest
24+
*/
25+
final class ControllerGetByTypeToConstructorInjectionRector extends AbstractRector
26+
{
27+
public function __construct(
28+
private readonly ClassDependencyManipulator $classDependencyManipulator,
29+
private readonly PropertyNaming $propertyNaming
30+
) {
31+
}
32+
33+
public function getRuleDefinition(): RuleDefinition
34+
{
35+
return new RuleDefinition(
36+
'From `$container->get(SomeType::class)` in controllers to constructor injection (step 1/x)',
37+
[
38+
new CodeSample(
39+
<<<'CODE_SAMPLE'
40+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
41+
42+
final class SomeCommand extends Controller
43+
{
44+
public function someMethod()
45+
{
46+
$someType = $this->get(SomeType::class);
47+
}
48+
}
49+
CODE_SAMPLE
50+
,
51+
<<<'CODE_SAMPLE'
52+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
53+
54+
final class SomeCommand extends Controller
55+
{
56+
public function __construct(private SomeType $someType)
57+
{
58+
}
59+
60+
public function someMethod()
61+
{
62+
$someType = $this->someType;
63+
}
64+
}
65+
CODE_SAMPLE
66+
),
67+
]
68+
);
69+
}
70+
71+
/**
72+
* @return array<class-string<Node>>
73+
*/
74+
public function getNodeTypes(): array
75+
{
76+
return [Class_::class];
77+
}
78+
79+
/**
80+
* @param Class_ $node
81+
*/
82+
public function refactor(Node $node): ?Node
83+
{
84+
if ($this->shouldSkipClass($node)) {
85+
return null;
86+
}
87+
88+
$propertyMetadatas = [];
89+
90+
$this->traverseNodesWithCallable($node, function (Node $node) use (&$propertyMetadatas): ?Node {
91+
if (! $node instanceof MethodCall) {
92+
return null;
93+
}
94+
95+
if ($node->isFirstClassCallable()) {
96+
return null;
97+
}
98+
99+
if (! $this->isName($node->name, 'get')) {
100+
return null;
101+
}
102+
103+
if (! $this->isName($node->var, 'this')) {
104+
return null;
105+
}
106+
107+
if (count($node->getArgs()) !== 1) {
108+
return null;
109+
}
110+
111+
$firstArg = $node->getArgs()[0];
112+
if (! $firstArg->value instanceof ClassConstFetch) {
113+
return null;
114+
}
115+
116+
// must be class const fetch
117+
if (! $this->isName($firstArg->value->name, 'class')) {
118+
return null;
119+
}
120+
121+
$className = $this->getName($firstArg->value->class);
122+
if (! is_string($className)) {
123+
return null;
124+
}
125+
126+
$propertyName = $this->propertyNaming->fqnToVariableName($className);
127+
$propertyMetadata = new PropertyMetadata($propertyName, new FullyQualifiedObjectType($className));
128+
129+
$propertyMetadatas[] = $propertyMetadata;
130+
return $this->nodeFactory->createPropertyFetch('this', $propertyMetadata->getName());
131+
});
132+
133+
if ($propertyMetadatas === []) {
134+
return null;
135+
}
136+
137+
foreach ($propertyMetadatas as $propertyMetadata) {
138+
$this->classDependencyManipulator->addConstructorDependency($node, $propertyMetadata);
139+
}
140+
141+
return $node;
142+
}
143+
144+
private function shouldSkipClass(Class_ $class): bool
145+
{
146+
// keep it safe
147+
if (! $class->isFinal()) {
148+
return true;
149+
}
150+
151+
$scope = ScopeFetcher::fetch($class);
152+
153+
$classReflection = $scope->getClassReflection();
154+
if (! $classReflection instanceof ClassReflection) {
155+
return true;
156+
}
157+
158+
return ! $classReflection->isSubclassOf(SymfonyClass::CONTROLLER);
159+
}
160+
}

rules/Symfony42/Rector/MethodCall/ContainerGetToConstructorInjectionRector.php

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@
77
use PhpParser\Node;
88
use PhpParser\Node\Expr\ClassConstFetch;
99
use PhpParser\Node\Expr\MethodCall;
10-
use PhpParser\Node\Expr\StaticCall;
11-
use PhpParser\Node\Name;
1210
use PhpParser\Node\Stmt\Class_;
13-
use PhpParser\Node\Stmt\ClassMethod;
14-
use PhpParser\Node\Stmt\Expression;
1511
use PHPStan\Type\ObjectType;
1612
use Rector\NodeManipulator\ClassDependencyManipulator;
1713
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
1814
use Rector\PostRector\ValueObject\PropertyMetadata;
1915
use Rector\Rector\AbstractRector;
16+
use Rector\Symfony\DependencyInjection\NodeDecorator\CommandConstructorDecorator;
2017
use Rector\Symfony\NodeAnalyzer\DependencyInjectionMethodCallAnalyzer;
21-
use Rector\ValueObject\MethodName;
2218
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
2319
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
2420

@@ -33,6 +29,7 @@ public function __construct(
3329
private readonly DependencyInjectionMethodCallAnalyzer $dependencyInjectionMethodCallAnalyzer,
3430
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
3531
private readonly ClassDependencyManipulator $classDependencyManipulator,
32+
private readonly CommandConstructorDecorator $commandConstructorDecorator,
3633
) {
3734
}
3835

@@ -141,27 +138,8 @@ public function refactor(Node $node): ?Node
141138
$this->classDependencyManipulator->addConstructorDependency($class, $propertyMetadata);
142139
}
143140

144-
$this->decorateCommandConstructor($class);
141+
$this->commandConstructorDecorator->decorate($class);
145142

146143
return $node;
147144
}
148-
149-
private function decorateCommandConstructor(Class_ $class): void
150-
{
151-
// special case for command to keep parent constructor call
152-
if (! $this->isObjectType($class, new ObjectType('Symfony\Component\Console\Command\Command'))) {
153-
return;
154-
}
155-
156-
$constuctClassMethod = $class->getMethod(MethodName::CONSTRUCT);
157-
if (! $constuctClassMethod instanceof ClassMethod) {
158-
return;
159-
}
160-
161-
// empty stmts? add parent::__construct() to setup command
162-
if ((array) $constuctClassMethod->stmts === []) {
163-
$parentConstructStaticCall = new StaticCall(new Name('parent'), '__construct');
164-
$constuctClassMethod->stmts[] = new Expression($parentConstructStaticCall);
165-
}
166-
}
167145
}

src/Enum/SymfonyClass.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
final class SymfonyClass
88
{
9+
/**
10+
* @var string
11+
*/
12+
public const CONTROLLER = 'Symfony\Bundle\FrameworkBundle\Controller\Controller';
13+
914
/**
1015
* @var string
1116
*/

src/NodeAnalyzer/ServiceTypeMethodCallResolver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public function resolve(MethodCall $methodCall): ?Type
2828
return new MixedType();
2929
}
3030

31-
$argument = $methodCall->getArgs()[0]
32-
->value;
31+
$firstArg = $methodCall->getArgs()[0];
32+
$argument = $firstArg->value;
3333
$serviceMap = $this->serviceMapProvider->provide();
3434

3535
if ($argument instanceof String_) {

0 commit comments

Comments
 (0)