Skip to content

Commit 8ed4a95

Browse files
committed
Add BreadcrumbsHelper title to content rector rule for CakePHP 6.0
In CakePHP 6.0, BreadcrumbsHelper renamed the 'title' array key and parameter to 'content' for clarity. This rector rule automatically transforms: - Array keys: ['title' => 'Home'] → ['content' => 'Home'] - Named parameters: add(title: 'Home') → add(content: 'Home') - matchingTitle parameter → matchingContent for insertBefore/insertAfter Refs cakephp/cakephp#18334
1 parent b53d7fc commit 8ed4a95

7 files changed

Lines changed: 309 additions & 0 deletions

File tree

config/rector/cakephp60.php

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\Set\CakePHPSetList;
5+
use Rector\Config\RectorConfig;
6+
7+
return static function (RectorConfig $rectorConfig): void {
8+
$rectorConfig->sets([CakePHPSetList::CAKEPHP_60]);
9+
};

config/rector/sets/cakephp60.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
use Cake\Upgrade\Rector\Rector\MethodCall\BreadcrumbsHelperTitleToContentRector;
5+
use Rector\Config\RectorConfig;
6+
7+
# @see https://book.cakephp.org/5/en/appendices/6-0-migration-guide.html
8+
return static function (RectorConfig $rectorConfig): void {
9+
// BreadcrumbsHelper 'title' key/parameter renamed to 'content'
10+
// @see https://github.com/cakephp/cakephp/pull/18334
11+
$rectorConfig->rule(BreadcrumbsHelperTitleToContentRector::class);
12+
};
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Cake\Upgrade\Rector\Rector\MethodCall;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr\Array_;
9+
use PhpParser\Node\Expr\ArrayItem;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Identifier;
12+
use PhpParser\Node\Scalar\String_;
13+
use PHPStan\Type\ObjectType;
14+
use Rector\Rector\AbstractRector;
15+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
16+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
17+
18+
/**
19+
* Transforms BreadcrumbsHelper 'title' array keys and named parameters to 'content'
20+
*
21+
* In CakePHP 6.0, the 'title' key/parameter was renamed to 'content' for clarity.
22+
*
23+
* @see https://github.com/cakephp/cakephp/pull/18334
24+
*/
25+
final class BreadcrumbsHelperTitleToContentRector extends AbstractRector
26+
{
27+
/**
28+
* Methods where we should rename 'title' to 'content' in array keys
29+
*/
30+
private const METHODS_WITH_TITLE_ARRAY = [
31+
'add',
32+
'prepend',
33+
'addMany',
34+
'prependMany',
35+
];
36+
37+
/**
38+
* Methods where first parameter name changed from matchingTitle to matchingContent
39+
*/
40+
private const METHODS_WITH_MATCHING_TITLE = [
41+
'insertBefore',
42+
'insertAfter',
43+
];
44+
45+
public function getRuleDefinition(): RuleDefinition
46+
{
47+
return new RuleDefinition(
48+
'Change BreadcrumbsHelper "title" array key and parameter to "content"',
49+
[
50+
new CodeSample(
51+
<<<'CODE_SAMPLE'
52+
$this->Breadcrumbs->add(['title' => 'Home', 'url' => '/']);
53+
$this->Breadcrumbs->addMany([
54+
['title' => 'Home', 'url' => '/'],
55+
['title' => 'Articles', 'url' => '/articles'],
56+
]);
57+
$this->Breadcrumbs->insertBefore('Home', 'Dashboard', '/dashboard');
58+
CODE_SAMPLE
59+
,
60+
<<<'CODE_SAMPLE'
61+
$this->Breadcrumbs->add(['content' => 'Home', 'url' => '/']);
62+
$this->Breadcrumbs->addMany([
63+
['content' => 'Home', 'url' => '/'],
64+
['content' => 'Articles', 'url' => '/articles'],
65+
]);
66+
$this->Breadcrumbs->insertBefore('Home', 'Dashboard', '/dashboard');
67+
CODE_SAMPLE,
68+
),
69+
],
70+
);
71+
}
72+
73+
/**
74+
* @return array<class-string<\PhpParser\Node>>
75+
*/
76+
public function getNodeTypes(): array
77+
{
78+
return [MethodCall::class];
79+
}
80+
81+
/**
82+
* @param \PhpParser\Node\Expr\MethodCall $node
83+
*/
84+
public function refactor(Node $node): ?Node
85+
{
86+
if (!$node->name instanceof Identifier) {
87+
return null;
88+
}
89+
90+
// Check if this is called on BreadcrumbsHelper
91+
if (!$this->isObjectType($node->var, new ObjectType('Cake\View\Helper\BreadcrumbsHelper'))) {
92+
return null;
93+
}
94+
95+
$methodName = $node->name->toString();
96+
$hasChanges = false;
97+
98+
// Handle methods where array keys need 'title' → 'content' rename
99+
if (in_array($methodName, self::METHODS_WITH_TITLE_ARRAY, true)) {
100+
$hasChanges = $this->renameArrayKeys($node) || $hasChanges;
101+
$hasChanges = $this->renameNamedParameter($node, 'title', 'content') || $hasChanges;
102+
}
103+
104+
// Handle insertBefore/insertAfter: matchingTitle → matchingContent named param
105+
if (in_array($methodName, self::METHODS_WITH_MATCHING_TITLE, true)) {
106+
$hasChanges = $this->renameNamedParameter($node, 'matchingTitle', 'matchingContent') || $hasChanges;
107+
$hasChanges = $this->renameNamedParameter($node, 'title', 'content') || $hasChanges;
108+
}
109+
110+
// Handle insertAt: title → content named param
111+
if ($methodName === 'insertAt') {
112+
$hasChanges = $this->renameNamedParameter($node, 'title', 'content') || $hasChanges;
113+
}
114+
115+
return $hasChanges ? $node : null;
116+
}
117+
118+
/**
119+
* Rename 'title' keys to 'content' in array arguments
120+
*/
121+
private function renameArrayKeys(MethodCall $node): bool
122+
{
123+
$hasChanges = false;
124+
125+
foreach ($node->args as $arg) {
126+
if (!$arg instanceof Arg) {
127+
continue;
128+
}
129+
130+
if ($arg->value instanceof Array_) {
131+
$hasChanges = $this->processArray($arg->value) || $hasChanges;
132+
}
133+
}
134+
135+
return $hasChanges;
136+
}
137+
138+
/**
139+
* Process an array, renaming 'title' keys to 'content'
140+
*/
141+
private function processArray(Array_ $array): bool
142+
{
143+
$hasChanges = false;
144+
145+
foreach ($array->items as $item) {
146+
if (!$item instanceof ArrayItem) {
147+
continue;
148+
}
149+
150+
// Rename 'title' key to 'content'
151+
if ($item->key instanceof String_ && $item->key->value === 'title') {
152+
$item->key = new String_('content');
153+
$hasChanges = true;
154+
}
155+
156+
// Recursively process nested arrays (for addMany/prependMany)
157+
if ($item->value instanceof Array_) {
158+
$hasChanges = $this->processArray($item->value) || $hasChanges;
159+
}
160+
}
161+
162+
return $hasChanges;
163+
}
164+
165+
/**
166+
* Rename a named parameter
167+
*/
168+
private function renameNamedParameter(MethodCall $node, string $oldName, string $newName): bool
169+
{
170+
foreach ($node->args as $arg) {
171+
if (!$arg instanceof Arg) {
172+
continue;
173+
}
174+
175+
if ($arg->name instanceof Identifier && $arg->name->toString() === $oldName) {
176+
$arg->name = new Identifier($newName);
177+
178+
return true;
179+
}
180+
}
181+
182+
return false;
183+
}
184+
}

