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
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ jobs:
run: |
if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then
make install-dev-lowest
elif ${{ matrix.php-version == '8.2' }}; then
make install-dev-ignore-reqs
else
make install-dev
fi
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DEV_DEPENDENCIES = cakephp/cakephp:5.x-dev \
cakephp/cakephp-codesniffer:^5.0 \
mikey179/vfsstream:^1.6.8 \
phpunit/phpunit:^10.5.38 \
cakephp/migrations:^4.5.0
cakephp/migrations:^5.0.0

install-dev:
composer require --dev $(DEV_DEPENDENCIES)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# CakePHP Upgrade tool

[![CI](https://github.com/cakephp/upgrade/actions/workflows/ci.yml/badge.svg)](https://github.com/cakephp/upgrade/actions/workflows/ci.yml)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/)
[![License](https://poser.pugx.org/cakephp/upgrade/license.svg)](LICENSE)

Upgrade tools for CakePHP meant to facilitate migrating between CakePHP 4.x
versions, from CakePHP 4.x to CakePHP 5.x, and between CakePHP 5.x versions.
Expand Down
4 changes: 4 additions & 0 deletions config/rector/sets/cakephp53.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?php
declare(strict_types=1);

use Cake\Upgrade\Rector\Cake5\BreadcrumbsHelperAddManyRector;
use Cake\Upgrade\Rector\Cake5\EntityIsEmptyRector;
use Cake\Upgrade\Rector\Cake5\EntityPatchRector;
use Cake\Upgrade\Rector\Cake5\FormExecuteToProcessRector;
use Cake\Upgrade\Rector\Cake5\NewExprToFuncRector;
use Cake\Upgrade\Rector\Cake5\QueryParamAccessRector;
use Cake\Upgrade\Rector\Cake5\TypeFactoryGetMappedRector;
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\Rector\Name\RenameClassRector;
Expand All @@ -24,8 +26,10 @@
'Cake\TestSuite\Fixture\TransactionFixtureStrategy' => 'Cake\TestSuite\Fixture\TransactionStrategy',
'Cake\TestSuite\Fixture\TruncateFixtureStrategy' => 'Cake\TestSuite\Fixture\TruncateStrategy',
]);
$rectorConfig->rule(BreadcrumbsHelperAddManyRector::class);
$rectorConfig->rule(EntityIsEmptyRector::class);
$rectorConfig->rule(EntityPatchRector::class);
$rectorConfig->rule(FormExecuteToProcessRector::class);
$rectorConfig->rule(QueryParamAccessRector::class);
$rectorConfig->rule(TypeFactoryGetMappedRector::class);
};
142 changes: 142 additions & 0 deletions src/Rector/Cake5/BreadcrumbsHelperAddManyRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\Cake5;

use PhpParser\Node;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use PHPStan\Type\ObjectType;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* Transforms BreadcrumbsHelper::add(array) to addMany() and prepend(array) to prependMany()
*
* In CakePHP 5.3, passing an array of crumbs to add() or prepend() is deprecated.
* Use addMany() or prependMany() instead.
*
* @see https://book.cakephp.org/5/en/appendices/5-3-migration-guide.html
*/
final class BreadcrumbsHelperAddManyRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change BreadcrumbsHelper::add(array) to addMany() and prepend(array) to prependMany()',
[
new CodeSample(
<<<'CODE_SAMPLE'
$this->Breadcrumbs->add([
['title' => 'Home', 'url' => '/'],
['title' => 'Articles', 'url' => '/articles'],
]);
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$this->Breadcrumbs->addMany([
['title' => 'Home', 'url' => '/'],
['title' => 'Articles', 'url' => '/articles'],
]);
CODE_SAMPLE,
),
],
);
}

public function getNodeTypes(): array
{
return [MethodCall::class];
}

public function refactor(Node $node): ?Node
{
if (!$node instanceof MethodCall) {
return null;
}

// Must be add or prepend method
if (!$node->name instanceof Identifier) {
return null;
}

$methodName = $node->name->toString();
if ($methodName !== 'add' && $methodName !== 'prepend') {
return null;
}

// Must have at least one argument
if (count($node->args) < 1) {
return null;
}

// First argument must be an array
$firstArg = $node->args[0]->value;
if (!$firstArg instanceof Array_) {
return null;
}

// Check if the array looks like an array of crumbs (array of arrays)
// rather than a single crumb with title/url keys
if (!$this->isArrayOfCrumbs($firstArg)) {
return null;
}

// Check if this is called on BreadcrumbsHelper
$callerType = $this->getType($node->var);
if (!$callerType instanceof ObjectType) {
return null;
}

if (!$callerType->isInstanceOf('Cake\View\Helper\BreadcrumbsHelper')->yes()) {
return null;
}

// Rename to addMany or prependMany
$newMethodName = $methodName === 'add' ? 'addMany' : 'prependMany';
$node->name = new Identifier($newMethodName);

return $node;
}

