diff --git a/composer.json b/composer.json index adb89576..9cb6d63e 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ }, "require-dev": { "doctrine/doctrine-bundle": "^3.0", - "doctrine/orm": "^3.0", + "doctrine/orm": "^3.2", "symplify/easy-coding-standard": "^13.2", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.2", diff --git a/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/DoctrineQueryBuilderSortDirectionRectorTest.php b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/DoctrineQueryBuilderSortDirectionRectorTest.php new file mode 100644 index 00000000..bed2aec9 --- /dev/null +++ b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/DoctrineQueryBuilderSortDirectionRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/configured_rule.php'; + } +} diff --git a/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/Fixture/replace_order_by.php.inc b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/Fixture/replace_order_by.php.inc new file mode 100644 index 00000000..802acec0 --- /dev/null +++ b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/Fixture/replace_order_by.php.inc @@ -0,0 +1,47 @@ +add( + 'orderBy', + $expr->orderBy('u.status', 'ASC') + ); + + $orderBy = new OrderBy('u.createdAt', 'desc'); + $qb->add('orderBy', $orderBy); + + $ignoreMe = new OrderBy('u.id'); + $qb->add('orderBy', $expr->orderBy('u.name')); +} + +?> +----- +add( + 'orderBy', + $expr->orderBy('u.status', \SortDirection::Ascending) + ); + + $orderBy = new OrderBy('u.createdAt', \SortDirection::Descending); + $qb->add('orderBy', $orderBy); + + $ignoreMe = new OrderBy('u.id'); + $qb->add('orderBy', $expr->orderBy('u.name')); +} + +?> diff --git a/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/Fixture/replace_query_builder.php.inc b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/Fixture/replace_query_builder.php.inc new file mode 100644 index 00000000..671bf6f3 --- /dev/null +++ b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/Fixture/replace_query_builder.php.inc @@ -0,0 +1,65 @@ +orderBy('e.id', 'ASC'); + $qb->addOrderBy('e.id', \Doctrine\Common\Collections\Criteria\Criteria::ASC); + $qb->addOrderBy('e.id', \Doctrine\Common\Collections\Criteria\Criteria::DESC); + $qb + ->addOrderBy('e.name', \Doctrine\Common\Collections\Order::Ascending) + ->addOrderBy('e.name', \Doctrine\Common\Collections\Order::Descending); + $qb->addOrderBy('e.name', 'desc'); + $qb->addOrderBy('e.title', 'DeSc'); +} + +class NotAQueryBuilder +{ + public function orderBy(string $field, string $dir): void + { + } +} + +function ignoreNonQueryBuilder(NotAQueryBuilder $notQb) +{ + $notQb->orderBy('e.id', 'ASC'); +} + +?> +----- +orderBy('e.id', \SortDirection::Ascending); + $qb->addOrderBy('e.id', \SortDirection::Ascending); + $qb->addOrderBy('e.id', \SortDirection::Descending); + $qb + ->addOrderBy('e.name', \SortDirection::Ascending) + ->addOrderBy('e.name', \SortDirection::Descending); + $qb->addOrderBy('e.name', \SortDirection::Descending); + $qb->addOrderBy('e.title', \SortDirection::Descending); +} + +class NotAQueryBuilder +{ + public function orderBy(string $field, string $dir): void + { + } +} + +function ignoreNonQueryBuilder(NotAQueryBuilder $notQb) +{ + $notQb->orderBy('e.id', 'ASC'); +} + +?> diff --git a/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/configured_rule.php b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/configured_rule.php new file mode 100644 index 00000000..1342c994 --- /dev/null +++ b/rules-tests/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector/configured_rule.php @@ -0,0 +1,9 @@ +withRules([DoctrineQueryBuilderSortDirectionRector::class]); diff --git a/rules/Collection22/Rector/CriteriaOrderingConstantsDeprecationRector.php b/rules/Collection22/Rector/CriteriaOrderingConstantsDeprecationRector.php index 62c04f7e..9191edd1 100644 --- a/rules/Collection22/Rector/CriteriaOrderingConstantsDeprecationRector.php +++ b/rules/Collection22/Rector/CriteriaOrderingConstantsDeprecationRector.php @@ -4,7 +4,6 @@ namespace Rector\Doctrine\Collection22\Rector; -use PHPStan\Type\IsSuperTypeOfResult; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\ArrayItem; @@ -15,6 +14,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use Rector\Doctrine\Enum\DoctrineClass; use Rector\Rector\AbstractRector; diff --git a/rules/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector.php b/rules/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector.php new file mode 100644 index 00000000..363281d3 --- /dev/null +++ b/rules/Orm32/Rector/MethodCall/DoctrineQueryBuilderSortDirectionRector.php @@ -0,0 +1,160 @@ +orderBy('e.id', 'ASC'); +$queryBuilder->addOrderBy('e.name', 'desc'); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$queryBuilder->orderBy('e.id', \SortDirection::Ascending); +$queryBuilder->addOrderBy('e.name', \SortDirection::Descending); +CODE_SAMPLE + ), + ] + ); + } + + public function provideComposerPackageConstraint(): ComposerPackageConstraint + { + return new ComposerPackageConstraint('doctrine/orm', '>=3.2'); + } + + public function getNodeTypes(): array + { + return [MethodCall::class, New_::class]; + } + + /** + * @param MethodCall|New_ $node + */ + public function refactor(Node $node): ?Node + { + if ($node->isFirstClassCallable()) { + return null; + } + + if ($node instanceof MethodCall) { + $isQueryBuilder = $this->isTargetType($node->var, 'Doctrine\ORM\QueryBuilder') + && $this->isNames($node->name, ['orderBy', 'addOrderBy']); + + $isExpr = $this->isObjectType($node->var, new ObjectType('Doctrine\ORM\Query\Expr')) + && $this->isName($node->name, 'orderBy'); + + if (! $isQueryBuilder && ! $isExpr) { + return null; + } + } + + if ($node instanceof New_ && ! $this->isObjectType($node->class, new ObjectType('Doctrine\ORM\Query\Expr\OrderBy'))) { + return null; + } + + $args = $node->getArgs(); + + if (count($args) < 2) { + return null; + } + + $orderArg = $args[1]; + $orderValue = $this->resolveSortDirectionValue($orderArg->value); + + if (! is_string($orderValue)) { + return null; + } + + $orderValue = strtolower($orderValue); + + if ($orderValue === 'asc') { + $orderArg->value = $this->nodeFactory->createClassConstFetch('SortDirection', 'Ascending'); + + return $node; + } + + if ($orderValue === 'desc') { + $orderArg->value = $this->nodeFactory->createClassConstFetch('SortDirection', 'Descending'); + + return $node; + } + + return null; + } + + /** + * Safely checks types even when fluent method chains lose their type mid-mutation. + */ + private function isTargetType(Node $node, string $className): bool + { + if ($this->isObjectType($node, new ObjectType($className))) { + return true; + } + + if ($node instanceof MethodCall) { + return $this->isTargetType($node->var, $className); + } + + return false; + } + + /** + * Extracts a normalized 'asc' or 'desc' string from Strings, Constants + */ + private function resolveSortDirectionValue(Node\Expr $expr): ?string + { + if ($expr instanceof ClassConstFetch) { + $constName = $this->getName($expr->name); + + if (is_string($constName)) { + $normalized = strtolower($constName); + if (in_array($normalized, ['asc', 'ascending'], true)) { + return 'asc'; + } + if (in_array($normalized, ['desc', 'descending'], true)) { + return 'desc'; + } + } + } + + $value = $this->valueResolver->getValue($expr); + if (is_string($value)) { + $normalized = strtolower($value); + if ($normalized === 'asc' || $normalized === 'desc') { + return $normalized; + } + } + + return null; + } +}