[NetteKdyby] Add ReplaceMagicPropertyEventWithEventClassRector

This commit is contained in:
TomasVotruba 2020-05-24 22:46:52 +02:00
parent 5d0eeab516
commit 47e163dbe7
9 changed files with 546 additions and 3 deletions

View File

@ -6,3 +6,4 @@ services:
Kdyby\Events\Subscriber: 'Symfony\Component\EventDispatcher\EventSubscriberInterface'
Rector\NetteKdyby\Rector\Class_\KdybyEventSubscriberToContributteEventSubscriberRector: null
Rector\NetteKdyby\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector: null

View File

@ -1,4 +1,4 @@
# All 488 Rectors Overview
# All 490 Rectors Overview
- [Projects](#projects)
- [General](#general)
@ -26,7 +26,7 @@
- [MysqlToMysqli](#mysqltomysqli) (4)
- [Naming](#naming) (1)
- [Nette](#nette) (12)
- [NetteKdyby](#nettekdyby) (1)
- [NetteKdyby](#nettekdyby) (2)
- [NetteTesterToPHPUnit](#nettetestertophpunit) (3)
- [NetteToSymfony](#nettetosymfony) (9)
- [Order](#order) (3)
@ -51,7 +51,7 @@
- [PhpDeglobalize](#phpdeglobalize) (1)
- [PhpSpecToPHPUnit](#phpspectophpunit) (7)
- [Polyfill](#polyfill) (2)
- [Privatization](#privatization) (5)
- [Privatization](#privatization) (6)
- [Refactoring](#refactoring) (2)
- [RemovingStatic](#removingstatic) (4)
- [Renaming](#renaming) (10)
@ -4626,6 +4626,29 @@ Change EventSubscriber from Kdyby to Contributte
<br>
### `ReplaceMagicPropertyEventWithEventClassRector`
- class: [`Rector\NetteKdyby\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector`](/../master/rules/nette-kdyby/src/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector.php)
- [test fixtures](/../master/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Fixture)
Change $onProperty magic call with event disptacher and class dispatch
```diff
final class FileManager
{
- public $onUpload;
-
public function run(User $user)
{
- $this->onUpload($user);
+ $onFileManagerUploadEvent = new FileManagerUploadEvent($user);
+ $this->eventDispatcher->dispatch($onFileManagerUploadEvent);
}
}
```
<br>
## NetteTesterToPHPUnit
### `NetteAssertToPHPUnitAssertRector`
@ -8322,6 +8345,23 @@ Change local property used in single method to local variable
<br>
### `PrivatizeFinalClassPropertyRector`
- class: [`Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector`](/../master/rules/privatization/src/Rector/Property/PrivatizeFinalClassPropertyRector.php)
- [test fixtures](/../master/rules/privatization/tests/Rector/Property/PrivatizeFinalClassPropertyRector/Fixture)
Change property to private if possible
```diff
final class SomeClass
{
- protected $value;
+ private $value;
}
```
<br>
### `PrivatizeLocalClassConstantRector`
- class: [`Rector\Privatization\Rector\ClassConst\PrivatizeLocalClassConstantRector`](/../master/rules/privatization/src/Rector/ClassConst/PrivatizeLocalClassConstantRector.php)

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Rector\NetteKdyby\Naming;
use Nette\Utils\Strings;
use PhpParser\Node\Expr\MethodCall;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\SmartFileSystem\SmartFileInfo;
final class EventClassNaming
{
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
/**
* @var ClassNaming
*/
private $classNaming;
public function __construct(NodeNameResolver $nodeNameResolver, ClassNaming $classNaming)
{
$this->nodeNameResolver = $nodeNameResolver;
$this->classNaming = $classNaming;
}
public function getShortEventClassName(MethodCall $methodCall): string
{
/** @var string $methodName */
$methodName = $this->nodeNameResolver->getName($methodCall->name);
/** @var string $className */
$className = $methodCall->getAttribute(AttributeKey::CLASS_NAME);
$shortClassName = $this->classNaming->getShortName($className);
// "onMagic" => "Magic"
$shortPropertyName = Strings::substring($methodName, strlen('on'));
return $shortClassName . $shortPropertyName . 'Event';
}
public function resolveEventFileLocation(MethodCall $methodCall): string
{
$shortEventClassName = $this->getShortEventClassName($methodCall);
/** @var SmartFileInfo $fileInfo */
$fileInfo = $methodCall->getAttribute(AttributeKey::FILE_INFO);
return $fileInfo->getPath() . DIRECTORY_SEPARATOR . 'Event' . DIRECTORY_SEPARATOR . $shortEventClassName . '.php';
}
}

View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Rector\NetteKdyby\NodeFactory;
use Nette\Utils\Strings;
use PhpParser\Builder\Class_ as ClassBuilder;
use PhpParser\Builder\Method;
use PhpParser\Builder\Namespace_ as NamespaceBuilder;
use PhpParser\Builder\Property as PropertyBuilder;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\Return_;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\Core\Exception\NotImplementedException;
use Rector\NodeNameResolver\NodeNameResolver;
final class CustomEventFactory
{
/**
* @var ClassNaming
*/
private $classNaming;
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(ClassNaming $classNaming, NodeNameResolver $nodeNameResolver)
{
$this->classNaming = $classNaming;
$this->nodeNameResolver = $nodeNameResolver;
}
/**
* @param Arg[] $args
*/
public function create(string $className, array $args): Namespace_
{
$namespace = Strings::before($className, '\\', -1);
$namespaceBuilder = new NamespaceBuilder($namespace);
$shortClassName = $this->classNaming->getShortName($className);
$classBuilder = new ClassBuilder($shortClassName);
$classBuilder->makeFinal();
$classBuilder->extend(new FullyQualified('Symfony\Contracts\EventDispatcher\Event'));
// 1. add __construct if args?
// 2. add getters
// 3. add property
if (count($args)) {
$methodBuilder = $this->createConstructClassMethod($args);
$classBuilder->addStmt($methodBuilder);
// add properties
foreach ($args as $arg) {
$property = $this->createProperty($arg);
$classBuilder->addStmt($property);
}
// add getters
foreach ($args as $arg) {
$getterClassMethod = $this->createGetterClassMethod($arg);
$classBuilder->addStmt($getterClassMethod);
}
}
$class = $classBuilder->getNode();
$namespaceBuilder->addStmt($class);
return $namespaceBuilder->getNode();
}
/**
* @param Arg[] $args
*/
private function createConstructClassMethod(array $args): ClassMethod
{
$methodBuilder = new Method('__construct');
$methodBuilder->makePublic();
foreach ($args as $arg) {
$paramName = $this->nodeNameResolver->getName($arg->value);
if ($paramName === null) {
// @todo
throw new NotImplementedException();
}
$param = new Param(new Variable($paramName));
$methodBuilder->addParam($param);
$assign = new Assign(new PropertyFetch(new Variable('this'), $paramName), new Variable($paramName));
$methodBuilder->addStmt($assign);
}
return $methodBuilder->getNode();
}
private function createProperty(Arg $arg): Property
{
$paramName = $this->nodeNameResolver->getName($arg->value);
if ($paramName === null) {
// @todo
throw new NotImplementedException();
}
$propertyBuilder = new PropertyBuilder($paramName);
$propertyBuilder->makePrivate();
return $propertyBuilder->getNode();
}
private function createGetterClassMethod(Arg $arg): ClassMethod
{
$paramName = $this->nodeNameResolver->getName($arg->value);
if ($paramName === null) {
// @todo
throw new NotImplementedException();
}
$methodBuilder = new Method($paramName);
$return = new Return_(new PropertyFetch(new Variable('this'), $paramName));
$methodBuilder->addStmt($return);
$methodBuilder->makePublic();
return $methodBuilder->getNode();
}
}

View File

@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Rector\NetteKdyby\Rector\MethodCall;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\NetteKdyby\Naming\EventClassNaming;
use Rector\NetteKdyby\NodeFactory\CustomEventFactory;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStan\Type\FullyQualifiedObjectType;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* @see \Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector\ReplaceMagicPropertyEventWithEventClassRectorTest
*/
final class ReplaceMagicPropertyEventWithEventClassRector extends AbstractRector
{
/**
* @var EventClassNaming
*/
private $eventClassNaming;
/**
* @var CustomEventFactory
*/
private $customEventFactory;
/**
* @var ClassNaming
*/
private $classNaming;
public function __construct(
EventClassNaming $eventClassNaming,
CustomEventFactory $customEventFactory,
ClassNaming $classNaming
) {
$this->eventClassNaming = $eventClassNaming;
$this->customEventFactory = $customEventFactory;
$this->classNaming = $classNaming;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Change $onProperty magic call with event disptacher and class dispatch', [
new CodeSample(
<<<'PHP'
final class FileManager
{
public $onUpload;
public function run(User $user)
{
$this->onUpload($user);
}
}
PHP
,
<<<'PHP'
final class FileManager
{
public function run(User $user)
{
$onFileManagerUploadEvent = new FileManagerUploadEvent($user);
$this->eventDispatcher->dispatch($onFileManagerUploadEvent);
}
}
PHP
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [MethodCall::class];
}
/**
* @param MethodCall $node
*/
public function refactor(Node $node): ?Node
{
// 1. is onProperty? call
if (! $this->isLocalOnPropertyCall($node)) {
return null;
}
// 2. guess event name
$eventClassName = $this->createEventClassName($node);
$eventFileLocation = $this->eventClassNaming->resolveEventFileLocation($node);
// 3. create new event class with args
$eventClass = $this->customEventFactory->create($eventClassName, (array) $node->args);
$this->printToFile($eventClass, $eventFileLocation);
// 4. ad disatch method call
$dispatchMethodCall = $this->createDispatchMethodCall($eventClassName);
$this->addNodeAfterNode($dispatchMethodCall, $node);
// 5. return evnet addign
// add event dispathcer depdency if needed
$assign = $this->createEventInstanceAssign($eventClassName, $node);
/** @var Class_ $class */
$class = $node->getAttribute(AttributeKey::CLASS_NODE);
$this->addPropertyToClass($class, new FullyQualifiedObjectType(EventDispatcher::class), 'eventDispatcher');
// 6. remove property
$this->removeMagicProperty($node);
return $assign;
}
private function isLocalOnPropertyCall(MethodCall $methodCall): bool
{
if (! $this->isName($methodCall->var, 'this')) {
return false;
}
if (! $this->isName($methodCall->name, 'on*')) {
return false;
}
$methodName = $this->getName($methodCall->name);
if ($methodName === null) {
return false;
}
$className = $methodCall->getAttribute(AttributeKey::CLASS_NAME);
if ($className === null) {
return false;
}
if (method_exists($className, $methodName)) {
return false;
}
return property_exists($className, $methodName);
}
/**
* "App\SomeNamespace\SomeClass"
*
* "App\SomeNamespace\Event\SomeClassUploadEvent"
*/
private function createEventClassName(MethodCall $methodCall): string
{
$shortEventClassName = $this->eventClassNaming->getShortEventClassName($methodCall);
/** @var string $className */
$className = $methodCall->getAttribute(AttributeKey::CLASS_NAME);
$namespaceAbove = Strings::before($className, '\\', -1);
return $namespaceAbove . '\\Event\\' . $shortEventClassName;
}
private function removeMagicProperty(MethodCall $methodCall): void
{
/** @var string $methodName */
$methodName = $this->getName($methodCall->name);
/** @var Class_ $class */
$class = $methodCall->getAttribute(AttributeKey::CLASS_NODE);
/** @var Property $property */
$property = $class->getProperty($methodName);
$this->removeNode($property);
}
private function createEventInstanceAssign(string $eventClassName, MethodCall $methodCall): Assign
{
$shortEventClassName = $this->classNaming->getVariableName($eventClassName);
$new = new New_(new FullyQualified($eventClassName));
if ($methodCall->args) {
$new->args = $methodCall->args;
}
return new Assign(new Variable($shortEventClassName), $new);
}
private function createDispatchMethodCall(string $eventClassName): MethodCall
{
$shortEventClassName = $this->classNaming->getVariableName($eventClassName);
$eventDispatcherPropertyFetch = new PropertyFetch(new Variable('this'), 'eventDispatcher');
$dispatchMethodCall = new MethodCall($eventDispatcherPropertyFetch, 'dispatch');
$dispatchMethodCall->args[] = new Arg(new Variable($shortEventClassName));
return $dispatchMethodCall;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector\Fixture;
final class FileManager
{
public $onUpload;
public function run(User $user)
{
$this->onUpload($user);
}
}
?>
-----
<?php
namespace Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector\Fixture;
final class FileManager
{
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcher
*/
private $eventDispatcher;
public function __construct(\Symfony\Component\EventDispatcher\EventDispatcher $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function run(User $user)
{
$fileManagerUploadEvent = new \Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector\Fixture\Event\FileManagerUploadEvent($user);
$this->eventDispatcher->dispatch($fileManagerUploadEvent);
}
}
?>

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector;
use Iterator;
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\NetteKdyby\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector;
final class ReplaceMagicPropertyEventWithEventClassRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(string $file): void
{
$this->doTestFile($file);
$expectedEventFilePath = dirname($this->originalTempFile) . '/Event/FileManagerUploadEvent.php';
$this->assertFileExists($expectedEventFilePath);
$this->assertFileEquals(__DIR__ . '/Source/ExpectedClass.php', $expectedEventFilePath);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return ReplaceMagicPropertyEventWithEventClassRector::class;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector\Fixture\Event;
final class FileManagerUploadEvent extends \Symfony\Contracts\EventDispatcher\Event
{
private $user;
public function __construct($user)
{
$this->user = $user;
}
public function user()
{
return $this->user;
}
}

View File

@ -35,6 +35,11 @@ abstract class AbstractRectorTestCase extends AbstractGenericRectorTestCase
*/
protected $parameterProvider;
/**
* @var string
*/
protected $originalTempFile;
/**
* @var bool
*/
@ -132,6 +137,8 @@ abstract class AbstractRectorTestCase extends AbstractGenericRectorTestCase
$changedFile,
$smartFileInfo->getRelativeFilePathFromCwd()
);
$this->originalTempFile = $originalFile;
}
protected function getTempPath(): string