/**
* Determine if an array is an array of crumbs (array of arrays)
* vs a single crumb (array with title/url keys)
*/
private function isArrayOfCrumbs(Array_ $array): bool
{
// An array of crumbs is typically a numerically indexed array of arrays
// e.g. [['title' => 'Home'], ['title' => 'Articles']]
// A single crumb would have string keys like 'title', 'url'
// e.g. ['title' => 'Home', 'url' => '/']

if (count($array->items) === 0) {
return false;
}

foreach ($array->items as $item) {
if (!$item instanceof ArrayItem) {
continue;
}

// If item has a string key like 'title' or 'url', it's a single crumb
if ($item->key instanceof String_) {
$keyValue = $item->key->value;
if ($keyValue === 'title' || $keyValue === 'url' || $keyValue === 'options') {
return false;
}
}

// If the item value is an array, it's likely an array of crumbs
if ($item->value instanceof Array_) {
return true;
}
}

return false;
}
}
82 changes: 82 additions & 0 deletions src/Rector/Cake5/TypeFactoryGetMappedRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\Cake5;

use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* Transforms TypeFactory::getMap($type) to TypeFactory::getMapped($type)
*
* In CakePHP 5.3, calling getMap() with a type argument is deprecated.
* Use getMapped() instead for single-type lookups.
*
* @see https://book.cakephp.org/5/en/appendices/5-3-migration-guide.html
*/
final class TypeFactoryGetMappedRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change TypeFactory::getMap($type) to TypeFactory::getMapped($type)',
[
new CodeSample(
<<<'CODE_SAMPLE'
use Cake\Database\TypeFactory;

$class = TypeFactory::getMap('datetime');
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Cake\Database\TypeFactory;

$class = TypeFactory::getMapped('datetime');
CODE_SAMPLE,
),
],
);
}

public function getNodeTypes(): array
{
return [StaticCall::class];
}

public function refactor(Node $node): ?Node
{
if (!$node instanceof StaticCall) {
return null;
}

// Must be getMap method
if (!$node->name instanceof Identifier || $node->name->toString() !== 'getMap') {
return null;
}

// Must have at least one argument (the type)
if (count($node->args) < 1) {
return null;
}

// Check if this is called on TypeFactory
if (!$node->class instanceof Name) {
return null;
}

$className = $node->class->toString();
if ($className !== 'Cake\Database\TypeFactory' && $className !== 'TypeFactory') {
return null;
}

// Rename to getMapped
$node->name = new Identifier('getMapped');

return $node;
}
}
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\BreadcrumbsHelperAddManyRector;

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

final class BreadcrumbsHelperAddManyRectorTest 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,53 @@
<?php

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

use Cake\View\Helper\BreadcrumbsHelper;

class SomeView
{
private BreadcrumbsHelper $Breadcrumbs;

public function someMethod()
{
// Should transform: add with array of crumbs
$this->Breadcrumbs->add([
['title' => 'Home', 'url' => '/'],
['title' => 'Articles', 'url' => '/articles'],
]);

// Should transform: prepend with array of crumbs
$this->Breadcrumbs->prepend([
['title' => 'Dashboard'],
]);
}
}

?>
-----
<?php

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

use Cake\View\Helper\BreadcrumbsHelper;

class SomeView
{
private BreadcrumbsHelper $Breadcrumbs;

public function someMethod()
{
// Should transform: add with array of crumbs
$this->Breadcrumbs->addMany([
['title' => 'Home', 'url' => '/'],
['title' => 'Articles', 'url' => '/articles'],
]);

// Should transform: prepend with array of crumbs
$this->Breadcrumbs->prependMany([
['title' => 'Dashboard'],
]);
}
}

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

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

use Cake\View\Helper\BreadcrumbsHelper;

class SomeView
{
private BreadcrumbsHelper $Breadcrumbs;

public function someMethod()
{
// Should NOT transform: single crumb with title/url
$this->Breadcrumbs->add('Home', '/');

// Should NOT transform: single crumb as array with title key
$this->Breadcrumbs->add(['title' => 'Home', 'url' => '/']);

// Should NOT transform: prepend single crumb
$this->Breadcrumbs->prepend('Dashboard');
}
}

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

use Cake\Upgrade\Rector\Cake5\BreadcrumbsHelperAddManyRector;
use Rector\Config\RectorConfig;

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