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
4 changes: 4 additions & 0 deletions config/rector/sets/cakephp60.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use Cake\Upgrade\Rector\Cake6\EventManagerOnRector;
use Cake\Upgrade\Rector\Cake6\ReplaceCommandArgsIoWithPropertiesRector;
use Cake\Upgrade\Rector\Cake6\RouteBuilderToCallbackFirstRector;
use PHPStan\Type\ObjectType;
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
Expand All @@ -23,6 +24,9 @@

$rectorConfig->rule(ReplaceCommandArgsIoWithPropertiesRector::class);

// RouteBuilder argument reordering
$rectorConfig->rule(RouteBuilderToCallbackFirstRector::class);

// Changes related to the accessible => patchable rename
$rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [
new MethodCallRename('Cake\ORM\Entity', 'setAccess', 'setPatchable'),
Expand Down
174 changes: 174 additions & 0 deletions src/Rector/Cake6/RouteBuilderToCallbackFirstRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\Cake6;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Name;
use PHPStan\Type\ObjectType;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* Reorders RouteBuilder scope/prefix/plugin/resources arguments
* to have callback second and params/options third.
*
* @see \Cake\Upgrade\Test\TestCase\Rector\MethodCall\RouteBuilderToCallbackFirstRector\RouteBuilderToCallbackFirstRectorTest
*/
final class RouteBuilderToCallbackFirstRector extends AbstractRector
{
private const METHODS = ['scope', 'prefix', 'plugin', 'resources'];

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Reorder RouteBuilder scope/prefix/plugin/resources arguments ' .
'to have callback second and params/options third',
[
new CodeSample(
<<<'CODE_SAMPLE'
$routes->scope('/api', ['prefix' => 'Api'], function ($routes) {
$routes->resources('Articles');
});
$routes->prefix('admin', [], function ($routes) {
$routes->connect('/', ['controller' => 'Dashboard']);
});
$routes->resources('Articles', ['only' => 'index']);
$routes->resources('Posts', ['only' => 'index'], function ($routes) {
$routes->resources('Comments');
});
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$routes->scope('/api', function ($routes) {
$routes->resources('Articles');
}, ['prefix' => 'Api']);
$routes->prefix('admin', function ($routes) {
$routes->connect('/', ['controller' => 'Dashboard']);
});
$routes->resources('Articles', null, ['only' => 'index']);
$routes->resources('Posts', function ($routes) {
$routes->resources('Comments');
}, ['only' => 'index']);
CODE_SAMPLE,
),
],
);
}

/**
* @return array<class-string<\PhpParser\Node>>
*/
public function getNodeTypes(): array
{
return [MethodCall::class];
}