src/Rector/Set/CakePHPSetList.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ final class CakePHPSetList
8585
*/
8686
public const CAKEPHP_53 = __DIR__ . '/../../../config/rector/sets/cakephp53.php';
8787

88+
/**
89+
* @var string
90+
*/
91+
public const CAKEPHP_60 = __DIR__ . '/../../../config/rector/sets/cakephp60.php';
92+
8893
/**
8994
* @var string
9095
*/

tests/TestCase/Command/RectorCommandTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ public function testApply53()
123123
$this->assertTestAppUpgraded();
124124
}
125125

126+
public function testApply60()
127+
{
128+
$this->setupTestApp(__FUNCTION__);
129+
$this->exec('upgrade rector --rules cakephp60 ' . TEST_APP);
130+
$this->assertTestAppUpgraded();
131+
}
132+
126133
public function testApplyMigrations45()
127134
{
128135
$this->setupTestApp(__FUNCTION__);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace MyPlugin;
5+
6+
use Cake\View\Helper\BreadcrumbsHelper;
7+
8+
class BreadcrumbsExample
9+
{
10+
private BreadcrumbsHelper $Breadcrumbs;
11+
12+
public function testBreadcrumbs(): void
13+
{
14+
// Single crumb with title key should be changed to content
15+
$this->Breadcrumbs->add(['title' => 'Home', 'url' => '/']);
16+
17+
// Multiple crumbs with title keys should be changed to content
18+
$this->Breadcrumbs->addMany([
19+
['title' => 'Home', 'url' => '/'],
20+
['title' => 'Articles', 'url' => '/articles'],
21+
]);
22+
23+
// Prepend with title key should be changed to content
24+
$this->Breadcrumbs->prepend(['title' => 'Dashboard', 'url' => '/dashboard']);
25+
26+
// PrependMany with title keys should be changed to content
27+
$this->Breadcrumbs->prependMany([
28+
['title' => 'Admin', 'url' => '/admin'],
29+
]);
30+
31+
// String argument should stay as is
32+
$this->Breadcrumbs->add('Contact', '/contact');
33+
34+
// Named parameter title should be changed to content
35+
$this->Breadcrumbs->add(title: 'About', url: '/about');
36+
37+
// insertBefore with named parameters
38+
$this->Breadcrumbs->insertBefore(matchingTitle: 'Home', title: 'Start', url: '/start');
39+
40+
// insertAfter with named parameters
41+
$this->Breadcrumbs->insertAfter(matchingTitle: 'Home', title: 'Next', url: '/next');
42+
43+
// insertAt with named parameter
44+
$this->Breadcrumbs->insertAt(0, title: 'First', url: '/first');
45+
}
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace MyPlugin;
5+
6+
use Cake\View\Helper\BreadcrumbsHelper;
7+
8+
class BreadcrumbsExample
9+
{
10+
private BreadcrumbsHelper $Breadcrumbs;
11+
12+
public function testBreadcrumbs(): void
13+
{
14+
// Single crumb with title key should be changed to content
15+
$this->Breadcrumbs->add(['content' => 'Home', 'url' => '/']);
16+
17+
// Multiple crumbs with title keys should be changed to content
18+
$this->Breadcrumbs->addMany([
19+
['content' => 'Home', 'url' => '/'],
20+
['content' => 'Articles', 'url' => '/articles'],
21+
]);
22+
23+
// Prepend with title key should be changed to content
24+
$this->Breadcrumbs->prepend(['content' => 'Dashboard', 'url' => '/dashboard']);
25+
26+
// PrependMany with title keys should be changed to content
27+
$this->Breadcrumbs->prependMany([
28+
['content' => 'Admin', 'url' => '/admin'],
29+
]);
30+
31+
// String argument should stay as is
32+
$this->Breadcrumbs->add('Contact', '/contact');
33+
34+
// Named parameter title should be changed to content
35+
$this->Breadcrumbs->add(content: 'About', url: '/about');
36+
37+
// insertBefore with named parameters
38+
$this->Breadcrumbs->insertBefore(matchingContent: 'Home', content: 'Start', url: '/start');
39+
40+
// insertAfter with named parameters
41+
$this->Breadcrumbs->insertAfter(matchingContent: 'Home', content: 'Next', url: '/next');
42+
43+
// insertAt with named parameter
44+
$this->Breadcrumbs->insertAt(0, content: 'First', url: '/first');
45+
}
46+
}

0 commit comments

Comments
 (0)