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
6 changes: 5 additions & 1 deletion config/rector/sets/cakephp44.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);

use Cake\Upgrade\Rector\Rector\MethodCall\NewExprToFuncRector;
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\Rector\Name\RenameClassRector;
Expand All @@ -16,8 +17,11 @@
'Cake\TestSuite\HttpClientTrait' => 'Cake\Http\TestSuite\HttpClientTrait',
]);

// Apply newExpr()->count() -> func()->count('*') transformation before general newExpr rename
$rectorConfig->rule(NewExprToFuncRector::class);

$rectorConfig->ruleWithConfiguration(
RenameMethodRector::class,
[new MethodCallRename('Cake\Database\Query', 'newExpr', 'expr')]
[new MethodCallRename('Cake\Database\Query', 'newExpr', 'expr')],
);
};
4 changes: 4 additions & 0 deletions config/rector/sets/cakephp53.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@

use Cake\Upgrade\Rector\Rector\MethodCall\EntityIsEmptyRector;
use Cake\Upgrade\Rector\Rector\MethodCall\EntityPatchRector;
use Cake\Upgrade\Rector\Rector\MethodCall\NewExprToFuncRector;
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Renaming\ValueObject\MethodCallRename;

# @see https://book.cakephp.org/5/en/appendices/5-3-migration-guide.html
return static function (RectorConfig $rectorConfig): void {
// Apply newExpr()->count() -> func()->count('*') transformation before general newExpr rename
$rectorConfig->rule(NewExprToFuncRector::class);

$rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [
new MethodCallRename('Cake\Database\Query', 'newExpr', 'expr'),
]);
Expand Down
11 changes: 11 additions & 0 deletions src/Rector/Rector/MethodCall/EntityPatchRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

namespace Cake\Upgrade\Rector\Rector\MethodCall;

use Cake\ORM\Entity;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Type\ObjectType;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
Expand Down Expand Up @@ -59,6 +61,15 @@ public function refactor(Node $node): ?Node
return null;
}

$callerType = $this->getType($node->var);
if (!$callerType instanceof ObjectType) {
return null;
}

if (!$callerType->isInstanceOf(Entity::class)->yes()) {
return null;
}

// change the method name
$node->name = new Identifier('patch');

Expand Down
137 changes: 137 additions & 0 deletions src/Rector/Rector/MethodCall/NewExprToFuncRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Rector\Rector\MethodCall;

use PhpParser\Node;
use PhpParser\Node\Arg;
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 $query->newExpr()->aggregate() to $query->func()->aggregate()
*
* newExpr() was used to create expression builders, but when followed by aggregate
* or SQL function calls, it should use func() instead which is the function builder.
*
* Handles common FunctionsBuilder methods like:
* - Aggregates: count(), sum(), avg(), min(), max(), rowNumber(), lag(), lead()
* - Date functions: dateDiff(), datePart(), extract(), dateAdd(), now()
* - Other functions: concat(), coalesce(), cast(), rand()
*/
final class NewExprToFuncRector extends AbstractRector
{
/**
* List of FunctionsBuilder methods that should trigger the transformation
*/
private const FUNC_BUILDER_METHODS = [
// Aggregate functions
'count',
'sum',
'avg',
'min',
'max',
'rowNumber',
'lag',
'lead',
// Date/time functions
'dateDiff',
'datePart',
'extract',
'dateAdd',
'now',
'weekday',
'dayOfWeek',
// Other SQL functions
'concat',
'coalesce',
'cast',
'rand',
'jsonValue',
'aggregate',
];

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change $query->newExpr()->funcMethod() to $query->func()->funcMethod() for FunctionsBuilder methods',
[
new CodeSample(
<<<'CODE_SAMPLE'
$query->newExpr()->count();
$query->newExpr()->sum('total');
$query->newExpr()->avg('score');
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$query->func()->count('*');
$query->func()->sum('total');
$query->func()->avg('score');
CODE_SAMPLE,
),
],
);
}

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

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

// Check if this is a FunctionsBuilder method call
if (!$node->name instanceof Identifier) {
return null;
}

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

// Check if the var is also a MethodCall (chained call)
if (!$node->var instanceof MethodCall) {
return null;
}

$innerMethodCall = $node->var;

// Check if the inner call is ->newExpr()
if (!$innerMethodCall->name instanceof Identifier || $innerMethodCall->name->toString() !== 'newExpr') {
return null;
}

// Check if the caller is a Query object (Cake\Database\Query or similar)
$callerType = $this->getType($innerMethodCall->var);
if (!$callerType instanceof ObjectType) {
return null;
}

if (
!$callerType->isInstanceOf('Cake\Database\Query')->yes() &&
!$callerType->isInstanceOf('Cake\ORM\Query')->yes()
) {
return null;
}

// Change newExpr to func
$innerMethodCall->name = new Identifier('func');

// Add '*' argument to count() if it doesn't have arguments
if ($methodName === 'count' && empty($node->args)) {
$node->args = [new Arg(new String_('*'))];
}

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\EntityPatchRector;

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

final class EntityPatchRectorTest 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,63 @@
<?php

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

use Cake\ORM\Entity;

class MyEntity extends Entity
{
}

class UserController
{
public function edit()
{
$entity = new MyEntity();

// Should transform: Entity->set with array literal
$entity->set(['name' => 'Test', 'email' => 'test@example.com']);

// Should NOT transform: set with variable
$data = ['name' => 'Test'];
$entity->set($data);

// Should NOT transform: set with non-array argument
$entity->set('name', 'Test');

return $entity;
}
}

?>
-----
<?php

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

use Cake\ORM\Entity;

class MyEntity extends Entity
{
}

class UserController
{
public function edit()
{
$entity = new MyEntity();

// Should transform: Entity->set with array literal
$entity->patch(['name' => 'Test', 'email' => 'test@example.com']);

// Should NOT transform: set with variable
$data = ['name' => 'Test'];
$entity->set($data);

// Should NOT transform: set with non-array argument
$entity->set('name', 'Test');

return $entity;
}
}

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

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

class SomeOtherClass
{
public function set(array $data): void
{
// Do something
}
}

class MyController
{
public function process()
{
$object = new SomeOtherClass();

// Should NOT transform: not an Entity
$object->set(['key' => 'value']);

// Should NOT transform: unknown object type
$someObject->set(['data' => 'test']);

return $object;
}
}

?>
-----
<?php

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

class SomeOtherClass
{
public function set(array $data): void
{
// Do something
}
}

class MyController
{
public function process()
{
$object = new SomeOtherClass();

// Should NOT transform: not an Entity
$object->set(['key' => 'value']);

// Should NOT transform: unknown object type
$someObject->set(['data' => 'test']);

return $object;
}
}

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

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