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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class DateWithStrtotimeExample
{
public function run()
{
$formatted = date('d.m.Y', strtotime('2025-02-21'));
}
}

-----
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class DateWithStrtotimeExample
{
public function run()
{
$formatted = \Carbon\Carbon::parse('2025-02-21')->format('d.m.Y');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class SimpleDateExample
{
public function run()
{
$date = date('Y-m-d');
}
}

-----
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class SimpleDateExample
{
public function run()
{
$date = \Carbon\Carbon::now()->format('Y-m-d');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class StrtotimeExample
{
public function run()
{
$timestamp = strtotime('2025-02-20');
}
}

-----
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class StrtotimeExample
{
public function run()
{
$timestamp = \Carbon\Carbon::parse('2025-02-20')->getTimestamp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class TimeCalculationExample
{
public function run()
{
$twoSecondsAgo = time() - 2;
$twoMinutesAgo = time() - (60 * 2);
$twoHoursAgo = time() - (60 * 60 * 2);
$twoDaysAgo = time() - (60 * 60 * 24 * 2);
$twoWeeksAgo = time() - (60 * 60 * 24 * 14);
}
}

-----
<?php

namespace Rector\Tests\Carbon\Rector\FuncCall\DateFuncCallToCarbonRector\Fixture;

class TimeCalculationExample
{
public function run()
{
$twoSecondsAgo = \Carbon\Carbon::now()->subSeconds(2)->getTimestamp();
$twoMinutesAgo = \Carbon\Carbon::now()->subMinutes(2)->getTimestamp();
$twoHoursAgo = \Carbon\Carbon::now()->subHours(2)->getTimestamp();
$twoDaysAgo = \Carbon\Carbon::now()->subDays(2)->getTimestamp();
$twoWeeksAgo = \Carbon\Carbon::now()->subWeeks(2)->getTimestamp();
}
}
180 changes: 171 additions & 9 deletions rules/Carbon/Rector/FuncCall/DateFuncCallToCarbonRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Minus;
use PhpParser\Node\Expr\BinaryOp\Mul;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
Expand All @@ -20,6 +24,14 @@
*/
final class DateFuncCallToCarbonRector extends AbstractRector
{
private const TIME_UNITS = [
['weeks', 604800],
['days', 86400],
['hours', 3600],
['minutes', 60],
['seconds', 1],
];

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Convert date() function call to Carbon::now()->format(*)', [
Expand All @@ -33,7 +45,6 @@ public function run()
}
}
CODE_SAMPLE

,
<<<'CODE_SAMPLE'
class SomeClass
Expand All @@ -53,34 +64,185 @@ public function run()
*/
public function getNodeTypes(): array
{
return [FuncCall::class];
return [Minus::class, FuncCall::class];
}

/**
* @param FuncCall $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isName($node->name, 'date')) {
if ($node instanceof Minus) {
$left = $node->left;
if ($left instanceof FuncCall && $this->isName($left->name, 'time')) {
$timeUnit = $this->detectTimeUnit($node->right);
if ($timeUnit !== null) {
return $this->createCarbonSubtract($timeUnit);
}
}

return null;
}

if (! $node instanceof FuncCall) {
return null;
}

if ($node->isFirstClassCallable()) {
return null;
}

if (count($node->getArgs()) !== 1) {
if ($this->isName($node->name, 'date') && isset($node->args[1]) && $node->args[1] instanceof Arg) {
$format = $this->getArgValue($node, 0);
if (! $format instanceof Expr) {
return null;
}

$timestamp = $node->args[1]->value;
if ($timestamp instanceof FuncCall
&& $this->isName($timestamp->name, 'strtotime')
&& isset($timestamp->args[0]) && $timestamp->args[0] instanceof Arg
) {
$dateExpr = $timestamp->args[0]->value;
return $this->createCarbonParseFormat($dateExpr, $format);
}
}

if ($this->isName($node->name, 'date') && isset($node->args[0])) {
$format = $this->getArgValue($node, 0);
if ($format instanceof String_) {
return $this->createCarbonNowFormat($format);
}
}

if ($this->isName($node->name, 'strtotime') && isset($node->args[0])) {
$dateExpr = $this->getArgValue($node, 0);
if ($dateExpr !== null) {
return $this->createCarbonParseTimestamp($dateExpr);
}
}

return null;
}

private function getArgValue(FuncCall $node, int $index): ?Expr
{
if (! isset($node->args[$index]) || ! $node->args[$index] instanceof Arg) {
return null;
}

return $node->args[$index]->value;
}

private function createCarbonNowFormat(String_ $format): MethodCall
{
$nowCall = new StaticCall(
new FullyQualified('Carbon\\Carbon'),
'now'
);

return new MethodCall($nowCall, 'format', [new Arg($format)]);
}

private function createCarbonParseTimestamp(Expr $dateExpr): MethodCall
{
$parseCall = new StaticCall(
new FullyQualified('Carbon\\Carbon'),
'parse',
[new Arg($dateExpr)]
);

return new MethodCall($parseCall, 'getTimestamp');
}

private function createCarbonParseFormat(Expr $dateExpr, Expr $format): MethodCall
{
$parseCall = new StaticCall(
new FullyQualified('Carbon\\Carbon'),
'parse',
[new Arg($dateExpr)]
);

return new MethodCall($parseCall, 'format', [new Arg($format)]);
}

/**
* @param array{unit: string, value: int} $timeUnit
*/
private function createCarbonSubtract(
array $timeUnit
): MethodCall {
$nowCall = new StaticCall(
new FullyQualified('Carbon\\Carbon'),
'now'
);
$methodName = 'sub' . ucfirst($timeUnit['unit']);
$subtractCall = new MethodCall($nowCall, $methodName, [new Arg(new LNumber($timeUnit['value']))]);
return new MethodCall($subtractCall, 'getTimestamp');
}

/**
* @return array{unit: string, value: int}|null
*/
private function detectTimeUnit(Expr $node): ?array
{
$product = $this->calculateProduct($node);
if ($product === null) {
return null;
}

foreach (self::TIME_UNITS as [$unit, $seconds]) {
if ($product % $seconds === 0) {
return [
'unit' => (string) $unit,
'value' => (int) ($product / $seconds),
];
}
}

return null;
}

private function calculateProduct(Expr $node): float|int|null
{
if ($node instanceof LNumber) {
return $node->value;
}

if (! $node instanceof Mul) {
return null;
}

$firstArg = $node->getArgs()[0];
if (! $firstArg->value instanceof String_) {
$multipliers = $this->extractMultipliers($node);
if ($multipliers === []) {
return null;
}

// create now and format()
$nowStaticCall = new StaticCall(new FullyQualified('Carbon\Carbon'), 'now');
return array_product($multipliers);
}

/**
* @return int[]
*/
private function extractMultipliers(Node $node): array
{
$multipliers = [];
if (! $node instanceof Mul) {
return $multipliers;
}

if ($node->left instanceof LNumber) {
$multipliers[] = $node->left->value;
} elseif ($node->left instanceof Mul) {
$multipliers = array_merge($multipliers, $this->extractMultipliers($node->left));
}

if ($node->right instanceof LNumber) {
$multipliers[] = $node->right->value;
} elseif ($node->right instanceof Mul) {
$multipliers = array_merge($multipliers, $this->extractMultipliers($node->right));
}

return new MethodCall($nowStaticCall, 'format', [new Arg($firstArg->value)]);
return $multipliers;
}
}