Skip to content

Commit b53d7fc

Browse files
Add BreadcrumbsHelper add/prepend => addMany/prependMany rector rule (#383)
* Add BreadcrumbsHelper add/prepend => addMany/prependMany rector rule In CakePHP 5.3, passing an array of crumbs to BreadcrumbsHelper::add() or prepend() is deprecated. Use addMany() or prependMany() instead. The rector detects when the first argument is an array containing nested arrays (array of crumbs) vs a single crumb with title/url keys. * Update CI to use PHP 8.2 (required by CakePHP 5.x) --------- Co-authored-by: Mark Story <mark@mark-story.com>
1 parent ea8b464 commit b53d7fc

File tree

8 files changed

+278
-0
lines changed

8 files changed

+278
-0
lines changed

config/rector/sets/cakephp53.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
declare(strict_types=1);
33

44
use Cake\Upgrade\Rector\Rector\ClassMethod\FormExecuteToProcessRector;
5+
use Cake\Upgrade\Rector\Rector\MethodCall\BreadcrumbsHelperAddManyRector;
56
use Cake\Upgrade\Rector\Rector\MethodCall\EntityIsEmptyRector;
67
use Cake\Upgrade\Rector\Rector\MethodCall\EntityPatchRector;
78
use Cake\Upgrade\Rector\Rector\MethodCall\NewExprToFuncRector;
@@ -25,6 +26,7 @@
2526
'Cake\TestSuite\Fixture\TransactionFixtureStrategy' => 'Cake\TestSuite\Fixture\TransactionStrategy',
2627
'Cake\TestSuite\Fixture\TruncateFixtureStrategy' => 'Cake\TestSuite\Fixture\TruncateStrategy',
2728
]);
29+
$rectorConfig->rule(BreadcrumbsHelperAddManyRector::class);
2830
$rectorConfig->rule(EntityIsEmptyRector::class);
2931
$rectorConfig->rule(EntityPatchRector::class);
3032
$rectorConfig->rule(FormExecuteToProcessRector::class);
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Cake\Upgrade\Rector\Rector\MethodCall;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr\Array_;
8+
use PhpParser\Node\Expr\ArrayItem;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Identifier;
11+
use PhpParser\Node\Scalar\String_;
12+
use PHPStan\Type\ObjectType;
13+
use Rector\Rector\AbstractRector;
14+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
15+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
16+
17+
/**
18+
* Transforms BreadcrumbsHelper::add(array) to addMany() and prepend(array) to prependMany()
19+
*
20+
* In CakePHP 5.3, passing an array of crumbs to add() or prepend() is deprecated.
21+
* Use addMany() or prependMany() instead.
22+
*
23+
* @see https://book.cakephp.org/5/en/appendices/5-3-migration-guide.html
24+
*/
25+
final class BreadcrumbsHelperAddManyRector extends AbstractRector
26+
{
27+
public function getRuleDefinition(): RuleDefinition
28+
{
29+
return new RuleDefinition(
30+
'Change BreadcrumbsHelper::add(array) to addMany() and prepend(array) to prependMany()',
31+
[
32+
new CodeSample(
33+
<<<'CODE_SAMPLE'
34+
$this->Breadcrumbs->add([
35+
['title' => 'Home', 'url' => '/'],
36+
['title' => 'Articles', 'url' => '/articles'],
37+
]);
38+
CODE_SAMPLE
39+
,
40+
<<<'CODE_SAMPLE'
41+
$this->Breadcrumbs->addMany([
42+
['title' => 'Home', 'url' => '/'],
43+
['title' => 'Articles', 'url' => '/articles'],
44+
]);
45+
CODE_SAMPLE,
46+
),
47+
],
48+
);
49+
}
50+
51+
public function getNodeTypes(): array
52+
{
53+
return [MethodCall::class];
54+
}
55+
56+
public function refactor(Node $node): ?Node
57+
{
58+
if (!$node instanceof MethodCall) {
59+
return null;
60+
}
61+
62+
// Must be add or prepend method
63+
if (!$node->name instanceof Identifier) {
64+
return null;
65+
}
66+
67+
$methodName = $node->name->toString();
68+
if ($methodName !== 'add' && $methodName !== 'prepend') {
69+
return null;
70+
}
71+
72+
// Must have at least one argument
73+
if (count($node->args) < 1) {
74+
return null;
75+
}
76+
77+
// First argument must be an array
78+
$firstArg = $node->args[0]->value;
79+
if (!$firstArg instanceof Array_) {
80+
return null;
81+
}
82+
83+
// Check if the array looks like an array of crumbs (array of arrays)
84+
// rather than a single crumb with title/url keys
85+
if (!$this->isArrayOfCrumbs($firstArg)) {
86+
return null;
87+
}
88+
89+
// Check if this is called on BreadcrumbsHelper
90+
$callerType = $this->getType($node->var);
91+
if (!$callerType instanceof ObjectType) {
92+
return null;
93+
}
94+
95+
if (!$callerType->isInstanceOf('Cake\View\Helper\BreadcrumbsHelper')->yes()) {
96+
return null;
97+
}
98+
99+
// Rename to addMany or prependMany
100+
$newMethodName = $methodName === 'add' ? 'addMany' : 'prependMany';
101+
$node->name = new Identifier($newMethodName);
102+
103+
return $node;
104+
}
105+
106+
/**
107+
* Determine if an array is an array of crumbs (array of arrays)
108+
* vs a single crumb (array with title/url keys)
109+
*/
110+
private function isArrayOfCrumbs(Array_ $array): bool
111+
{
112+
// An array of crumbs is typically a numerically indexed array of arrays
113+
// e.g. [['title' => 'Home'], ['title' => 'Articles']]
114+
// A single crumb would have string keys like 'title', 'url'
115+
// e.g. ['title' => 'Home', 'url' => '/']
116+
117+
if (count($array->items) === 0) {
118+
return false;
119+
}
120+
121+
foreach ($array->items as $item) {
122+
if (!$item instanceof ArrayItem) {
123+
continue;
124+
}
125+
126+
// If item has a string key like 'title' or 'url', it's a single crumb
127+
if ($item->key instanceof String_) {
128+
$keyValue = $item->key->value;
129+
if ($keyValue === 'title' || $keyValue === 'url' || $keyValue === 'options') {
130+
return false;
131+
}
132+
}
133+
134+
// If the item value is an array, it's likely an array of crumbs
135+
if ($item->value instanceof Array_) {
136+
return true;
137+
}
138+
}
139+
140+
return false;
141+
}
142+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\BreadcrumbsHelperAddManyRector;
5+
6+
use Iterator;
7+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
8+
9+
final class BreadcrumbsHelperAddManyRectorTest extends AbstractRectorTestCase
10+
{
11+
/**
12+
* @dataProvider provideData()
13+
*/
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\BreadcrumbsHelperAddManyRector\Fixture;
4+
5+
use Cake\View\Helper\BreadcrumbsHelper;
6+
7+
class SomeView
8+
{
9+
private BreadcrumbsHelper $Breadcrumbs;
10+
11+
public function someMethod()
12+
{
13+
// Should transform: add with array of crumbs
14+
$this->Breadcrumbs->add([
15+
['title' => 'Home', 'url' => '/'],
16+
['title' => 'Articles', 'url' => '/articles'],
17+
]);
18+
19+
// Should transform: prepend with array of crumbs
20+
$this->Breadcrumbs->prepend([
21+
['title' => 'Dashboard'],
22+
]);
23+
}
24+
}
25+
26+
?>
27+
-----
28+
<?php
29+
30+
namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\BreadcrumbsHelperAddManyRector\Fixture;
31+
32+
use Cake\View\Helper\BreadcrumbsHelper;
33+
34+
class SomeView
35+
{
36+
private BreadcrumbsHelper $Breadcrumbs;
37+
38+
public function someMethod()
39+
{
40+
// Should transform: add with array of crumbs
41+
$this->Breadcrumbs->addMany([
42+
['title' => 'Home', 'url' => '/'],
43+
['title' => 'Articles', 'url' => '/articles'],
44+
]);
45+
46+
// Should transform: prepend with array of crumbs
47+
$this->Breadcrumbs->prependMany([
48+
['title' => 'Dashboard'],
49+
]);
50+
}
51+
}
52+
53+
?>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Cake\Upgrade\Test\TestCase\Rector\MethodCall\BreadcrumbsHelperAddManyRector\Fixture;
4+
5+
use Cake\View\Helper\BreadcrumbsHelper;
6+
7+
class SomeView
8+
{
9+
private BreadcrumbsHelper $Breadcrumbs;
10+
11+
public function someMethod()
12+
{
13+
// Should NOT transform: single crumb with title/url
14+
$this->Breadcrumbs->add('Home', '/');
15+
16+
// Should NOT transform: single crumb as array with title key
17+
$this->Breadcrumbs->add(['title' => 'Home', 'url' => '/']);
18+
19+
// Should NOT transform: prepend single crumb
20+
$this->Breadcrumbs->prepend('Dashboard');
21+
}
22+
}
23+
24+
?>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
use Cake\Upgrade\Rector\Rector\MethodCall\BreadcrumbsHelperAddManyRector;
5+
use Rector\Config\RectorConfig;
6+
7+
return static function (RectorConfig $rectorConfig): void {
8+
$rectorConfig->rule(BreadcrumbsHelperAddManyRector::class);
9+
};

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
use Cake\ORM\Entity;
88
use Cake\ORM\Locator\LocatorAwareTrait;
99
use Cake\ORM\Query;
10+
use Cake\View\Helper\BreadcrumbsHelper;
1011

1112
class SomeTest
1213
{
1314
use LocatorAwareTrait;
1415

16+
private BreadcrumbsHelper $Breadcrumbs;
17+
1518
public function testRenames(): void
1619
{
1720
$entity = new Entity();
@@ -24,6 +27,13 @@ public function testRenames(): void
2427
$table = $this->fetchTable('Articles');
2528
$expr = $table->find()->newExpr();
2629

30+
// BreadcrumbsHelper::add(array) should be changed to addMany()
31+
$this->Breadcrumbs->add([
32+
['title' => 'Home', 'url' => '/'],
33+
]);
34+
// Single crumb should stay as is
35+
$this->Breadcrumbs->add('Articles', '/articles');
36+
2737
// TypeFactory::getMap($type) should be changed to getMapped($type)
2838
$class = TypeFactory::getMap('datetime');
2939
$allTypes = TypeFactory::getMap(); // This should stay as is

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
use Cake\ORM\Entity;
88
use Cake\ORM\Locator\LocatorAwareTrait;
99
use Cake\ORM\Query;
10+
use Cake\View\Helper\BreadcrumbsHelper;
1011

1112
class SomeTest
1213
{
1314
use LocatorAwareTrait;
1415

16+
private BreadcrumbsHelper $Breadcrumbs;
17+
1518
public function testRenames(): void
1619
{
1720
$entity = new Entity();
@@ -24,6 +27,13 @@ public function testRenames(): void
2427
$table = $this->fetchTable('Articles');
2528
$expr = $table->find()->expr();
2629

30+
// BreadcrumbsHelper::add(array) should be changed to addMany()
31+
$this->Breadcrumbs->addMany([
32+
['title' => 'Home', 'url' => '/'],
33+
]);
34+
// Single crumb should stay as is
35+
$this->Breadcrumbs->add('Articles', '/articles');
36+
2737
// TypeFactory::getMap($type) should be changed to getMapped($type)
2838
$class = TypeFactory::getMapped('datetime');
2939
$allTypes = TypeFactory::getMap(); // This should stay as is

0 commit comments

Comments
 (0)