Skip to content
Closed
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
10 changes: 10 additions & 0 deletions config/rector/sets/cakephp60.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);

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

# @see https://book.cakephp.org/5/en/appendices/6-0-migration-guide.html
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(RouteBuilderToCallbackFirstRector::class);
};
138 changes: 138 additions & 0 deletions src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\Rector\MethodCall;

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 Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Cake\Upgrade\Test\TestCase\Rector\MethodCall\RouteBuilderToCallbackFirstRector\RouteBuilderToCallbackFirstRectorTest
*/
final class RouteBuilderToCallbackFirstRector extends AbstractRector
{
/**
* Methods that need argument reordering
*/
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',

Check warning on line 30 in src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Line exceeds 120 characters; contains 124 characters
[
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']);
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']);
CODE_SAMPLE

Check failure on line 51 in src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Multi-line function calls must have a trailing comma after the last parameter.
),
]

Check failure on line 53 in src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Multi-line function calls must have a trailing comma after the last parameter.
);
}

/**
* @return array<class-string<Node>>

Check failure on line 58 in src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Class name \PhpParser\Node in @return should be referenced via a fully qualified name.
*/
public function getNodeTypes(): array
{
return [MethodCall::class];
}

/**
* @param MethodCall $node

Check failure on line 66 in src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Class name \PhpParser\Node\Expr\MethodCall in @param should be referenced via a fully qualified name.
*/
public function refactor(Node $node): ?Node
{
if (!$this->isObjectType($node->var, new \PHPStan\Type\ObjectType('Cake\Routing\RouteBuilder'))) {

Check failure on line 70 in src/Rector/Rector/MethodCall/RouteBuilderToCallbackFirstRector.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Class \PHPStan\Type\ObjectType should not be referenced via a fully qualified name, but via a use statement.
return null;
}

$methodName = $this->getName($node->name);
if (!in_array($methodName, self::METHODS, true)) {
return null;
}

$args = $node->getArgs();
$argCount = count($args);

// Handle resources() special case: 2 args with options array (no callback)
// resources('Name', ['only' => 'index']) -> resources('Name', null, ['only' => 'index'])
if ($methodName === 'resources' && $argCount === 2) {
$secondArg = $args[1]->value;
if ($secondArg instanceof Array_ && !$this->isEmptyArray($secondArg)) {
$node->args = [
$args[0],
new Arg(new ConstFetch(new Name('null'))),
$args[1],
];

return $node;
}
}

// Need exactly 3 arguments for swap
if ($argCount !== 3) {
return null;
}

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

// Check if second arg is array and third is closure (old style)
if (!$secondArg instanceof Array_) {
return null;
}

if (!$thirdArg instanceof Closure) {
return null;
}

// If array is empty, just remove it (callback becomes second arg)
if ($this->isEmptyArray($secondArg)) {
$node->args = [
$args[0],
$args[2],
];

return $node;
}

// Swap: callback second, array third
$node->args = [
$args[0],
$args[2],
$args[1],
];

return $node;
}

private function isEmptyArray(Array_ $array): bool
{
return $array->items === [];
}
}
5 changes: 5 additions & 0 deletions src/Rector/Set/CakePHPSetList.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ final class CakePHPSetList
*/
public const CAKEPHP_53 = __DIR__ . '/../../../config/rector/sets/cakephp53.php';

/**
* @var string
*/
public const CAKEPHP_60 = __DIR__ . '/../../../config/rector/sets/cakephp60.php';

