diff --git a/src/Decorators/KeepAnnotatedMethodsDecorator.php b/src/Decorators/KeepAnnotatedMethodsDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..ee6c7d85fabb6df3d8b7eb5ac2b2b029e6b45a7c --- /dev/null +++ b/src/Decorators/KeepAnnotatedMethodsDecorator.php @@ -0,0 +1,99 @@ +<?php declare(strict_types=1); + +namespace Grifart\ClassScaffolder\Decorators; + +use Grifart\ClassScaffolder\Definition\ClassDefinition; +use Grifart\ClassScaffolder\KeepMethod; +use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Method; +use Nette\PhpGenerator\PhpNamespace; +use Nette\Utils\Strings; + + +final class KeepAnnotatedMethodsDecorator implements ClassDecorator +{ + public function decorate(PhpNamespace $classToBeGeneratedNamespace, ClassType $classToBeGenerated, ClassDefinition $definition): void + { + $alreadyExistingClass = self::getAlreadyExistingClass($definition); + if ($alreadyExistingClass === null) { + return; + } + + foreach ($alreadyExistingClass->getMethods() as $existingMethod) { + foreach ($existingMethod->getAttributes() as $attribute) { + if ($attribute->getName() === KeepMethod::class) { + self::transferMethod($classToBeGeneratedNamespace, $classToBeGenerated, $existingMethod); + break; // continue to next method + } + } + } + } + + private static function transferMethod(PhpNamespace $targetClassNamespace, ClassType $targetClass, Method $methodToBeTransferred): void + { + $targetClassNamespace->addUse(KeepMethod::class); + self::addClassesUsedInMethodToUses($methodToBeTransferred, $targetClassNamespace); + + $targetClass->setMethods([ + ...\array_values($targetClass->getMethods()), + $methodToBeTransferred, + ]); + } + + private static function getAlreadyExistingClass(ClassDefinition $definition): ?ClassType + { + $namespace = $definition->getNamespaceName(); + $classFqn = ($namespace === null ? '' : $namespace) . '\\' . $definition->getClassName(); + + if ( ! \class_exists($classFqn)) { + return null; + } + + return ClassType::withBodiesFrom($classFqn); + } + + + /* + * Converts FQNs in parameters and method body to class name only. + */ + private static function addClassesUsedInMethodToUses(Method $method, PhpNamespace $namespace): void + { + /* + * Parameters. + * So that `fromDTO(\Path\To\DTO $dto)` becomes `fromDTO(DTO $dto)` + */ + foreach ($method->getParameters() as $parameter) { + $type = $parameter->getType(); + if ($type !== null && (\class_exists($type) || \interface_exists($type))) { + $namespace->addUse($type); + } + } + + /* + * Body class usages. + * So that `\Path\To\CampaignRole::ROLE_MANAGER` becomes `CampaignRole::ROLE_MANAGER` + */ + $body = $method->getBody(); + if ($body === null) { + return; + } + + // find all FQN classes + $usedClasses = Strings::matchAll($body, '/(\\\\([\\\\\w]+))/'); // search for \A\B\C + foreach ($usedClasses as $match) { + $usedClass = $match[2]; // A\B\C + if (\class_exists($usedClass)) { + // add to uses + $namespace->addUse($usedClass); + + // replace FQN with just class name + $a = \explode('\\', $usedClass); + $b = \array_pop($a); // C + \assert($b !== null, 'Array can not be empty.'); + $body = \str_replace($match[1], $b, $body); // \A\B\C -> C + } + } + + $method->setBody($body); + } +} diff --git a/src/Decorators/KeepMethodDecorator.php b/src/Decorators/KeepMethodDecorator.php index b9a34af1ee95bf7abf1c623a124bfa9218308d72..0cfd2c8f6b679dd3ee78b69c24bafb7ce317b098 100644 --- a/src/Decorators/KeepMethodDecorator.php +++ b/src/Decorators/KeepMethodDecorator.php @@ -18,16 +18,13 @@ final class KeepMethodDecorator implements ClassDecorator $this->methodToBeKept = $methodName; } - public function decorate(PhpNamespace $namespace, ClassType $classToBeGenerated, ClassDefinition $definition): void + public function decorate(PhpNamespace $classToBeGeneratedNamespace, ClassType $classToBeGenerated, ClassDefinition $definition): void { - $alreadyExistentClass = self::getAlreadyExistentClass($definition); - - $classToBeGeneratedNamespace = $classToBeGenerated->getNamespace(); - \assert($classToBeGeneratedNamespace !== NULL, 'Class Generator always generates class in namespace.'); + $alreadyExistingClass = self::getAlreadyExistingClass($definition); // method already exists, just transfer it to new class - if ($alreadyExistentClass !== null && $alreadyExistentClass->hasMethod($this->methodToBeKept)) { - $keptMethod = $alreadyExistentClass->getMethod($this->methodToBeKept); + if ($alreadyExistingClass !== null && $alreadyExistingClass->hasMethod($this->methodToBeKept)) { + $keptMethod = $alreadyExistingClass->getMethod($this->methodToBeKept); self::addClassesUsedInMethodToUses($keptMethod, $classToBeGeneratedNamespace); @@ -55,7 +52,7 @@ final class KeepMethodDecorator implements ClassDecorator ); } - private static function getAlreadyExistentClass(ClassDefinition $definition): ?ClassType + private static function getAlreadyExistingClass(ClassDefinition $definition): ?ClassType { $namespace = $definition->getNamespaceName(); $classFqn = ($namespace === null ? '' : $namespace) . '\\' . $definition->getClassName(); diff --git a/src/KeepMethod.php b/src/KeepMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..73bdd738fa7fd42b5b2fd4ac68e59d51799cb7f7 --- /dev/null +++ b/src/KeepMethod.php @@ -0,0 +1,10 @@ +<?php declare(strict_types = 1); + +namespace Grifart\ClassScaffolder; + +use Attribute; + + +#[Attribute(Attribute::TARGET_METHOD)] +final class KeepMethod +{} diff --git a/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.overwritten.phps b/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.overwritten.phps new file mode 100644 index 0000000000000000000000000000000000000000..e945e540e0696688246794db6982a32729e051b1 --- /dev/null +++ b/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.overwritten.phps @@ -0,0 +1,5 @@ +namespace Grifart\ClassScaffolder\Test\KeepAnnotatedMethodsDecorator\Stub; + +final class StubKeepMethod +{ +} diff --git a/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.php b/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..567f2d1ee872526a262e678576945eddba74e7f7 --- /dev/null +++ b/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.php @@ -0,0 +1,42 @@ +<?php declare(strict_types=1); + +namespace Grifart\ClassScaffolder\Test\KeepAnnotatedMethodsDecorator\Stub; + +use Grifart\ClassScaffolder\Definition\ClassDefinition; +use Grifart\ClassScaffolder\Definition\ClassDefinitionBuilder; +use Grifart\ClassScaffolder\KeepMethod; + + +final class StubKeepMethod +{ + + #[KeepMethod] + public function methodToBeKept(): mixed + { + return 'whatever'; + } + + #[KeepMethod] + public function methodToBeKeptWithParam(int $whatever): void {} + + #[KeepMethod] + public function methodToBeKeptWithMixedParam(mixed $whatever): void {} + + #[KeepMethod] + public function methodToBeKeptWithImportedUses(ClassDefinitionBuilder $builder): string { + return ClassDefinition::class; + } + + /** + * Php doc! + * @return void + * @throws \Throwable + */ + #[KeepMethod] + public function methodToBeKeptWithAnnotation(): void { + } + + public function methodToBeRemoved(): void + {} + +} diff --git a/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.preserved.phps b/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.preserved.phps new file mode 100644 index 0000000000000000000000000000000000000000..879d1ec5cf48a7dfaee82bc9b671756160007fda --- /dev/null +++ b/tests/KeepAnnotatedMethodsDecorator/Stub/StubKeepMethod.preserved.phps @@ -0,0 +1,44 @@ +namespace Grifart\ClassScaffolder\Test\KeepAnnotatedMethodsDecorator\Stub; + +use Grifart\ClassScaffolder\Definition\ClassDefinition; +use Grifart\ClassScaffolder\Definition\ClassDefinitionBuilder; +use Grifart\ClassScaffolder\KeepMethod; + +final class StubKeepMethod +{ + #[KeepMethod] + public function methodToBeKept(): mixed + { + return 'whatever'; + } + + + #[KeepMethod] + public function methodToBeKeptWithParam(int $whatever): void + { + } + + + #[KeepMethod] + public function methodToBeKeptWithMixedParam(mixed $whatever): void + { + } + + + #[KeepMethod] + public function methodToBeKeptWithImportedUses(ClassDefinitionBuilder $builder): string + { + return ClassDefinition::class; + } + + + /** + * Php doc! + * @return void + * @throws \Throwable + */ + #[KeepMethod] + public function methodToBeKeptWithAnnotation(): void + { + } +} diff --git a/tests/KeepAnnotatedMethodsDecorator/keepAnnotatedMethodsDecorator.phpt b/tests/KeepAnnotatedMethodsDecorator/keepAnnotatedMethodsDecorator.phpt new file mode 100644 index 0000000000000000000000000000000000000000..fa8a5e4b6120ca9070ea1dda5937224739be76f8 --- /dev/null +++ b/tests/KeepAnnotatedMethodsDecorator/keepAnnotatedMethodsDecorator.phpt @@ -0,0 +1,54 @@ +<?php declare(strict_types=1); + +namespace Grifart\ClassScaffolder\Test; + +use Grifart\ClassScaffolder\ClassGenerator; +use Grifart\ClassScaffolder\Decorators\ClassDecorator; +use Grifart\ClassScaffolder\Decorators\KeepAnnotatedMethodsDecorator; +use Grifart\ClassScaffolder\Definition\ClassDefinition; +use Grifart\ClassScaffolder\Definition\Field; +use Grifart\ClassScaffolder\Definition\Types; +use Tester\Assert; + +require_once __DIR__ . '/../bootstrap.php'; +$generator = new ClassGenerator(); + +/** + * @param ClassDecorator[] $decorators + */ +$generateClass = static fn (array $decorators) => $generator->generateClass( + new ClassDefinition( + 'Grifart\ClassScaffolder\Test\KeepAnnotatedMethodsDecorator\Stub', + 'StubKeepMethod', + [], + [new Field('field', Types\resolve('mixed'))], + $decorators, + ), +); + +// methods are preserved +Assert::matchFile( + __DIR__ . '/Stub/StubKeepMethod.preserved.phps', + (string) $generateClass([ + new KeepAnnotatedMethodsDecorator(), + ]), +); + +// methods are overwritten +Assert::matchFile( + __DIR__ . '/Stub/StubKeepMethod.overwritten.phps', + (string) $generateClass([]), +); + +// class does not exist yet +Assert::noError(function () use ($generator) { + $generator->generateClass( + new ClassDefinition( + 'Grifart\ClassScaffolder\Test\KeepAnnotatedMethodsDecorator', + 'NonExistentClass', + [], + [new Field('field', Types\resolve('mixed'))], + [new KeepAnnotatedMethodsDecorator()], + ), + ); +});