Skip to content

Commit 6a4bb79

Browse files
committed
add custom rector rule to cleanup RouteBuilder
1 parent 2496725 commit 6a4bb79

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed

config/rector/sets/cakephp53.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use Cake\Upgrade\Rector\Rector\MethodCall\EntityIsEmptyRector;
55
use Cake\Upgrade\Rector\Rector\MethodCall\EntityPatchRector;
6+
use Cake\Upgrade\Rector\Rector\MethodCall\RouteBuilderCleanupRector;
67
use Rector\Config\RectorConfig;
78
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
89
use Rector\Renaming\Rector\Name\RenameClassRector;
@@ -18,4 +19,12 @@
1819
]);
1920
$rectorConfig->rule(EntityIsEmptyRector::class);
2021
$rectorConfig->rule(EntityPatchRector::class);
22+
$rectorConfig->ruleWithConfiguration(RouteBuilderCleanupRector::class, [
23+
'methods' => [
24+
'scope' => ['path', 'params', 'callback'],
25+
'resources' => ['name', 'options', 'callback'],
26+
'prefix' => ['name', 'params', 'callback'],
27+
'plugin' => ['name', 'options', 'callback'],
28+
],
29+
]);
2130
};
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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)
96+
if (isset($argNames[0]) && $pathValue !== null) {
97+
$newArgs[] = new Arg(
98+
$pathValue,
99+
false,
100+
false,
101+
[],
102+
new Identifier($argNames[0]),
103+
);
104+
}
105+
106+
// only add options if it existed in original call
107+
if (isset($argNames[1]) && $optionsValue !== null) {
108+
$newArgs[] = new Arg(
109+
$optionsValue,
110+
false,
111+
false,
112+
[],
113+
new Identifier($argNames[1]),
114+
);
115+
} elseif ($methodName === 'scope') {
116+
// scope() must always have options
117+
$newArgs[] = new Arg(
118+
$optionsValue ?? $this->nodeFactory->createArray([]),
119+
false,
120+
false,
121+
[],
122+
new Identifier($argNames[1]),
123+
);
124+
}
125+
126+
// always add callback if present
127+
if (isset($argNames[2])) {
128+
$newArgs[] = new Arg(
129+
$callbackValue,
130+
false,
131+
false,
132+
[],
133+
new Identifier($argNames[2]),
134+
);
135+
}
136+
137+
$node->args = $newArgs;
138+
139+
return $node;
140+
}
141+
142+
private function isCallableNode(Node $node): bool
143+
{
144+
return $node instanceof Closure
145+
|| $node instanceof ArrowFunction
146+
|| $node instanceof FuncCall;
147+
}
148+
}

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(path: '/api', params: [], callback: function ($routes): void {});
38+
$routes->scope(path: '/api', params: ['something'], callback: function ($routes): void {});
39+
40+
$routes->resources(name: '/api', callback: function ($routes): void {});
41+
$routes->resources(name: '/api', options: ['something'], callback: function ($routes): void {});
42+
43+
$routes->prefix(name: '/api', callback: function ($routes): void {});
44+
$routes->prefix(name: '/api', params: ['something'], callback: function ($routes): void {});
45+
46+
$routes->plugin(name: '/api', callback: function ($routes): void {});
47+
$routes->plugin(name: '/api', options: ['something'], callback: function ($routes): void {});
48+
}
3049
}

0 commit comments

Comments
 (0)