Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 36 additions & 58 deletions src/Rector/MethodCall/EloquentOrderByToLatestOrOldestRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\VariadicPlaceholder;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use Rector\PhpParser\Node\Value\ValueResolver;
use RectorLaravel\AbstractRector;
use RectorLaravel\NodeAnalyzer\QueryBuilderAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
Expand All @@ -31,7 +30,10 @@ class EloquentOrderByToLatestOrOldestRector extends AbstractRector implements Co
*/
private array $allowedPatterns = ['*_at', '*_date', '*_on'];

public function __construct(private readonly QueryBuilderAnalyzer $queryBuilderAnalyzer) {}
public function __construct(
private readonly QueryBuilderAnalyzer $queryBuilderAnalyzer,
private readonly ValueResolver $valueResolver,
) {}

public function getRuleDefinition(): RuleDefinition
{
Expand Down Expand Up @@ -77,17 +79,31 @@ public function getNodeTypes(): array
return [MethodCall::class];
}

/** @param MethodCall $node */
public function refactor(Node $node): ?Node
{
if (! $node instanceof MethodCall) {
if (! $this->isOrderByMethodCall($node)) {
return null;
}

if ($this->isOrderByMethodCall($node) && $this->isAllowedPattern($node)) {
return $this->convertOrderByToLatest($node);
$columnArg = $node->getArg('column', 0);
$directionArg = $node->getArg('direction', 1);

if ($columnArg === null) {
return null;
}

if (! $this->isAllowedPattern($columnArg->value)) {
return null;
}

return null;
$direction = $directionArg === null ? 'asc' : $this->valueResolver->getValue($directionArg);

if (! is_string($direction)) {
return null;
}

return $this->convertOrderByToLatest($node, $columnArg, $direction);
}

/**
Expand All @@ -104,38 +120,30 @@ public function configure(array $configuration): void

private function isOrderByMethodCall(MethodCall $methodCall): bool
{
// Check if it's a method call to `orderBy`

return $this->queryBuilderAnalyzer->isMatchingCall($methodCall, 'orderBy')
|| $this->queryBuilderAnalyzer->isMatchingCall($methodCall, 'orderByDesc');
}

private function isAllowedPattern(MethodCall $methodCall): bool
private function isAllowedPattern(Expr $expr): bool
{
$columnArg = $methodCall->args !== [] && $methodCall->args[0] instanceof Arg
? $methodCall->args[0]->value
: null;

// If no patterns are specified, consider all column names as matching
if ($this->allowedPatterns === []) {
return true;
}

if ($columnArg instanceof String_) {
$columnName = $columnArg->value;
$value = $this->valueResolver->getValue($expr);

// If specified, only allow certain patterns
if (is_string($value)) {
foreach ($this->allowedPatterns as $allowedPattern) {
if (fnmatch($allowedPattern, $columnName)) {
if (fnmatch($allowedPattern, $value)) {
return true;
}
}
}

if ($columnArg instanceof Variable && is_string($columnArg->name)) {
// Check against allowed patterns
if ($expr instanceof Variable && is_string($expr->name)) {
foreach ($this->allowedPatterns as $allowedPattern) {
if (fnmatch(ltrim($allowedPattern, '$'), $columnArg->name)) {
if (fnmatch(ltrim($allowedPattern, '$'), $expr->name)) {
return true;
}
}
Expand All @@ -144,48 +152,18 @@ private function isAllowedPattern(MethodCall $methodCall): bool
return false;
}

private function convertOrderByToLatest(MethodCall $methodCall): ?MethodCall
private function convertOrderByToLatest(MethodCall $methodCall, Arg $columnArg, string $direction): MethodCall
{
if (! isset($methodCall->args[0]) && ! $methodCall->args[0] instanceof VariadicPlaceholder) {
return null;
}

$columnVar = $methodCall->args[0] instanceof Arg ? $methodCall->args[0]->value : null;
if (! $columnVar instanceof Expr) {
return null;
}

if (isset($methodCall->args[1]) && (! $methodCall->args[1] instanceof Arg || ! $methodCall->args[1]->value instanceof String_)) {
return null;
}

if (isset($methodCall->args[1]) && $methodCall->args[1] instanceof Arg && $methodCall->args[1]->value instanceof String_) {
$direction = $methodCall->args[1]->value->value;
} else {
$direction = 'asc';
}

if ($this->isName($methodCall->name, 'orderByDesc')) {
$newMethod = 'latest';
} else {
$newMethod = $direction === 'asc' ? 'oldest' : 'latest';
}

return $this->createMethodCall($methodCall, $newMethod, $columnVar);
}

private function createMethodCall(MethodCall $methodCall, string $newMethod, Expr $expr): MethodCall
{
if ($expr instanceof String_ && $expr->value === 'created_at') {
$args = [];
} elseif ($expr instanceof String_) {
$args = [new Arg(new String_($expr->value))];
$method = 'latest';
} else {
$args = [new Arg($expr)];
$method = strtolower($direction) === 'asc' ? 'oldest' : 'latest';
}

$methodCall->name = new Identifier($newMethod);
$methodCall->args = $args;
$methodCall->name = new Identifier($method);
$methodCall->args = $this->valueResolver->isValue($columnArg->value, 'created_at')
? []
: [$columnArg];

return $methodCall;
}
Expand Down
12 changes: 12 additions & 0 deletions stubs/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ class Builder implements \Illuminate\Contracts\Database\Query\Builder
{
public function publicMethodBelongsToQueryBuilder(): void {}

/**
* @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column
* @return $this
*/
public function orderBy($column, string $direction): static {}

/**
* @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column
* @return $this
*/
public function orderByDesc($column): static {}

protected function protectedMethodBelongsToQueryBuilder(): void {}

private function privateMethodBelongsToQueryBuilder(): void {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ namespace RectorLaravel\Tests\Rector\Cast\DatabaseExpressionCastsToMethodCall\Fi
use Illuminate\Database\Query\Builder;

$column = 'tested_at';
$dir = 'desc';

/** @var Builder $query */
$query->orderBy('created_at');
$query->orderBy('created_at', 'ASC');
$query->orderBy('created_at', 'desc');
$query->orderBy('submitted_at');
$query->orderByDesc('submitted_at');
$query->orderBy($column);
$query->orderBy($column, $dir);
$query->orderBy($allowed_variable_name);

?>
Expand All @@ -22,12 +26,16 @@ namespace RectorLaravel\Tests\Rector\Cast\DatabaseExpressionCastsToMethodCall\Fi
use Illuminate\Database\Query\Builder;

$column = 'tested_at';
$dir = 'desc';

/** @var Builder $query */
$query->oldest();
$query->oldest();
$query->latest();
$query->oldest('submitted_at');
$query->latest('submitted_at');
$query->oldest($column);
$query->latest($column);
$query->oldest($allowed_variable_name);

?>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace RectorLaravel\Tests\Rector\Cast\DatabaseExpressionCastsToMethodCall\Fixture;

use Illuminate\Database\Query\Builder;

/** @param 'asc'|'desc' $union */
function (Builder $query, string $dir, string $union) {
$query->orderBy('created_at', $dir);
$query->orderBy('created_at', $union);
};