Skip to content

Instantly share code, notes, and snippets.

@mortenscheel
Created April 28, 2025 06:46
Show Gist options
  • Save mortenscheel/7b90133ba7432ad99dadda70a6d63f80 to your computer and use it in GitHub Desktop.
Save mortenscheel/7b90133ba7432ad99dadda70a6d63f80 to your computer and use it in GitHub Desktop.
The purpose of this Rector is to refactor a code base using Carbon V2 to V3 without changing behavior.
<?php
declare(strict_types=1);
namespace Dev\Rector;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Cast\Int_ as IntCast;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
class CarbonV3UpgradeRector extends AbstractRector
{
private const diffMethodNames = [
'diffInSeconds',
'diffInMinutes',
'diffInHours',
'diffInDays',
'diffInWeekdays',
'diffInWeeks',
'diffInMonths',
'diffInYears',
];
private const isSameMethodNames = [
'isSameSecond',
'isSameMinute',
'isSameHour',
'isSameDay',
'isSameWeek',
'isSameMonth',
'isSameYear',
'isSameDecade',
];
private const addSubtractMethodNames = [
'addSeconds',
'addMinutes',
'addHours',
'addDays',
'addWeeks',
'addMonths',
'addYears',
'subSeconds',
'subMinutes',
'subHours',
'subDays',
'subWeeks',
'subMonths',
'subYears',
];
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Node\Expr\MethodCall::class, Node\Expr\StaticCall::class];
}
public function refactor(Node $node): ?Node
{
if ($node instanceof Node\Expr\StaticCall) {
return $this->refactorStaticCall($node);
}
/** @var MethodCall $node */
// Only process method calls on Carbon objects
$calledType = $this->getType($node->var);
if (!$calledType->isSuperTypeOf(new ObjectType(CarbonInterface::class))) {
return null;
}
if ($this->isNames($node->name, self::diffMethodNames)) {
return $this->refactorDiffMethodCall($node);
}
if ($this->isNames($node->name, self::isSameMethodNames)) {
return $this->refactorIsSameMethodCall($node);
}
if ($this->isNames($node->name, self::addSubtractMethodNames)) {
return $this->refactorAddOrSubtractUnitsMethodCall($node);
}
return null;
}
private function hasCallArgument(int $index, string $name, MethodCall $node): bool
{
foreach ($node->args as $arg) {
if ($arg instanceof Arg && $arg->name && $this->isName($arg->name, $name)) {
return true;
}
}
return isset($node->args[$index]);
}
public function getRuleDefinition(): RuleDefinition
{
/** @noinspection PhpUnhandledExceptionInspection */
return new RuleDefinition('', []);
}
/**
* - default for `absolute` was changed from true to false, so add `absolute: true` unless explicitly set already
* - return value was changed from int to float. add int cast if necessary
*/
private function refactorDiffMethodCall(MethodCall|Node $node): ?Node
{
if (!$this->hasCallArgument(1, 'absolute', $node)) {
$arg = new Arg(new Node\Expr\ConstFetch(new Node\Name('true')));
$arg->name = new Identifier('absolute');
$node->args[] = $arg;
}
return new IntCast($node);
}
/**
* isSame* methods can no longer be called without arguments
*/
private function refactorIsSameMethodCall(MethodCall $node): ?Node
{
if (!$this->hasCallArgument(0, 'date', $node)) {
$node->args[] = $this->nodeFactory->createArg(new Node\Expr\FuncCall(new Node\Name('now')));
return $node;
}
return null;
}
/**
* Argument must now be int or float (or no argument). Cast possible string arguments
*/
private function refactorAddOrSubtractUnitsMethodCall(MethodCall $node): ?Node
{
if (!isset($node->args[0])) {
// It's still okay to call e.g. addDays() without arguments (default is 1)
return null;
}
$arg = $node->args[0];
$argType = $this->nodeTypeResolver->getType($arg->value);
if ($argType instanceof StringType || $argType instanceof MixedType) {
$node->args[0] = $this->nodeFactory->createArg(new Node\Expr\Cast\Double($arg->value, ['kind' => Node\Expr\Cast\Double::KIND_FLOAT]));
return $node;
}
return null;
}
/**
* Carbon::createFromTimestamp() now uses UTC as default timezone (previously it used php's cofigured timezone).
* Add "Europe/Copenhagen" as second argument if there's only one argument.'
*/
private function refactorStaticCall(Node\Expr\StaticCall $node): ?Node
{
// Check if this is a call to \Carbon\Carbon::createFromTimestamp()
if (!$this->isName($node->name, 'createFromTimestamp') || !$this->isName($node->class, Carbon::class)) {
return null;
}
// If there's only one argument (the timestamp), add the timezone
if (\count($node->args) === 1) {
$node->args[] = $this->nodeFactory->createArg(new String_('Europe/Copenhagen'));
return $node;
}
return null;
}
}
<?php
declare(strict_types=1);
namespace Dev\Rector;
use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
class RemoveIdenticalConsecutiveCastsRector extends AbstractRector
{
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Node\Expr\Cast::class];
}
/**
* @param Node\Expr\Cast $node
*/
public function refactor(Node $node): ?Node
{
$wasChanged = false;
while ($node::class === $node->expr::class) {
$node = $node->expr;
$wasChanged = true;
}
return $wasChanged ? $node : null;
}
public function getRuleDefinition(): RuleDefinition
{
/** @noinspection PhpUnhandledExceptionInspection */
return new RuleDefinition('', []);
}
}
@mortenscheel
Copy link
Author

See the docs for a description of all breaking changes in Carbon v3.

Note: The diffIn* methods return float in Carbon 3, so this rector adds an (int) cast to those method calls to avoid changing return types. I was unable to detect if an int cast was already applied, so the rector will add ´(int)cast every time it's run. To remove consecutive casts, use theRemoveIdenticalConsecutiveCastsRector`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment