Skip to content

Commit c3280a4

Browse files
committed
add custom rector rule to cleanup RouteBuilder
1 parent 45bc159 commit c3280a4

File tree

4 files changed

+198
-4
lines changed

4 files changed

+198
-4
lines changed

config/rector/sets/cakephp53.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Cake\Upgrade\Rector\Rector\MethodCall\EntityPatchRector;
77
use Cake\Upgrade\Rector\Rector\MethodCall\NewExprToFuncRector;
88
use Cake\Upgrade\Rector\Rector\MethodCall\QueryParamAccessRector;
9+
use Cake\Upgrade\Rector\Rector\MethodCall\RouteBuilderCleanupRector;
910
use Rector\Config\RectorConfig;
1011
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
1112
use Rector\Renaming\Rector\Name\RenameClassRector;
@@ -15,6 +16,10 @@
1516
return static function (RectorConfig $rectorConfig): void {
1617
// Apply newExpr()->count() -> func()->count('*') transformation before general newExpr rename
1718
$rectorConfig->rule(NewExprToFuncRector::class);
19+
$rectorConfig->rule(EntityIsEmptyRector::class);
20+
$rectorConfig->rule(EntityPatchRector::class);
21+
$rectorConfig->rule(FormExecuteToProcessRector::class);
22+
$rectorConfig->rule(QueryParamAccessRector::class);
1823

1924
$rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [
2025
new MethodCallRename('Cake\Database\Query', 'newExpr', 'expr'),
@@ -24,8 +29,12 @@
2429
'Cake\TestSuite\Fixture\TransactionFixtureStrategy' => 'Cake\TestSuite\Fixture\TransactionStrategy',
2530
'Cake\TestSuite\Fixture\TruncateFixtureStrategy' => 'Cake\TestSuite\Fixture\TruncateStrategy',
2631
]);
27-
$rectorConfig->rule(EntityIsEmptyRector::class);
28-
$rectorConfig->rule(EntityPatchRector::class);
29-
$rectorConfig->rule(FormExecuteToProcessRector::class);
30-
$rectorConfig->rule(QueryParamAccessRector::class);
32+
$rectorConfig->ruleWithConfiguration(RouteBuilderCleanupRector::class, [
33+
'methods' => [
34+
'scope' => ['path', 'params', 'callback'],
35+
'resources' => ['name', 'options', 'callback'],
36+
'prefix' => ['name', 'params', 'callback'],
37+
'plugin' => ['name', 'options', 'callback'],
38+
],
39+
]);
3140
};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Cake\Upgrade\Rector\Rector\MethodCall;
5+
6+
use Cake\Routing\RouteBuilder;
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr\ArrowFunction;
10+
use PhpParser\Node\Expr\Closure;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node\Expr\MethodCall;
13+
use PhpParser\Node\Identifier;
14+
use PHPStan\Type\ObjectType;
15+
use Rector\Contract\Rector\ConfigurableRectorInterface;
16+
use Rector\Rector\AbstractRector;
17+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
18+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
19+
20+
final class RouteBuilderCleanupRector extends AbstractRector implements ConfigurableRectorInterface
21+
{
22+
/**
23+
* @var array<string, string[]>
24+
* e.g. ['scope' => ['path', 'options', 'callback']]
25+
*/
26+
private array $methods = [];
27+
28+
public function configure(array $configuration): void
29+
{
30+
$this->methods = $configuration['methods'] ?? [];
31+
}
32+
33+
public function getRuleDefinition(): RuleDefinition
34+
{
35+
return new RuleDefinition('Normalize RouteBuilder calls to always use named arguments based on configuration', [
36+
new CodeSample(
37+
<<<'CODE_SAMPLE'
38+
$routes->scope('/api', function (RouteBuilder $routes): void {});
39+
CODE_SAMPLE,
40+
<<<'CODE_SAMPLE'
41+
$routes->scope(path: '/api', options: [], callback: function (RouteBuilder $routes): void {});
42+
CODE_SAMPLE,
43+
),
44+
]);
45+
}
46+
47+
public function getNodeTypes(): array
48+
{
49+
return [MethodCall::class];
50+
}
51+
52+
public function refactor(Node $node): ?Node
53+
{
54+
if (! $node instanceof MethodCall) {
55+
return null;
56+
}
57+
58+
$methodName = $this->getName($node->name);
59+
if (! isset($this->methods[$methodName])) {
60+
return null;
61+
}
62+
63+
// Must be called on a Cake\Routing\RouteBuilder
64+
$callerType = $this->getType($node->var);
65+
if (! (new ObjectType(RouteBuilder::class))->isSuperTypeOf($callerType)->yes()) {
66+
return null;
67+
}
68+
69+
$argNames = $this->methods[$methodName];
70+
$args = $node->args;
71+
72+
$pathValue = $args[0]->value ?? null;
73+
$optionsValue = null;
74+
$callbackValue = null;
75+
76+
// Handle case where 2nd param is callable or array
77+
if (isset($args[1])) {
78+
if ($this->isCallableNode($args[1]->value)) {
79+
// Case: scope('/api', fn() => null)
80+
$callbackValue = $args[1]->value;
81+
} else {
82+
// Case: scope('/api', [], fn() => null)
83+
$optionsValue = $args[1]->value;
84+
$callbackValue = $args[2]->value ?? null;
85+
}
86+
}
87+
88+
if ($callbackValue === null) {
89+
// no callable = no change
90+
return null;
91+
}
92+
93+
$newArgs = [];
94+
95+
// always add first argument (path/name) without a named argument
96+
if (isset($argNames[0]) && $pathValue !== null) {
97+
$newArgs[] = new Arg(
98+
$pathValue,
99+
false,
100+
false,
101+
[],
102+
);
103+
}
104+
105+
// only add options if it existed in original call
106+
if (isset($argNames[1]) && $optionsValue !== null) {
107+
$newArgs[] = new Arg(
108+
$optionsValue,
109+
false,
110+
false,
111+
[],
112+
new Identifier($argNames[1]),
113+
);
114+
} elseif ($methodName === 'scope') {
115+
// scope() must always have options
116+
$newArgs[] = new Arg(
117+
$optionsValue ?? $this->nodeFactory->createArray([]),
118+
false,
119+
false,
120+
[],
121+
new Identifier($argNames[1]),
122+
);
123+
}
124+
125+
// always add callback if present
126+
if (isset($argNames[2])) {
127+
$newArgs[] = new Arg(
128+
$callbackValue,
129+
false,
130+
false,
131+
[],
132+
new Identifier($argNames[2]),
133+
);
134+
}
135+
136+
$node->args = $newArgs;
137+
138+
return $node;
139+
}
140+
141+
private function isCallableNode(Node $node): bool
142+
{
143+
return $node instanceof Closure
144+
|| $node instanceof ArrowFunction
145+
|| $node instanceof FuncCall;
146+
}
147+
}

tests/test_apps/original/RectorCommand-testApply53/src/SomeTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Cake\ORM\Entity;
77
use Cake\ORM\Locator\LocatorAwareTrait;
88
use Cake\ORM\Query;
9+
use Cake\Routing\RouteBuilder;
10+
use Cake\Routing\RouteCollection;
911

1012
class SomeTest
1113
{
@@ -27,4 +29,21 @@ public function testRenames(): void
2729
public function findSomething(Query $query, array $options): Query {
2830
return $query;
2931
}
32+
33+
public function routes(): void
34+
{
35+
$routes = new RouteBuilder(new RouteCollection(), '/');
36+
37+
$routes->scope('/api', function ($routes): void {});
38+
$routes->scope('/api', ['something'], function ($routes): void {});
39+
40+
$routes->resources('/api', function ($routes): void {});
41+
$routes->resources('/api', ['something'], function ($routes): void {});
42+
43+
$routes->prefix('/api', function ($routes): void {});
44+
$routes->prefix('/api', ['something'], function ($routes): void {});
45+
46+
$routes->plugin('/api', function ($routes): void {});
47+
$routes->plugin('/api', ['something'], function ($routes): void {});
48+
}
3049
}

tests/test_apps/upgraded/RectorCommand-testApply53/src/SomeTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Cake\ORM\Entity;
77
use Cake\ORM\Locator\LocatorAwareTrait;
88
use Cake\ORM\Query;
9+
use Cake\Routing\RouteBuilder;
10+
use Cake\Routing\RouteCollection;
911

1012
class SomeTest
1113
{
@@ -27,4 +29,21 @@ public function testRenames(): void
2729
public function findSomething(\Cake\ORM\Query\SelectQuery $query, array $options): \Cake\ORM\Query\SelectQuery {
2830
return $query;
2931
}
32+
33+
public function routes(): void
34+
{
35+
$routes = new RouteBuilder(new RouteCollection(), '/');
36+
37+
$routes->scope('/api', params: [], callback: function ($routes): void {});
38+
$routes->scope('/api', params: ['something'], callback: function ($routes): void {});
39+
40+
$routes->resources('/api', callback: function ($routes): void {});
41+
$routes->resources('/api', options: ['something'], callback: function ($routes): void {});
42+
43+
$routes->prefix('/api', callback: function ($routes): void {});
44+
$routes->prefix('/api', params: ['something'], callback: function ($routes): void {});
45+
46+
$routes->plugin('/api', callback: function ($routes): void {});
47+
$routes->plugin('/api', options: ['something'], callback: function ($routes): void {});
48+
}
3049
}

0 commit comments

Comments
 (0)