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