/**
* @param \PhpParser\Node\Expr\MethodCall $node
*/
public function refactor(Node $node): ?Node
{
// Check if the object is a RouteBuilder
if (!$this->isObjectType($node->var, new ObjectType('Cake\Routing\RouteBuilder'))) {
return null;
}

// Check if this is one of our target methods
if (!$this->isNames($node->name, self::METHODS)) {
return null;
}

$methodName = $this->getName($node->name);
$argCount = count($node->args);

// For resources method
// Old signature: resources(string $name, array $options = [], ?callable $callback = null)
// New signature: resources(string $name, ?Closure $callback = null, array $options = [])
if ($methodName === 'resources') {
// resources('Articles', ['only' => 'index']) -> resources('Articles', null, ['only' => 'index'])
if ($argCount === 2) {
$secondArg = $node->args[1];
// Check if second arg is an array (options)
if ($secondArg->value instanceof Array_) {
// Insert null as second argument, move array to third
$node->args = [
$node->args[0],
new Arg(new ConstFetch(new Name('null'))),
$secondArg,
];

return $node;
}
}

// resources('Articles', ['only' => 'index'], fn) -> resources('Articles', fn, ['only' => 'index'])
if ($argCount === 3) {
$secondArg = $node->args[1];
$thirdArg = $node->args[2];

// Check if third arg is closure and second is array
if ($thirdArg->value instanceof Closure && $secondArg->value instanceof Array_) {
// Check if second argument is an empty array - if so, just remove it
$isEmptyArray = count($secondArg->value->items) === 0;

if ($isEmptyArray) {
$node->args = [
$node->args[0],
$thirdArg,
];
} else {
// Swap: callback becomes second, options becomes third
$node->args[1] = $thirdArg;
$node->args[2] = $secondArg;
}

return $node;
}
}

return null;
}

// For scope/prefix/plugin methods
// Old signature: method(string $path, array|callable $params, ?callable $callback = null)
// New signature: method(string $path, Closure $callback, array $params = [])

// Only process if there are exactly 3 arguments
if ($argCount !== 3) {
return null;
}

$secondArg = $node->args[1];
$thirdArg = $node->args[2];

// Check if the third argument is a closure/callable
// and second argument is array (the old signature)
if ($thirdArg->value instanceof Closure) {
// Check if second argument is an empty array - if so, just remove it
$isEmptyArray = $secondArg->value instanceof Array_ && count($secondArg->value->items) === 0;

if ($isEmptyArray) {
// Just use callback as second arg, drop the empty array
$node->args = [
$node->args[0],
$thirdArg,
];
} else {
// Swap: callback becomes second, params becomes third
$node->args[1] = $thirdArg;
$node->args[2] = $secondArg;
}

return $node;
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\RouteBuilderToCallbackFirstRector\Fixture;

use Cake\Routing\RouteBuilder;

class Fixture
{
public function run(RouteBuilder $routes)
{
// Should be transformed - scope with params and callback (3 args)
$routes->scope('/api', ['prefix' => 'Api'], function ($routes) {
$routes->resources('Articles');
});

// Should be transformed - prefix with empty array and callback
$routes->prefix('admin', [], function ($routes) {
$routes->connect('/', ['controller' => 'Dashboard']);
});

// Should be transformed - plugin with params
$routes->plugin('Blog', ['path' => '/blog'], function ($routes) {
$routes->resources('Posts');
});

// Should be transformed - resources with only options (no callback)
$routes->resources('Articles', ['only' => 'index']);

// Should be transformed - resources with options and callback (3 args)
$routes->resources('Posts', ['only' => ['index', 'view']], function ($routes) {
$routes->resources('Comments');
});

// Should be transformed - resources with empty options and callback
$routes->resources('Tags', [], function ($routes) {
$routes->connect('/popular', ['action' => 'popular']);
});

// Should NOT be transformed - scope with only callback (2 args, new style)
$routes->scope('/v2', function ($routes) {
$routes->resources('Users');
});

// Should NOT be transformed - prefix with only callback
$routes->prefix('api', function ($routes) {
$routes->connect('/', []);
});

// Should NOT be transformed - resources with callback (3 args, but callback is second)
$routes->resources('Comments', function ($routes) {
$routes->resources('Replies');
});
}
}

?>
-----
<?php

namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\RouteBuilderToCallbackFirstRector\Fixture;

use Cake\Routing\RouteBuilder;

class Fixture
{
public function run(RouteBuilder $routes)
{
// Should be transformed - scope with params and callback (3 args)
$routes->scope('/api', function ($routes) {
$routes->resources('Articles');
}, ['prefix' => 'Api']);

// Should be transformed - prefix with empty array and callback
$routes->prefix('admin', function ($routes) {
$routes->connect('/', ['controller' => 'Dashboard']);
});

// Should be transformed - plugin with params
$routes->plugin('Blog', function ($routes) {
$routes->resources('Posts');
}, ['path' => '/blog']);

// Should be transformed - resources with only options (no callback)
$routes->resources('Articles', null, ['only' => 'index']);

// Should be transformed - resources with options and callback (3 args)
$routes->resources('Posts', function ($routes) {
$routes->resources('Comments');
}, ['only' => ['index', 'view']]);

// Should be transformed - resources with empty options and callback
$routes->resources('Tags', function ($routes) {
$routes->connect('/popular', ['action' => 'popular']);
});

// Should NOT be transformed - scope with only callback (2 args, new style)
$routes->scope('/v2', function ($routes) {
$routes->resources('Users');
});

// Should NOT be transformed - prefix with only callback
$routes->prefix('api', function ($routes) {
$routes->connect('/', []);
});

// Should NOT be transformed - resources with callback (3 args, but callback is second)
$routes->resources('Comments', function ($routes) {
$routes->resources('Replies');
});
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\RouteBuilderToCallbackFirstRector;

use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class RouteBuilderToCallbackFirstRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

use Cake\Upgrade\Rector\Cake6\RouteBuilderToCallbackFirstRector;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(RouteBuilderToCallbackFirstRector::class);
};
Loading