diff --git a/config/set/nette/kdyby-events-to-contributte-event-dispatcher.yaml b/config/set/nette/kdyby-events-to-contributte-event-dispatcher.yaml index 1a1e12a9978c..fad214e7c8e4 100644 --- a/config/set/nette/kdyby-events-to-contributte-event-dispatcher.yaml +++ b/config/set/nette/kdyby-events-to-contributte-event-dispatcher.yaml @@ -6,3 +6,4 @@ services: Kdyby\Events\Subscriber: 'Symfony\Component\EventDispatcher\EventSubscriberInterface' Rector\NetteKdyby\Rector\Class_\KdybyEventSubscriberToContributteEventSubscriberRector: null + Rector\NetteKdyby\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector: null diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index a685986b7771..8effba601ac5 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -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
+### `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); + } + } +``` + +
+ ## NetteTesterToPHPUnit ### `NetteAssertToPHPUnitAssertRector` @@ -8322,6 +8345,23 @@ Change local property used in single method to local variable
+### `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; + } +``` + +
+ ### `PrivatizeLocalClassConstantRector` - class: [`Rector\Privatization\Rector\ClassConst\PrivatizeLocalClassConstantRector`](/../master/rules/privatization/src/Rector/ClassConst/PrivatizeLocalClassConstantRector.php) diff --git a/rules/nette-kdyby/src/Naming/EventClassNaming.php b/rules/nette-kdyby/src/Naming/EventClassNaming.php new file mode 100644 index 000000000000..13e91a3ad8cc --- /dev/null +++ b/rules/nette-kdyby/src/Naming/EventClassNaming.php @@ -0,0 +1,57 @@ +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'; + } +} diff --git a/rules/nette-kdyby/src/NodeFactory/CustomEventFactory.php b/rules/nette-kdyby/src/NodeFactory/CustomEventFactory.php new file mode 100644 index 000000000000..85dbce03a806 --- /dev/null +++ b/rules/nette-kdyby/src/NodeFactory/CustomEventFactory.php @@ -0,0 +1,139 @@ +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 Class_($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) > 0) { + $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(); + } +} diff --git a/rules/nette-kdyby/src/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector.php b/rules/nette-kdyby/src/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector.php new file mode 100644 index 000000000000..d2367c4c334d --- /dev/null +++ b/rules/nette-kdyby/src/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector.php @@ -0,0 +1,211 @@ +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; + } +} diff --git a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Fixture/fixture.php.inc b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..9b18a845166a --- /dev/null +++ b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Fixture/fixture.php.inc @@ -0,0 +1,38 @@ +onUpload($user); + } +} + +?> +----- +eventDispatcher = $eventDispatcher; + } + public function run(User $user) + { + $fileManagerUploadEvent = new \Rector\NetteKdyby\Tests\Rector\MethodCall\ReplaceMagicPropertyEventWithEventClassRector\Fixture\Event\FileManagerUploadEvent($user); + $this->eventDispatcher->dispatch($fileManagerUploadEvent); + } +} + +?> diff --git a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php new file mode 100644 index 000000000000..595fc547c317 --- /dev/null +++ b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/ReplaceMagicPropertyEventWithEventClassRectorTest.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Source/ExpectedClass.php b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Source/ExpectedClass.php new file mode 100644 index 000000000000..bb440b87c238 --- /dev/null +++ b/rules/nette-kdyby/tests/Rector/MethodCall/ReplaceMagicPropertyEventWithEventClassRector/Source/ExpectedClass.php @@ -0,0 +1,16 @@ +user = $user; + } + public function user() + { + return $this->user; + } +} diff --git a/src/Testing/PHPUnit/AbstractRectorTestCase.php b/src/Testing/PHPUnit/AbstractRectorTestCase.php index 9a527597159e..a40c6845b2df 100644 --- a/src/Testing/PHPUnit/AbstractRectorTestCase.php +++ b/src/Testing/PHPUnit/AbstractRectorTestCase.php @@ -35,6 +35,11 @@ abstract class AbstractRectorTestCase extends AbstractGenericRectorTestCase */ protected $parameterProvider; + /** + * @var string + */ + protected $originalTempFile; + /** * @var bool */ @@ -132,6 +137,8 @@ protected function doTestFile(string $fixtureFile): void $changedFile, $smartFileInfo->getRelativeFilePathFromCwd() ); + + $this->originalTempFile = $originalFile; } protected function getTempPath(): string