[Symfony] add CommandToConstructorInjectionRector

This commit is contained in:
TomasVotruba 2017-09-02 15:04:19 +02:00
parent 84bcd3bbb4
commit 3142225473
6 changed files with 252 additions and 178 deletions

View File

@ -13,4 +13,9 @@ final class SetNames
* @var string
*/
public const SYMFONY = 'Symfony';
/**
* @var string
*/
public const SYMFONY_EXTRA = 'SymfonyExtra';
}

View File

@ -1,178 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\Rector\Contrib\Symfony;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use Rector\Builder\Class_\ClassPropertyCollector;
use Rector\Builder\Kernel\ServiceFromKernelResolver;
use Rector\Builder\Naming\NameResolver;
use Rector\Deprecation\SetNames;
use Rector\Rector\AbstractRector;
use Rector\Tests\Rector\Contrib\Symfony\GetterToPropertyRector\Source\LocalKernel;
/**
* Converts all:
* $this->get('some_service') # where "some_service" is name of the service in container.
*
* into:
* $this->someService # where "someService" is type of the service
*/
final class GetterToPropertyRector extends AbstractRector
{
/**
* @var string
*/
private $className;
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var ServiceFromKernelResolver
*/
private $serviceFromKernelResolver;
/**
* @var ClassPropertyCollector
*/
private $classPropertyCollector;
public function __construct(
NameResolver $nameResolver,
ServiceFromKernelResolver $serviceFromKernelResolver,
ClassPropertyCollector $classPropertyCollector
) {
$this->nameResolver = $nameResolver;
$this->serviceFromKernelResolver = $serviceFromKernelResolver;
$this->classPropertyCollector = $classPropertyCollector;
}
/**
* @param Node[] $nodes
* @return null|Node[]
*/
public function beforeTraverse(array $nodes): ?array
{
$this->className = null;
foreach ($nodes as $node) {
if ($node instanceof Class_) {
$this->className = (string) $node->name;
}
}
return null;
}
public function isCandidate(Node $node): bool
{
// $var = $this->get('some_service');
// $var = $this->get('some_service')->getData();
if ($node instanceof Assign && ($node->expr instanceof MethodCall || $node->var instanceof MethodCall)) {
if ($this->isContainerGetCall($node->expr)) {
return true;
}
}
// ['var => $this->get('some_service')->getData()]
if ($node instanceof MethodCall && $node->var instanceof MethodCall) {
if ($this->isContainerGetCall($node->var)) {
return true;
}
}
return false;
}
public function refactor(Node $assignOrMethodCallNode): ?Node
{
if ($assignOrMethodCallNode instanceof Assign) {
$refactoredMethodCall = $this->processMethodCallNode($assignOrMethodCallNode->expr);
if ($refactoredMethodCall) {
$assignOrMethodCallNode->expr = $refactoredMethodCall;
}
}
if ($assignOrMethodCallNode instanceof MethodCall) {
$refactoredMethodCall = $this->processMethodCallNode($assignOrMethodCallNode->var);
if ($refactoredMethodCall) {
$assignOrMethodCallNode->var = $refactoredMethodCall;
}
}
return $assignOrMethodCallNode;
}
public function getSetName(): string
{
return SetNames::SYMFONY;
}
public function sinceVersion(): float
{
return 3.3;
}
/**
* Is "$this->get('string')" statements?
*/
private function isContainerGetCall(MethodCall $methodCall): bool
{
if ($methodCall->var->name !== 'this') {
return false;
}
if ((string) $methodCall->name !== 'get') {
return false;
}
if (! $methodCall->args[0]->value instanceof String_) {
return false;
}
return true;
}
private function processMethodCallNode(MethodCall $methodCall): ?PropertyFetch
{
/** @var String_ $argument */
$argument = $methodCall->args[0]->value;
$serviceName = $argument->value;
$serviceType = $this->serviceFromKernelResolver->resolveServiceClassByNameFromKernel(
$serviceName,
LocalKernel::class
);
if ($serviceType === null) {
return null;
}
$propertyName = $this->nameResolver->resolvePropertyNameFromType($serviceType);
$this->classPropertyCollector->addPropertyForClass($this->className, $serviceType, $propertyName);
return $this->createPropertyFetch($propertyName);
}
/**
* Creates "$this->propertyName".
*/
private function createPropertyFetch(string $propertyName): PropertyFetch
{
return new PropertyFetch(
new Variable('this', [
'name' => $propertyName,
]),
$propertyName
);
}
}

View File