/**
* @var string
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

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

use Cake\Routing\RouteBuilder;

class RoutesConfig
{
public function __invoke(RouteBuilder $routes): void
{
// scope with params - should swap
$routes->scope('/api', ['prefix' => 'Api'], function (RouteBuilder $routes): void {
$routes->connect('/articles', ['controller' => 'Articles']);
});

// scope with empty array - should remove empty array
$routes->scope('/admin', [], function (RouteBuilder $routes): void {
$routes->connect('/dashboard', ['controller' => 'Dashboard']);
});

// prefix with params - should swap
$routes->prefix('admin', ['_namePrefix' => 'admin:'], function (RouteBuilder $routes): void {
$routes->connect('/users', ['controller' => 'Users']);
});

// prefix with empty array - should remove
$routes->prefix('api', [], function (RouteBuilder $routes): void {
$routes->connect('/status', ['controller' => 'Status']);
});

// plugin with params - should swap
$routes->plugin('Blog', ['path' => '/blog'], function (RouteBuilder $routes): void {
$routes->connect('/posts', ['controller' => 'Posts']);
});

// plugin with empty array - should remove
$routes->plugin('Forum', [], function (RouteBuilder $routes): void {
$routes->connect('/threads', ['controller' => 'Threads']);
});

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

// resources with callback and options - should swap
$routes->resources('Comments', ['only' => ['index', 'view']], function (RouteBuilder $routes): void {
$routes->resources('Replies');
});

// resources with empty options and callback - should remove empty array
$routes->resources('Tags', [], function (RouteBuilder $routes): void {
$routes->connect('/popular', ['action' => 'popular']);
});

// Should NOT transform: already new style (callback second)
$routes->scope('/new', function (RouteBuilder $routes): void {
$routes->connect('/test', ['controller' => 'Test']);
});

// Should NOT transform: resources with just name
$routes->resources('Users');
}
}

?>
-----
<?php

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

use Cake\Routing\RouteBuilder;

class RoutesConfig
{
public function __invoke(RouteBuilder $routes): void
{
// scope with params - should swap
$routes->scope('/api', function (RouteBuilder $routes): void {
$routes->connect('/articles', ['controller' => 'Articles']);
}, ['prefix' => 'Api']);

// scope with empty array - should remove empty array
$routes->scope('/admin', function (RouteBuilder $routes): void {
$routes->connect('/dashboard', ['controller' => 'Dashboard']);
});

// prefix with params - should swap
$routes->prefix('admin', function (RouteBuilder $routes): void {
$routes->connect('/users', ['controller' => 'Users']);
}, ['_namePrefix' => 'admin:']);

// prefix with empty array - should remove
$routes->prefix('api', function (RouteBuilder $routes): void {
$routes->connect('/status', ['controller' => 'Status']);
});

// plugin with params - should swap
$routes->plugin('Blog', function (RouteBuilder $routes): void {
$routes->connect('/posts', ['controller' => 'Posts']);
}, ['path' => '/blog']);

// plugin with empty array - should remove
$routes->plugin('Forum', function (RouteBuilder $routes): void {
$routes->connect('/threads', ['controller' => 'Threads']);
});

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

// resources with callback and options - should swap
$routes->resources('Comments', function (RouteBuilder $routes): void {
$routes->resources('Replies');
}, ['only' => ['index', 'view']]);

// resources with empty options and callback - should remove empty array
$routes->resources('Tags', function (RouteBuilder $routes): void {
$routes->connect('/popular', ['action' => 'popular']);
});

// Should NOT transform: already new style (callback second)
$routes->scope('/new', function (RouteBuilder $routes): void {
$routes->connect('/test', ['controller' => 'Test']);
});

// Should NOT transform: resources with just name
$routes->resources('Users');
}
}

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

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

class SomeOtherClass
{
public function scope(string $path, array $options, callable $callback): void
{
}

public function prefix(string $name, array $options, callable $callback): void
{
}
}

class SomeController
{
public function test(): void
{
// Should NOT transform: not a RouteBuilder
$other = new SomeOtherClass();
$other->scope('/path', ['key' => 'value'], function () {
});
$other->prefix('admin', [], function () {
});
}
}

?>
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\Rector\MethodCall\RouteBuilderToCallbackFirstRector;
use Rector\Config\RectorConfig;

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