Skip to content

Commit

Permalink
[DependencyInjection] Extract GetByTypeMethodCallToConstructorInjecti…
Browse files Browse the repository at this point in the history
…onRector to make migration smoother
  • Loading branch information
TomasVotruba committed Dec 1, 2024
1 parent cd07337 commit 0a162c9
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector;

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

final class ControllerGetByTypeToConstructorInjectionRectorTest 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,36 @@
<?php

namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Fixture;

use Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source\SomeService;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

final class ControllerGetWithType extends Controller
{
public function configure()
{
$someType = $this->get(SomeService::class);
}
}

?>
-----
<?php

namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Fixture;

use Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source\SomeService;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

final class ControllerGetWithType extends Controller
{
public function __construct(private \Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source\SomeService $someService)
{
}
public function configure()
{
$someType = $this->someService;
}
}

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

namespace Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\Source;

final class SomeService
{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(
\Rector\Symfony\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector::class
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Rector\Symfony\DependencyInjection\NodeDecorator;

use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PHPStan\Type\ObjectType;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\ValueObject\MethodName;

final class CommandConstructorDecorator
{
public function __construct(
private NodeTypeResolver $nodeTypeResolver
) {
}

public function decorate(Class_ $class): void
{
// special case for command to keep parent constructor call
if (! $this->nodeTypeResolver->isObjectType(
$class,
new ObjectType('Symfony\Component\Console\Command\Command')
)) {
return;
}

$constuctClassMethod = $class->getMethod(MethodName::CONSTRUCT);
if (! $constuctClassMethod instanceof ClassMethod) {
return;
}

// empty stmts? add parent::__construct() to setup command
if ((array) $constuctClassMethod->stmts === []) {
$parentConstructStaticCall = new StaticCall(new Name('parent'), '__construct');
$constuctClassMethod->stmts[] = new Expression($parentConstructStaticCall);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

declare(strict_types=1);

namespace Rector\Symfony\DependencyInjection\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Reflection\ClassReflection;
use Rector\Naming\Naming\PropertyNaming;
use Rector\NodeManipulator\ClassDependencyManipulator;
use Rector\PHPStan\ScopeFetcher;
use Rector\PostRector\ValueObject\PropertyMetadata;
use Rector\Rector\AbstractRector;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Rector\Symfony\Enum\SymfonyClass;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Rector\Symfony\Tests\DependencyInjection\Rector\Class_\ControllerGetByTypeToConstructorInjectionRector\ControllerGetByTypeToConstructorInjectionRectorTest
*/
final class ControllerGetByTypeToConstructorInjectionRector extends AbstractRector
{
public function __construct(
private readonly ClassDependencyManipulator $classDependencyManipulator,
private readonly PropertyNaming $propertyNaming
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'From `$container->get(SomeType::class)` in controllers to constructor injection (step 1/x)',
[
new CodeSample(
<<<'CODE_SAMPLE'
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
final class SomeCommand extends Controller
{
public function someMethod()
{
$someType = $this->get(SomeType::class);
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
final class SomeCommand extends Controller
{
public function __construct(private SomeType $someType)
{
}
public function someMethod()
{
$someType = $this->someType;
}
}
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

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

$propertyMetadatas = [];

$this->traverseNodesWithCallable($node, function (Node $node) use (&$propertyMetadatas): ?Node {
if (! $node instanceof MethodCall) {
return null;
}

if ($node->isFirstClassCallable()) {
return null;
}

if (! $this->isName($node->name, 'get')) {
return null;
}

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

if (count($node->getArgs()) !== 1) {
return null;
}

$firstArg = $node->getArgs()[0];
if (! $firstArg->value instanceof ClassConstFetch) {
return null;
}

// must be class const fetch
if (! $this->isName($firstArg->value->name, 'class')) {
return null;
}

$className = $this->getName($firstArg->value->class);
if (! is_string($className)) {
return null;
}

$propertyName = $this->propertyNaming->fqnToVariableName($className);
$propertyMetadata = new PropertyMetadata($propertyName, new FullyQualifiedObjectType($className));

$propertyMetadatas[] = $propertyMetadata;
return $this->nodeFactory->createPropertyFetch('this', $propertyMetadata->getName());
});

if ($propertyMetadatas === []) {
return null;
}

foreach ($propertyMetadatas as $propertyMetadata) {
$this->classDependencyManipulator->addConstructorDependency($node, $propertyMetadata);
}

return $node;
}

private function shouldSkipClass(Class_ $class): bool
{
// keep it safe
if (! $class->isFinal()) {
return true;
}

$scope = ScopeFetcher::fetch($class);

$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return true;
}

return ! $classReflection->isSubclassOf(SymfonyClass::CONTROLLER);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PHPStan\Type\ObjectType;
use Rector\NodeManipulator\ClassDependencyManipulator;
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
use Rector\PostRector\ValueObject\PropertyMetadata;
use Rector\Rector\AbstractRector;
use Rector\Symfony\DependencyInjection\NodeDecorator\CommandConstructorDecorator;
use Rector\Symfony\NodeAnalyzer\DependencyInjectionMethodCallAnalyzer;
use Rector\ValueObject\MethodName;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

Expand All @@ -33,6 +29,7 @@ public function __construct(
private readonly DependencyInjectionMethodCallAnalyzer $dependencyInjectionMethodCallAnalyzer,
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
private readonly ClassDependencyManipulator $classDependencyManipulator,
private readonly CommandConstructorDecorator $commandConstructorDecorator,
) {
}

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

$this->decorateCommandConstructor($class);
$this->commandConstructorDecorator->decorate($class);

return $node;
}

private function decorateCommandConstructor(Class_ $class): void
{
// special case for command to keep parent constructor call
if (! $this->isObjectType($class, new ObjectType('Symfony\Component\Console\Command\Command'))) {
return;
}

$constuctClassMethod = $class->getMethod(MethodName::CONSTRUCT);
if (! $constuctClassMethod instanceof ClassMethod) {
return;
}

// empty stmts? add parent::__construct() to setup command
if ((array) $constuctClassMethod->stmts === []) {
$parentConstructStaticCall = new StaticCall(new Name('parent'), '__construct');
$constuctClassMethod->stmts[] = new Expression($parentConstructStaticCall);
}
}
}
5 changes: 5 additions & 0 deletions src/Enum/SymfonyClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

final class SymfonyClass
{
/**
* @var string
*/
public const CONTROLLER = 'Symfony\Bundle\FrameworkBundle\Controller\Controller';

/**
* @var string
*/
Expand Down
4 changes: 2 additions & 2 deletions src/NodeAnalyzer/ServiceTypeMethodCallResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public function resolve(MethodCall $methodCall): ?Type
return new MixedType();
}

$argument = $methodCall->getArgs()[0]
->value;
$firstArg = $methodCall->getArgs()[0];
$argument = $firstArg->value;
$serviceMap = $this->serviceMapProvider->provide();

if ($argument instanceof String_) {
Expand Down

0 comments on commit 0a162c9

Please sign in to comment.