diff --git a/composer.json b/composer.json index 5b49713efe..06f1565968 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,8 @@ "require": { "php": "^8.1", "ext-json": "*", + "psr/clock": "^1.0", + "symfony/clock": "^6.3", "symfony/polyfill-mbstring": "^1.0", "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0" }, @@ -57,6 +59,9 @@ "phpunit/phpunit": "^10.2.2", "squizlabs/php_codesniffer": "^3.7.2" }, + "provide": { + "psr/clock-implementation": "1.0" + }, "minimum-stability": "dev", "prefer-stable": true, "autoload": { diff --git a/phpdoc.php b/phpdoc.php index b0137a29e2..1550726753 100644 --- a/phpdoc.php +++ b/phpdoc.php @@ -12,6 +12,8 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Carbon\CarbonInterface; +use Carbon\Factory; +use Carbon\FactoryImmutable; $tags = [ 'property', @@ -108,7 +110,30 @@ function cleanClassName($name) return preg_replace('/^\\\\(Date(?:Time(?:Immutable|Interface|Zone)?|Interval)|[a-z]*Exception|Closure)$/i', '$1', preg_replace('/^\\\\Carbon\\\\/', '', $name)); } -function dumpParameter($method, ReflectionParameter $parameter) +function dumpType(ReflectionType $type, bool $deep = true, bool $allowsNull = false): string +{ + if ($type instanceof ReflectionUnionType) { + return ($deep ? '(' : '').implode('|', array_map( + dumpType(...), + $type->getTypes(), + )).($deep ? ')' : ''); + } + + if ($type instanceof ReflectionIntersectionType) { + return ($deep ? '(' : '').implode('&', array_map( + dumpType(...), + $type->getTypes(), + )).($deep ? ')' : ''); + } + + $name = cleanClassName($type->getName()); + + return (!$deep && $allowsNull ? '?' : ''). + $name. + ($deep && $allowsNull ? '|null' : ''); +} + +function dumpParameter(string $method, ReflectionParameter $parameter): string { global $defaultValues; @@ -119,14 +144,8 @@ function dumpParameter($method, ReflectionParameter $parameter) $output = "...$output"; } - if ($parameter->getType()) { - $name = cleanClassName($parameter->getType()->getName()); - - if ($parameter->allowsNull()) { - $name = "?$name"; - } - - $output = "$name $output"; + if ($parameter->hasType()) { + $output = dumpType($parameter->getType(), false, $parameter->allowsNull())." $output"; } if (isset($defaultValues[$method])) { @@ -137,11 +156,8 @@ function dumpParameter($method, ReflectionParameter $parameter) return $output; } - try { - if ($parameter->isDefaultValueAvailable()) { - $output .= ' = '.dumpValue($parameter->getDefaultValue()); - } - } catch (ReflectionException) { + if ($parameter->isDefaultValueAvailable()) { + $output .= ' = '.dumpValue($parameter->getDefaultValue()); } return $output; @@ -683,22 +699,30 @@ function getMethodReturnType(ReflectionMethod $method) } $returnType = str_replace('static|CarbonInterface', 'static', $returnType ?: 'static'); - $staticMethods[] = [ - '@method', - str_replace(['self', 'static'], 'Carbon', $returnType), - "$method($parameters)", - $doc[0], - ]; - $staticImmutableMethods[] = [ - '@method', - str_replace(['self', 'static'], 'CarbonImmutable', $returnType), - "$method($parameters)", - $doc[0], - ]; + if (!method_exists(Factory::class, $method)) { + $staticMethods[] = [ + '@method', + str_replace(['self', 'static'], 'Carbon', $returnType), + "$method($parameters)", + $doc[0], + ]; + + for ($i = 1; $i < \count($doc); $i++) { + $staticMethods[] = ['', '', '', $doc[$i]]; + } + } - for ($i = 1; $i < \count($doc); $i++) { - $staticMethods[] = ['', '', '', $doc[$i]]; - $staticImmutableMethods[] = ['', '', '', $doc[$i]]; + if (!method_exists(FactoryImmutable::class, $method)) { + $staticImmutableMethods[] = [ + '@method', + str_replace(['self', 'static'], 'CarbonImmutable', $returnType), + "$method($parameters)", + $doc[0], + ]; + + for ($i = 1; $i < \count($doc); $i++) { + $staticImmutableMethods[] = ['', '', '', $doc[$i]]; + } } } diff --git a/src/Carbon/CarbonImmutable.php b/src/Carbon/CarbonImmutable.php index 011e6e8ed1..cadafa84b0 100644 --- a/src/Carbon/CarbonImmutable.php +++ b/src/Carbon/CarbonImmutable.php @@ -513,7 +513,7 @@ class CarbonImmutable extends DateTimeImmutable implements CarbonInterface __clone as dateTraitClone; } - public function __clone() + public function __clone(): void { $this->dateTraitClone(); $this->endOfTime = false; diff --git a/src/Carbon/CarbonInterface.php b/src/Carbon/CarbonInterface.php index 797832bb25..73e0529cff 100644 --- a/src/Carbon/CarbonInterface.php +++ b/src/Carbon/CarbonInterface.php @@ -667,7 +667,7 @@ public static function __callStatic($method, $parameters); /** * Update constructedObjectId on cloned. */ - public function __clone(); + public function __clone(): void; /** * Create a new Carbon instance. @@ -675,12 +675,9 @@ public function __clone(); * Please see the testing aids section (specifically static::setTestNow()) * for more on the possibility of this constructor returning a test instance. * - * @param DateTimeInterface|string|null $time - * @param DateTimeZone|string|null $tz - * * @throws InvalidFormatException */ - public function __construct($time = null, $tz = null); + public function __construct(DateTimeInterface|string|int|float|null $time = null, DateTimeZone|string|int|null $tz = null); /** * Show truthy properties on var_dump(). @@ -2379,12 +2376,8 @@ public static function hasTestNow(); /** * Create a Carbon instance from a DateTime one. - * - * @param DateTimeInterface $date - * - * @return static */ - public static function instance($date): static; + public static function instance(DateTimeInterface $date): static; /** * Returns true if the current date matches the given string. @@ -3289,7 +3282,7 @@ public function notEqualTo($date): bool; * * @return static */ - public static function now($tz = null); + public static function now(DateTimeZone|string|int|null $tz = null): static; /** * Returns a present instance in the same timezone. @@ -3961,6 +3954,8 @@ public function since($other = null, $syntax = null, $short = false, $parts = 1, */ public static function singularUnit(string $unit): string; + public static function sleep(int|float $seconds): void; + /** * Modify to start of current given unit. * diff --git a/src/Carbon/CarbonTimeZone.php b/src/Carbon/CarbonTimeZone.php index d7666aa047..49cf31398b 100644 --- a/src/Carbon/CarbonTimeZone.php +++ b/src/Carbon/CarbonTimeZone.php @@ -83,8 +83,8 @@ public function cast(string $className) /** * Create a CarbonTimeZone from mixed input. * - * @param DateTimeZone|string|int|null $object original value to get CarbonTimeZone from it. - * @param DateTimeZone|string|int|null $objectDump dump of the object for error messages. + * @param DateTimeZone|string|int|false|null $object original value to get CarbonTimeZone from it. + * @param DateTimeZone|string|int|false|null $objectDump dump of the object for error messages. * * @throws InvalidTimeZoneException * @@ -102,12 +102,14 @@ public static function instance($object = null, $objectDump = null): ?self return new static(); } - if (!$tz instanceof DateTimeZone) { - $tz = static::getDateTimeZoneFromName($object); - } + if ($tz !== false) { + if (!$tz instanceof DateTimeZone) { + $tz = static::getDateTimeZoneFromName($object); + } - if ($tz !== null) { - return new static($tz->getName()); + if ($tz !== null) { + return new static($tz->getName()); + } } if (Carbon::isStrictModeEnabled()) { diff --git a/src/Carbon/Factory.php b/src/Carbon/Factory.php index d8dbaaa006..2fb08c1a5d 100644 --- a/src/Carbon/Factory.php +++ b/src/Carbon/Factory.php @@ -91,7 +91,7 @@ * @method bool hasRelativeKeywords($time) Determine if a time string will produce a relative date. * @method bool hasTestNow() Determine if there is a valid test instance set. A valid test instance * is anything that is not null. - * @method Carbon instance($date) Create a Carbon instance from a DateTime one. + * @method Carbon instance(DateTimeInterface $date) Create a Carbon instance from a DateTime one. * @method bool isImmutable() Returns true if the current class/instance is immutable. * @method bool isModifiableUnit($unit) Returns true if a property can be changed via setter. * @method bool isMutable() Returns true if the current class/instance is mutable. @@ -112,7 +112,7 @@ * Always return a new instance. Parse only strings and only these likely to be dates (skip intervals * and recurrences). Throw an exception for invalid format, but otherwise return null. * @method void mixin($mixin) Mix another object into the class. - * @method Carbon now($tz = null) Get a Carbon instance for the current date and time. + * @method Carbon now(DateTimeZone|string|int|null $tz = null) Get a Carbon instance for the current date and time. * @method Carbon parse($time = null, $tz = null) Create a carbon instance from a string. * This is an alias for the constructor that allows better fluent syntax * as it allows you to do Carbon::parse('Monday next week')->fn() rather @@ -206,6 +206,7 @@ * @method bool shouldOverflowMonths() Get the month overflow global behavior (can be overridden in specific instances). * @method bool shouldOverflowYears() Get the month overflow global behavior (can be overridden in specific instances). * @method string singularUnit(string $unit) Returns standardized singular of a given singular/plural unit name (in English). + * @method void sleep(int|float $seconds) * @method Carbon today($tz = null) Create a Carbon instance for today. * @method Carbon tomorrow($tz = null) Create a Carbon instance for tomorrow. * @method string translateTimeString(string $timeString, ?string $from = null, ?string $to = null, int $mode = CarbonInterface::TRANSLATE_ALL) Translate a time string from a locale to an other. diff --git a/src/Carbon/FactoryImmutable.php b/src/Carbon/FactoryImmutable.php index 2da53df1f4..70e00b9c15 100644 --- a/src/Carbon/FactoryImmutable.php +++ b/src/Carbon/FactoryImmutable.php @@ -12,6 +12,9 @@ namespace Carbon; use Closure; +use DateTimeZone; +use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\Clock\NativeClock; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -89,7 +92,7 @@ * @method bool hasRelativeKeywords($time) Determine if a time string will produce a relative date. * @method bool hasTestNow() Determine if there is a valid test instance set. A valid test instance * is anything that is not null. - * @method CarbonImmutable instance($date) Create a Carbon instance from a DateTime one. + * @method CarbonImmutable instance(DateTimeInterface $date) Create a Carbon instance from a DateTime one. * @method bool isImmutable() Returns true if the current class/instance is immutable. * @method bool isModifiableUnit($unit) Returns true if a property can be changed via setter. * @method bool isMutable() Returns true if the current class/instance is mutable. @@ -110,7 +113,6 @@ * Always return a new instance. Parse only strings and only these likely to be dates (skip intervals * and recurrences). Throw an exception for invalid format, but otherwise return null. * @method void mixin($mixin) Mix another object into the class. - * @method CarbonImmutable now($tz = null) Get a Carbon instance for the current date and time. * @method CarbonImmutable parse($time = null, $tz = null) Create a carbon instance from a string. * This is an alias for the constructor that allows better fluent syntax * as it allows you to do Carbon::parse('Monday next week')->fn() rather @@ -226,7 +228,40 @@ * * */ -class FactoryImmutable extends Factory +class FactoryImmutable extends Factory implements ClockInterface { protected $className = CarbonImmutable::class; + + protected DateTimeZone|string|int|null $defaultTimezone = null; + + /** + * Get a Carbon instance for the current date and time. + */ + public function now(DateTimeZone|string|int|null $tz = null): CarbonImmutable + { + $className = $this->className; + + return new $className(null, $tz ?? $this->defaultTimezone); + } + + public function sleep(int|float $seconds): void + { + $className = $this->className; + + if ($className::hasTestNow()) { + $className::setTestNow($className::getTestNow()->avoidMutation()->addSeconds($seconds)); + + return; + } + + (new NativeClock('UTC'))->sleep($seconds); + } + + public function withTimeZone(DateTimeZone|string|int|null $timezone): static + { + $factory = new static(); + $factory->defaultTimezone = $timezone; + + return $factory; + } } diff --git a/src/Carbon/Traits/Creator.php b/src/Carbon/Traits/Creator.php index ae26194c42..e426d5664f 100644 --- a/src/Carbon/Traits/Creator.php +++ b/src/Carbon/Traits/Creator.php @@ -52,24 +52,25 @@ trait Creator * Please see the testing aids section (specifically static::setTestNow()) * for more on the possibility of this constructor returning a test instance. * - * @param DateTimeInterface|string|null $time - * @param DateTimeZone|string|null $tz - * * @throws InvalidFormatException */ - public function __construct($time = null, $tz = null) - { + public function __construct( + DateTimeInterface|string|int|float|null $time = null, + DateTimeZone|string|int|null $tz = null, + ) { if ($time instanceof DateTimeInterface) { $time = $this->constructTimezoneFromDateTime($time, $tz)->format('Y-m-d H:i:s.u'); } if (\is_string($time) && str_starts_with($time, '@')) { $time = static::createFromTimestampUTC(substr($time, 1))->format('Y-m-d\TH:i:s.uP'); + } elseif (is_numeric($time) && (!\is_string($time) || !preg_match('/^\d{1,14}$/', $time))) { + $time = static::createFromTimestampUTC($time)->format('Y-m-d\TH:i:s.uP'); } // If the class has a test now set and we are trying to create a now() // instance then override as required - $isNow = empty($time) || $time === 'now'; + $isNow = \in_array($time, [null, '', 'now'], true); if (method_exists(static::class, 'hasTestNow') && method_exists(static::class, 'getTestNow') && @@ -80,7 +81,7 @@ public function __construct($time = null, $tz = null) } try { - parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); + parent::__construct($time ?? 'now', static::safeCreateDateTimeZone($tz) ?? null); } catch (Exception $exception) { throw new InvalidFormatException($exception->getMessage(), 0, $exception); } @@ -92,14 +93,11 @@ public function __construct($time = null, $tz = null) /** * Get timezone from a datetime instance. - * - * @param DateTimeInterface $date - * @param DateTimeZone|string|null $tz - * - * @return DateTimeInterface */ - private function constructTimezoneFromDateTime(DateTimeInterface $date, &$tz): DateTimeInterface - { + private function constructTimezoneFromDateTime( + DateTimeInterface $date, + DateTimeZone|string|int|null &$tz, + ): DateTimeInterface { if ($tz !== null) { $safeTz = static::safeCreateDateTimeZone($tz); @@ -118,19 +116,15 @@ private function constructTimezoneFromDateTime(DateTimeInterface $date, &$tz): D /** * Update constructedObjectId on cloned. */ - public function __clone() + public function __clone(): void { $this->constructedObjectId = spl_object_hash($this); } /** * Create a Carbon instance from a DateTime one. - * - * @param DateTimeInterface $date - * - * @return static */ - public static function instance($date): static + public static function instance(DateTimeInterface $date): static { if ($date instanceof static) { return clone $date; @@ -242,7 +236,7 @@ public static function parseFromLocale($time, $locale = null, $tz = null) * * @return static */ - public static function now($tz = null) + public static function now(DateTimeZone|string|int|null $tz = null): static { return new static(null, $tz); } diff --git a/src/Carbon/Traits/Date.php b/src/Carbon/Traits/Date.php index a379b83617..af3d0131c1 100644 --- a/src/Carbon/Traits/Date.php +++ b/src/Carbon/Traits/Date.php @@ -35,6 +35,7 @@ use InvalidArgumentException; use ReflectionException; use ReturnTypeWillChange; +use Symfony\Component\Clock\NativeClock; use Throwable; /** @@ -615,27 +616,26 @@ trait Date /** * Creates a DateTimeZone from a string, DateTimeZone or integer offset. * - * @param DateTimeZone|string|int|null $object original value to get CarbonTimeZone from it. - * @param DateTimeZone|string|int|null $objectDump dump of the object for error messages. + * @param DateTimeZone|string|int|false|null $object original value to get CarbonTimeZone from it. + * @param DateTimeZone|string|int|false|null $objectDump dump of the object for error messages. * * @throws InvalidTimeZoneException * * @return CarbonTimeZone|null */ - protected static function safeCreateDateTimeZone($object, $objectDump = null): ?CarbonTimeZone - { + protected static function safeCreateDateTimeZone( + DateTimeZone|string|int|false|null $object, + DateTimeZone|string|int|false|null $objectDump = null, + ): ?CarbonTimeZone { return CarbonTimeZone::instance($object, $objectDump); } /** * Get the TimeZone associated with the Carbon instance (as CarbonTimeZone). * - * @return CarbonTimeZone - * * @link https://php.net/manual/en/datetime.gettimezone.php */ - #[ReturnTypeWillChange] - public function getTimezone() + public function getTimezone(): CarbonTimeZone { return CarbonTimeZone::instance(parent::getTimezone()); } @@ -2391,6 +2391,17 @@ public static function pluralUnit(string $unit): string return "{$unit}s"; } + public static function sleep(int|float $seconds): void + { + if (static::hasTestNow()) { + static::setTestNow(static::getTestNow()->avoidMutation()->addSeconds($seconds)); + + return; + } + + (new NativeClock('UTC'))->sleep($seconds); + } + /** * Dynamically handle calls to the class. * diff --git a/tests/Carbon/ConstructTest.php b/tests/Carbon/ConstructTest.php index 5600a947f9..bb0d6c3b75 100644 --- a/tests/Carbon/ConstructTest.php +++ b/tests/Carbon/ConstructTest.php @@ -90,6 +90,12 @@ public function testParseWithYYYMMDD() $this->assertCarbon($c, 2020, 11, 28, 0, 0, 0); } + public function testParseWithYYYMMDDHHMMSS() + { + $c = Carbon::parse('20201128192533'); + $this->assertCarbon($c, 2020, 11, 28, 19, 25, 33); + } + public function testDefaultTimezone() { $c = new Carbon('now'); @@ -171,7 +177,19 @@ public function testMockingWithMicroseconds() public function testTimestamp() { - $date = new Carbon('@1367186296'); - $this->assertSame('Sunday 28 April 2013 21:58:16', $date->format('l j F Y H:i:s')); + $date = new Carbon(1367186296); + $this->assertSame('Sunday 28 April 2013 21:58:16.000000', $date->format('l j F Y H:i:s.u')); + + $date = new Carbon(123); + $this->assertSame('Thursday 1 January 1970 00:02:03.000000', $date->format('l j F Y H:i:s.u')); + } + + public function testFloatTimestamp() + { + $date = new Carbon(1367186296.654321); + $this->assertSame('Sunday 28 April 2013 21:58:16.654321', $date->format('l j F Y H:i:s.u')); + + $date = new Carbon(123.5); + $this->assertSame('Thursday 1 January 1970 00:02:03.500000', $date->format('l j F Y H:i:s.u')); } } diff --git a/tests/Carbon/TestingAidsTest.php b/tests/Carbon/TestingAidsTest.php index b90f66eb0f..3f306bc86a 100644 --- a/tests/Carbon/TestingAidsTest.php +++ b/tests/Carbon/TestingAidsTest.php @@ -14,6 +14,7 @@ namespace Tests\Carbon; use Carbon\Carbon; +use DateTimeImmutable; use DateTimeZone; use Exception; use InvalidArgumentException; @@ -365,4 +366,32 @@ public function modify($modify) $currentTime = new Carbon('tomorrow'); $this->assertSame('2000-01-02 00:00:00 UTC', $currentTime->format('Y-m-d H:i:s e')); } + + public function testSleep() + { + $initial = Carbon::now('UTC'); + Carbon::setTestNow($initial); + $before = microtime(true); + Carbon::sleep(5); + Carbon::sleep(20); + $after = microtime(true); + + $this->assertLessThan(0.1, $after - $before); + + $this->assertSame( + $initial->copy()->addSeconds(25)->format('Y-m-d H:i:s.u'), + Carbon::now('UTC')->format('Y-m-d H:i:s.u'), + ); + + Carbon::setTestNow(null); + + $before = new DateTimeImmutable('now UTC'); + Carbon::sleep(0.5); + $after = new DateTimeImmutable('now UTC'); + + $this->assertSame( + 5, + (int) round(10 * ((float) $after->format('U.u') - ((float) $before->format('U.u')))), + ); + } } diff --git a/tests/CarbonImmutable/ConstructTest.php b/tests/CarbonImmutable/ConstructTest.php index bf84d6da93..d8bee29c47 100644 --- a/tests/CarbonImmutable/ConstructTest.php +++ b/tests/CarbonImmutable/ConstructTest.php @@ -92,6 +92,12 @@ public function testParseWithYYYMMDD() $this->assertCarbon($c, 2020, 11, 28, 0, 0, 0); } + public function testParseWithYYYMMDDHHMMSS() + { + $c = Carbon::parse('20201128192533'); + $this->assertCarbon($c, 2020, 11, 28, 19, 25, 33); + } + public function testDefaultTimezone() { $c = new Carbon('now'); @@ -174,7 +180,19 @@ public function testMockingWithMicroseconds() public function testTimestamp() { - $date = new Carbon('@1367186296'); - $this->assertSame('Sunday 28 April 2013 21:58:16', $date->format('l j F Y H:i:s')); + $date = new Carbon(1367186296); + $this->assertSame('Sunday 28 April 2013 21:58:16.000000', $date->format('l j F Y H:i:s.u')); + + $date = new Carbon(123); + $this->assertSame('Thursday 1 January 1970 00:02:03.000000', $date->format('l j F Y H:i:s.u')); + } + + public function testFloatTimestamp() + { + $date = new Carbon(1367186296.654321); + $this->assertSame('Sunday 28 April 2013 21:58:16.654321', $date->format('l j F Y H:i:s.u')); + + $date = new Carbon(123.5); + $this->assertSame('Thursday 1 January 1970 00:02:03.500000', $date->format('l j F Y H:i:s.u')); } } diff --git a/tests/CarbonPeriod/AliasTest.php b/tests/CarbonPeriod/AliasTest.php index eeeaabc882..a2e0c52bbb 100644 --- a/tests/CarbonPeriod/AliasTest.php +++ b/tests/CarbonPeriod/AliasTest.php @@ -33,7 +33,7 @@ public function testSetStartDate() $this->assertEquals(Carbon::parse($date), $period->start); } - $this->assertSame(CarbonPeriod::EXCLUDE_START_DATE, $period->getOptions()); + $this->assertSame($periodClass::EXCLUDE_START_DATE, $period->getOptions()); $period = $period->since($date = '2014-10-12 15:42:34', true); $this->assertEquals(Carbon::parse($date), $period->getStartDate()); @@ -48,6 +48,7 @@ public function testSetStartDateWithNamedParameters() { $periodClass = static::$periodClass; $date = '2017-09-13 12:30:45'; + /** @var CarbonPeriod $period */ $period = $periodClass::start(date: $date, inclusive: false); $this->assertEquals(Carbon::parse($date), $period->getStartDate()); $this->assertEquals(Carbon::parse($date), $period->start()); @@ -70,11 +71,11 @@ public function testSetStartDateWithNamedParameters() $this->assertEmpty($period->getOptions()); $date = '2014-10-12 15:42:34'; - $period->since(date: $date, inclusive: true); + $period = $period->since(date: $date, inclusive: true); $this->assertEquals(Carbon::parse($date), $period->getStartDate()); $this->assertEmpty($period->getOptions()); - $period->sinceNow(inclusive: false); + $period = $period->sinceNow(inclusive: false); $this->assertEquals(Carbon::now(), $period->getStartDate()); $this->assertSame($periodClass::EXCLUDE_START_DATE, $period->getOptions()); @@ -104,7 +105,7 @@ public function testSetEndDate() $this->assertEquals(Carbon::now(), $period->getEndDate()); $this->assertSame($periodClass::EXCLUDE_END_DATE, $period->getOptions()); - $period->end(null); + $period = $period->end(null); $this->assertNull($period->getEndDate()); } @@ -124,11 +125,11 @@ public function testSetEndDateWithNamedParameters() $this->assertSame(CarbonPeriod::EXCLUDE_END_DATE, $period->getOptions()); $date = '2014-10-12 15:42:34'; - $period->until(date: $date, inclusive: true); + $period = $period->until(date: $date, inclusive: true); $this->assertEquals(Carbon::parse($date), $period->getEndDate()); $this->assertEmpty($period->getOptions()); - $period->untilNow(inclusive: false); + $period = $period->untilNow(inclusive: false); $this->assertEquals(Carbon::now(), $period->getEndDate()); $this->assertSame($periodClass::EXCLUDE_END_DATE, $period->getOptions()); } @@ -171,7 +172,7 @@ public function testSetRecurrences() $period = $period->times(3); $this->assertSame(3, $period->getRecurrences()); - $period->recurrences(null); + $period = $period->recurrences(null); $this->assertNull($period->getRecurrences()); } @@ -223,7 +224,10 @@ public function testInvertInterval() { $periodClass = static::$periodClass; $period = $periodClass::invert(); - $this->assertEquals(CarbonInterval::create('P1D')->invert()->optimize(), $period->getDateInterval()->optimize()); + $this->assertEquals( + CarbonInterval::create('P1D')->invert()->optimize(), + $period->getDateInterval()->optimize(), + ); } public function testModifyIntervalPlural() @@ -309,10 +313,10 @@ public function testNamedParameters() $this->assertEquals('2022-09-13', $period->getStartDate()->format('Y-m-d')); $this->assertEquals('2022-10-12', $period->getEndDate()->format('Y-m-d')); - $period->years(years: 5); + $period = $period->years(years: 5); $this->assertEquals('5 years', (string) $period->getDateInterval()); - $period->interval(interval: CarbonInterval::year(years: 3)); + $period = $period->interval(interval: \Carbon\CarbonInterval::year(years: 3)); $this->assertEquals('3 years', (string) $period->getDateInterval()); $period = $periodClass::between(start: '2022-09-13', end: '2022-10-12')->months(months: 5); diff --git a/tests/Factory/FactoryTest.php b/tests/Factory/FactoryTest.php index 33b428d909..bc19e63885 100644 --- a/tests/Factory/FactoryTest.php +++ b/tests/Factory/FactoryTest.php @@ -17,6 +17,8 @@ use Carbon\CarbonImmutable; use Carbon\Factory; use Carbon\FactoryImmutable; +use DateTimeImmutable; +use Psr\Clock\ClockInterface; use Tests\AbstractTestCase; use Tests\Carbon\Fixtures\MyCarbon; @@ -146,4 +148,56 @@ public function testFactoryTimezone() $date = $factory->make('2021-08-01 08:00:00 Europe/Paris'); $this->assertSame('2021-08-01T08:00:00+02:00', $date->format('c')); } + + public function testPsrClock() + { + $initial = Carbon::now('UTC'); + $factory = new FactoryImmutable(); + $factory->setTestNow($initial); + $this->assertInstanceOf(ClockInterface::class, $factory); + $this->assertInstanceOf(DateTimeImmutable::class, $factory->now()); + $this->assertInstanceOf(CarbonImmutable::class, $factory->now()); + $this->assertSame('America/Toronto', $factory->now()->tzName); + $this->assertSame('UTC', $factory->now('UTC')->tzName); + + $timezonedFactory = $factory->withTimeZone('Asia/Tokyo'); + $this->assertInstanceOf(CarbonImmutable::class, $timezonedFactory->now()); + $this->assertSame('Asia/Tokyo', $timezonedFactory->now()->tzName); + $this->assertSame('America/Toronto', $timezonedFactory->now('America/Toronto')->tzName); + + $this->assertSame( + $initial->format('Y-m-d H:i:s.u'), + $factory->now('UTC')->format('Y-m-d H:i:s.u'), + ); + + $before = microtime(true); + $factory->sleep(5); + $factory->sleep(20); + $after = microtime(true); + + $this->assertLessThan(0.1, $after - $before); + + $this->assertSame( + $initial->copy()->addSeconds(25)->format('Y-m-d H:i:s.u'), + $factory->now('UTC')->format('Y-m-d H:i:s.u'), + ); + + $factory = new FactoryImmutable(); + $factory->setTestNow(null); + $before = new DateTimeImmutable('now UTC'); + $now = $factory->now('UTC'); + $after = new DateTimeImmutable('now UTC'); + + $this->assertGreaterThanOrEqual($before, $now); + $this->assertLessThanOrEqual($after, $now); + + $before = new DateTimeImmutable('now UTC'); + $factory->sleep(0.5); + $after = new DateTimeImmutable('now UTC'); + + $this->assertSame( + 5, + (int) round(10 * ((float) $after->format('U.u') - ((float) $before->format('U.u')))), + ); + } }