diff --git a/rector.php b/rector.php index 2f9b04be1..32538b3dc 100644 --- a/rector.php +++ b/rector.php @@ -85,10 +85,8 @@ return static function (RectorConfig $rectorConfig): void { new \Rector\Php80\ValueObject\AnnotationToAttribute('Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency'), new \Rector\Php80\ValueObject\AnnotationToAttribute('Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation'), ]); -*/ // upgrade config for rector $rectorConfig->rules([\Rector\Transform\Rector\FileWithoutNamespace\RectorConfigBuilderRector::class]); - // upgrade of chill exports }; diff --git a/utils/rector/src/Rector/ChillBundleAddNormalizationMethodsOnExportRector.php b/utils/rector/src/Rector/ChillBundleAddNormalizationMethodsOnExportRector.php new file mode 100644 index 000000000..3410ee543 --- /dev/null +++ b/utils/rector/src/Rector/ChillBundleAddNormalizationMethodsOnExportRector.php @@ -0,0 +1,342 @@ +classAnalyzer->hasImplements($node, FilterInterface::class) + && !$this->classAnalyzer->hasImplements($node, AggregatorInterface::class) + && !$this->classAnalyzer->hasImplements($node, ExportInterface::class) + && !$this->classAnalyzer->hasImplements($node, DirectExportInterface::class) + && !$this->classAnalyzer->hasImplements($node, ListInterface::class) + && !$this->classAnalyzer->hasImplements($node, FormatterInterface::class) + ) { + return null; + } + + $hasDenormalizeMethod = false; + $hasNormalizedMethod = false; + $hasGetVersionMethod = false; + $buildFormStmtIndex = null; + $buildForm = null; + $hasTraitHelper = false; + + foreach ($node->stmts as $k => $stmt) { + if ($stmt instanceof Node\Stmt\TraitUse) { + if (ExportDataNormalizerTrait::class === $stmt->traits[0]->toString()) { + $hasTraitHelper = true; + } + } elseif (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + + if ('buildForm' === $stmt->name->name) { + $buildForm = $stmt; + $buildFormStmtIndex = $k; + } elseif ('normalizeFormData' === $stmt->name->name) { + $hasNormalizedMethod = true; + } elseif ('denormalizeFormData' === $stmt->name->name) { + $hasDenormalizeMethod = true; + } elseif ('getNormalizationVersion' === $stmt->name->name) { + $hasGetVersionMethod = true; + } + } + + $toAdd = []; + + $stmtBefore = array_slice($node->stmts, 0, $buildFormStmtIndex, false); + $stmtAfter = array_slice($node->stmts, $buildFormStmtIndex + 1); + $propertiesOnForm = $this->getPropertiesOnForm($buildForm); + + // if the trait is not present, we add it into the statements + if (!$hasTraitHelper && array_reduce( + $propertiesOnForm, + function (bool $carry, string $item): bool { + if ('entity' === $item || 'date' === $item) { + return true; + } + + return $carry; + }, + false, + )) { + array_unshift($stmtBefore, new Node\Stmt\TraitUse([ + new Node\Name('\\'.ExportDataNormalizerTrait::class), + ])); + } + + // if we do not have the `getNormalizerVersion` method + if (!$hasGetVersionMethod) { + $toAdd[] = $this->buildGetNormalizationVersionMethod(); + } + + // if we do not have the `normalizeFormData` method + if (!$hasNormalizedMethod) { + $toAdd[] = $this->buildNormalizeFormDataMethod($propertiesOnForm); + } + + if (!$hasDenormalizeMethod) { + $toAdd[] = $this->buildDenormalizeFormDataMethod($propertiesOnForm); + } + + $node->stmts = [ + ...array_values($stmtBefore), + $buildForm, + ...array_values($toAdd), + ...array_values($stmtAfter), + ]; + + return $node; + } + + private function buildDenormalizeFormDataMethod(array $propertiesOnForm): Node\Stmt\ClassMethod + { + $array = new Node\Expr\Array_([]); + + foreach ($propertiesOnForm as $name => $kind) { + $arrayDimFetch = new Node\Expr\ArrayDimFetch( + new Node\Expr\Variable('formData'), + new Node\Scalar\String_($name), + ); + + $array->items[] = new Node\Expr\ArrayItem( + match ($kind) { + 'entity' => new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + new Node\Identifier('denormalizeDoctrineEntity'), + [ + new Node\Arg($arrayDimFetch), + new Node\Arg( + new PropertyFetch( + new Node\Expr\Variable('this'), + 'someRepository', + ), + ), + ], + ), + 'rolling_date' => new Node\Expr\StaticCall( + new Node\Name\FullyQualified(RollingDate::class), + 'fromNormalized', + [new Node\Arg($arrayDimFetch)], + ), + 'date' => new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + new Node\Identifier('denormalizeDate'), + [ + new Node\Arg($arrayDimFetch), + ], + ), + 'scalar' => $arrayDimFetch, + default => $arrayDimFetch, + }, + new Node\Scalar\String_($name), + ); + } + + return new Node\Stmt\ClassMethod( + 'denormalizeFormData', + [ + 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, + 'returnType' => new Node\Identifier('array'), + 'params' => [ + new Node\Param(new Node\Expr\Variable('formData'), type: new Node\Identifier('array')), + new Node\Param(new Node\Expr\Variable('fromVersion'), type: new Node\Identifier('int')), + ], + 'stmts' => [ + new Node\Stmt\Return_($array), + ], + ], + ); + } + + private function buildNormalizeFormDataMethod(array $propertiesOnForm): Node\Stmt\ClassMethod + { + $array = new Node\Expr\Array_([]); + + foreach ($propertiesOnForm as $name => $kind) { + $arrayDimFetch = new Node\Expr\ArrayDimFetch( + new Node\Expr\Variable('formData'), + new Node\Scalar\String_($name), + ); + + $array->items[] = new Node\Expr\ArrayItem( + match ($kind) { + 'entity' => new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + new Node\Identifier('normalizeDoctrineEntity'), + [ + new Node\Arg($arrayDimFetch), + ], + ), + 'rolling_date' => new Node\Expr\MethodCall( + $arrayDimFetch, + new Node\Identifier('normalize'), + ), + 'date' => new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + new Node\Identifier('normalizeDate'), + [ + new Node\Arg($arrayDimFetch), + ], + ), + 'scalar' => $arrayDimFetch, + default => $arrayDimFetch, + }, + new Node\Scalar\String_($name), + ); + } + + return new Node\Stmt\ClassMethod( + 'normalizeFormData', + [ + 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, + 'returnType' => new Node\Identifier('array'), + 'params' => [ + new Node\Param(new Node\Expr\Variable('formData'), type: new Node\Identifier('array')), + ], + 'stmts' => [ + new Node\Stmt\Return_($array), + ], + ], + ); + } + + private function buildGetNormalizationVersionMethod(): Node\Stmt\ClassMethod + { + return new Node\Stmt\ClassMethod( + 'getNormalizationVersion', + [ + 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, + 'returnType' => new Node\Identifier('int'), + 'stmts' => [ + new Node\Stmt\Return_( + new Node\Scalar\LNumber(1) + ), + ], + ], + ); + } + + /** + * @return array + */ + private function getPropertiesOnForm(Node\Stmt\ClassMethod $buildFormMethod): array + { + $builderName = $buildFormMethod->params[0]->var->name; + $values = []; + + foreach ($buildFormMethod->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\Expression + // it must be a method call + && $stmt->expr instanceof Node\Expr\MethodCall + && 'add' === $stmt->expr->name->toString() + ) { + $newValues = $this->handleMethodCallAdd($stmt->expr, $builderName); + + if (false === $newValues) { + continue; + } + + $values = [...$values, ...$newValues]; + } + } + + return $values; + } + + private function handleMethodCallAdd(Node\Expr\MethodCall $call, string $builderName): array|false + { + if ($call->var instanceof Node\Expr\Variable) { + // in this case, the call is done on the form builder: $formBuilder->add + // or the last method call on the form builder, for instance $formBuilder->add( )->add( ) + if ($builderName !== $call->var->name) { + return false; + } + } elseif ($call->var instanceof Node\Expr\MethodCall && 'add' === $call->var->name->toString()) { + // in this case, we have a chained method call: something like $formbuilder->add()->add(). + // we have to go deeper into the call to get the information from them + $previous = $this->handleMethodCallAdd($call->var, $builderName); + + if (false === $previous) { + return false; + } + } + + $arg0 = $call->args[0] ?? null; + + if (null === $arg0) { + throw new \UnexpectedValueException("The first argument of an 'add' call method is empty"); + } + if (!$arg0->value instanceof Node\Scalar\String_) { + throw new \UnexpectedValueException("The first argument of an 'add' call is not a string"); + } + + $key = $arg0->value->value; + /** @var Node\Expr\ClassConstFetch $argType */ + $argType = $call->args[1]->value; + + return [ + ...$previous ?? [], + $key => match($argType->class->toString()) { + EntityType::class, PickUserDynamicType::class => 'entity', + PickRollingDateType::class => 'rolling_date', + ChillDateType::class, ChillDateTimeType::class, DateTimeType::class, DateType::class => 'date', + default => 'scalar', + }, + ]; + } +} diff --git a/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/ChillBundleAddNormalizationMethodsOnExportRectorTest.php b/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/ChillBundleAddNormalizationMethodsOnExportRectorTest.php new file mode 100644 index 000000000..81d3655a6 --- /dev/null +++ b/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/ChillBundleAddNormalizationMethodsOnExportRectorTest.php @@ -0,0 +1,40 @@ +doTestFile($file); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/config.php'; + } +} diff --git a/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/Fixture/add-method-simple-field.php.inc b/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/Fixture/add-method-simple-field.php.inc new file mode 100644 index 000000000..3a31f1ac2 --- /dev/null +++ b/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/Fixture/add-method-simple-field.php.inc @@ -0,0 +1,126 @@ +add('field', ChoiceType::class, [ + 'choices' => ['one', 'two', 'three', 'four', 'five'], + 'multiple' => false, + ]); + + $builder->add('entity', EntityType::class, []); + + $builder->add('user', \Chill\MainBundle\Form\Type\PickUserDynamicType::class, []); + + $builder->add('rolling_date', \Chill\MainBundle\Form\Type\PickRollingDateType::class); + + $builder->add('date', \Chill\MainBundle\Form\Type\ChillDateType::class, []); + } + + public function getFormDefaultData(): array + { + return []; + } + + public function describeAction($data, $format = 'string') + { + return []; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext) + { + // nothing to add here + } + + public function applyOn() + { + return ['something']; + } +} + +?> +----- +add('field', ChoiceType::class, [ + 'choices' => ['one', 'two', 'three', 'four', 'five'], + 'multiple' => false, + ]); + + $builder->add('entity', EntityType::class, []); + + $builder->add('user', \Chill\MainBundle\Form\Type\PickUserDynamicType::class, []); + + $builder->add('rolling_date', \Chill\MainBundle\Form\Type\PickRollingDateType::class); + + $builder->add('date', \Chill\MainBundle\Form\Type\ChillDateType::class, []); + } + public function getNormalizationVersion(): int + { + return 1; + } + public function normalizeFormData(array $formData): array + { + return ['field' => $formData['field'], 'entity' => $this->normalizeDoctrineEntity($formData['entity']), 'user' => $this->normalizeDoctrineEntity($formData['user']), 'rolling_date' => $formData['rolling_date']->normalize(), 'date' => $this->normalizeDate($formData['date'])]; + } + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['field' => $formData['field'], 'entity' => $this->denormalizeDoctrineEntity($formData['entity'], $this->someRepository), 'user' => $this->denormalizeDoctrineEntity($formData['user'], $this->someRepository), 'rolling_date' => \Chill\MainBundle\Service\RollingDate\RollingDate::fromNormalized($formData['rolling_date']), 'date' => $this->denormalizeDate($formData['date'])]; + } + + public function getFormDefaultData(): array + { + return []; + } + + public function describeAction($data, $format = 'string') + { + return []; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext) + { + // nothing to add here + } + + public function applyOn() + { + return ['something']; + } +} + +?> diff --git a/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/Fixture/skip-existing-method.php.inc b/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/Fixture/skip-existing-method.php.inc new file mode 100644 index 000000000..25e843a9a --- /dev/null +++ b/utils/rector/tests/ChillBundleAddNormalizationMethodsOnExportRector/Fixture/skip-existing-method.php.inc @@ -0,0 +1,55 @@ +rule(Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class); +};