Skip to content

Commit 826e9cd

Browse files
committed
add custom rector rule to cleanup RouteBuilder
1 parent 2496725 commit 826e9cd

4 files changed

Lines changed: 186 additions & 0 deletions

File tree

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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
}
116+
117+
// always add callback if present
118+
if (isset($argNames[2])) {
119+
$newArgs[] = new Arg(
120+
$callbackValue,
121+
false,
122+
false,
123+
[],
124+
new Identifier($argNames[2]),
125+
);
126+
}
127+
128+
$node->args = $newArgs;
129+
130+
return $node;
131+
}
132+
133+
private function isCallableNode(Node $node): bool
134+
{
135+
return $node instanceof Closure
136+
|| $node instanceof ArrowFunction
137+
|| $node instanceof FuncCall;
138+
}
139+
}

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

0 commit comments

Comments
 (0)