@ -0,0 +1,192 @@
<?php declare(strict_types=1);
namespace Rector\Rector\Contrib\SymfonyExtra;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use Rector\Builder\Class_\ClassPropertyCollector;
use Rector\Builder\Kernel\ServiceFromKernelResolver;
use Rector\Builder\Naming\NameResolver;
use Rector\Deprecation\SetNames;
use Rector\Rector\AbstractRector;
use Rector\Tests\Rector\Contrib\SymfonyExtra\GetterToPropertyRector\Source\LocalKernel;
use Symfony\Component\Console\Command\Command;
/**
* Ref: https://github.com/symfony/symfony/blob/master/UPGRADE-4.0.md#console
*
* Similar to @see \Rector\Rector\Contrib\Symfony\GetterToPropertyRector
* @todo Extract common logic!
*
* Before:
* class MyCommand extends ContainerAwareCommand
*
* $this->getContainer()->get('some_service');
*
* After:
* class MyCommand extends Command
*
* public function construct(SomeService $someService)
* {
* $this->someService = $someService;
* }
*
* ...
*
* $this->someService
*/
final class CommandToConstructorInjectionRector extends AbstractRector
{
/**
* @var ServiceFromKernelResolver
*/
private $serviceFromKernelResolver;
/**
* @var ClassPropertyCollector
*/
private $classPropertyCollector;
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var Class_
*/
private $classNode;
public function __construct(
ServiceFromKernelResolver $serviceFromKernelResolver,
ClassPropertyCollector $classPropertyCollector,
NameResolver $nameResolver
) {
$this->serviceFromKernelResolver = $serviceFromKernelResolver;
$this->classPropertyCollector = $classPropertyCollector;
$this->nameResolver = $nameResolver;
}
public function getSetName(): string
{
return SetNames::SYMFONY_EXTRA;
}
public function sinceVersion(): float
{
return 3.3;
}
/**
* @todo add node traverser for this or to AbstractRector
* @param Node[] $nodes
* @return null|Node[]
*/
public function beforeTraverse(array $nodes): ?array
{
$this->classNode = null;
foreach ($nodes as $node) {
if ($node instanceof Class_) {
$this->classNode = $node;
break;
}
}
return null;
}
public function isCandidate(Node $node): bool
{
if (! Strings::endsWith($this->getClassName(), 'Command')) {
return false;
}
// $this->getContainer()->get('some_service');
if (! $node instanceof MethodCall) {
return false;
}
if (! $node->var instanceof MethodCall) {
return false;
}
if ((string) $node->var->var->name !== 'this') {
return false;
}
if ((string) $node->name !== 'get') {
return false;
}
if (! isset($node->args[0])) {
return false;
}
if (! $node->args[0]->value instanceof String_) {
return false;
}
return true;
}
/**
* @param MethodCall $node
*/
public function refactor(Node $node): ?Node
{
$this->replaceParentContainerAwareCommandWithCommand();
$serviceName = $node->args[0]->value->value;
$serviceType = $this->serviceFromKernelResolver->resolveServiceClassByNameFromKernel(
$serviceName,
LocalKernel::class
);
if ($serviceType === null) {
return null;
}
$propertyName = $this->nameResolver->resolvePropertyNameFromType($serviceType);
$this->classPropertyCollector->addPropertyForClass($this->getClassName(), $serviceType, $propertyName);
return $this->createPropertyFetch($propertyName);
}
/**
* @todo move to NodeFactory...
* Creates "$this->propertyName".
*/
private function createPropertyFetch(string $propertyName): PropertyFetch
{
return new PropertyFetch(
new Node\Expr\Variable('this', [
'name' => $propertyName,
]),
$propertyName
);
}
/**
* @todo move to parent class?
*/
private function getClassName(): string
{
return $this->classNode->namespacedName->toString();
}
private function replaceParentContainerAwareCommandWithCommand(): void
{
$this->classNode->extends = new Name('\Symfony\Component\Console\Command\Command');
}
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
final class MyCommand extends \Symfony\Component\Console\Command\Command
{
/**
* @var stdClass
*/
private $stdClass;
public function __construct(stdClass $stdClass)
{
$this->stdClass = $stdClass;
}
protected function execute()
{
$someService = $this->stdClass;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace Rector\Tests\Rector\Contrib\Symfony\CommandToConstructorInjectionRector;
use Rector\Rector\Contrib\SymfonyExtra\CommandToConstructorInjectionRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class Test extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFileMatchesExpectedContent(
__DIR__ . '/Wrong/wrong.php.inc',
__DIR__ . '/Correct/correct.php.inc'
);
}
/**
* @return string[]
*/
protected function getRectorClasses(): array
{
return [CommandToConstructorInjectionRector::class];
}
}

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
final class MyCommand extends ContainerAwareCommand
{
protected function execute()
{
$someService = $this->getContainer()->get('some_service');
}
}