Files
dualfroz b3fc145df6 Add #[NoDiscard] to CarbonImmutable modify(), next(), previous() and __call()
On PHP 8.5, DateTimeImmutable::modify() warns when its result is
discarded, but CarbonImmutable overrides it and lost the attribute.
Override modify(), next(), previous() and __call() in CarbonImmutable
with #[NoDiscard] so a discarded result warns again. The attribute is
ignored on older PHP versions, and mutable Carbon is unchanged.

Existing immutable tests that discarded results now keep them; in
IsTest::testIsSameMonth this was a real slip, the loop never moved $dt.

Fixes #3377
2026-10-03 02:24:35 +02:00

269 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* This file is part of the Carbon package.
*
* (c) Brian Nesbitt <brian@nesbot.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\CarbonImmutable;
use BadMethodCallException;
use Carbon\CarbonImmutable as Carbon;
use CarbonTimezoneTrait;
use Closure;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use SubCarbonImmutable;
use Tests\AbstractTestCaseWithOldNow;
use Tests\Carbon\Fixtures\FooBar;
use Tests\CarbonImmutable\Fixtures\Mixin;
class MacroTest extends AbstractTestCaseWithOldNow
{
#[RequiresPhpExtension('calendar')]
public function testCarbonIsMacroableWhenNotCalledDynamically()
{
Carbon::macro('easterDays', function ($year = 2019) {
return easter_days($year);
});
/** @var mixed $now */
$now = Carbon::now();
$this->assertSame(22, $now->easterDays(2020));
$this->assertSame(31, $now->easterDays());
Carbon::macro('otherParameterName', function ($other = true) {
return $other;
});
$this->assertTrue($now->otherParameterName());
}
#[RequiresPhpExtension('calendar')]
public function testCarbonIsMacroableWhenNotCalledDynamicallyUsingThis()
{
Carbon::macro('diffFromEaster', function ($year) {
/** @var Carbon $date */
$date = $this;
return $date->toDateTime()->diff(
Carbon::create($year, 3, 21)
->setTimezone($date->getTimezone())
->addDays(easter_days($year))
->endOfDay(),
);
});
/** @var mixed $now */
$now = Carbon::now();
$this->assertSame(1020, $now->diffFromEaster(2020)->days);
}
#[RequiresPhpExtension('calendar')]
public function testCarbonIsMacroableWhenCalledStatically()
{
Carbon::macro('easterDate', function ($year) {
return Carbon::create($year, 3, 21)->addDays(easter_days($year));
});
$this->assertSame('05/04', Carbon::easterDate(2015)->format('d/m'));
}
public function testCarbonIsMacroableWithNonClosureCallables()
{
Carbon::macro('lower2', 'strtolower');
/** @var mixed $now */
$now = Carbon::now();
$this->assertSame('abc', $now->lower2('ABC'));
$this->assertSame('abc', Carbon::lower2('ABC'));
}
public function testCarbonIsMixinable()
{
include_once __DIR__.'/Fixtures/Mixin.php';
$mixin = new Mixin();
Carbon::mixin($mixin);
Carbon::setUserTimezone('America/Belize');
/** @var mixed $date */
$date = Carbon::parse('2000-01-01 12:00:00', 'UTC');
$this->assertSame('06:00 America/Belize', $date->userFormat('H:i e'));
}
public function testCarbonRaisesExceptionWhenStaticMacroIsNotFound()
{
$this->expectExceptionObject(new BadMethodCallException(
'Method Carbon\CarbonImmutable::nonExistingStaticMacro does not exist.',
));
Carbon::nonExistingStaticMacro();
}
public function testCarbonRaisesExceptionWhenMacroIsNotFound()
{
$this->expectExceptionObject(new BadMethodCallException(
'Method nonExistingMacro does not exist.',
));
/** @var mixed $date */
$date = Carbon::now();
$result = $date->nonExistingMacro();
}
public function testTraitMixin()
{
Carbon::mixin(FooBar::class);
Carbon::setTestNow('2019-07-19 00:00:00');
$this->assertSame('supergirl / Friday / immutable', Carbon::super('girl'));
$this->assertSame('superboy / Thursday / immutable', Carbon::parse('2019-07-18')->super('boy'));
$this->assertInstanceOf(Carbon::class, Carbon::me());
}
public function testTraitWithNamedParameters()
{
require_once __DIR__.'/../Fixtures/CarbonTimezoneTrait.php';
Carbon::mixin(CarbonTimezoneTrait::class);
$now = Carbon::now();
$now = eval("return \$now->toAppTz(tz: 'Europe/Paris');");
$this->assertSame('Europe/Paris', $now->format('e'));
}
public function testSerializationAfterTraitChaining()
{
require_once __DIR__.'/../Fixtures/CarbonTimezoneTrait.php';
Carbon::mixin(CarbonTimezoneTrait::class);
Carbon::setTestNow('2023-05-24 14:49');
$date = Carbon::toAppTz(false, 'Europe/Paris');
$this->assertSame('2023-05-24 16:49 Europe/Paris', unserialize(serialize($date))->format('Y-m-d H:i e'));
$date = Carbon::parse('2023-06-12 11:49')->toAppTz(false, 'Europe/Paris');
$this->assertSame('2023-06-12 13:49 Europe/Paris', unserialize(serialize($date))->format('Y-m-d H:i e'));
}
public function testSubClassMacro()
{
require_once __DIR__.'/../Fixtures/SubCarbonImmutable.php';
$subCarbon = new SubCarbonImmutable('2024-01-24 00:00');
SubCarbonImmutable::macro('diffInDecades', function (SubCarbonImmutable|string|null $dt = null, $abs = true) {
return (int) ($this->diffInYears($dt, $abs) / 10);
});
$this->assertSame(2, $subCarbon->diffInDecades(new SubCarbonImmutable('2049-01-24 00:00')));
$this->assertSame(2, $subCarbon->diffInDecades('2049-01-24 00:00'));
SubCarbonImmutable::resetMacros();
}
public function testLazyMixinMethodCall()
{
$test = new class() {
public static array $calledMethods = [];
public function __construct()
{
self::$calledMethods[] = __METHOD__;
}
public function __destruct()
{
self::$calledMethods[] = __METHOD__;
}
public static function noReturnType()
{
self::$calledMethods[] = __METHOD__;
return static fn () => 'foo';
}
public static function returnVoid(): void
{
self::$calledMethods[] = __METHOD__;
}
public static function returnArray(): array
{
self::$calledMethods[] = __METHOD__;
return [];
}
public static function returnObject(): object
{
self::$calledMethods[] = __METHOD__;
return (object) [];
}
public static function returnClosure(): Closure
{
self::$calledMethods[] = __METHOD__;
return static fn () => 'foo';
}
public static function returnMixed(): mixed
{
self::$calledMethods[] = __METHOD__;
return static fn () => 'foo';
}
public static function returnOtherBuiltIn(): bool
{
self::$calledMethods[] = __METHOD__;
return true;
}
public static function returnUnion(): bool|array
{
self::$calledMethods[] = __METHOD__;
return true;
}
public static function getCalledMethods(): array
{
return self::$calledMethods;
}
};
Carbon::mixin($test);
Carbon::resetMacros();
$this->assertSame([
'__construct', // Only happening because of $test = new class()... but none from Carbon::mixin()
'noReturnType',
'returnArray',
'returnObject',
'returnClosure',
'returnMixed',
'returnUnion',
], array_map(
static fn (string $name) => explode('::', $name, 2)[1],
$test::getCalledMethods(),
));
}
}