From ded6367a3429412062623d81f8943aa22d60f669 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:13:59 +0000 Subject: [PATCH 01/34] feat(support): add Hypervel CarbonImmutable Introduce the canonical immutable counterpart to Hypervel Support Carbon while retaining the existing mutable class as an explicit opt-out. Move the shared conditionable, dumpable, identifier, plus, and minus behavior into one trait. Override mutability conversions so both directions preserve the Hypervel class pair, subclasses, settings, timezone, and immutable identity semantics. Cover the complete helper surface, serialization, conversions, late-static behavior, and immutable modifier behavior on both classes. --- src/support/src/Carbon.php | 73 ++-------- src/support/src/CarbonImmutable.php | 41 ++++++ src/support/src/Traits/DateHelpers.php | 72 ++++++++++ tests/Support/SupportCarbonImmutableTest.php | 139 +++++++++++++++++++ tests/Support/SupportCarbonTest.php | 64 ++++++--- 5 files changed, 307 insertions(+), 82 deletions(-) create mode 100644 src/support/src/CarbonImmutable.php create mode 100644 src/support/src/Traits/DateHelpers.php create mode 100644 tests/Support/SupportCarbonImmutableTest.php diff --git a/src/support/src/Carbon.php b/src/support/src/Carbon.php index 966dad293..c856d10c1 100644 --- a/src/support/src/Carbon.php +++ b/src/support/src/Carbon.php @@ -5,82 +5,25 @@ namespace Hypervel\Support; use Carbon\Carbon as BaseCarbon; -use Carbon\CarbonImmutable as BaseCarbonImmutable; -use Hypervel\Support\Traits\Conditionable; -use Hypervel\Support\Traits\Dumpable; -use InvalidArgumentException; -use Symfony\Component\Uid\TimeBasedUidInterface; -use Symfony\Component\Uid\Ulid; -use Symfony\Component\Uid\Uuid; +use Hypervel\Support\Traits\DateHelpers; class Carbon extends BaseCarbon { - use Conditionable; - use Dumpable; + use DateHelpers; /** - * Tests only. Sets Carbon's process-wide test clock for both mutable and - * immutable Carbon instances. + * Convert the instance to a mutable date. */ - public static function setTestNow(mixed $testNow = null): void + public function toMutable(): static { - BaseCarbon::setTestNow($testNow); - BaseCarbonImmutable::setTestNow($testNow); + return $this->cast(static::class); } /** - * Create a Carbon instance from a given time-based UUID or ULID. + * Convert the instance to an immutable date. */ - public static function createFromId(Uuid|Ulid|string $id): static + public function toImmutable(): CarbonImmutable { - if (is_string($id)) { - $id = Ulid::isValid($id) ? Ulid::fromString($id) : Uuid::fromString($id); - } - - if (! $id instanceof TimeBasedUidInterface) { - throw new InvalidArgumentException( - 'The given UUID is not time-based and cannot be converted to a date.' - ); - } - - return static::createFromInterface($id->getDateTime()); - } - - /** - * Get the current date / time plus a given amount of time. - */ - public function plus( - int $years = 0, - int $months = 0, - int $weeks = 0, - int $days = 0, - int $hours = 0, - int $minutes = 0, - int $seconds = 0, - int $microseconds = 0 - ): static { - return $this->add(" - {$years} years {$months} months {$weeks} weeks {$days} days - {$hours} hours {$minutes} minutes {$seconds} seconds {$microseconds} microseconds - "); - } - - /** - * Get the current date / time minus a given amount of time. - */ - public function minus( - int $years = 0, - int $months = 0, - int $weeks = 0, - int $days = 0, - int $hours = 0, - int $minutes = 0, - int $seconds = 0, - int $microseconds = 0 - ): static { - return $this->sub(" - {$years} years {$months} months {$weeks} weeks {$days} days - {$hours} hours {$minutes} minutes {$seconds} seconds {$microseconds} microseconds - "); + return $this->cast(CarbonImmutable::class); } } diff --git a/src/support/src/CarbonImmutable.php b/src/support/src/CarbonImmutable.php new file mode 100644 index 000000000..4af83f23d --- /dev/null +++ b/src/support/src/CarbonImmutable.php @@ -0,0 +1,41 @@ +cast(Carbon::class); + } + + /** + * Convert the instance to an immutable date. + */ + public function toImmutable(): static + { + return $this; + } +} diff --git a/src/support/src/Traits/DateHelpers.php b/src/support/src/Traits/DateHelpers.php new file mode 100644 index 000000000..01fdf8b98 --- /dev/null +++ b/src/support/src/Traits/DateHelpers.php @@ -0,0 +1,72 @@ +getDateTime()); + } + + /** + * Get the current date / time plus a given amount of time. + */ + public function plus( + int $years = 0, + int $months = 0, + int $weeks = 0, + int $days = 0, + int $hours = 0, + int $minutes = 0, + int $seconds = 0, + int $microseconds = 0 + ): static { + return $this->add(" + {$years} years {$months} months {$weeks} weeks {$days} days + {$hours} hours {$minutes} minutes {$seconds} seconds {$microseconds} microseconds + "); + } + + /** + * Get the current date / time minus a given amount of time. + */ + public function minus( + int $years = 0, + int $months = 0, + int $weeks = 0, + int $days = 0, + int $hours = 0, + int $minutes = 0, + int $seconds = 0, + int $microseconds = 0 + ): static { + return $this->sub(" + {$years} years {$months} months {$weeks} weeks {$days} days + {$hours} hours {$minutes} minutes {$seconds} seconds {$microseconds} microseconds + "); + } +} diff --git a/tests/Support/SupportCarbonImmutableTest.php b/tests/Support/SupportCarbonImmutableTest.php new file mode 100644 index 000000000..23f184ac5 --- /dev/null +++ b/tests/Support/SupportCarbonImmutableTest.php @@ -0,0 +1,139 @@ +assertSame(CarbonImmutable::class, $date::class); + $this->assertInstanceOf(BaseCarbonImmutable::class, $date); + $this->assertInstanceOf(DateTimeInterface::class, $date); + $this->assertSame(CarbonImmutable::class, $deserialized::class); + $this->assertSame('2026-07-22T12:34:56.123456+12:00', $date->format('Y-m-d\TH:i:s.uP')); + $this->assertSame('2026-07-22T00:34:56.123456Z', $date->jsonSerialize()); + } + + public function testConditionableBehaviorRetainsImmutability(): void + { + $date = CarbonImmutable::parse('2026-07-22 12:34:56'); + $tomorrow = $date->when(true, fn (CarbonImmutable $value): CarbonImmutable => $value->addDay()); + + $this->assertSame('2026-07-22', $date->toDateString()); + $this->assertSame('2026-07-23', $tomorrow->toDateString()); + } + + public function testDumpReturnsTheSameInstance(): void + { + $date = CarbonImmutable::parse('2026-07-22 12:34:56'); + $dumped = []; + + VarDumper::setHandler(function (mixed $value) use (&$dumped): void { + $dumped[] = $value; + }); + + try { + $result = $date->dump('context'); + } finally { + VarDumper::setHandler(null); + } + + $this->assertSame($date, $result); + $this->assertSame([$date, 'context'], $dumped); + } + + #[DataProvider('timeBasedIdProvider')] + public function testCreateFromId(string $id, string $expected): void + { + $date = CarbonImmutable::createFromId($id); + + $this->assertSame(CarbonImmutable::class, $date::class); + $this->assertSame($expected, $date->toDateTimeString('microsecond')); + } + + public static function timeBasedIdProvider(): array + { + return [ + 'ulid' => ['01DXH9C4P0ED4AGJJP9CRKQ55C', '2020-01-01 19:30:00.000000'], + 'uuid v1' => ['71513cb4-f071-11ed-a0cf-325096b39f47', '2023-05-12 03:02:34.147346'], + 'uuid v6' => ['1edf0746-5d1c-6ce8-88ad-e0cb4effa035', '2023-05-12 03:23:43.347428'], + 'uuid v7' => ['01880dfa-2825-72e4-acbb-b1e4981cf8af', '2023-05-12 03:21:18.117185'], + ]; + } + + public function testCreateFromIdRejectsNonTimeBasedUuid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The given UUID is not time-based and cannot be converted to a date.'); + + CarbonImmutable::createFromId('a0a2a2d2-0b87-4a18-83f2-2529882be2de'); + } + + #[DataProvider('dateUnitProvider')] + public function testPlusAndMinusSupportEveryNamedUnit(string $unit): void + { + $date = CarbonImmutable::parse('2026-07-22 12:34:56.123456'); + $method = ucfirst($unit); + $plus = $date->plus(...[$unit => 1]); + $minus = $date->minus(...[$unit => 1]); + + $this->assertSame($date->{'add' . $method}()->format('Y-m-d H:i:s.u'), $plus->format('Y-m-d H:i:s.u')); + $this->assertSame($date->{'sub' . $method}()->format('Y-m-d H:i:s.u'), $minus->format('Y-m-d H:i:s.u')); + $this->assertSame('2026-07-22 12:34:56.123456', $date->format('Y-m-d H:i:s.u')); + } + + public static function dateUnitProvider(): array + { + return [ + 'years' => ['years'], + 'months' => ['months'], + 'weeks' => ['weeks'], + 'days' => ['days'], + 'hours' => ['hours'], + 'minutes' => ['minutes'], + 'seconds' => ['seconds'], + 'microseconds' => ['microseconds'], + ]; + } + + public function testConversionsPreserveHypervelClassesAndDateState(): void + { + $immutable = CarbonImmutable::parse('2026-07-22 12:34:56.123456', 'Pacific/Auckland') + ->locale('fr'); + + $mutable = $immutable->toMutable(); + + $this->assertSame(Carbon::class, $mutable::class); + $this->assertSame($immutable, $immutable->toImmutable()); + $this->assertSame($immutable->format('Y-m-d H:i:s.u e'), $mutable->format('Y-m-d H:i:s.u e')); + $this->assertSame('fr', $mutable->locale()); + $this->assertTrue(method_exists($mutable, 'plus')); + } + + public function testImmutableSubclassRetainsLateStaticTypeAndIdentity(): void + { + $date = ImmutableCarbonSubclass::parse('2026-07-22 12:34:56'); + + $this->assertSame($date, $date->toImmutable()); + $this->assertSame(ImmutableCarbonSubclass::class, $date->toImmutable()::class); + } +} + +class ImmutableCarbonSubclass extends CarbonImmutable +{ +} diff --git a/tests/Support/SupportCarbonTest.php b/tests/Support/SupportCarbonTest.php index 17f3b30f3..2dcc78b59 100644 --- a/tests/Support/SupportCarbonTest.php +++ b/tests/Support/SupportCarbonTest.php @@ -9,6 +9,7 @@ use Carbon\CarbonImmutable as BaseCarbonImmutable; use DateTimeInterface; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use InvalidArgumentException; @@ -23,7 +24,7 @@ protected function setUp(): void Carbon::setTestNow($this->now = Carbon::create(2017, 6, 27, 13, 14, 15, 'UTC')); } - public function testInstance() + public function testInstance(): void { $this->assertInstanceOf(Carbon::class, $this->now); $this->assertInstanceOf(DateTimeInterface::class, $this->now); @@ -31,25 +32,25 @@ public function testInstance() $this->assertInstanceOf(Carbon::class, $this->now); } - public function testCarbonIsMacroableWhenNotCalledStatically() + public function testCarbonIsMacroableWhenNotCalledStatically(): void { - Carbon::macro('diffInDecades', function (?Carbon $dt = null, $abs = true) { - return (int) ($this->diffInYears($dt, $abs) / 10); + Carbon::macro('diffInDecades', function (?Carbon $date = null, bool $absolute = true): int { + return (int) ($this->diffInYears($date, $absolute) / 10); }); $this->assertSame(2, $this->now->diffInDecades(Carbon::now()->addYears(25))); } - public function testCarbonIsMacroableWhenCalledStatically() + public function testCarbonIsMacroableWhenCalledStatically(): void { - Carbon::macro('twoDaysAgoAtNoon', function () { + Carbon::macro('twoDaysAgoAtNoon', function (): Carbon { return Carbon::now()->subDays(2)->setTime(12, 0, 0); }); $this->assertSame('2017-06-25 12:00:00', Carbon::twoDaysAgoAtNoon()->toDateTimeString()); } - public function testCarbonRaisesExceptionWhenStaticMacroIsNotFound() + public function testCarbonRaisesExceptionWhenStaticMacroIsNotFound(): void { $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage('nonExistingStaticMacro does not exist.'); @@ -57,7 +58,7 @@ public function testCarbonRaisesExceptionWhenStaticMacroIsNotFound() Carbon::nonExistingStaticMacro(); } - public function testCarbonRaisesExceptionWhenMacroIsNotFound() + public function testCarbonRaisesExceptionWhenMacroIsNotFound(): void { $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage('nonExistingMacro does not exist.'); @@ -65,9 +66,9 @@ public function testCarbonRaisesExceptionWhenMacroIsNotFound() Carbon::now()->nonExistingMacro(); } - public function testCarbonAllowsCustomSerializer() + public function testCarbonAllowsCustomSerializer(): void { - Carbon::serializeUsing(function (Carbon $carbon) { + Carbon::serializeUsing(function (Carbon $carbon): int { return $carbon->getTimestamp(); }); @@ -76,12 +77,12 @@ public function testCarbonAllowsCustomSerializer() $this->assertSame(1498569255, $result); } - public function testCarbonCanSerializeToJson() + public function testCarbonCanSerializeToJson(): void { $this->assertSame('2017-06-27T13:14:15.000000Z', $this->now->jsonSerialize()); } - public function testSetStateReturnsCorrectType() + public function testSetStateReturnsCorrectType(): void { $carbon = Carbon::__set_state([ 'date' => '2017-06-27 13:14:15.000000', @@ -92,7 +93,7 @@ public function testSetStateReturnsCorrectType() $this->assertInstanceOf(Carbon::class, $carbon); } - public function testDeserializationOccursCorrectly() + public function testDeserializationOccursCorrectly(): void { $carbon = new Carbon('2017-06-27 13:14:15.000000'); $serialized = 'return ' . var_export($carbon, true) . ';'; @@ -101,7 +102,7 @@ public function testDeserializationOccursCorrectly() $this->assertInstanceOf(Carbon::class, $deserialized); } - public function testSetTestNowWillPersistBetweenImmutableAndMutableInstance() + public function testSetTestNowWillPersistBetweenImmutableAndMutableInstance(): void { Carbon::setTestNow(new Carbon('2017-06-27 13:14:15.000000')); @@ -110,13 +111,13 @@ public function testSetTestNowWillPersistBetweenImmutableAndMutableInstance() $this->assertSame('2017-06-27 13:14:15', BaseCarbonImmutable::now()->toDateTimeString()); } - public function testCarbonIsConditionable() + public function testCarbonIsConditionable(): void { $this->assertTrue(Carbon::now()->when(null, fn (Carbon $carbon) => $carbon->addDays(1))->isToday()); $this->assertTrue(Carbon::now()->when(true, fn (Carbon $carbon) => $carbon->addDays(1))->isTomorrow()); } - public function testCreateFromUid() + public function testCreateFromId(): void { $ulid = Carbon::createFromId('01DXH9C4P0ED4AGJJP9CRKQ55C'); $this->assertEquals('2020-01-01 19:30:00.000000', $ulid->toDateTimeString('microsecond')); @@ -131,11 +132,40 @@ public function testCreateFromUid() $this->assertEquals('2023-05-12 03:21:18.117185', $uuidv7->toDateTimeString('microsecond')); } - public function testCreateFromIdRejectsNonTimeBasedUuid() + public function testCreateFromIdRejectsNonTimeBasedUuid(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The given UUID is not time-based and cannot be converted to a date.'); Carbon::createFromId('a0a2a2d2-0b87-4a18-83f2-2529882be2de'); // v4 UUID } + + public function testConversionsPreserveHypervelClassesAndDateState(): void + { + $mutable = Carbon::parse('2026-07-22 12:34:56.123456', 'Pacific/Auckland') + ->locale('fr'); + + $mutableCopy = $mutable->toMutable(); + $immutable = $mutable->toImmutable(); + + $this->assertSame(Carbon::class, $mutableCopy::class); + $this->assertNotSame($mutable, $mutableCopy); + $this->assertSame(CarbonImmutable::class, $immutable::class); + $this->assertSame($mutable->format('Y-m-d H:i:s.u e'), $mutableCopy->format('Y-m-d H:i:s.u e')); + $this->assertSame($mutable->format('Y-m-d H:i:s.u e'), $immutable->format('Y-m-d H:i:s.u e')); + $this->assertSame('fr', $mutableCopy->locale()); + $this->assertSame('fr', $immutable->locale()); + $this->assertTrue(method_exists($immutable, 'plus')); + } + + public function testMutableSubclassRetainsLateStaticTypeWhenConvertedToMutable(): void + { + $date = MutableCarbonSubclass::parse('2026-07-22 12:34:56'); + + $this->assertSame(MutableCarbonSubclass::class, $date->toMutable()::class); + } +} + +class MutableCarbonSubclass extends Carbon +{ } From 221fada93ce0334ebd419a6d5f46ce6c3f9baa30 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:14:09 +0000 Subject: [PATCH 02/34] fix(facade-documenter): resolve owner docblock types Resolve class-level @method metadata against the class that owns the docblock instead of borrowing constructor context or falling back to unresolved text. Make generation fail fast when parsing or type resolution fails, and render imported global classes in the same stable form as PHP CS Fixer without shortening namespaced collisions. Replace the obsolete fallback test with regressions for constructor-less owners, inherited constructors, global imports, namespaced imports, and colliding basenames. --- src/facade-documenter/facade.php | 148 +++++++--- .../AstVsTextFallbackTest.php | 120 -------- .../ClassDocblockResolutionTest.php | 268 ++++++++++++++++++ 3 files changed, 375 insertions(+), 161 deletions(-) delete mode 100644 tests/FacadeDocumenter/AstVsTextFallbackTest.php create mode 100644 tests/FacadeDocumenter/ClassDocblockResolutionTest.php diff --git a/src/facade-documenter/facade.php b/src/facade-documenter/facade.php index 015df2035..6b45e557b 100755 --- a/src/facade-documenter/facade.php +++ b/src/facade-documenter/facade.php @@ -88,27 +88,32 @@ // Prepare the @method docblocks... - $methods = $resolvedMethods->map(function ($method) { + $globalClassImports = resolveClassImports($facade) + ->filter(fn ($fqcn) => ! str_contains(ltrim($fqcn, '\\'), '\\')); + + $methods = $resolvedMethods->map(function ($method) use ($globalClassImports) { if (is_string($method)) { // AST-parsed @method tags can already emit "static foo(...)" when the // source docblock declared @method static …; prefixing again would // produce invalid "@method static static …". if (Str::startsWith($method, 'static ')) { - return " * @method {$method}"; + $method = " * @method {$method}"; + } else { + $method = " * @method static {$method}"; } + } else { + $parameters = $method['parameters']->map(function ($parameter) { + $rest = $parameter['variadic'] ? '...' : ''; - return " * @method static {$method}"; - } - - $parameters = $method['parameters']->map(function ($parameter) { - $rest = $parameter['variadic'] ? '...' : ''; + $default = $parameter['optional'] ? ' = ' . resolveDefaultValue($parameter) : ''; - $default = $parameter['optional'] ? ' = ' . resolveDefaultValue($parameter) : ''; + return "{$parameter['type']} {$rest}{$parameter['name']}{$default}"; + }); - return "{$parameter['type']} {$rest}{$parameter['name']}{$default}"; - }); + $method = " * @method static {$method['returns']} {$method['name']}({$parameters->join(', ')})"; + } - return " * @method static {$method['returns']} {$method['name']}({$parameters->join(', ')})"; + return shortenImportedGlobalTypes($method, $globalClassImports); }); // Fix: ensure we keep the references to the Carbon library on the Date Facade... @@ -238,35 +243,26 @@ function resolveDocSees($class) */ function resolveDocMethods($class) { - try { - $dummyReflectionMethod = new ReflectionMethodDecorator( - new ReflectionMethod($class->getName(), '__construct'), - $class->getName() - ); + $context = new ReflectionClassDocblockContext($class); - return collect(parseDocblock($class->getDocComment())->getTags()) - ->filter(fn ($tag) => $tag->value instanceof MethodTagValueNode) - ->map(function ($tag) use ($dummyReflectionMethod) { - /** @var MethodTagValueNode $method */ - $method = $tag->value; + return collect(parseDocblock($class->getDocComment())->getTags()) + ->filter(fn ($tag) => $tag->value instanceof MethodTagValueNode) + ->map(function ($tag) use ($context) { + /** @var MethodTagValueNode $method */ + $method = $tag->value; - $method->parameters = collect($method->parameters)->map(function ($parameter) use ($dummyReflectionMethod) { - $parameter->type = new IdentifierTypeNode($parameter->type ? resolveDocblockTypes($dummyReflectionMethod, $parameter->type) : 'mixed'); + $method->parameters = collect($method->parameters)->map(function ($parameter) use ($context) { + $parameter->type = new IdentifierTypeNode($parameter->type ? resolveDocblockTypes($context, $parameter->type) : 'mixed'); - return $parameter; - })->toArray(); + return $parameter; + })->toArray(); - $method->returnType = $method->returnType - ? new IdentifierTypeNode(resolveDocblockTypes($dummyReflectionMethod, $method->returnType)) - : new IdentifierTypeNode('void'); + $method->returnType = $method->returnType + ? new IdentifierTypeNode(resolveDocblockTypes($context, $method->returnType)) + : new IdentifierTypeNode('void'); - return (string) $method; - }); - } catch (Throwable) { - return resolveDocTags($class->getDocComment() ?: '', '@method ') - ->map(fn ($tag) => Str::squish($tag)) - ->map(fn ($tag) => Str::before($tag, ')') . ')'); - } + return (string) $method; + }); } /** @@ -335,7 +331,7 @@ function parseDocblock($docblock) /** * Resolve the types from the docblock. * - * @param \ReflectionMethodDecorator $method + * @param \ReflectionClassDocblockContext|\ReflectionMethodDecorator $method * @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode * @param mixed $depth * @return null|string @@ -554,7 +550,7 @@ function resolveDocblockTypes($method, $typeNode, $depth = 1) /** * Handle unknown identifier types. * - * @param \ReflectionMethodDecorator $method + * @param \ReflectionClassDocblockContext|\ReflectionMethodDecorator $method * @param \PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode $typeNode * @return string */ @@ -583,7 +579,7 @@ function handleUnknownIdentifierType($method, $typeNode) * when the constant or class cannot be resolved. * * @param \PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode $node - * @param \ReflectionMethodDecorator $method + * @param \ReflectionClassDocblockContext|\ReflectionMethodDecorator $method * @return string */ function resolveConstFetchType($node, $method) @@ -623,7 +619,7 @@ function resolveConstFetchType($node, $method) * Resolve key-of<...> / value-of<...> when the inner type is a ConstFetchNode. * * @param \PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode $node - * @param \ReflectionMethodDecorator $method + * @param \ReflectionClassDocblockContext|\ReflectionMethodDecorator $method * @param bool $keyType true to resolve the key type, false to resolve the value type * @return string */ @@ -667,7 +663,7 @@ function resolveKeyOrValueOf($node, $method, $keyType) * null when the class cannot be found. * * @param string $className - * @param \ReflectionMethodDecorator $method + * @param \ReflectionClassDocblockContext|\ReflectionMethodDecorator $method * @return null|string */ function resolveClassConstantClass($className, $method) @@ -1108,7 +1104,7 @@ function resolveParameters($method) * the trait's own file (not the declaring class's file) holds the relevant * `use` statements. * - * @param \ReflectionMethodDecorator $method + * @param \ReflectionClassDocblockContext|\ReflectionMethodDecorator $method * @return \ReflectionClass */ function resolveImportSource($method) @@ -1136,6 +1132,26 @@ function resolveClassImports($class) ]); } +/** + * Shorten global method types already imported by the facade. + * + * PHP-CS-Fixer's global_namespace_import rule manages this exact set, so + * matching its representation keeps generated metadata stable after formatting. + * + * @param string $method + * @param \Hypervel\Support\Collection $imports + * @return string + */ +function shortenImportedGlobalTypes($method, $imports) +{ + return $imports->reduce( + fn ($method, $fqcn, $alias) => Str::of($method) + ->replaceMatches('/(?toString(), + $method + ); +} + /** * Resolve the default value for the parameter. * @@ -1163,6 +1179,56 @@ function resolveDefaultValue($parameter) ->toString(); } +class ReflectionClassDocblockContext +{ + /** + * Create a class docblock context. + */ + public function __construct(private ReflectionClass $class) + { + } + + /** + * Return the class that declares the docblock. + */ + public function getDeclaringClass(): ReflectionClass + { + return $this->class; + } + + /** + * Return the class docblock. + */ + public function getDocComment(): false|string + { + return $this->class->getDocComment(); + } + + /** + * Return the source filename. + */ + public function getFileName(): false|string + { + return $this->class->getFileName(); + } + + /** + * Return the diagnostic context name. + */ + public function getName(): string + { + return $this->class->getName(); + } + + /** + * Return the source class. + */ + public function sourceClass(): ReflectionClass + { + return $this->class; + } +} + /** * @mixin \ReflectionMethod */ diff --git a/tests/FacadeDocumenter/AstVsTextFallbackTest.php b/tests/FacadeDocumenter/AstVsTextFallbackTest.php deleted file mode 100644 index 9f41b6602..000000000 --- a/tests/FacadeDocumenter/AstVsTextFallbackTest.php +++ /dev/null @@ -1,120 +0,0 @@ - collapses to string). - */ - public function testAstPathNormalisesMethodTagTypes() - { - $this->writeAppFile( - 'AstFallback/AstPath/Proxy.php', - <<<'PHP' - $class) - */ - class Proxy - { - public function __construct() - { - } - } - PHP - ); - - $this->writeAppFile( - 'AstFallback/AstPath/Facade.php', - <<<'PHP' - runDocumenter(['App\AstFallback\AstPath\Facade']); - - $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput() . $process->getOutput()); - - $contents = $this->appFileContents('App\AstFallback\AstPath\Facade'); - - $this->assertStringContainsString('@method static string fetch(string $class)', $contents); - $this->assertStringNotContainsString('class-string<', $contents); - } - - /** - * When the proxy class has no __construct, reflection on '__construct' - * throws ReflectionException, the try/catch kicks in, and the text-scrape - * fallback emits the @method tag verbatim. The test asserts the tool - * still produces output (no crash) even though types are not normalised. - */ - public function testFallbackPathEmitsTagVerbatimWithoutCrashing() - { - $this->writeAppFile( - 'AstFallback/FallbackPath/Proxy.php', - <<<'PHP' - $class) - */ - class Proxy - { - // Intentionally no __construct defined. - } - PHP - ); - - $this->writeAppFile( - 'AstFallback/FallbackPath/Facade.php', - <<<'PHP' - runDocumenter(['App\AstFallback\FallbackPath\Facade']); - - $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput() . $process->getOutput()); - - $contents = $this->appFileContents('App\AstFallback\FallbackPath\Facade'); - - // The fallback does not normalise types, so the tag is emitted - // verbatim (modulo Str::squish whitespace normalisation). - $this->assertStringContainsString('@method static string fetch(class-string<\stdClass> $class)', $contents); - } -} diff --git a/tests/FacadeDocumenter/ClassDocblockResolutionTest.php b/tests/FacadeDocumenter/ClassDocblockResolutionTest.php new file mode 100644 index 000000000..b281a800f --- /dev/null +++ b/tests/FacadeDocumenter/ClassDocblockResolutionTest.php @@ -0,0 +1,268 @@ +writeAppFile( + 'AstFallback/AstPath/Proxy.php', + <<<'PHP' + $class) + */ + class Proxy + { + public function __construct() + { + } + } + PHP + ); + + $this->writeAppFile( + 'AstFallback/AstPath/Facade.php', + <<<'PHP' + runDocumenter(['App\AstFallback\AstPath\Facade']); + + $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput() . $process->getOutput()); + + $contents = $this->appFileContents('App\AstFallback\AstPath\Facade'); + + $this->assertStringContainsString('@method static string fetch(string $class)', $contents); + $this->assertStringNotContainsString('class-string<', $contents); + } + + /** + * Resolve magic method types against a constructor-less owner. + */ + public function testResolvesMagicMethodTypesForClassWithoutConstructor(): void + { + $this->writeAppFile( + 'AstFallback/FallbackPath/Proxy.php', + <<<'PHP' + $class) + */ + class Proxy + { + // Intentionally no __construct defined. + } + PHP + ); + + $this->writeAppFile( + 'AstFallback/FallbackPath/Facade.php', + <<<'PHP' + runDocumenter(['App\AstFallback\FallbackPath\Facade']); + + $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput() . $process->getOutput()); + + $contents = $this->appFileContents('App\AstFallback\FallbackPath\Facade'); + + $this->assertStringContainsString('@method static \DateTimeImmutable fetch(string $class)', $contents); + $this->assertStringNotContainsString('class-string<', $contents); + } + + /** + * Resolve magic method types against the child that owns the docblock. + */ + public function testResolvesMagicMethodTypesAgainstChildWithInheritedConstructor(): void + { + $this->writeAppFile( + 'ClassDocblock/Inherited/ParentProxy.php', + <<<'PHP' + writeAppFile( + 'ClassDocblock/Inherited/Proxy.php', + <<<'PHP' + writeAppFile( + 'ClassDocblock/Inherited/Facade.php', + <<<'PHP' + runDocumenter(['App\ClassDocblock\Inherited\Facade']); + + $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput() . $process->getOutput()); + + $contents = $this->appFileContents('App\ClassDocblock\Inherited\Facade'); + + $this->assertStringContainsString('@method static \DateTimeImmutable fetch()', $contents); + $this->assertStringNotContainsString('@method static \stdClass fetch()', $contents); + } + + /** + * Use facade imports only for global method types managed by the formatter. + */ + public function testUsesFacadeImportsOnlyForGlobalMethodTypes(): void + { + $this->writeAppFile( + 'ClassDocblock/FacadeImports/Proxy.php', + <<<'PHP' + writeAppFile( + 'ClassDocblock/FacadeImports/DateTimeImmutable.php', + <<<'PHP' + writeAppFile( + 'ClassDocblock/FacadeImports/Facade.php', + <<<'PHP' + runDocumenter(['App\ClassDocblock\FacadeImports\Facade']); + + $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput() . $process->getOutput()); + + $contents = $this->appFileContents('App\ClassDocblock\FacadeImports\Facade'); + + $this->assertStringContainsString('@method static Payload transform(Payload $value)', $contents); + $this->assertStringContainsString('@method static \stdClass unimported()', $contents); + $this->assertStringContainsString('@method static \Hypervel\Support\Collection namespaced()', $contents); + $this->assertStringContainsString('@method static \App\ClassDocblock\FacadeImports\DateTimeImmutable collidingBasename()', $contents); + $this->assertStringNotContainsString('@method static \App\ClassDocblock\FacadeImports\Payload collidingBasename()', $contents); + $this->assertStringNotContainsString('@method static ImportedCollection namespaced()', $contents); + } +} From e76cb8d45c708b704d7a1f8fac7a35fb0f63296c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:14:20 +0000 Subject: [PATCH 03/34] feat(support): make immutable dates the factory default Change DateFactory, the Date facade, global helpers, and Stringable date conversion to produce Hypervel CarbonImmutable by default while retaining CarbonInterface as the configurable public contract. Restrict class handlers to instantiable CarbonInterface implementations, normalize callable handlers once, remove the dead generic constructor path, and keep Carbon Factory handlers and callable transformations available. Regenerate facade metadata from the corrected owner and cover immutable defaults, mutable opt-out, custom Carbon subclasses, factories, callables, macros, locale behavior, invalid handlers, and helper return types. --- src/foundation/src/helpers.php | 4 +- src/support/src/DateFactory.php | 134 +++++++------- src/support/src/Facades/Date.php | 120 ++++++++----- src/support/src/Stringable.php | 3 +- src/support/src/functions.php | 4 +- tests/Foundation/FoundationHelpersTest.php | 74 +++++--- tests/Support/DateFacadeTest.php | 197 +++++++++++++++------ tests/Support/Fixtures/CustomDateClass.php | 25 --- tests/Support/SupportStringableTest.php | 32 +++- 9 files changed, 362 insertions(+), 231 deletions(-) delete mode 100644 tests/Support/Fixtures/CustomDateClass.php diff --git a/src/foundation/src/helpers.php b/src/foundation/src/helpers.php index e2a5da90e..6dd19b306 100644 --- a/src/foundation/src/helpers.php +++ b/src/foundation/src/helpers.php @@ -591,7 +591,7 @@ function method_field(string $method): HtmlString if (! function_exists('now')) { /** - * Create a new Carbon instance for the current time. + * Create a new configured Carbon instance for the current time. */ function now(\UnitEnum|\DateTimeZone|string|null $tz = null): CarbonInterface { @@ -909,7 +909,7 @@ function to_route(string $route, array $parameters = [], int $status = 302, arra if (! function_exists('today')) { /** - * Create a new Carbon instance for the current date. + * Create a new configured Carbon instance for the current date. */ function today(\UnitEnum|\DateTimeZone|string|null $tz = null): CarbonInterface { diff --git a/src/support/src/DateFactory.php b/src/support/src/DateFactory.php index 6855fb0d1..d92083597 100644 --- a/src/support/src/DateFactory.php +++ b/src/support/src/DateFactory.php @@ -4,35 +4,36 @@ namespace Hypervel\Support; +use Carbon\CarbonInterface; use Carbon\Factory; use Closure; use InvalidArgumentException; -use RuntimeException; +use ReflectionClass; /** * @see https://carbon.nesbot.com/docs/ * @see https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Factory.php * * @method bool canBeCreatedFromFormat(?string $date, string $format) - * @method null|\Hypervel\Support\Carbon create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = null) - * @method \Hypervel\Support\Carbon createFromDate($year = null, $month = null, $day = null, $timezone = null) - * @method null|\Hypervel\Support\Carbon createFromFormat($format, $time, $timezone = null) - * @method null|\Hypervel\Support\Carbon createFromIsoFormat(string $format, string $time, $timezone = null, ?string $locale = 'en', ?\Symfony\Contracts\Translation\TranslatorInterface $translator = null) - * @method null|\Hypervel\Support\Carbon createFromLocaleFormat(string $format, string $locale, string $time, $timezone = null) - * @method null|\Hypervel\Support\Carbon createFromLocaleIsoFormat(string $format, string $locale, string $time, $timezone = null) - * @method \Hypervel\Support\Carbon createFromTime($hour = 0, $minute = 0, $second = 0, $timezone = null) - * @method \Hypervel\Support\Carbon createFromTimeString(string $time, \DateTimeZone|string|int|null $timezone = null) - * @method \Hypervel\Support\Carbon createFromTimestamp(string|int|float $timestamp, \DateTimeZone|string|int|null $timezone = null) - * @method \Hypervel\Support\Carbon createFromTimestampMs(string|int|float $timestamp, \DateTimeZone|string|int|null $timezone = null) - * @method \Hypervel\Support\Carbon createFromTimestampMsUTC($timestamp) - * @method \Hypervel\Support\Carbon createFromTimestampUTC(float|int|string $timestamp) - * @method \Hypervel\Support\Carbon createMidnightDate($year = null, $month = null, $day = null, $timezone = null) - * @method null|\Hypervel\Support\Carbon createSafe($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $timezone = null) - * @method \Hypervel\Support\Carbon createStrict(?int $year = 0, ?int $month = 1, ?int $day = 1, ?int $hour = 0, ?int $minute = 0, ?int $second = 0, $timezone = null) + * @method null|\Carbon\CarbonInterface create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = null) + * @method \Carbon\CarbonInterface createFromDate($year = null, $month = null, $day = null, $timezone = null) + * @method null|\Carbon\CarbonInterface createFromFormat($format, $time, $timezone = null) + * @method null|\Carbon\CarbonInterface createFromIsoFormat(string $format, string $time, $timezone = null, ?string $locale = 'en', ?\Symfony\Contracts\Translation\TranslatorInterface $translator = null) + * @method null|\Carbon\CarbonInterface createFromLocaleFormat(string $format, string $locale, string $time, $timezone = null) + * @method null|\Carbon\CarbonInterface createFromLocaleIsoFormat(string $format, string $locale, string $time, $timezone = null) + * @method \Carbon\CarbonInterface createFromTime($hour = 0, $minute = 0, $second = 0, $timezone = null) + * @method \Carbon\CarbonInterface createFromTimeString(string $time, \DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface createFromTimestamp(string|int|float $timestamp, \DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface createFromTimestampMs(string|int|float $timestamp, \DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface createFromTimestampMsUTC($timestamp) + * @method \Carbon\CarbonInterface createFromTimestampUTC(float|int|string $timestamp) + * @method \Carbon\CarbonInterface createMidnightDate($year = null, $month = null, $day = null, $timezone = null) + * @method null|\Carbon\CarbonInterface createSafe($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $timezone = null) + * @method \Carbon\CarbonInterface createStrict(?int $year = 0, ?int $month = 1, ?int $day = 1, ?int $hour = 0, ?int $minute = 0, ?int $second = 0, $timezone = null) * @method void disableHumanDiffOption($humanDiffOption) * @method void enableHumanDiffOption($humanDiffOption) * @method mixed executeWithLocale(string $locale, callable $func) - * @method \Hypervel\Support\Carbon fromSerialized($value) + * @method \Carbon\CarbonInterface fromSerialized($value) * @method array getAvailableLocales() * @method array getAvailableLocalesInfo() * @method array getDays() @@ -45,7 +46,7 @@ * @method int getMidDayAt() * @method string getTimeFormatByPrecision(string $unitPrecision) * @method null|Closure|string getTranslationMessageWith($translator, string $key, ?string $locale = null, ?string $default = null) - * @method null|\Hypervel\Support\Carbon getTestNow() + * @method null|\Carbon\CarbonInterface getTestNow() * @method \Symfony\Contracts\Translation\TranslatorInterface getTranslator() * @method int getWeekEndsAt(?string $locale = null) * @method int getWeekStartsAt(?string $locale = null) @@ -55,7 +56,7 @@ * @method bool hasMacro($name) * @method bool hasRelativeKeywords(?string $time) * @method bool hasTestNow() - * @method \Hypervel\Support\Carbon instance(\DateTimeInterface $date) + * @method \Carbon\CarbonInterface instance(\DateTimeInterface $date) * @method bool isImmutable() * @method bool isModifiableUnit($unit) * @method bool isMutable() @@ -66,14 +67,14 @@ * @method bool localeHasPeriodSyntax($locale) * @method bool localeHasShortUnits(string $locale) * @method void macro(string $name, ?callable $macro) - * @method null|\Hypervel\Support\Carbon make($var, \DateTimeZone|string|null $timezone = null) + * @method null|\Carbon\CarbonInterface make($var, \DateTimeZone|string|null $timezone = null) * @method void mixin(object|string $mixin) - * @method \Hypervel\Support\Carbon now(\DateTimeZone|string|int|null $timezone = null) - * @method \Hypervel\Support\Carbon parse(\DateTimeInterface|\Carbon\WeekDay|\Carbon\Month|string|int|float|null $time, \DateTimeZone|string|int|null $timezone = null) - * @method \Hypervel\Support\Carbon parseFromLocale(string $time, ?string $locale = null, \DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface now(\DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface parse(\DateTimeInterface|\Carbon\WeekDay|\Carbon\Month|string|int|float|null $time, \DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface parseFromLocale(string $time, ?string $locale = null, \DateTimeZone|string|int|null $timezone = null) * @method string pluralUnit(string $unit) - * @method null|\Hypervel\Support\Carbon rawCreateFromFormat(string $format, string $time, $timezone = null) - * @method \Hypervel\Support\Carbon rawParse(\DateTimeInterface|\Carbon\WeekDay|\Carbon\Month|string|int|float|null $time, \DateTimeZone|string|int|null $timezone = null) + * @method null|\Carbon\CarbonInterface rawCreateFromFormat(string $format, string $time, $timezone = null) + * @method \Carbon\CarbonInterface rawParse(\DateTimeInterface|\Carbon\WeekDay|\Carbon\Month|string|int|float|null $time, \DateTimeZone|string|int|null $timezone = null) * @method void resetMonthsOverflow() * @method void resetToStringFormat() * @method void resetYearsOverflow() @@ -84,51 +85,49 @@ * @method void setMidDayAt($hour) * @method void setTestNow(mixed $testNow = null) * @method void setTestNowAndTimezone(mixed $testNow = null, $timezone = null) - * @method void setToStringFormat(null|\Closure|string $format) + * @method void setToStringFormat(null|Closure|string $format) * @method void setTranslator(\Symfony\Contracts\Translation\TranslatorInterface $translator) - * @method void setWeekEndsAt($day) - * @method void setWeekStartsAt($day) * @method void setWeekendDays($days) * @method bool shouldOverflowMonths() * @method bool shouldOverflowYears() * @method string singularUnit(string $unit) * @method void sleep(float|int $seconds) - * @method \Hypervel\Support\Carbon today(\DateTimeZone|string|int|null $timezone = null) - * @method \Hypervel\Support\Carbon tomorrow(\DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface today(\DateTimeZone|string|int|null $timezone = null) + * @method \Carbon\CarbonInterface tomorrow(\DateTimeZone|string|int|null $timezone = null) * @method string translateTimeString(string $timeString, ?string $from = null, ?string $to = null, int $mode = \Carbon\CarbonInterface::TRANSLATE_ALL) * @method string translateWith(\Symfony\Contracts\Translation\TranslatorInterface $translator, string $key, array $parameters = [], $number = null) * @method void useMonthsOverflow($monthsOverflow = true) * @method void useStrictMode($strictModeEnabled = true) * @method void useYearsOverflow($yearsOverflow = true) * @method mixed withTestNow(mixed $testNow, callable $callback) - * @method static withTimeZone(null|\DateTimeZone|int|string $timezone) - * @method \Hypervel\Support\Carbon yesterday(\DateTimeZone|string|int|null $timezone = null) + * @method Factory withTimeZone(null|\DateTimeZone|int|string $timezone) + * @method \Carbon\CarbonInterface yesterday(\DateTimeZone|string|int|null $timezone = null) */ class DateFactory { /** * The default class that will be used for all created dates. * - * @var string + * @var class-string */ - public const DEFAULT_CLASS_NAME = Carbon::class; + public const string DEFAULT_CLASS_NAME = CarbonImmutable::class; /** * The type (class) of dates that should be created. + * + * @var null|class-string */ protected static ?string $dateClass = null; /** * This callable may be used to intercept date creation. - * - * @var null|callable */ - protected static $callable; + protected static ?Closure $callable = null; /** * The Carbon factory that should be used when creating dates. */ - protected static ?object $factory = null; + protected static ?Factory $factory = null; /** * Use the given handler when generating dates (class name, callable, or factory). @@ -140,20 +139,24 @@ class DateFactory */ public static function use(mixed $handler): void { - if (is_callable($handler) && is_object($handler)) { - static::useCallable($handler); + if ($handler instanceof Factory) { + static::useFactory($handler); return; } - if (is_string($handler)) { + + if (is_string($handler) && is_a($handler, CarbonInterface::class, true)) { static::useClass($handler); return; } - if ($handler instanceof Factory) { - static::useFactory($handler); + + if (is_callable($handler)) { + static::useCallable($handler); return; } - throw new InvalidArgumentException('Invalid date creation handler. Please provide a class name, callable, or Carbon factory.'); + throw new InvalidArgumentException( + 'Invalid date creation handler. Please provide a Carbon class, callable, or Carbon factory.' + ); } /** @@ -177,7 +180,7 @@ public static function useDefault(): void */ public static function useCallable(callable $callable): void { - static::$callable = $callable; + static::$callable = Closure::fromCallable($callable); static::$dateClass = null; static::$factory = null; @@ -188,9 +191,20 @@ public static function useCallable(callable $callable): void * * Boot-only. The class name persists in a static property for the worker * lifetime and is used for every date creation across all coroutines. + * + * @param class-string $dateClass */ public static function useClass(string $dateClass): void { + if ( + ! is_a($dateClass, CarbonInterface::class, true) + || ! (new ReflectionClass($dateClass))->isInstantiable() + ) { + throw new InvalidArgumentException( + 'The date class must be an instantiable CarbonInterface implementation.' + ); + } + static::$dateClass = $dateClass; static::$factory = null; @@ -203,7 +217,7 @@ public static function useClass(string $dateClass): void * Boot-only. The factory persists in a static property for the worker * lifetime and is used for every date creation across all coroutines. */ - public static function useFactory(object $factory): void + public static function useFactory(Factory $factory): void { static::$factory = $factory; @@ -221,42 +235,28 @@ public static function flushState(): void /** * Handle dynamic calls to generate dates. - * - * @throws RuntimeException */ - public function __call(string $method, array $parameters) + public function __call(string $method, array $parameters): mixed { $defaultClassName = static::DEFAULT_CLASS_NAME; - // Using callable to generate dates... - if (static::$callable) { - return call_user_func(static::$callable, $defaultClassName::$method(...$parameters)); + if (static::$callable !== null) { + return (static::$callable)($defaultClassName::$method(...$parameters)); } - // Using Carbon factory to generate dates... - if (static::$factory) { + if (static::$factory !== null) { return static::$factory->{$method}(...$parameters); } - $dateClass = static::$dateClass ?: $defaultClassName; + $dateClass = static::$dateClass ?? $defaultClassName; - // Check if the date can be created using the public class method... if ( method_exists($dateClass, $method) - || method_exists($dateClass, 'hasMacro') && $dateClass::hasMacro($method) + || $dateClass::hasMacro($method) ) { return $dateClass::$method(...$parameters); } - // If that fails, create the date with the default class... - $date = $defaultClassName::$method(...$parameters); - - // If the configured class has an "instance" method, we'll try to pass our date into there... - if (method_exists($dateClass, 'instance')) { - return $dateClass::instance($date); - } - - // Otherwise, assume the configured class has a DateTime compatible constructor... - return new $dateClass($date->format('Y-m-d H:i:s.u'), $date->getTimezone()); + return $dateClass::instance($defaultClassName::$method(...$parameters)); } } diff --git a/src/support/src/Facades/Date.php b/src/support/src/Facades/Date.php index 5febec9d3..8311425fa 100644 --- a/src/support/src/Facades/Date.php +++ b/src/support/src/Facades/Date.php @@ -4,6 +4,7 @@ namespace Hypervel\Support\Facades; +use Closure; use Hypervel\Support\DateFactory; /** @@ -14,75 +15,96 @@ * @method static void useDefault() * @method static void useCallable(callable $callable) * @method static void useClass(string $dateClass) - * @method static void useFactory(object $factory) - * @method static \Hypervel\Support\Carbon create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $tz = null) - * @method static \Hypervel\Support\Carbon createFromDate($year = null, $month = null, $day = null, $tz = null) - * @method static null|\Hypervel\Support\Carbon createFromFormat($format, $time, $tz = null) - * @method static \Hypervel\Support\Carbon createFromTime($hour = 0, $minute = 0, $second = 0, $tz = null) - * @method static \Hypervel\Support\Carbon createFromTimeString($time, $tz = null) - * @method static \Hypervel\Support\Carbon createFromTimestamp($timestamp, $tz = null) - * @method static \Hypervel\Support\Carbon createFromTimestampMs($timestamp, $tz = null) - * @method static \Hypervel\Support\Carbon createFromTimestampUTC($timestamp) - * @method static \Hypervel\Support\Carbon createMidnightDate($year = null, $month = null, $day = null, $tz = null) - * @method static null|\Hypervel\Support\Carbon createSafe($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $tz = null) - * @method static void disableHumanDiffOption($humanDiffOption) - * @method static void enableHumanDiffOption($humanDiffOption) - * @method static mixed executeWithLocale($locale, $func) - * @method static \Hypervel\Support\Carbon fromSerialized($value) + * @method static void useFactory(\Carbon\Factory $factory) + * @method static void flushState() + * @method static bool canBeCreatedFromFormat(?string $date, string $format) + * @method static (null|\Carbon\CarbonInterface) create(mixed $year = 0, mixed $month = 1, mixed $day = 1, mixed $hour = 0, mixed $minute = 0, mixed $second = 0, mixed $timezone = null) + * @method static \Carbon\CarbonInterface createFromDate(mixed $year = null, mixed $month = null, mixed $day = null, mixed $timezone = null) + * @method static (null|\Carbon\CarbonInterface) createFromFormat(mixed $format, mixed $time, mixed $timezone = null) + * @method static (null|\Carbon\CarbonInterface) createFromIsoFormat(string $format, string $time, mixed $timezone = null, ?string $locale = 'en', ?\Symfony\Contracts\Translation\TranslatorInterface $translator = null) + * @method static (null|\Carbon\CarbonInterface) createFromLocaleFormat(string $format, string $locale, string $time, mixed $timezone = null) + * @method static (null|\Carbon\CarbonInterface) createFromLocaleIsoFormat(string $format, string $locale, string $time, mixed $timezone = null) + * @method static \Carbon\CarbonInterface createFromTime(mixed $hour = 0, mixed $minute = 0, mixed $second = 0, mixed $timezone = null) + * @method static \Carbon\CarbonInterface createFromTimeString(string $time, (\DateTimeZone|string|int|null) $timezone = null) + * @method static \Carbon\CarbonInterface createFromTimestamp((string|int|float) $timestamp, (\DateTimeZone|string|int|null) $timezone = null) + * @method static \Carbon\CarbonInterface createFromTimestampMs((string|int|float) $timestamp, (\DateTimeZone|string|int|null) $timezone = null) + * @method static \Carbon\CarbonInterface createFromTimestampMsUTC(mixed $timestamp) + * @method static \Carbon\CarbonInterface createFromTimestampUTC((float|int|string) $timestamp) + * @method static \Carbon\CarbonInterface createMidnightDate(mixed $year = null, mixed $month = null, mixed $day = null, mixed $timezone = null) + * @method static (null|\Carbon\CarbonInterface) createSafe(mixed $year = null, mixed $month = null, mixed $day = null, mixed $hour = null, mixed $minute = null, mixed $second = null, mixed $timezone = null) + * @method static \Carbon\CarbonInterface createStrict(?int $year = 0, ?int $month = 1, ?int $day = 1, ?int $hour = 0, ?int $minute = 0, ?int $second = 0, mixed $timezone = null) + * @method static void disableHumanDiffOption(mixed $humanDiffOption) + * @method static void enableHumanDiffOption(mixed $humanDiffOption) + * @method static mixed executeWithLocale(string $locale, callable $func) + * @method static \Carbon\CarbonInterface fromSerialized(mixed $value) * @method static array getAvailableLocales() + * @method static array getAvailableLocalesInfo() * @method static array getDays() + * @method static (null|string) getFallbackLocale() + * @method static array getFormatsToIsoReplacements() * @method static int getHumanDiffOptions() * @method static array getIsoUnits() - * @method static array getLastErrors() + * @method static (array|false) getLastErrors() * @method static string getLocale() * @method static int getMidDayAt() - * @method static null|\Hypervel\Support\Carbon getTestNow() + * @method static string getTimeFormatByPrecision(string $unitPrecision) + * @method static (null|Closure|string) getTranslationMessageWith(mixed $translator, string $key, ?string $locale = null, ?string $default = null) + * @method static (null|\Carbon\CarbonInterface) getTestNow() * @method static \Symfony\Contracts\Translation\TranslatorInterface getTranslator() - * @method static int getWeekEndsAt() - * @method static int getWeekStartsAt() + * @method static int getWeekEndsAt(?string $locale = null) + * @method static int getWeekStartsAt(?string $locale = null) * @method static array getWeekendDays() - * @method static bool hasFormat($date, $format) - * @method static bool hasMacro($name) - * @method static bool hasRelativeKeywords($time) + * @method static bool hasFormat(string $date, string $format) + * @method static bool hasFormatWithModifiers(string $date, string $format) + * @method static bool hasMacro(mixed $name) + * @method static bool hasRelativeKeywords(?string $time) * @method static bool hasTestNow() - * @method static \Hypervel\Support\Carbon instance($date) + * @method static \Carbon\CarbonInterface instance(\DateTimeInterface $date) * @method static bool isImmutable() - * @method static bool isModifiableUnit($unit) + * @method static bool isModifiableUnit(mixed $unit) * @method static bool isMutable() * @method static bool isStrictModeEnabled() - * @method static bool localeHasDiffOneDayWords($locale) - * @method static bool localeHasDiffSyntax($locale) - * @method static bool localeHasDiffTwoDayWords($locale) - * @method static bool localeHasPeriodSyntax($locale) - * @method static bool localeHasShortUnits($locale) - * @method static void macro($name, $macro) - * @method static null|\Hypervel\Support\Carbon make($var) - * @method static \Hypervel\Support\Carbon maxValue() - * @method static \Hypervel\Support\Carbon minValue() - * @method static void mixin($mixin) - * @method static \Hypervel\Support\Carbon now($tz = null) - * @method static \Hypervel\Support\Carbon parse($time = null, $tz = null) + * @method static bool localeHasDiffOneDayWords(string $locale) + * @method static bool localeHasDiffSyntax(string $locale) + * @method static bool localeHasDiffTwoDayWords(string $locale) + * @method static bool localeHasPeriodSyntax(mixed $locale) + * @method static bool localeHasShortUnits(string $locale) + * @method static void macro(string $name, ?callable $macro) + * @method static (null|\Carbon\CarbonInterface) make(mixed $var, (\DateTimeZone|string|null) $timezone = null) + * @method static void mixin((object|string) $mixin) + * @method static \Carbon\CarbonInterface now((\DateTimeZone|string|int|null) $timezone = null) + * @method static \Carbon\CarbonInterface parse((\DateTimeInterface|\Carbon\WeekDay|\Carbon\Month|string|int|float|null) $time, (\DateTimeZone|string|int|null) $timezone = null) + * @method static \Carbon\CarbonInterface parseFromLocale(string $time, ?string $locale = null, (\DateTimeZone|string|int|null) $timezone = null) * @method static string pluralUnit(string $unit) + * @method static (null|\Carbon\CarbonInterface) rawCreateFromFormat(string $format, string $time, mixed $timezone = null) + * @method static \Carbon\CarbonInterface rawParse((\DateTimeInterface|\Carbon\WeekDay|\Carbon\Month|string|int|float|null) $time, (\DateTimeZone|string|int|null) $timezone = null) * @method static void resetMonthsOverflow() * @method static void resetToStringFormat() * @method static void resetYearsOverflow() - * @method static void serializeUsing($callback) - * @method static void setHumanDiffOptions($humanDiffOptions) - * @method static bool setLocale($locale) - * @method static void setMidDayAt($hour) - * @method static void setTestNow($testNow = null) - * @method static void setToStringFormat($format) + * @method static void serializeUsing(mixed $callback) + * @method static void setHumanDiffOptions(mixed $humanDiffOptions) + * @method static void setFallbackLocale(string $locale) + * @method static void setLocale(string $locale) + * @method static void setMidDayAt(mixed $hour) + * @method static void setTestNow(mixed $testNow = null) + * @method static void setTestNowAndTimezone(mixed $testNow = null, mixed $timezone = null) + * @method static void setToStringFormat((null|Closure|string) $format) * @method static void setTranslator(\Symfony\Contracts\Translation\TranslatorInterface $translator) - * @method static void setWeekendDays($days) + * @method static void setWeekendDays(mixed $days) * @method static bool shouldOverflowMonths() * @method static bool shouldOverflowYears() * @method static string singularUnit(string $unit) - * @method static \Hypervel\Support\Carbon today($tz = null) - * @method static \Hypervel\Support\Carbon tomorrow($tz = null) - * @method static void useMonthsOverflow($monthsOverflow = true) - * @method static void useStrictMode($strictModeEnabled = true) - * @method static void useYearsOverflow($yearsOverflow = true) - * @method static \Hypervel\Support\Carbon yesterday($tz = null) + * @method static void sleep((float|int) $seconds) + * @method static \Carbon\CarbonInterface today((\DateTimeZone|string|int|null) $timezone = null) + * @method static \Carbon\CarbonInterface tomorrow((\DateTimeZone|string|int|null) $timezone = null) + * @method static string translateTimeString(string $timeString, ?string $from = null, ?string $to = null, int $mode = \Carbon\CarbonInterface::TRANSLATE_ALL) + * @method static string translateWith(\Symfony\Contracts\Translation\TranslatorInterface $translator, string $key, array $parameters = [], mixed $number = null) + * @method static void useMonthsOverflow(mixed $monthsOverflow = true) + * @method static void useStrictMode(mixed $strictModeEnabled = true) + * @method static void useYearsOverflow(mixed $yearsOverflow = true) + * @method static mixed withTestNow(mixed $testNow, callable $callback) + * @method static \Carbon\Factory withTimeZone((null|\DateTimeZone|int|string) $timezone) + * @method static \Carbon\CarbonInterface yesterday((\DateTimeZone|string|int|null) $timezone = null) * * @see \Hypervel\Support\DateFactory */ diff --git a/src/support/src/Stringable.php b/src/support/src/Stringable.php index 520505247..683d143e3 100644 --- a/src/support/src/Stringable.php +++ b/src/support/src/Stringable.php @@ -5,6 +5,7 @@ namespace Hypervel\Support; use ArrayAccess; +use Carbon\CarbonInterface; use Closure; use Countable; use Hypervel\Support\Facades\Date; @@ -1161,7 +1162,7 @@ public function toBoolean(): bool * * @throws \Carbon\Exceptions\InvalidFormatException */ - public function toDate(?string $format = null, ?string $tz = null): mixed + public function toDate(?string $format = null, ?string $tz = null): ?CarbonInterface { if (is_null($format)) { return Date::parse($this->value, $tz); diff --git a/src/support/src/functions.php b/src/support/src/functions.php index 19043989a..3c64cf149 100644 --- a/src/support/src/functions.php +++ b/src/support/src/functions.php @@ -60,9 +60,7 @@ function swoole_hook_flags(): int // Time functions... /** - * Create a new Carbon instance for the current time. - * - * @return \Hypervel\Support\Carbon + * Create a new configured Carbon instance for the current time. */ function now(DateTimeZone|UnitEnum|string|null $tz = null): CarbonInterface { diff --git a/tests/Foundation/FoundationHelpersTest.php b/tests/Foundation/FoundationHelpersTest.php index 5d05bea15..bde1d9f9d 100644 --- a/tests/Foundation/FoundationHelpersTest.php +++ b/tests/Foundation/FoundationHelpersTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Foundation\FoundationHelpersTest; -use Carbon\Carbon; use DateTimeZone; use Hypervel\Broadcasting\FakePendingBroadcast; use Hypervel\Broadcasting\PendingBroadcast; @@ -14,8 +13,11 @@ use Hypervel\Contracts\Support\Responsable; use Hypervel\Http\Exceptions\HttpResponseException; use Hypervel\Log\LogManager; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Defer\DeferredCallback; use Hypervel\Support\Defer\DeferredCallbackCollection; +use Hypervel\Support\Facades\Date; use Hypervel\Support\Facades\Event; use Hypervel\Testbench\TestCase; use Mockery as m; @@ -43,118 +45,132 @@ enum UnitEnum class FoundationHelpersTest extends TestCase { - public function testNowReturnsCarbon() + public function testNowReturnsCarbonImmutableByDefault(): void { $result = now(); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); } - public function testNowWithStringTimezone() + public function testNowHonorsTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + + $this->assertSame(Carbon::class, now()::class); + } + + public function testNowWithStringTimezone(): void { $result = now('America/New_York'); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } - public function testNowWithDateTimeZone() + public function testNowWithDateTimeZone(): void { $tz = new DateTimeZone('America/New_York'); $result = now($tz); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } - public function testNowWithStringBackedEnum() + public function testNowWithStringBackedEnum(): void { $result = now(StringEnum::NewYork); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } - public function testNowWithUnitEnum() + public function testNowWithUnitEnum(): void { $result = now(UnitEnum::UTC); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('UTC', $result->timezone->getName()); } - public function testNowWithIntBackedEnum() + public function testNowWithIntBackedEnum(): void { // Int-backed enum returns int, Carbon interprets as UTC offset $result = now(IntEnum::One); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('+01:00', $result->timezone->getName()); } - public function testNowWithNull() + public function testNowWithNull(): void { $result = now(null); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); } - public function testTodayReturnsCarbon() + public function testTodayReturnsCarbonImmutableByDefault(): void { $result = today(); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('00:00:00', $result->format('H:i:s')); } - public function testTodayWithStringTimezone() + public function testTodayHonorsTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + + $this->assertSame(Carbon::class, today()::class); + } + + public function testTodayWithStringTimezone(): void { $result = today('America/New_York'); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); $this->assertEquals('00:00:00', $result->format('H:i:s')); } - public function testTodayWithDateTimeZone() + public function testTodayWithDateTimeZone(): void { $tz = new DateTimeZone('America/New_York'); $result = today($tz); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } - public function testTodayWithStringBackedEnum() + public function testTodayWithStringBackedEnum(): void { $result = today(StringEnum::NewYork); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } - public function testTodayWithUnitEnum() + public function testTodayWithUnitEnum(): void { $result = today(UnitEnum::UTC); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('UTC', $result->timezone->getName()); } - public function testTodayWithIntBackedEnum() + public function testTodayWithIntBackedEnum(): void { // Int-backed enum returns int, Carbon interprets as UTC offset $result = today(IntEnum::One); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('+01:00', $result->timezone->getName()); } - public function testTodayWithNull() + public function testTodayWithNull(): void { $result = today(null); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); } public function testCache() diff --git a/tests/Support/DateFacadeTest.php b/tests/Support/DateFacadeTest.php index 63bc7abad..fb4dd28da 100644 --- a/tests/Support/DateFacadeTest.php +++ b/tests/Support/DateFacadeTest.php @@ -4,103 +4,200 @@ namespace Hypervel\Tests\Support; -use Carbon\CarbonImmutable; +use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; +use Carbon\CarbonInterface; use Carbon\Factory; +use Carbon\FactoryImmutable; use DateTime; +use DateTimeInterface; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\DateFactory; use Hypervel\Support\Facades\Date; -use Hypervel\Tests\Support\Fixtures\CustomDateClass; use Hypervel\Tests\TestCase; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; +use stdClass; class DateFacadeTest extends TestCase { - protected static function assertBetweenStartAndNow($start, $actual) + protected static function assertBetweenStartAndNow(int $start, int $actual): void { static::assertThat( $actual, static::logicalAnd( static::greaterThanOrEqual($start), - static::lessThanOrEqual(Carbon::now()->getTimestamp()) + static::lessThanOrEqual(CarbonImmutable::now()->getTimestamp()) ) ); } - public function testUseClosure() + public function testDefaultDateRoutesReturnHypervelCarbonImmutable(): void { - $start = Carbon::now()->getTimestamp(); - $this->assertSame(Carbon::class, get_class(Date::now())); + $start = CarbonImmutable::now()->getTimestamp(); + + $this->assertSame(CarbonImmutable::class, Date::now()::class); + $this->assertSame(CarbonImmutable::class, Date::parse('2026-07-22')::class); + $this->assertSame(CarbonImmutable::class, Date::today()::class); + $this->assertSame(CarbonImmutable::class, now()::class); + $this->assertSame(CarbonImmutable::class, today()::class); $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); - DateFactory::use(function (Carbon $date) { - return new DateTime($date->format('Y-m-d H:i:s.u'), $date->getTimezone()); + } + + public function testMutableClassIsAnExplicitOptOutAcrossDateRoutes(): void + { + DateFactory::use(Carbon::class); + + $this->assertSame(Carbon::class, Date::now()::class); + $this->assertSame(Carbon::class, Date::parse('2026-07-22')::class); + $this->assertSame(Carbon::class, Date::today()::class); + $this->assertSame(Carbon::class, now()::class); + $this->assertSame(Carbon::class, today()::class); + } + + public function testUseClosureCanTransformTheGeneratedCarbonValue(): void + { + DateFactory::use(function (CarbonImmutable $date): Carbon { + return $date->toMutable()->addDay(); }); - $start = Carbon::now()->getTimestamp(); - $this->assertSame(DateTime::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); + + $date = Date::parse('2026-07-22 12:34:56'); + + $this->assertSame(Carbon::class, $date::class); + $this->assertSame('2026-07-23 12:34:56', $date->toDateTimeString()); } - public function testUseClassName() + public function testUseSupportsInvokableObjectsAndCallableStrings(): void { - $start = Carbon::now()->getTimestamp(); - $this->assertSame(Carbon::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); - DateFactory::use(DateTime::class); - $start = Carbon::now()->getTimestamp(); - $this->assertSame(DateTime::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); + DateFactory::use(new DateFacadeInvokableHandler); + $this->assertSame('2026-07-23', Date::parse('2026-07-22')->toDateString()); + + DateFactory::use(DateFacadeCallableHandler::class . '::handle'); + $this->assertSame('2026-07-24', Date::parse('2026-07-22')->toDateString()); } - public function testFlushStateRestoresDefaultDateFactory() + public function testConcreteCarbonClassHandlersReturnTheExactConfiguredClass(): void { - DateFactory::use(DateTime::class); - $this->assertSame(DateTime::class, get_class(Date::now())); + foreach ([ + Carbon::class, + CarbonImmutable::class, + BaseCarbon::class, + BaseCarbonImmutable::class, + DateFacadeCustomCarbon::class, + ] as $dateClass) { + DateFactory::use($dateClass); - DateFactory::flushState(); + $this->assertSame($dateClass, Date::now()::class); + } + } + + public function testMutableAndImmutableFactoriesReturnTheirConfiguredClasses(): void + { + DateFactory::use(new Factory(['locale' => 'fr'], Carbon::class)); + $mutable = Date::now(); - $this->assertSame(Carbon::class, get_class(Date::now())); + $this->assertSame(Carbon::class, $mutable::class); + $this->assertSame('fr', $mutable->locale()); + + DateFactory::use(new FactoryImmutable(['locale' => 'de'], CarbonImmutable::class)); + $immutable = Date::now(); + + $this->assertSame(CarbonImmutable::class, $immutable::class); + $this->assertSame('de', $immutable->locale()); } - public function testCarbonImmutable() + public function testFactoryTimezoneConfigurationReturnsAClonedFactory(): void { - DateFactory::use(CarbonImmutable::class); - $this->assertSame(CarbonImmutable::class, get_class(Date::now())); - DateFactory::use(Carbon::class); - $this->assertSame(Carbon::class, get_class(Date::now())); - DateFactory::use(function (Carbon $date) { - return $date->toImmutable(); - }); - $this->assertSame(CarbonImmutable::class, get_class(Date::now())); - DateFactory::use(function ($date) { - return $date; - }); - $this->assertSame(Carbon::class, get_class(Date::now())); + $factory = new Factory; + DateFactory::use($factory); - DateFactory::use(new Factory([ - 'locale' => 'fr', - ])); - $this->assertSame('fr', Date::now()->locale); - DateFactory::use(Carbon::class); - $this->assertSame('en', Date::now()->locale); - DateFactory::use(CustomDateClass::class); - $this->assertInstanceOf(CustomDateClass::class, Date::now()); - $this->assertInstanceOf(Carbon::class, Date::now()->getOriginal()); + $configured = Date::withTimeZone('America/Toronto'); + + $this->assertSame(Factory::class, $configured::class); + $this->assertNotSame($factory, $configured); + $this->assertSame('America/Toronto', $configured->now()->timezoneName); + } + + public function testFlushStateRestoresDefaultDateFactory(): void + { DateFactory::use(Carbon::class); + $this->assertSame(Carbon::class, Date::now()::class); + + DateFactory::flushState(); + + $this->assertSame(CarbonImmutable::class, Date::now()::class); + } + + #[DataProvider('invalidClassHandlerProvider')] + public function testUseAndUseClassRejectInvalidClassHandlers(string $dateClass): void + { + foreach (['use', 'useClass'] as $method) { + try { + DateFactory::{$method}($dateClass); + $this->fail("{$method} accepted invalid date class {$dateClass}."); + } catch (InvalidArgumentException) { + $this->addToAssertionCount(1); + } + } + } + + public static function invalidClassHandlerProvider(): array + { + return [ + 'native DateTime' => [DateTime::class], + 'Carbon interface' => [CarbonInterface::class], + 'abstract Carbon class' => [DateFacadeAbstractCarbon::class], + 'non-Carbon wrapper' => [DateFacadeNonCarbonWrapper::class], + 'non-Carbon class' => [stdClass::class], + ]; } - public function testUseInvalidHandler() + public function testUseRejectsInvalidScalarHandler(): void { $this->expectException(InvalidArgumentException::class); DateFactory::use(42); } - public function testMacro() + public function testMacro(): void { - Date::macro('returnNonDate', function () { + Date::macro('returnNonDate', function (): string { return 'string'; }); $this->assertSame('string', Date::returnNonDate()); } } + +class DateFacadeCustomCarbon extends CarbonImmutable +{ +} + +abstract class DateFacadeAbstractCarbon extends CarbonImmutable +{ +} + +class DateFacadeNonCarbonWrapper +{ + public static function instance(DateTimeInterface $date): static + { + return new static; + } +} + +class DateFacadeInvokableHandler +{ + public function __invoke(CarbonImmutable $date): CarbonInterface + { + return $date->addDay(); + } +} + +class DateFacadeCallableHandler +{ + public static function handle(CarbonImmutable $date): CarbonInterface + { + return $date->addDays(2); + } +} diff --git a/tests/Support/Fixtures/CustomDateClass.php b/tests/Support/Fixtures/CustomDateClass.php deleted file mode 100644 index e32c112c5..000000000 --- a/tests/Support/Fixtures/CustomDateClass.php +++ /dev/null @@ -1,25 +0,0 @@ -original = $original; - } - - public static function instance(mixed $original): static - { - return new static($original); - } - - public function getOriginal(): mixed - { - return $this->original; - } -} diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 5403ad69a..8979c7f50 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -7,7 +7,9 @@ use Hypervel\Container\Container; use Hypervel\Encryption\Encrypter; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; +use Hypervel\Support\Facades\Date; use Hypervel\Support\HtmlString; use Hypervel\Support\Stringable; use Hypervel\Support\Uri; @@ -1556,19 +1558,39 @@ public function testNumbers() $this->assertSame('5551234567', (string) $this->stringable('(555) 123-4567')->numbers()); } - public function testToDate() + public function testToDate(): void { - $current = Carbon::create(2020, 1, 1, 16, 30, 25); + $current = CarbonImmutable::create(2020, 1, 1, 16, 30, 25); - $this->assertEquals($current, $this->stringable('20-01-01 16:30:25')->toDate()); - $this->assertEquals($current, $this->stringable('1577896225')->toDate('U')); + $parsed = $this->stringable('20-01-01 16:30:25')->toDate(); + $formatted = $this->stringable('1577896225')->toDate('U'); + + $this->assertSame(CarbonImmutable::class, $parsed::class); + $this->assertSame(CarbonImmutable::class, $formatted::class); + $this->assertEquals($current, $parsed); + $this->assertEquals($current, $formatted); $this->assertEquals($current, $this->stringable('20-01-01 13:30:25')->toDate(null, 'America/Santiago')); $this->assertTrue($this->stringable('2020-01-01')->toDate()->isSameDay($current)); $this->assertTrue($this->stringable('16:30:25')->toDate()->isSameSecond('16:30:25')); } - public function testToDateThrowsException() + public function testToDateUsesConfiguredMutableClass(): void + { + Date::use(Carbon::class); + + $this->assertSame(Carbon::class, $this->stringable('2020-01-01')->toDate()::class); + $this->assertSame(Carbon::class, $this->stringable('2020-01-01')->toDate('Y-m-d')::class); + } + + public function testToDateReturnsNullWhenFormattedCreationFailsInNonStrictMode(): void + { + Date::useStrictMode(false); + + $this->assertNull($this->stringable('not a date')->toDate('Y-m-d')); + } + + public function testToDateThrowsException(): void { $this->expectException(\Carbon\Exceptions\InvalidFormatException::class); From 4355112b40029524bdb3ab5224bf5430ed94153c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:14:28 +0000 Subject: [PATCH 04/34] feat(support): make DataObject date targets exact Route DataObject date hydration through the configured Date factory, then convert the result according to the property declaration across native, Carbon interface, Hypervel Carbon, and base Carbon mutable and immutable targets. Preserve configured subclasses only at interface boundaries, honor exact concrete targets without losing Carbon settings, accept timestamp zero, and let nullability fail through the constructor contract. Unify date serialization on DateTimeInterface and add a full input, target, factory-mode, subclass, nullable, nested, serialization, and failure matrix. --- src/support/src/DataObject.php | 108 +++++++------ tests/Support/DataObjectTest.php | 260 +++++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+), 48 deletions(-) diff --git a/src/support/src/DataObject.php b/src/support/src/DataObject.php index 3844b2ab5..d6e976688 100644 --- a/src/support/src/DataObject.php +++ b/src/support/src/DataObject.php @@ -7,9 +7,13 @@ use ArrayAccess; use BackedEnum; use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; use Carbon\CarbonInterface; +use Carbon\Exceptions\InvalidFormatException; use DateTime; +use DateTimeImmutable; use DateTimeInterface; +use Hypervel\Support\Facades\Date; use JsonSerializable; use LogicException; use OutOfBoundsException; @@ -114,13 +118,23 @@ public static function from(array $data, bool $autoResolve = false): static */ protected static function getCustomizedDependencies(): array { - return [ - DateTimeInterface::class => $asDateTime = fn ($value) => $value ? static::asDateTime($value) : null, - CarbonInterface::class => $asDateTime, - DateTime::class => $asDateTime, - Carbon::class => $asDateTime, - BaseCarbon::class => $asDateTime, + $dependencies = []; + $dateTargets = [ + DateTimeInterface::class, + CarbonInterface::class, + DateTime::class, + DateTimeImmutable::class, + Carbon::class, + CarbonImmutable::class, + BaseCarbon::class, + BaseCarbonImmutable::class, ]; + + foreach ($dateTargets as $target) { + $dependencies[$target] = static fn (mixed $value): ?DateTimeInterface => $value === [] ? null : static::asDateTime($value, $target); + } + + return $dependencies; } /** @@ -131,58 +145,48 @@ protected static function getCustomizedDependencies(): array protected static function getSerializers(): array { return [ - DateTimeInterface::class => $asIsoString = fn ($value) => $value->format('c'), - DateTime::class => $asIsoString, - CarbonInterface::class => $asIsoString, - Carbon::class => $asIsoString, - BaseCarbon::class => $asIsoString, + DateTimeInterface::class => static fn (DateTimeInterface $value): string => $value->format('c'), ]; } /** - * Return a timestamp as DateTime object. + * Convert a value to the declared date target. + * + * @param BaseCarbon::class|BaseCarbonImmutable::class|Carbon::class|CarbonImmutable::class|CarbonInterface::class|DateTime::class|DateTimeImmutable::class|DateTimeInterface::class $target */ - protected static function asDateTime(mixed $value): CarbonInterface + protected static function asDateTime(mixed $value, string $target): DateTimeInterface { - // If this value is already a Carbon instance, we shall just return it as is. - // This prevents us having to re-instantiate a Carbon instance when we know - // it already is one, which wouldn't be fulfilled by the DateTime check. - if ($value instanceof CarbonInterface) { - return Carbon::instance($value); - } - - // If the value is already a DateTime instance, we will just skip the rest of - // these checks since they will be a waste of time, and hinder performance - // when checking the field. We will just return the DateTime right away. if ($value instanceof DateTimeInterface) { - return Carbon::parse( - $value->format('Y-m-d H:i:s.u'), - $value->getTimezone() + $date = Date::instance($value); + } elseif (is_numeric($value)) { + $date = Date::createFromTimestamp( + $value, + date_default_timezone_get() ); - } - - // If this value is an integer, we will assume it is a UNIX timestamp's value - // and format a Carbon object from this timestamp. This allows flexibility - // when defining your date fields as they might be UNIX timestamps here. - if (is_numeric($value)) { - return Carbon::createFromTimestamp($value, date_default_timezone_get()); - } - - // If the value is in simply year, month, day format, we will instantiate the - // Carbon instances from that format. Again, this provides for simple date - // fields on the database, while still supporting Carbonized conversion. - if (static::isStandardDateFormat($value)) { - return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay()); - } + } elseif (static::isStandardDateFormat($value)) { + $date = Date::parse($value)->startOfDay(); + } else { + try { + $date = Date::createFromFormat(static::$dateFormat, $value); + // @phpstan-ignore catch.neverThrown (the Date facade's magic dispatch hides Carbon's @throws from analysis) + } catch (InvalidFormatException) { + $date = null; + } - // Finally, we will just assume this date is in the format used by default on - // the database connection and use that format to create the Carbon object - // that is returned back out to the developers after we convert it here. - if (Carbon::hasFormat($value, static::$dateFormat)) { - return Carbon::createFromFormat(static::$dateFormat, $value); + $date ??= Date::parse($value); } - return Carbon::parse($value); + return match ($target) { + DateTimeInterface::class, CarbonInterface::class => $date, + DateTime::class => DateTime::createFromInterface($date), + DateTimeImmutable::class => DateTimeImmutable::createFromInterface($date), + // instance() clones same-mutability subclasses, so cross the mutability + // boundary first to honor the exact target while retaining Carbon settings. + Carbon::class => Carbon::instance($date->toImmutable()), + CarbonImmutable::class => CarbonImmutable::instance($date->toMutable()), + BaseCarbon::class => BaseCarbon::instance($date->toImmutable()), + BaseCarbonImmutable::class => BaseCarbonImmutable::instance($date->toMutable()), + }; } /** @@ -581,7 +585,15 @@ public function toArray(): array // recursively convert nested objects to arrays if ($value instanceof self) { $value = $value->toArray(); - } elseif (is_object($value) && $serializer = $serializers[get_class($value)] ?? null) { + } elseif ( + $value instanceof DateTimeInterface + && $serializer = $serializers[DateTimeInterface::class] ?? null + ) { + $value = $serializer($value); + } elseif ( + is_object($value) + && $serializer = $serializers[$value::class] ?? null + ) { $value = $serializer($value); } elseif (is_object($value) && method_exists($value, 'toArray')) { $value = $value->toArray(); diff --git a/tests/Support/DataObjectTest.php b/tests/Support/DataObjectTest.php index 5e323bad4..ca97cf586 100644 --- a/tests/Support/DataObjectTest.php +++ b/tests/Support/DataObjectTest.php @@ -4,15 +4,26 @@ namespace Hypervel\Tests\Support; +use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; +use Carbon\CarbonInterface; use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\DataObject; +use Hypervel\Support\Facades\Date; use Hypervel\Support\Str; use Hypervel\Tests\TestCase; use LogicException; use OutOfBoundsException; +use PHPUnit\Framework\Attributes\DataProvider; use ReflectionClass; use RuntimeException; use stdClass; +use TypeError; class DataObjectTest extends TestCase { @@ -328,6 +339,216 @@ public function testMakeWithAutoResolveDataObject(): void $this->assertSame(TestGenderEnum::Male, $user->gender); } + #[DataProvider('dateInputProvider')] + public function testAutoResolveHydratesEveryDeclaredDateTarget( + mixed $input, + DateTimeImmutable $expected + ): void { + $object = DateTargetDataObject::make(array_fill_keys([ + 'date_time_interface', + 'carbon_interface', + 'date_time', + 'date_time_immutable', + 'carbon', + 'carbon_immutable', + 'base_carbon', + 'base_carbon_immutable', + ], $input), true); + + $expectedClasses = [ + 'dateTimeInterface' => CarbonImmutable::class, + 'carbonInterface' => CarbonImmutable::class, + 'dateTime' => DateTime::class, + 'dateTimeImmutable' => DateTimeImmutable::class, + 'carbon' => Carbon::class, + 'carbonImmutable' => CarbonImmutable::class, + 'baseCarbon' => BaseCarbon::class, + 'baseCarbonImmutable' => BaseCarbonImmutable::class, + ]; + + foreach ($expectedClasses as $property => $expectedClass) { + $date = $object->{$property}; + + $this->assertSame($expectedClass, $date::class); + $this->assertSame( + $expected->format('Y-m-d H:i:s.u e'), + $date->format('Y-m-d H:i:s.u e') + ); + + if ($input instanceof CarbonInterface && $date instanceof CarbonInterface) { + $this->assertSame($input->locale(), $date->locale()); + } + } + } + + public static function dateInputProvider(): array + { + $defaultTimezone = new DateTimeZone(date_default_timezone_get()); + $auckland = new DateTimeZone('Pacific/Auckland'); + $instant = new DateTimeImmutable('2026-07-22 12:34:56.123456', $auckland); + $epoch = (new DateTimeImmutable('@0'))->setTimezone($defaultTimezone); + + return [ + 'database format' => [ + '2026-07-22 12:34:56', + new DateTimeImmutable('2026-07-22 12:34:56', $defaultTimezone), + ], + 'standard date' => [ + '2026-07-22', + new DateTimeImmutable('2026-07-22 00:00:00', $defaultTimezone), + ], + 'integer epoch' => [0, $epoch], + 'string epoch' => ['0', $epoch], + 'native mutable' => [DateTime::createFromInterface($instant), $instant], + 'native immutable' => [$instant, $instant], + 'base mutable Carbon' => [BaseCarbon::instance($instant)->locale('fr'), $instant], + 'base immutable Carbon' => [BaseCarbonImmutable::instance($instant)->locale('fr'), $instant], + 'Hypervel mutable Carbon' => [Carbon::instance($instant)->locale('fr'), $instant], + 'Hypervel immutable Carbon' => [CarbonImmutable::instance($instant)->locale('fr'), $instant], + ]; + } + + public function testAutoResolveSupportsFormatModifiersAndTrailingData(): void + { + $keys = [ + 'date_time_interface', + 'carbon_interface', + 'date_time', + 'date_time_immutable', + 'carbon', + 'carbon_immutable', + 'base_carbon', + 'base_carbon_immutable', + ]; + + $this->setDataObjectStaticProperty('dateFormat', '!Y-d-m \Y'); + $object = DateTargetDataObject::make(array_fill_keys($keys, '2017-05-11 Y'), true); + + $this->assertSame(CarbonImmutable::class, $object->carbonInterface::class); + $this->assertSame('2017-11-05 00:00:00.000000', $object->carbonInterface->format('Y-m-d H:i:s.u')); + + $this->setDataObjectStaticProperty('dateFormat', '!Y-m-d+'); + $object = DateTargetDataObject::make(array_fill_keys($keys, '2020-09-11 trailing data'), true); + + $this->assertSame(CarbonImmutable::class, $object->carbonInterface::class); + $this->assertSame('2020-09-11 00:00:00.000000', $object->carbonInterface->format('Y-m-d H:i:s.u')); + } + + public function testInterfaceDateTargetsFollowConfiguredFactoryWithoutChangingConcreteTargets(): void + { + Date::use(Carbon::class); + + $object = DateTargetDataObject::make(array_fill_keys([ + 'date_time_interface', + 'carbon_interface', + 'date_time', + 'date_time_immutable', + 'carbon', + 'carbon_immutable', + 'base_carbon', + 'base_carbon_immutable', + ], '2026-07-22 12:34:56'), true); + + $this->assertSame(Carbon::class, $object->dateTimeInterface::class); + $this->assertSame(Carbon::class, $object->carbonInterface::class); + $this->assertSame(DateTime::class, $object->dateTime::class); + $this->assertSame(DateTimeImmutable::class, $object->dateTimeImmutable::class); + $this->assertSame(Carbon::class, $object->carbon::class); + $this->assertSame(CarbonImmutable::class, $object->carbonImmutable::class); + $this->assertSame(BaseCarbon::class, $object->baseCarbon::class); + $this->assertSame(BaseCarbonImmutable::class, $object->baseCarbonImmutable::class); + } + + public function testConcreteDateTargetsRemainExactWithConfiguredCarbonSubclasses(): void + { + foreach ([ + DataObjectMutableCarbonSubclass::class, + DataObjectImmutableCarbonSubclass::class, + ] as $dateClass) { + Date::use($dateClass); + + $object = DateTargetDataObject::make(array_fill_keys([ + 'date_time_interface', + 'carbon_interface', + 'date_time', + 'date_time_immutable', + 'carbon', + 'carbon_immutable', + 'base_carbon', + 'base_carbon_immutable', + ], '2026-07-22 12:34:56'), true); + + $this->assertSame($dateClass, $object->dateTimeInterface::class); + $this->assertSame($dateClass, $object->carbonInterface::class); + $this->assertSame(Carbon::class, $object->carbon::class); + $this->assertSame(CarbonImmutable::class, $object->carbonImmutable::class); + $this->assertSame(BaseCarbon::class, $object->baseCarbon::class); + $this->assertSame(BaseCarbonImmutable::class, $object->baseCarbonImmutable::class); + } + } + + public function testExplicitNullForRequiredDateReachesConstructorAsNull(): void + { + $data = array_fill_keys([ + 'date_time_interface', + 'carbon_interface', + 'date_time', + 'date_time_immutable', + 'carbon', + 'carbon_immutable', + 'base_carbon', + 'base_carbon_immutable', + ], '2026-07-22 12:34:56'); + $data['date_time_interface'] = null; + + try { + DateTargetDataObject::make($data, true); + + $this->fail('Expected the required date constructor argument to reject null.'); + } catch (TypeError $exception) { + $this->assertStringContainsString('$dateTimeInterface', $exception->getMessage()); + $this->assertStringContainsString('null given', $exception->getMessage()); + } + } + + public function testEveryDeclaredDateTargetSerializesThroughDateTimeInterface(): void + { + $input = new DateTimeImmutable( + '2026-07-22 12:34:56', + new DateTimeZone('Pacific/Auckland') + ); + $object = DateTargetDataObject::make(array_fill_keys([ + 'date_time_interface', + 'carbon_interface', + 'date_time', + 'date_time_immutable', + 'carbon', + 'carbon_immutable', + 'base_carbon', + 'base_carbon_immutable', + ], $input), true); + $array = $object->toArray(); + + foreach ($array as $value) { + $this->assertSame('2026-07-22T12:34:56+12:00', $value); + } + + $this->assertSame($array, json_decode(json_encode($object), true)); + } + + public function testCustomNonDateSerializerRemainsAvailableWithDateSerialization(): void + { + $object = CustomSerializerDataObject::make([ + 'date' => CarbonImmutable::parse('2026-07-22 12:34:56', 'UTC'), + 'value' => new ResolverValue('custom'), + ]); + + $this->assertSame([ + 'date' => '2026-07-22T12:34:56+00:00', + 'value' => 'serialized:custom', + ], $object->toArray()); + } + /** * Test autoResolve with deep nesting. */ @@ -595,6 +816,29 @@ public function __construct( } } +class DateTargetDataObject extends DataObject +{ + public function __construct( + public DateTimeInterface $dateTimeInterface, + public CarbonInterface $carbonInterface, + public DateTime $dateTime, + public DateTimeImmutable $dateTimeImmutable, + public Carbon $carbon, + public CarbonImmutable $carbonImmutable, + public BaseCarbon $baseCarbon, + public BaseCarbonImmutable $baseCarbonImmutable, + ) { + } +} + +class DataObjectMutableCarbonSubclass extends Carbon +{ +} + +class DataObjectImmutableCarbonSubclass extends CarbonImmutable +{ +} + enum TestGenderEnum: string { case Male = 'male'; @@ -709,6 +953,22 @@ public function __construct(public string $value) } } +class CustomSerializerDataObject extends DataObject +{ + public function __construct( + public CarbonInterface $date, + public ResolverValue $value, + ) { + } + + protected static function getSerializers(): array + { + return parent::getSerializers() + [ + ResolverValue::class => static fn (ResolverValue $value): string => 'serialized:' . $value->value, + ]; + } +} + class DependencylessDataObject extends DataObject { public static int $dependencyLookups = 0; From be9bd3be22413447ac157d4a026c0df6be350448 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:14:39 +0000 Subject: [PATCH 05/34] feat(foundation): align the clock and Carbon cleanup Configure the existing PSR clock binding to construct Hypervel CarbonImmutable independently from application Date factory opt-outs. Keep one authoritative Carbon static-state reset in the PHPUnit subscriber and restore the shared test clock, macros, serializer, string format, and strict-mode state between tests. Verify exact clock output, frozen-time agreement, mutable factory isolation, and complete cleanup through both Hypervel Carbon variants. --- .../Providers/FoundationServiceProvider.php | 6 ++- .../src/PHPUnit/AfterEachTestSubscriber.php | 2 +- .../FoundationServiceProviderTest.php | 15 ++++--- .../PHPUnit/AfterEachTestSubscriberTest.php | 45 +++++++++++++++++++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/foundation/src/Providers/FoundationServiceProvider.php b/src/foundation/src/Providers/FoundationServiceProvider.php index b73cc8cc5..7bb4f0d8e 100644 --- a/src/foundation/src/Providers/FoundationServiceProvider.php +++ b/src/foundation/src/Providers/FoundationServiceProvider.php @@ -91,6 +91,7 @@ use Hypervel\Http\Request; use Hypervel\Log\Events\MessageLogged; use Hypervel\Queue\Events\JobAttempted; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Composer; use Hypervel\Support\Defer\DeferredCallback; use Hypervel\Support\Defer\DeferredCallbackCollection; @@ -230,7 +231,10 @@ public function register(): void */ protected function registerClock(): void { - $this->app->singleton(ClockInterface::class, fn () => new FactoryImmutable); + $this->app->singleton( + ClockInterface::class, + fn () => new FactoryImmutable(className: CarbonImmutable::class) + ); } /** diff --git a/src/testing/src/PHPUnit/AfterEachTestSubscriber.php b/src/testing/src/PHPUnit/AfterEachTestSubscriber.php index c1561ef56..a6f6ee381 100644 --- a/src/testing/src/PHPUnit/AfterEachTestSubscriber.php +++ b/src/testing/src/PHPUnit/AfterEachTestSubscriber.php @@ -109,8 +109,8 @@ protected function flushFrameworkState(): void \Carbon\Carbon::resetMacros(); \Carbon\Carbon::resetToStringFormat(); \Carbon\Carbon::serializeUsing(null); + \Carbon\Carbon::useStrictMode(); \Carbon\Carbon::setTestNow(); - \Carbon\CarbonImmutable::setTestNow(); if (class_exists(\Laravel\SerializableClosure\SerializableClosure::class)) { \Laravel\SerializableClosure\SerializableClosure::setSecretKey(null); diff --git a/tests/Foundation/Providers/FoundationServiceProviderTest.php b/tests/Foundation/Providers/FoundationServiceProviderTest.php index d8e1c5181..3cd6852be 100644 --- a/tests/Foundation/Providers/FoundationServiceProviderTest.php +++ b/tests/Foundation/Providers/FoundationServiceProviderTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Foundation\Providers; -use Carbon\CarbonImmutable; use Hypervel\Console\Scheduling\Schedule; use Hypervel\Contracts\Console\Kernel; use Hypervel\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract; @@ -14,6 +13,7 @@ use Hypervel\Foundation\WorkerCachedMaintenanceMode; use Hypervel\Http\Request; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Testbench\TestCase; use Psr\Clock\ClockInterface; @@ -82,15 +82,15 @@ public function testClockSingletonIsRegistered(): void public function testClockReturnsCarbonImmutable(): void { - $this->assertInstanceOf( + $this->assertSame( CarbonImmutable::class, - $this->app->make(ClockInterface::class)->now() + $this->app->make(ClockInterface::class)->now()::class ); } public function testClockHonorsCarbonTestTime(): void { - Carbon::setTestNow($expected = Carbon::parse('2026-07-03 12:34:56', 'UTC')); + Carbon::setTestNow($expected = CarbonImmutable::parse('2026-07-03 12:34:56', 'UTC')); $this->assertSame( $expected->format('Y-m-d H:i:s.u P'), @@ -100,10 +100,11 @@ public function testClockHonorsCarbonTestTime(): void public function testClockAgreesWithDateFacadeAndNowHelper(): void { - Carbon::setTestNow(Carbon::parse('2026-07-03 12:34:56', 'UTC')); + Carbon::setTestNow(CarbonImmutable::parse('2026-07-03 12:34:56', 'UTC')); $clockNow = $this->app->make(ClockInterface::class)->now(); + $this->assertSame(CarbonImmutable::class, $clockNow::class); $this->assertSame(now()->format('Y-m-d H:i:s.u P'), $clockNow->format('Y-m-d H:i:s.u P')); $this->assertSame(Date::now()->format('Y-m-d H:i:s.u P'), $clockNow->format('Y-m-d H:i:s.u P')); } @@ -112,9 +113,9 @@ public function testClockReturnsCarbonImmutableWhenDateFacadeUsesMutableCarbon() { Date::useClass(Carbon::class); - $this->assertInstanceOf( + $this->assertSame( CarbonImmutable::class, - $this->app->make(ClockInterface::class)->now() + $this->app->make(ClockInterface::class)->now()::class ); } diff --git a/tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php b/tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php index bed2eb613..b2b05f6ee 100644 --- a/tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php +++ b/tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Testing\PHPUnit; +use Carbon\CarbonInterface; use Hypervel\Contracts\Pool\ConnectionInterface; use Hypervel\Database\Eloquent\Factories\Factory as EloquentFactory; use Hypervel\Encryption\Commands\KeyGenerateCommand; @@ -19,6 +20,8 @@ use Hypervel\Http\Response as HttpResponse; use Hypervel\Http\UploadedFile; use Hypervel\Process\InvokedProcess; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Testing\Fakes\NotificationFake; use Hypervel\Testing\PHPUnit\AfterEachTestCleanup; use Hypervel\Testing\PHPUnit\AfterEachTestSubscriber; @@ -89,6 +92,48 @@ public function flushFrameworkStateForTest(): void } } + public function testFrameworkCleanupFlushesSharedCarbonStateThroughOneOwner(): void + { + $immutable = CarbonImmutable::parse('2026-01-02 03:04:05', 'UTC'); + $mutable = Carbon::instance($immutable); + + CarbonImmutable::macro('carbonCleanupProbe', static fn (): string => 'macro'); + CarbonImmutable::setToStringFormat('Y'); + CarbonImmutable::serializeUsing(static fn (CarbonInterface $date): string => 'serialized'); + CarbonImmutable::setTestNow($immutable); + CarbonImmutable::useStrictMode(false); + + $this->assertTrue(Carbon::hasMacro('carbonCleanupProbe')); + $this->assertSame('2026', (string) $mutable); + $this->assertSame('"serialized"', json_encode($mutable)); + $this->assertSame($immutable->toDateTimeString(), Carbon::now()->toDateTimeString()); + $this->assertFalse(Carbon::isStrictModeEnabled()); + $this->assertFalse(CarbonImmutable::isStrictModeEnabled()); + + $subscriber = new class extends AfterEachTestSubscriber { + public function flushFrameworkStateForTest(): void + { + $this->flushFrameworkState(); + } + }; + + $subscriber->flushFrameworkStateForTest(); + + $immutable = CarbonImmutable::parse('2026-01-02 03:04:05', 'UTC'); + $mutable = Carbon::instance($immutable); + + $this->assertFalse(Carbon::hasMacro('carbonCleanupProbe')); + $this->assertFalse(CarbonImmutable::hasMacro('carbonCleanupProbe')); + $this->assertSame('2026-01-02 03:04:05', (string) $immutable); + $this->assertSame('2026-01-02 03:04:05', (string) $mutable); + $this->assertSame('"2026-01-02T03:04:05.000000Z"', json_encode($immutable)); + $this->assertSame('"2026-01-02T03:04:05.000000Z"', json_encode($mutable)); + $this->assertFalse(Carbon::hasTestNow()); + $this->assertFalse(CarbonImmutable::hasTestNow()); + $this->assertTrue(Carbon::isStrictModeEnabled()); + $this->assertTrue(CarbonImmutable::isStrictModeEnabled()); + } + public function testFrameworkCleanupFlushesInheritedRequestStaticState(): void { $request = new Request; From 774a4d117d00f1fab558ff244b9d3e047881f37b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:14:51 +0000 Subject: [PATCH 06/34] feat(testing): route time travel through DateFactory Create frozen dates, wormhole targets, and Sleep deadlines through the configured Date factory while continuing to use Carbon shared static test-clock state. Capture immutable modifier results for every time-travel operation and preserve callback return values and exception-safe restoration. Cover immutable defaults, mutable opt-out, numeric deadlines, synchronization, each supported travel unit, and restoration after callback failures. --- .../Testing/Concerns/InteractsWithTime.php | 12 ++++---- src/foundation/src/Testing/Wormhole.php | 25 +++++++++-------- src/support/src/InteractsWithTime.php | 7 +++-- src/support/src/Sleep.php | 7 +++-- .../FoundationInteractsWithTimeTest.php | 28 +++++++++++++++++-- tests/Foundation/Testing/WormholeTest.php | 28 +++++++++++++------ tests/Support/SleepTest.php | 26 +++++++++++++++-- 7 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/foundation/src/Testing/Concerns/InteractsWithTime.php b/src/foundation/src/Testing/Concerns/InteractsWithTime.php index a9024aa85..060dfa2c4 100644 --- a/src/foundation/src/Testing/Concerns/InteractsWithTime.php +++ b/src/foundation/src/Testing/Concerns/InteractsWithTime.php @@ -4,10 +4,12 @@ namespace Hypervel\Foundation\Testing\Concerns; +use Carbon\CarbonInterface; use Closure; use DateTimeInterface; use Hypervel\Foundation\Testing\Wormhole; use Hypervel\Support\Carbon; +use Hypervel\Support\Facades\Date; trait InteractsWithTime { @@ -19,7 +21,7 @@ trait InteractsWithTime */ public function freezeTime($callback = null) { - $result = $this->travelTo($now = Carbon::now(), $callback); + $result = $this->travelTo($now = Date::now(), $callback); return $callback === null ? $now : $result; } @@ -32,7 +34,7 @@ public function freezeTime($callback = null) */ public function freezeSecond($callback = null) { - $result = $this->travelTo($now = Carbon::now()->startOfSecond(), $callback); + $result = $this->travelTo($now = Date::now()->startOfSecond(), $callback); return $callback === null ? $now : $result; } @@ -48,7 +50,7 @@ public function travel(int $value): Wormhole /** * Travel to another time. * - * @param null|bool|Carbon|Closure|DateTimeInterface|string $date + * @param null|bool|CarbonInterface|Closure|DateTimeInterface|string $date * @param null|callable $callback * @return mixed */ @@ -67,10 +69,8 @@ public function travelTo($date, $callback = null) /** * Travel back to the current time. - * - * @return DateTimeInterface */ - public function travelBack() + public function travelBack(): CarbonInterface { return Wormhole::back(); } diff --git a/src/foundation/src/Testing/Wormhole.php b/src/foundation/src/Testing/Wormhole.php index 758c6cc52..222a9b253 100644 --- a/src/foundation/src/Testing/Wormhole.php +++ b/src/foundation/src/Testing/Wormhole.php @@ -4,8 +4,9 @@ namespace Hypervel\Foundation\Testing; -use DateTimeInterface; +use Carbon\CarbonInterface; use Hypervel\Support\Carbon; +use Hypervel\Support\Facades\Date; class Wormhole { @@ -41,7 +42,7 @@ public function microsecond($callback = null) */ public function microseconds($callback = null) { - Carbon::setTestNow(Carbon::now()->addMicroseconds($this->value)); + Carbon::setTestNow(Date::now()->addMicroseconds($this->value)); return $this->handleCallback($callback); } @@ -65,7 +66,7 @@ public function millisecond($callback = null) */ public function milliseconds($callback = null) { - Carbon::setTestNow(Carbon::now()->addMilliseconds($this->value)); + Carbon::setTestNow(Date::now()->addMilliseconds($this->value)); return $this->handleCallback($callback); } @@ -89,7 +90,7 @@ public function second($callback = null) */ public function seconds($callback = null) { - Carbon::setTestNow(Carbon::now()->addSeconds($this->value)); + Carbon::setTestNow(Date::now()->addSeconds($this->value)); return $this->handleCallback($callback); } @@ -113,7 +114,7 @@ public function minute($callback = null) */ public function minutes($callback = null) { - Carbon::setTestNow(Carbon::now()->addMinutes($this->value)); + Carbon::setTestNow(Date::now()->addMinutes($this->value)); return $this->handleCallback($callback); } @@ -137,7 +138,7 @@ public function hour($callback = null) */ public function hours($callback = null) { - Carbon::setTestNow(Carbon::now()->addHours($this->value)); + Carbon::setTestNow(Date::now()->addHours($this->value)); return $this->handleCallback($callback); } @@ -161,7 +162,7 @@ public function day($callback = null) */ public function days($callback = null) { - Carbon::setTestNow(Carbon::now()->addDays($this->value)); + Carbon::setTestNow(Date::now()->addDays($this->value)); return $this->handleCallback($callback); } @@ -185,7 +186,7 @@ public function week($callback = null) */ public function weeks($callback = null) { - Carbon::setTestNow(Carbon::now()->addWeeks($this->value)); + Carbon::setTestNow(Date::now()->addWeeks($this->value)); return $this->handleCallback($callback); } @@ -209,7 +210,7 @@ public function month($callback = null) */ public function months($callback = null) { - Carbon::setTestNow(Carbon::now()->addMonths($this->value)); + Carbon::setTestNow(Date::now()->addMonths($this->value)); return $this->handleCallback($callback); } @@ -233,7 +234,7 @@ public function year($callback = null) */ public function years($callback = null) { - Carbon::setTestNow(Carbon::now()->addYears($this->value)); + Carbon::setTestNow(Date::now()->addYears($this->value)); return $this->handleCallback($callback); } @@ -241,11 +242,11 @@ public function years($callback = null) /** * Travel back to the current time. */ - public static function back(): DateTimeInterface + public static function back(): CarbonInterface { Carbon::setTestNow(); - return Carbon::now(); + return Date::now(); } /** diff --git a/src/support/src/InteractsWithTime.php b/src/support/src/InteractsWithTime.php index f71d409b5..0d4f36bb9 100644 --- a/src/support/src/InteractsWithTime.php +++ b/src/support/src/InteractsWithTime.php @@ -7,6 +7,7 @@ use Carbon\CarbonInterval; use DateInterval; use DateTimeInterface; +use Hypervel\Support\Facades\Date; trait InteractsWithTime { @@ -31,7 +32,7 @@ protected function availableAt(DateInterval|DateTimeInterface|int|null $delay = return $delay instanceof DateTimeInterface ? $delay->getTimestamp() - : Carbon::now()->addSeconds($delay)->getTimestamp(); + : Date::now()->addSeconds($delay)->getTimestamp(); } /** @@ -44,7 +45,7 @@ protected function parseDateInterval(DateInterval|DateTimeInterface|int|null $de } if ($delay instanceof DateInterval) { - $delay = Carbon::now()->add($delay); + $delay = Date::now()->add($delay); } return $delay; @@ -55,7 +56,7 @@ protected function parseDateInterval(DateInterval|DateTimeInterface|int|null $de */ protected function currentTime(): int { - return Carbon::now()->getTimestamp(); + return Date::now()->getTimestamp(); } /** diff --git a/src/support/src/Sleep.php b/src/support/src/Sleep.php index 048a907b4..227ea1819 100644 --- a/src/support/src/Sleep.php +++ b/src/support/src/Sleep.php @@ -8,6 +8,7 @@ use Closure; use DateInterval; use DateTimeInterface; +use Hypervel\Support\Facades\Date; use Hypervel\Support\Traits\Macroable; use PHPUnit\Framework\Assert as PHPUnit; use RuntimeException; @@ -87,10 +88,10 @@ public static function for(DateInterval|float|int $duration): static public static function until(DateTimeInterface|float|int|string $timestamp): static { if (is_numeric($timestamp)) { - $timestamp = Carbon::createFromTimestamp($timestamp, date_default_timezone_get()); + $timestamp = Date::createFromTimestamp($timestamp, date_default_timezone_get()); } - return new static(Carbon::now()->diff($timestamp)); + return new static(Date::now()->diff($timestamp)); } /** @@ -261,7 +262,7 @@ protected function goodnight(): void static::$sequence[] = $this->duration; if (static::$syncWithCarbon) { - Carbon::setTestNow(Carbon::now()->add($this->duration)); + Carbon::setTestNow(Date::now()->add($this->duration)); } foreach (static::$fakeSleepCallbacks as $callback) { diff --git a/tests/Foundation/FoundationInteractsWithTimeTest.php b/tests/Foundation/FoundationInteractsWithTimeTest.php index 67d319209..096121064 100644 --- a/tests/Foundation/FoundationInteractsWithTimeTest.php +++ b/tests/Foundation/FoundationInteractsWithTimeTest.php @@ -4,9 +4,10 @@ namespace Hypervel\Tests\Foundation; -use DateTimeInterface; use Hypervel\Foundation\Testing\Concerns\InteractsWithTime; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; use RuntimeException; @@ -19,7 +20,17 @@ public function testFreezeTimeReturnsFrozenTime(): void $actual = $this->freezeTime(); $this->assertTrue(Carbon::hasTestNow()); - $this->assertInstanceOf(DateTimeInterface::class, $actual); + $this->assertSame(CarbonImmutable::class, $actual::class); + $this->assertTrue(Carbon::getTestNow()->eq($actual)); + } + + public function testFreezeTimeHonorsTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + + $actual = $this->freezeTime(); + + $this->assertSame(Carbon::class, $actual::class); $this->assertTrue(Carbon::getTestNow()->eq($actual)); } @@ -48,7 +59,18 @@ public function testFreezeSecondReturnsFrozenTime(): void $actual = $this->freezeSecond(); $this->assertTrue(Carbon::hasTestNow()); - $this->assertInstanceOf(DateTimeInterface::class, $actual); + $this->assertSame(CarbonImmutable::class, $actual::class); + $this->assertTrue(Carbon::getTestNow()->eq($actual)); + $this->assertSame(0, $actual->milliseconds); + } + + public function testFreezeSecondHonorsTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + + $actual = $this->freezeSecond(); + + $this->assertSame(Carbon::class, $actual::class); $this->assertTrue(Carbon::getTestNow()->eq($actual)); $this->assertSame(0, $actual->milliseconds); } diff --git a/tests/Foundation/Testing/WormholeTest.php b/tests/Foundation/Testing/WormholeTest.php index 83bbe4656..653df4277 100644 --- a/tests/Foundation/Testing/WormholeTest.php +++ b/tests/Foundation/Testing/WormholeTest.php @@ -4,9 +4,9 @@ namespace Hypervel\Tests\Foundation\Testing; -use Carbon\CarbonImmutable; use Hypervel\Foundation\Testing\Wormhole; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; use RuntimeException; @@ -29,15 +29,14 @@ public function testCanTravelBackToPresent(): void $this->assertEquals($present->format('Y-m-d'), Wormhole::back()->format('Y-m-d')); } - public function testCarbonImmutableCompatibility(): void + public function testDefaultTravelUsesImmutableDates(): void { - // Tell the Date Factory to use CarbonImmutable... - Date::use(CarbonImmutable::class); - // Record what time it is in 10 days... $present = now(); $future = $present->addDays(10); + $this->assertSame(CarbonImmutable::class, $present::class); + // Travel in time... (new Wormhole(10))->days(); @@ -45,20 +44,31 @@ public function testCarbonImmutableCompatibility(): void $this->assertNotEquals($future->format('Y-m-d'), $present->format('Y-m-d')); // Assert the time travel was successful... - $this->assertEquals($future->format('Y-m-d'), now()->format('Y-m-d')); + $now = now(); + + $this->assertSame(CarbonImmutable::class, $now::class); + $this->assertEquals($future->format('Y-m-d'), $now->format('Y-m-d')); + } + + public function testTravelHonorsTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + + (new Wormhole(10))->days(); + + $this->assertSame(Carbon::class, Date::now()::class); + $this->assertSame(Carbon::class, Wormhole::back()::class); } public function testItCanTravelByMicroseconds(): void { - Carbon::setTestNow(Carbon::parse('2000-01-01 00:00:00')->startOfSecond()); + Carbon::setTestNow(CarbonImmutable::parse('2000-01-01 00:00:00')->startOfSecond()); (new Wormhole(1))->microsecond(); $this->assertSame('2000-01-01 00:00:00.000001', Date::now()->format('Y-m-d H:i:s.u')); (new Wormhole(5))->microseconds(); $this->assertSame('2000-01-01 00:00:00.000006', Date::now()->format('Y-m-d H:i:s.u')); - - Carbon::setTestnow(); } public function testCallbackResultIsReturnedAndRealTimeIsRestored(): void diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index bad143f5b..ad194e4ef 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -7,6 +7,7 @@ use Carbon\CarbonInterval; use Exception; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Support\Sleep; use Hypervel\Tests\TestCase; @@ -595,7 +596,7 @@ public function testItDoesNotSyncCarbon() $this->assertSame('2000-01-01 00:00:00', Date::now()->toDateTimeString()); } - public function testItCanSyncCarbon() + public function testItCanSyncCarbon(): void { Carbon::setTestNow('2000-01-01 00:00:00'); Sleep::fake(); @@ -607,7 +608,26 @@ public function testItCanSyncCarbon() Sleep::assertSequence([ Sleep::for(303)->seconds(), ]); - $this->assertSame('2000-01-01 00:05:03', Date::now()->toDateTimeString()); + $now = Date::now(); + + $this->assertSame(CarbonImmutable::class, $now::class); + $this->assertSame('2000-01-01 00:05:03', $now->toDateTimeString()); + } + + public function testItCanSyncCarbonUsingTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + Carbon::setTestNow('2000-01-01 00:00:00'); + Sleep::fake(); + Sleep::syncWithCarbon(); + + Sleep::for(5)->minutes() + ->and(3)->seconds(); + + $now = Date::now(); + + $this->assertSame(Carbon::class, $now::class); + $this->assertSame('2000-01-01 00:05:03', $now->toDateTimeString()); } #[TestWith([ @@ -618,7 +638,7 @@ public function testItCanSyncCarbon() 'syncWithCarbon' => false, 'datetime' => '2000-01-01 00:00:00', ])] - public function testFakeCanSetSyncWithCarbon(bool $syncWithCarbon, string $datetime) + public function testFakeCanSetSyncWithCarbon(bool $syncWithCarbon, string $datetime): void { Carbon::setTestNow('2000-01-01 00:00:00'); Sleep::fake(syncWithCarbon: $syncWithCarbon); From d5d96393f6b2fe0bb369abf81d769ca96181c1c0 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:15:05 +0000 Subject: [PATCH 07/34] fix(foundation): retain immutable lifecycle timestamps Store framework-owned HTTP request and console command start times as exact immutable values and capture timezone conversions instead of relying on mutable side effects. Write the converted HTTP start value back to coroutine context so duration handlers and lifecycle accessors observe the same instance, then clear lifecycle state after completion or failure. Exercise configured timezones, handler arguments, thresholds, coroutine-visible values, exception cleanup, and post-termination null state. --- src/foundation/src/Console/Kernel.php | 12 +-- src/foundation/src/Http/Kernel.php | 13 ++- .../Console/KernelTerminateTest.php | 93 ++++++++++--------- tests/Foundation/Http/KernelTest.php | 39 +++++++- .../Console/CommandDurationThresholdTest.php | 84 +++++++++-------- 5 files changed, 145 insertions(+), 96 deletions(-) diff --git a/src/foundation/src/Console/Kernel.php b/src/foundation/src/Console/Kernel.php index d74e0d0bd..91ce5363c 100644 --- a/src/foundation/src/Console/Kernel.php +++ b/src/foundation/src/Console/Kernel.php @@ -21,7 +21,7 @@ use Hypervel\Foundation\Bus\PendingDispatch; use Hypervel\Foundation\Events\Terminating; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Env; use Hypervel\Support\InteractsWithTime; @@ -85,7 +85,7 @@ class Kernel implements KernelContract /** * When the currently handled command started. */ - protected ?Carbon $commandStartedAt = null; + protected ?CarbonImmutable $commandStartedAt = null; /** * The console application bootstrappers. @@ -153,7 +153,7 @@ public function rerouteSymfonyCommandEvents(): static */ public function handle(InputInterface $input, ?OutputInterface $output = null): int { - $this->commandStartedAt = Carbon::now(); + $this->commandStartedAt = CarbonImmutable::now(); $output ??= new ConsoleOutput; try { @@ -194,7 +194,7 @@ public function terminate(InputInterface $input, int $status): void if ($this->commandStartedAt !== null) { try { - $this->commandStartedAt->setTimezone( + $this->commandStartedAt = $this->commandStartedAt->setTimezone( $this->app->make('config')->string('app.timezone') ); } catch (Throwable $throwable) { @@ -203,7 +203,7 @@ public function terminate(InputInterface $input, int $status): void foreach ($this->commandLifecycleDurationHandlers as ['threshold' => $threshold, 'handler' => $handler]) { try { - $end ??= Carbon::now(); + $end ??= CarbonImmutable::now(); if ($this->commandStartedAt->diffInMilliseconds($end) > $threshold) { $handler($this->commandStartedAt, $input, $status); @@ -243,7 +243,7 @@ public function whenCommandLifecycleIsLongerThan(CarbonInterval|DateTimeInterfac /** * When the command being handled started. */ - public function commandStartedAt(): ?Carbon + public function commandStartedAt(): ?CarbonImmutable { return $this->commandStartedAt; } diff --git a/src/foundation/src/Http/Kernel.php b/src/foundation/src/Http/Kernel.php index bb773e94f..83a6d1f90 100644 --- a/src/foundation/src/Http/Kernel.php +++ b/src/foundation/src/Http/Kernel.php @@ -18,7 +18,7 @@ use Hypervel\Http\Request; use Hypervel\Routing\Pipeline; use Hypervel\Routing\Router; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use InvalidArgumentException; use Symfony\Component\HttpFoundation\Response; @@ -128,7 +128,7 @@ public function __construct(Application $app, Router $router) */ public function handle(Request $request): Response { - CoroutineContext::set(self::REQUEST_STARTED_AT_CONTEXT_KEY, Carbon::now()); + CoroutineContext::set(self::REQUEST_STARTED_AT_CONTEXT_KEY, CarbonImmutable::now()); try { $request->enableHttpMethodParameterOverride(); @@ -231,12 +231,15 @@ public function terminate(Request $request, Response $response): void $requestStartedAt = CoroutineContext::get(self::REQUEST_STARTED_AT_CONTEXT_KEY); if ($requestStartedAt !== null && $this->requestLifecycleDurationHandlers !== []) { - $requestStartedAt->setTimezone($this->app->make('config')->string('app.timezone')); + $requestStartedAt = $requestStartedAt->setTimezone( + $this->app->make('config')->string('app.timezone') + ); + CoroutineContext::set(self::REQUEST_STARTED_AT_CONTEXT_KEY, $requestStartedAt); $end = null; foreach ($this->requestLifecycleDurationHandlers as ['threshold' => $threshold, 'handler' => $handler]) { try { - $end ??= Carbon::now(); + $end ??= CarbonImmutable::now(); if ($requestStartedAt->diffInMilliseconds($end) > $threshold) { $handler($requestStartedAt, $request, $response); @@ -324,7 +327,7 @@ public function whenRequestLifecycleIsLongerThan(DateTimeInterface|CarbonInterva /** * Get when the kernel started handling the current request. */ - public function requestStartedAt(): ?Carbon + public function requestStartedAt(): ?CarbonImmutable { return CoroutineContext::get(self::REQUEST_STARTED_AT_CONTEXT_KEY); } diff --git a/tests/Foundation/Console/KernelTerminateTest.php b/tests/Foundation/Console/KernelTerminateTest.php index 6baacaf59..c027ac786 100644 --- a/tests/Foundation/Console/KernelTerminateTest.php +++ b/tests/Foundation/Console/KernelTerminateTest.php @@ -8,7 +8,7 @@ use Hypervel\Contracts\Console\Kernel as KernelContract; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Foundation\Events\Terminating; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use RuntimeException; use Symfony\Component\Console\Input\StringInput; @@ -16,7 +16,7 @@ class KernelTerminateTest extends TestCase { - public function testTerminateDispatchesTerminatingEventAndAppTerminateInOrder() + public function testTerminateDispatchesTerminatingEventAndAppTerminateInOrder(): void { $called = []; @@ -36,7 +36,7 @@ public function testTerminateDispatchesTerminatingEventAndAppTerminateInOrder() ], $called); } - public function testTerminateDispatchesTerminatingEventEvenWithoutHandle() + public function testTerminateDispatchesTerminatingEventEvenWithoutHandle(): void { // Calling terminate without a prior handle should not throw. $kernel = $this->app->make(KernelContract::class); @@ -46,31 +46,34 @@ public function testTerminateDispatchesTerminatingEventEvenWithoutHandle() $this->assertTrue(true); } - public function testCommandStartedAtIsNullBeforeHandle() + public function testCommandStartedAtIsNullBeforeHandle(): void { $kernel = $this->app->make(KernelContract::class); $this->assertNull($kernel->commandStartedAt()); } - public function testCommandStartedAtIsSetAfterHandle() + public function testCommandStartedAtIsSetAfterHandle(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $kernel->handle(new StringInput('foo'), new ConsoleOutput); - $this->assertNotNull($kernel->commandStartedAt()); + $startedAt = $kernel->commandStartedAt(); + + $this->assertNotNull($startedAt); + $this->assertSame(CarbonImmutable::class, $startedAt::class); } - public function testCommandStartedAtIsClearedAfterTerminate() + public function testCommandStartedAtIsClearedAfterTerminate(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); @@ -80,7 +83,7 @@ public function testCommandStartedAtIsClearedAfterTerminate() $this->assertNull($kernel->commandStartedAt()); } - public function testDurationThresholdHandlerCalledWhenExceeded() + public function testDurationThresholdHandlerCalledWhenExceeded(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); @@ -90,19 +93,19 @@ public function testDurationThresholdHandlerCalledWhenExceeded() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMilliseconds(1)); $kernel->terminate($input, 0); $this->assertTrue($called); } - public function testDurationThresholdHandlerNotCalledWhenExactlyAtThreshold() + public function testDurationThresholdHandlerNotCalledWhenExactlyAtThreshold(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); @@ -112,17 +115,17 @@ public function testDurationThresholdHandlerNotCalledWhenExactlyAtThreshold() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 0); $this->assertFalse($called); } - public function testDurationThresholdHandlerReceivesCorrectArguments() + public function testDurationThresholdHandlerReceivesCorrectArguments(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); @@ -132,20 +135,21 @@ public function testDurationThresholdHandlerReceivesCorrectArguments() $receivedArgs = func_get_args(); }); - Carbon::setTestNow($startedAt = Carbon::now()); + CarbonImmutable::setTestNow($startedAt = CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 21); $this->assertCount(3, $receivedArgs); + $this->assertSame(CarbonImmutable::class, $receivedArgs[0]::class); $this->assertTrue($startedAt->eq($receivedArgs[0])); $this->assertSame($input, $receivedArgs[1]); $this->assertSame(21, $receivedArgs[2]); } - public function testDurationThresholdWithMilliseconds() + public function testDurationThresholdWithMilliseconds(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); @@ -155,19 +159,19 @@ public function testDurationThresholdWithMilliseconds() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMilliseconds(1)); $kernel->terminate($input, 0); $this->assertTrue($called); } - public function testDurationThresholdWithMillisecondsNotExceeded() + public function testDurationThresholdWithMillisecondsNotExceeded(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); @@ -177,17 +181,17 @@ public function testDurationThresholdWithMillisecondsNotExceeded() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 0); $this->assertFalse($called); } - public function testDurationThresholdWithDateTimeInterface() + public function testDurationThresholdWithDateTimeInterface(): void { $this->freezeSecond(); @@ -195,7 +199,7 @@ public function testDurationThresholdWithDateTimeInterface() $kernel->command('foo', fn () => null); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(Carbon::now()->addSecond()->addMillisecond(), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonImmutable::now()->addSecond()->addMillisecond(), function () use (&$called) { $called = true; }); @@ -204,13 +208,13 @@ public function testDurationThresholdWithDateTimeInterface() $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMillisecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMillisecond()); $kernel->terminate($input, 0); $this->assertTrue($called); } - public function testDurationThresholdWithDateTimeInterfaceNotExceeded() + public function testDurationThresholdWithDateTimeInterfaceNotExceeded(): void { $this->freezeSecond(); @@ -218,43 +222,46 @@ public function testDurationThresholdWithDateTimeInterfaceNotExceeded() $kernel->command('foo', fn () => null); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(Carbon::now()->addSecond()->addMillisecond(), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonImmutable::now()->addSecond()->addMillisecond(), function () use (&$called) { $called = true; }); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 0); $this->assertFalse($called); } - public function testTerminateUsesConfiguredTimezone() + public function testTerminateUsesConfiguredTimezone(): void { $this->app['config']->set('app.timezone', 'UTC'); $startedAt = null; $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); - $kernel->whenCommandLifecycleIsLongerThan(0, function ($started) use (&$startedAt) { + $kernel->whenCommandLifecycleIsLongerThan(0, function (CarbonImmutable $started) use (&$startedAt, $kernel): void { $startedAt = $started; + + $this->assertSame($started, $kernel->commandStartedAt()); }); $this->app['config']->set('app.timezone', 'Australia/Melbourne'); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(now()->addMinute()); + CarbonImmutable::setTestNow(now()->addMinute()); $kernel->terminate($input, 0); + $this->assertSame(CarbonImmutable::class, $startedAt::class); $this->assertSame('Australia/Melbourne', $startedAt->timezone->getName()); } - public function testMultipleDurationHandlers() + public function testMultipleDurationHandlers(): void { $kernel = $this->app->make(KernelContract::class); $kernel->command('foo', fn () => null); @@ -270,12 +277,12 @@ public function testMultipleDurationHandlers() $calledSecond = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); // Advance 1 second — exceeds first threshold (500ms) but not second (2000ms). - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 0); $this->assertTrue($calledFirst); @@ -310,10 +317,10 @@ public function testEventFailureDoesNotSkipApplicationOrDurationHandlers(): void $calls[] = 'second duration'; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSecond()); try { $kernel->terminate($input, 0); @@ -346,10 +353,10 @@ public function testApplicationFailurePrecedesDurationHandlerFailure(): void throw $durationException; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSecond()); try { $kernel->terminate($input, 0); @@ -382,10 +389,10 @@ public function testFirstDurationHandlerFailureDoesNotSkipLaterHandlers(): void throw $secondException; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $input = new StringInput('foo'); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSecond()); try { $kernel->terminate($input, 0); diff --git a/tests/Foundation/Http/KernelTest.php b/tests/Foundation/Http/KernelTest.php index 7db142547..6d3fdc9d5 100644 --- a/tests/Foundation/Http/KernelTest.php +++ b/tests/Foundation/Http/KernelTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Foundation\Http; -use Carbon\Carbon; use Hypervel\Config\Repository; use Hypervel\Context\ResponseContext; use Hypervel\Contracts\Debug\ExceptionHandler; @@ -15,6 +14,7 @@ use Hypervel\Http\Request; use Hypervel\Http\Response; use Hypervel\Routing\Router; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; use RuntimeException; @@ -373,7 +373,40 @@ public function terminate(Request $request, Response $response): void $this->assertNull($kernel->requestStartedAt()); } - public function testRequestStartedAtIsIsolatedBetweenConcurrentCoroutines() + public function testDurationHandlerReceivesConvertedImmutableStartTimeFromContext(): void + { + $app = new Application; + $events = new Dispatcher($app); + $app->instance('events', $events); + $app->instance('config', new Repository(['app' => ['timezone' => 'America/New_York']])); + $app->bootstrapWith([]); + + $router = m::mock(Router::class); + $router->shouldReceive('dispatch')->once()->andReturn(new Response); + + $kernel = new Kernel($app, $router); + $request = Request::create('/'); + $captured = null; + CarbonImmutable::setTestNow('2026-07-23 12:34:56 UTC'); + + $kernel->whenRequestLifecycleIsLongerThan(-1, function (CarbonImmutable $startedAt) use ($kernel, &$captured): void { + $captured = $startedAt; + + $this->assertSame(CarbonImmutable::class, $startedAt::class); + $this->assertSame('America/New_York', $startedAt->timezoneName); + $this->assertSame($startedAt, $kernel->requestStartedAt()); + }); + + $response = $kernel->handle($request); + $original = $kernel->requestStartedAt(); + $kernel->terminate($request, $response); + + $this->assertNotSame($original, $captured); + $this->assertSame($original?->getTimestamp(), $captured?->getTimestamp()); + $this->assertNull($kernel->requestStartedAt()); + } + + public function testRequestStartedAtIsIsolatedBetweenConcurrentCoroutines(): void { $app = new Application; $events = new Dispatcher($app); @@ -387,7 +420,7 @@ public function testRequestStartedAtIsIsolatedBetweenConcurrentCoroutines() $kernel = new Kernel($app, $router); $captured = []; - $kernel->whenRequestLifecycleIsLongerThan(0, function (Carbon $startedAt, Request $request) use (&$captured) { + $kernel->whenRequestLifecycleIsLongerThan(0, function (CarbonImmutable $startedAt, Request $request) use (&$captured): void { $captured[$request->headers->get('X-Coroutine')] = $startedAt; }); diff --git a/tests/Integration/Console/CommandDurationThresholdTest.php b/tests/Integration/Console/CommandDurationThresholdTest.php index aeba7f219..ba2ef84e6 100644 --- a/tests/Integration/Console/CommandDurationThresholdTest.php +++ b/tests/Integration/Console/CommandDurationThresholdTest.php @@ -6,7 +6,7 @@ use Carbon\CarbonInterval; use Hypervel\Contracts\Console\Kernel; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Config; use Hypervel\Testbench\TestCase; use Symfony\Component\Console\Input\StringInput; @@ -14,9 +14,9 @@ class CommandDurationThresholdTest extends TestCase { - public function testItCanHandleExceedingCommandDuration() + public function testItCanHandleExceedingCommandDuration(): void { - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $called = false; @@ -24,20 +24,20 @@ public function testItCanHandleExceedingCommandDuration() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMilliseconds(1)); $kernel->terminate($input, 21); $this->assertTrue($called); } - public function testItDoesntCallWhenExactlyThresholdDuration() + public function testItDoesntCallWhenExactlyThresholdDuration(): void { - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $called = false; @@ -45,20 +45,20 @@ public function testItDoesntCallWhenExactlyThresholdDuration() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 21); $this->assertFalse($called); } - public function testItProvidesArgsToHandler() + public function testItProvidesArgsToHandler(): void { - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $args = null; @@ -66,20 +66,21 @@ public function testItProvidesArgsToHandler() $args = func_get_args(); }); - Carbon::setTestNow($startedAt = Carbon::now()); + CarbonImmutable::setTestNow($startedAt = CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 21); $this->assertCount(3, $args); + $this->assertSame(CarbonImmutable::class, $args[0]::class); $this->assertTrue($startedAt->eq($args[0])); $this->assertSame($input, $args[1]); $this->assertSame(21, $args[2]); } - public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds() + public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds(): void { - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $called = false; @@ -87,20 +88,20 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds() $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMilliseconds(1)); $kernel->terminate($input, 21); $this->assertTrue($called); } - public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds() + public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds(): void { - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $called = false; @@ -108,27 +109,27 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds( $called = true; }); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 21); $this->assertFalse($called); } - public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime() + public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime(): void { $this->freezeSecond(); $input = new StringInput('foo'); $called = false; - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); - $kernel->whenCommandLifecycleIsLongerThan(Carbon::now()->addSecond()->addMillisecond(), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonImmutable::now()->addSecond()->addMillisecond(), function () use (&$called) { $called = true; }); @@ -136,21 +137,21 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime() $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMillisecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMillisecond()); $kernel->terminate($input, 21); $this->assertTrue($called); } - public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime() + public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime(): void { $this->freezeSecond(); - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(Carbon::now()->addSecond()->addMillisecond(), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonImmutable::now()->addSecond()->addMillisecond(), function () use (&$called) { $called = true; }); @@ -158,50 +159,55 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime() $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); $kernel->terminate($input, 21); $this->assertFalse($called); } - public function testItClearsStartTimeAfterHandlingCommand() + public function testItClearsStartTimeAfterHandlingCommand(): void { - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); $input = new StringInput('foo'); $this->assertNull($kernel->commandStartedAt()); $kernel->handle($input, new ConsoleOutput); - $this->assertNotNull($kernel->commandStartedAt()); + $startedAt = $kernel->commandStartedAt(); + $this->assertNotNull($startedAt); + $this->assertSame(CarbonImmutable::class, $startedAt::class); $kernel->terminate($input, 21); $this->assertNull($kernel->commandStartedAt()); } - public function testUsesTheConfiguredDateTimezone() + public function testUsesTheConfiguredDateTimezone(): void { Config::set('app.timezone', 'UTC'); $startedAt = null; - $kernel = $this->app[Kernel::class]; + $kernel = $this->app->make(Kernel::class); $kernel->command('foo', fn () => null); - $kernel->whenCommandLifecycleIsLongerThan(0, function ($started) use (&$startedAt) { + $kernel->whenCommandLifecycleIsLongerThan(0, function (CarbonImmutable $started) use (&$startedAt, $kernel): void { $startedAt = $started; + + $this->assertSame($started, $kernel->commandStartedAt()); }); Config::set('app.timezone', 'Australia/Melbourne'); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $kernel->handle($input = new StringInput('foo'), new ConsoleOutput); - Carbon::setTestNow(now()->addMinute()); + CarbonImmutable::setTestNow(now()->addMinute()); $kernel->terminate($input, 21); + $this->assertSame(CarbonImmutable::class, $startedAt::class); $this->assertSame('Australia/Melbourne', $startedAt->timezone->getName()); } - public function testItHandlesCallingTerminateWithoutHandle() + public function testItHandlesCallingTerminateWithoutHandle(): void { - $this->app[Kernel::class]->terminate(new StringInput('foo'), 21); + $this->app->make(Kernel::class)->terminate(new StringInput('foo'), 21); // This is a placeholder just to show that the above did not throw an exception. $this->assertTrue(true); From 978a674b7dc58606b9884d3c4afdea0d1c45fd3f Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:15:18 +0000 Subject: [PATCH 08/34] refactor(foundation): keep maintenance dates immutable Use Hypervel CarbonImmutable for maintenance retry timestamps, bypass-cookie expiry, and worker-cached maintenance metadata. Keep these framework-owned values independent from application Date factory configuration while preserving their existing wire formats and expiry behavior. Update unit and integration coverage to assert the canonical Hypervel immutable class at each boundary. --- src/foundation/src/Console/DownCommand.php | 4 ++-- .../src/Http/MaintenanceModeBypassCookie.php | 6 ++--- .../src/WorkerCachedMaintenanceMode.php | 2 +- .../Http/MaintenanceModeBypassCookieTest.php | 8 +++---- .../WorkerCachedMaintenanceModeTest.php | 2 +- .../Foundation/MaintenanceModeTest.php | 24 +++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/foundation/src/Console/DownCommand.php b/src/foundation/src/Console/DownCommand.php index 367db075e..86d6d4d31 100644 --- a/src/foundation/src/Console/DownCommand.php +++ b/src/foundation/src/Console/DownCommand.php @@ -11,7 +11,7 @@ use Hypervel\Foundation\Events\MaintenanceModeEnabled; use Hypervel\Foundation\Exceptions\RegisterErrorViewPaths; use Hypervel\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Symfony\Component\Console\Attribute\AsCommand; use Throwable; @@ -169,7 +169,7 @@ protected function getRetryTime(): int|string|null if (is_string($retry) && ! empty($retry)) { try { - $date = Carbon::parse($retry); + $date = CarbonImmutable::parse($retry); return $date->format(DateTimeInterface::RFC7231); } catch (Exception) { diff --git a/src/foundation/src/Http/MaintenanceModeBypassCookie.php b/src/foundation/src/Http/MaintenanceModeBypassCookie.php index b4739e214..c0f71e7bd 100644 --- a/src/foundation/src/Http/MaintenanceModeBypassCookie.php +++ b/src/foundation/src/Http/MaintenanceModeBypassCookie.php @@ -4,7 +4,7 @@ namespace Hypervel\Foundation\Http; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Symfony\Component\HttpFoundation\Cookie; class MaintenanceModeBypassCookie @@ -14,7 +14,7 @@ class MaintenanceModeBypassCookie */ public static function create(string $key): Cookie { - $expiresAt = Carbon::now()->addHours(12); + $expiresAt = CarbonImmutable::now()->addHours(12); return new Cookie('hypervel_maintenance', base64_encode(json_encode([ 'expires_at' => $expiresAt->getTimestamp(), @@ -33,6 +33,6 @@ public static function isValid(string $cookie, string $key): bool && is_numeric($payload['expires_at'] ?? null) && isset($payload['mac']) && hash_equals(hash_hmac('sha256', (string) $payload['expires_at'], $key), $payload['mac']) - && (int) $payload['expires_at'] >= Carbon::now()->getTimestamp(); + && (int) $payload['expires_at'] >= CarbonImmutable::now()->getTimestamp(); } } diff --git a/src/foundation/src/WorkerCachedMaintenanceMode.php b/src/foundation/src/WorkerCachedMaintenanceMode.php index 43d22af00..1e67efa31 100644 --- a/src/foundation/src/WorkerCachedMaintenanceMode.php +++ b/src/foundation/src/WorkerCachedMaintenanceMode.php @@ -4,8 +4,8 @@ namespace Hypervel\Foundation; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract; +use Hypervel\Support\CarbonImmutable; class WorkerCachedMaintenanceMode implements MaintenanceModeContract { diff --git a/tests/Foundation/Http/MaintenanceModeBypassCookieTest.php b/tests/Foundation/Http/MaintenanceModeBypassCookieTest.php index f7d96e4b2..6b460026f 100644 --- a/tests/Foundation/Http/MaintenanceModeBypassCookieTest.php +++ b/tests/Foundation/Http/MaintenanceModeBypassCookieTest.php @@ -5,7 +5,7 @@ namespace Hypervel\Tests\Foundation\Http; use Hypervel\Foundation\Http\MaintenanceModeBypassCookie; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Symfony\Component\HttpFoundation\Cookie; @@ -43,7 +43,7 @@ public function testIsValidReturnsFalseForExpiredCookie() { $cookie = MaintenanceModeBypassCookie::create('test-key'); - Carbon::setTestNow(now()->addMonths(6)); + CarbonImmutable::setTestNow(now()->addMonths(6)); $this->assertFalse(MaintenanceModeBypassCookie::isValid($cookie->getValue(), 'test-key')); } @@ -73,12 +73,12 @@ public function testIsValidReturnsFalseForMissingExpiresAt() public function testCookieExpiresIn12Hours() { - Carbon::setTestNow('2026-01-15 10:00:00'); + CarbonImmutable::setTestNow('2026-01-15 10:00:00'); $cookie = MaintenanceModeBypassCookie::create('test-key'); $this->assertSame( - Carbon::parse('2026-01-15 22:00:00')->getTimestamp(), + CarbonImmutable::parse('2026-01-15 22:00:00')->getTimestamp(), $cookie->getExpiresTime() ); } diff --git a/tests/Foundation/WorkerCachedMaintenanceModeTest.php b/tests/Foundation/WorkerCachedMaintenanceModeTest.php index ef3e3dda7..b1810cb52 100644 --- a/tests/Foundation/WorkerCachedMaintenanceModeTest.php +++ b/tests/Foundation/WorkerCachedMaintenanceModeTest.php @@ -4,10 +4,10 @@ namespace Hypervel\Tests\Foundation; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract; use Hypervel\Foundation\ArrayMaintenanceMode; use Hypervel\Foundation\WorkerCachedMaintenanceMode; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; diff --git a/tests/Integration/Foundation/MaintenanceModeTest.php b/tests/Integration/Foundation/MaintenanceModeTest.php index aac14b346..b3953de3a 100644 --- a/tests/Integration/Foundation/MaintenanceModeTest.php +++ b/tests/Integration/Foundation/MaintenanceModeTest.php @@ -15,7 +15,7 @@ use Hypervel\Foundation\Events\MaintenanceModeEnabled; use Hypervel\Foundation\Http\MaintenanceModeBypassCookie; use Hypervel\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Support\Facades\Route; use Hypervel\Testbench\TestCase; @@ -243,7 +243,7 @@ public function testMaintenanceModeCantBeBypassedWithInvalidCookie() $response->assertStatus(503); } - public function testCanCreateBypassCookies() + public function testCanCreateBypassCookies(): void { $cookie = MaintenanceModeBypassCookie::create('test-key'); @@ -253,7 +253,7 @@ public function testCanCreateBypassCookies() $this->assertTrue(MaintenanceModeBypassCookie::isValid($cookie->getValue(), 'test-key')); $this->assertFalse(MaintenanceModeBypassCookie::isValid($cookie->getValue(), 'wrong-key')); - Carbon::setTestNow(now()->addMonths(6)); + CarbonImmutable::setTestNow(now()->addMonths(6)); $this->assertFalse(MaintenanceModeBypassCookie::isValid($cookie->getValue(), 'test-key')); } @@ -376,18 +376,18 @@ public function testUpReportsDriverFailureBeforeMaintenanceStateIsCommitted(): v } #[DataProvider('retryAfterDatetimeProvider')] - public function testMaintenanceModeRetryCanAcceptDatetime(string $datetime) + public function testMaintenanceModeRetryCanAcceptDatetime(string $datetime): void { - Carbon::setTestNow('2023-01-01 00:00:00'); + CarbonImmutable::setTestNow('2023-01-01 00:00:00'); $this->artisan(DownCommand::class, ['--retry' => $datetime]); $data = json_decode(file_get_contents(storage_path('framework/down')), true); - $expectedDate = Carbon::parse($datetime)->format(DateTimeInterface::RFC7231); + $expectedDate = CarbonImmutable::parse($datetime)->format(DateTimeInterface::RFC7231); $this->assertSame($expectedDate, $data['retry']); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public static function retryAfterDatetimeProvider(): array @@ -399,9 +399,9 @@ public static function retryAfterDatetimeProvider(): array ]; } - public function testMaintenanceModeRetryWithHttpDateHeader() + public function testMaintenanceModeRetryWithHttpDateHeader(): void { - $retryDate = Carbon::now()->addWeek(); + $retryDate = CarbonImmutable::now()->addWeek(); $expectedHeader = $retryDate->format(DateTimeInterface::RFC7231); file_put_contents(storage_path('framework/down'), json_encode([ @@ -416,7 +416,7 @@ public function testMaintenanceModeRetryWithHttpDateHeader() $response->assertHeader('Retry-After', $expectedHeader); } - public function testMaintenanceModeRetryWithInvalidDatetimeReturnsNull() + public function testMaintenanceModeRetryWithInvalidDatetimeReturnsNull(): void { $this->artisan(DownCommand::class, ['--retry' => 'not-a-valid-date']); @@ -425,7 +425,7 @@ public function testMaintenanceModeRetryWithInvalidDatetimeReturnsNull() $this->assertNull($data['retry']); } - public function testMaintenanceModeRetryWithAtTimestampNotation() + public function testMaintenanceModeRetryWithAtTimestampNotation(): void { $futureTimestamp = time() + 3600; @@ -433,7 +433,7 @@ public function testMaintenanceModeRetryWithAtTimestampNotation() $data = json_decode(file_get_contents(storage_path('framework/down')), true); - $expectedDate = Carbon::createFromTimestamp($futureTimestamp)->format(DateTimeInterface::RFC7231); + $expectedDate = CarbonImmutable::createFromTimestamp($futureTimestamp)->format(DateTimeInterface::RFC7231); $this->assertSame($expectedDate, $data['retry']); } From e479c4bb564af5569cff981665cfcc3e4c54de42 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:15:28 +0000 Subject: [PATCH 09/34] fix(http): parse request dates through the configured factory Remove the mutable Carbon intermediate from request date casts and create standard, formatted, and fallback dates directly through DateFactory. Let Carbon createFromFormat own modifier grammar, catch only its exact invalid-format exception, and retain generic parsing as the mismatch fallback. Cover immutable output and valid PHP format modifiers, escaped literals, and trailing-data formats that Carbon preflight helpers do not model completely. --- src/foundation/src/Http/Traits/HasCasts.php | 13 +++-- tests/Foundation/Http/CustomCastingTest.php | 54 +++++++++++++++++++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/foundation/src/Http/Traits/HasCasts.php b/src/foundation/src/Http/Traits/HasCasts.php index ccd2d1307..94ece55d5 100644 --- a/src/foundation/src/Http/Traits/HasCasts.php +++ b/src/foundation/src/Http/Traits/HasCasts.php @@ -6,11 +6,11 @@ use BackedEnum; use Carbon\CarbonInterface; +use Carbon\Exceptions\InvalidFormatException; use DateTimeInterface; use Hypervel\Database\Eloquent\InvalidCastException; use Hypervel\Foundation\Http\Contracts\Castable; use Hypervel\Foundation\Http\Contracts\CastInputs; -use Hypervel\Support\Carbon; use Hypervel\Support\Collection; use Hypervel\Support\DataObject; use Hypervel\Support\Facades\Date; @@ -482,7 +482,7 @@ protected function asDateTime(mixed $value): CarbonInterface // Carbon instances from that format. Again, this provides for simple date // fields on the database, while still supporting Carbonized conversion. if ($this->isStandardDateFormat($value)) { - return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay()); + return Date::parse($value)->startOfDay(); } $format = $this->getDateFormat(); @@ -490,11 +490,14 @@ protected function asDateTime(mixed $value): CarbonInterface // Finally, we will just assume this date is in the format used by default on // the database connection and use that format to create the Carbon object // that is returned back out to the developers after we convert it here. - if (Carbon::hasFormat($value, $format)) { - return Date::createFromFormat($format, $value); + try { + $date = Date::createFromFormat($format, $value); + // @phpstan-ignore catch.neverThrown (the Date facade's magic dispatch hides Carbon's @throws from analysis) + } catch (InvalidFormatException) { + $date = null; } - return Date::parse($value); + return $date ?? Date::parse($value); } /** diff --git a/tests/Foundation/Http/CustomCastingTest.php b/tests/Foundation/Http/CustomCastingTest.php index 8fd56e173..17a550d22 100644 --- a/tests/Foundation/Http/CustomCastingTest.php +++ b/tests/Foundation/Http/CustomCastingTest.php @@ -5,7 +5,6 @@ namespace Hypervel\Tests\Foundation\Http; use ArrayObject; -use Carbon\Carbon; use Carbon\CarbonInterface; use Hypervel\Foundation\Http\Casts\AsDataObjectArray; use Hypervel\Foundation\Http\Casts\AsDataObjectCollection; @@ -13,8 +12,11 @@ use Hypervel\Foundation\Http\Casts\AsEnumCollection; use Hypervel\Foundation\Http\Contracts\CastInputs; use Hypervel\Foundation\Http\FormRequest; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\DataObject; +use Hypervel\Support\Facades\Date; use Hypervel\Testbench\TestCase; use Hypervel\Validation\Rule; @@ -209,7 +211,7 @@ public function testPrimitiveCollectionCasting() /** * Test primitive type casting - datetime. */ - public function testPrimitiveDatetimeCasting() + public function testPrimitiveDatetimeCasting(): void { $request = DatetimeCastingRequest::create('/', 'POST', [ 'created_at' => 1705315800, // 2024-01-15 10:50:00 UTC @@ -221,24 +223,63 @@ public function testPrimitiveDatetimeCasting() // Test datetime casting $createdAt = $request->casted('created_at', false); $this->assertInstanceOf(CarbonInterface::class, $createdAt); + $this->assertSame(CarbonImmutable::class, $createdAt::class); $this->assertSame('2024-01-15 10:50:00', $createdAt->format('Y-m-d H:i:s')); // Test date casting (time should be 00:00:00) $publishedDate = $request->casted('published_date', false); $this->assertInstanceOf(CarbonInterface::class, $publishedDate); + $this->assertSame(CarbonImmutable::class, $publishedDate::class); $this->assertSame('2024-01-15 00:00:00', $publishedDate->format('Y-m-d H:i:s')); // Test timestamp casting (returns int timestamp) - /** @var Carbon $updatedTimestamp */ $updatedTimestamp = $request->casted('updated_timestamp', false); $this->assertIsInt($updatedTimestamp); $this->assertSame(1705315800, $updatedTimestamp); } + public function testPrimitiveDatetimeCastingUsesConfiguredMutableClass(): void + { + Date::use(Carbon::class); + + $request = DatetimeCastingRequest::create('/', 'POST', [ + 'created_at' => 1705315800, + 'published_date' => '2024-01-15', + 'updated_timestamp' => '2024-01-15 10:50:00', + ]); + $request->setContainer($this->app); + + $this->assertSame(Carbon::class, $request->casted('created_at', false)::class); + $this->assertSame(Carbon::class, $request->casted('published_date', false)::class); + } + + public function testDatetimeCastingSupportsFormatModifiersAndTrailingData(): void + { + $request = DatetimeCastingRequest::create('/', 'POST', [ + 'created_at' => '2017-05-11 Y', + ]); + $request->useDateFormat('!Y-d-m \Y'); + + $createdAt = $request->casted('created_at', false); + + $this->assertSame(CarbonImmutable::class, $createdAt::class); + $this->assertSame('2017-11-05 00:00:00.000000', $createdAt->format('Y-m-d H:i:s.u')); + + $request = DatetimeCastingRequest::create('/', 'POST', [ + 'created_at' => '2020-09-11 trailing data', + ]); + $request->useDateFormat('!Y-m-d+'); + + $createdAt = $request->casted('created_at', false); + + $this->assertSame(CarbonImmutable::class, $createdAt::class); + $this->assertSame('2020-09-11 00:00:00.000000', $createdAt->format('Y-m-d H:i:s.u')); + } + /** * Test datetime casting preserves app timezone for Unix timestamps. */ - public function testDatetimeCastingPreservesAppTimezone() + public function testDatetimeCastingPreservesAppTimezone(): void { $originalTimezone = date_default_timezone_get(); date_default_timezone_set('America/New_York'); @@ -436,6 +477,11 @@ class DatetimeCastingRequest extends FormRequest 'updated_timestamp' => 'timestamp', ]; + public function useDateFormat(string $format): void + { + $this->dateFormat = $format; + } + public function rules(): array { return [ From 9aef7389645f35da83bc60597d853a04fb09f2e2 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:15:45 +0000 Subject: [PATCH 10/34] fix(console): make scheduling immutable-safe Use immutable framework dates for schedule listing, frequency calculations, and command logging while retaining CarbonInterface for the configurable scheduler start time. Compute the repeat-loop end boundary from a copy, stop before work that crosses the minute, and leave the original start instant unchanged for single-server mutex decisions under both mutable and immutable Date modes. Update scheduler unit and integration coverage for boundaries, frequencies, context propagation, grouped schedules, sub-minute execution, and mutable opt-out behavior. --- .../src/Commands/ScheduleListCommand.php | 16 ++-- .../src/Commands/ScheduleRunCommand.php | 13 ++- .../src/Scheduling/ManagesFrequencies.php | 10 +-- .../Scheduling/CacheSchedulingMutexTest.php | 6 +- tests/Console/Scheduling/EventTest.php | 20 ++--- tests/Console/Scheduling/FrequencyTest.php | 4 +- .../Scheduling/ScheduleRunCommandTest.php | 90 ++++++++++++++++--- .../ScheduleRunContextPropagationTest.php | 4 +- tests/Console/Scheduling/ScheduleTest.php | 10 +-- .../Console/CommandSchedulingTest.php | 4 +- .../Console/Scheduling/ScheduleGroupTest.php | 13 +-- .../Scheduling/ScheduleListCommandTest.php | 4 +- .../Scheduling/ScheduleTestCommandTest.php | 4 +- .../Scheduling/SubMinuteSchedulingTest.php | 66 +++++++------- .../Configuration/WithScheduleTest.php | 4 +- 15 files changed, 170 insertions(+), 98 deletions(-) diff --git a/src/console/src/Commands/ScheduleListCommand.php b/src/console/src/Commands/ScheduleListCommand.php index 44be097a6..768abffa9 100644 --- a/src/console/src/Commands/ScheduleListCommand.php +++ b/src/console/src/Commands/ScheduleListCommand.php @@ -12,7 +12,7 @@ use Hypervel\Console\Scheduling\CallbackEvent; use Hypervel\Console\Scheduling\Event; use Hypervel\Console\Scheduling\Schedule; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use ReflectionClass; use ReflectionFunction; @@ -242,11 +242,11 @@ private function sortEvents(Collection $events, DateTimeZone $timezone): Collect /** * Get the next due date for an event. */ - private function getNextDueDateForEvent(Event $event, DateTimeZone $timezone): Carbon + private function getNextDueDateForEvent(Event $event, DateTimeZone $timezone): CarbonImmutable { - $nextDueDate = Carbon::instance( + $nextDueDate = CarbonImmutable::instance( (new CronExpression($event->expression)) - ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) + ->getNextRunDate(CarbonImmutable::now()->setTimezone($event->timezone)) ->setTimezone($timezone) ); @@ -254,15 +254,15 @@ private function getNextDueDateForEvent(Event $event, DateTimeZone $timezone): C return $nextDueDate; } - $previousDueDate = Carbon::instance( + $previousDueDate = CarbonImmutable::instance( (new CronExpression($event->expression)) - ->getPreviousRunDate(Carbon::now()->setTimezone($event->timezone), allowCurrentDate: true) + ->getPreviousRunDate(CarbonImmutable::now()->setTimezone($event->timezone), allowCurrentDate: true) ->setTimezone($timezone) ); - $now = Carbon::now()->setTimezone($event->timezone); + $now = CarbonImmutable::now()->setTimezone($event->timezone); - if (! $now->copy()->startOfMinute()->eq($previousDueDate)) { + if (! $now->startOfMinute()->eq($previousDueDate)) { return $nextDueDate; } diff --git a/src/console/src/Commands/ScheduleRunCommand.php b/src/console/src/Commands/ScheduleRunCommand.php index 490c6f336..88b3e85f4 100644 --- a/src/console/src/Commands/ScheduleRunCommand.php +++ b/src/console/src/Commands/ScheduleRunCommand.php @@ -20,7 +20,7 @@ use Hypervel\Coroutine\Concurrent; use Hypervel\Coroutine\Waiter; use Hypervel\Log\Context\Repository as ContextRepository; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Date; use Hypervel\Support\Sleep; @@ -219,8 +219,9 @@ protected function runOnce(): void protected function repeatEvents(Collection $events): void { $hasEnteredMaintenanceMode = false; + $endOfMinute = $this->startedAt->copy()->endOfMinute(); - while (Date::now()->lte($this->startedAt->endOfMinute())) { + while (Date::now()->lte($endOfMinute)) { $paused = $this->isPaused(); foreach ($events as $event) { @@ -232,6 +233,10 @@ protected function repeatEvents(Collection $events): void continue; } + if (Date::now()->gt($endOfMinute)) { + return; + } + $hasEnteredMaintenanceMode = $hasEnteredMaintenanceMode || $this->hypervel->isDownForMaintenance(); if ($hasEnteredMaintenanceMode && ! $event->runsInMaintenanceMode()) { @@ -359,7 +364,7 @@ protected function runEvent(Event $event): void $description = sprintf( '%s Running [%s]%s', - Carbon::now()->format('Y-m-d H:i:s'), + CarbonImmutable::now()->format('Y-m-d H:i:s'), $command, $event->runInBackground ? ' in background (coroutine)' : '', ); @@ -391,7 +396,7 @@ protected function runEvent(Event $event): void $finishDescription = sprintf( '%s %s [%s] %sms', - Carbon::now()->format('Y-m-d H:i:s'), + CarbonImmutable::now()->format('Y-m-d H:i:s'), $event->exitCode() === 0 ? 'Finished' : 'Failed', $command, round(microtime(true) - $start, 2), diff --git a/src/console/src/Scheduling/ManagesFrequencies.php b/src/console/src/Scheduling/ManagesFrequencies.php index eb88ce8eb..21575c9d2 100644 --- a/src/console/src/Scheduling/ManagesFrequencies.php +++ b/src/console/src/Scheduling/ManagesFrequencies.php @@ -6,7 +6,7 @@ use Closure; use DateTimeZone; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use InvalidArgumentException; use UnitEnum; @@ -46,9 +46,9 @@ public function unlessBetween(string $startTime, string $endTime): static private function inTimeInterval(string $startTime, string $endTime): Closure { [$now, $startTime, $endTime] = [ - Carbon::now($this->timezone), - Carbon::parse($startTime, $this->timezone), - Carbon::parse($endTime, $this->timezone), + CarbonImmutable::now($this->timezone), + CarbonImmutable::parse($startTime, $this->timezone), + CarbonImmutable::parse($endTime, $this->timezone), ]; if ($endTime->lessThan($startTime)) { @@ -464,7 +464,7 @@ public function lastDayOfMonth(string $time = '0:0'): static { $this->dailyAt($time); - return $this->spliceIntoPosition(3, Carbon::now()->endOfMonth()->day); + return $this->spliceIntoPosition(3, CarbonImmutable::now()->endOfMonth()->day); } /** diff --git a/tests/Console/Scheduling/CacheSchedulingMutexTest.php b/tests/Console/Scheduling/CacheSchedulingMutexTest.php index 5c5c13df9..178fcca70 100644 --- a/tests/Console/Scheduling/CacheSchedulingMutexTest.php +++ b/tests/Console/Scheduling/CacheSchedulingMutexTest.php @@ -11,7 +11,7 @@ use Hypervel\Contracts\Cache\Factory as CacheFactory; use Hypervel\Contracts\Cache\Repository; use Hypervel\Contracts\Cache\Store; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -21,7 +21,7 @@ class CacheSchedulingMutexTest extends TestCase protected ?Event $event = null; - protected ?Carbon $time = null; + protected ?CarbonImmutable $time = null; protected ?CacheFactory $cacheFactory = null; @@ -36,7 +36,7 @@ protected function setUp(): void $this->cacheFactory->shouldReceive('store')->andReturn($this->cacheRepository); $this->cacheMutex = new CacheSchedulingMutex($this->cacheFactory); $this->event = new Event(new CacheEventMutex($this->cacheFactory), 'command'); - $this->time = Carbon::now(); + $this->time = CarbonImmutable::now(); } public function testMutexReceivesCorrectCreate() diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index dcfb17b33..85a890f40 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -14,7 +14,7 @@ use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Filesystem\Filesystem; use Hypervel\Foundation\Application; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Tests\TestCase; use Mockery as m; @@ -371,7 +371,7 @@ public function testEventIsDueCheck() $app = m::mock(ApplicationContract::class); $app->shouldReceive('isDownForMaintenance')->andReturn(false); $app->shouldReceive('environment')->andReturn('production'); - Carbon::setTestNow(Carbon::create(2015, 1, 1, 0, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2015, 1, 1, 0, 0, 0)); $event = new Event(m::mock(EventMutex::class), 'php foo'); $this->assertSame('* * * * 4', $event->thursdays()->getExpression()); @@ -389,15 +389,15 @@ public function testEventIsDueAtUsesGivenTime() $app->shouldReceive('environment')->andReturn('production'); try { - Carbon::setTestNow(Carbon::parse('2026-05-29 13:00:00')); + CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-05-29 13:00:00')); $event = new Event(m::mock(EventMutex::class), 'php foo'); $event->dailyAt('13:00'); - $this->assertFalse($event->isDueAt($app, Carbon::parse('2026-05-29 12:59:59'))); - $this->assertTrue($event->isDueAt($app, Carbon::parse('2026-05-29 13:00:00'))); + $this->assertFalse($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 12:59:59'))); + $this->assertTrue($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 13:00:00'))); } finally { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } } @@ -410,8 +410,8 @@ public function testEventIsDueAtUsesEventTimezone() $event = new Event(m::mock(EventMutex::class), 'php foo'); $event->dailyAt('09:00')->timezone('America/New_York'); - $this->assertTrue($event->isDueAt($app, Carbon::parse('2026-05-29 13:00:00', 'UTC'))); - $this->assertFalse($event->isDueAt($app, Carbon::parse('2026-05-29 12:59:59', 'UTC'))); + $this->assertTrue($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 13:00:00', 'UTC'))); + $this->assertFalse($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 12:59:59', 'UTC'))); } public function testTimeBetweenChecks() @@ -421,7 +421,7 @@ public function testTimeBetweenChecks() $app->shouldReceive('environment')->andReturn('production'); $app->shouldReceive('call')->andReturnUsing(fn (callable $callback) => $callback()); - Carbon::setTestNow(Carbon::now()->startOfDay()->addHours(9)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->startOfDay()->addHours(9)); $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); $this->assertTrue($event->between('8:00', '10:00')->filtersPass($app)); @@ -449,7 +449,7 @@ public function testTimeUnlessBetweenChecks() $app->shouldReceive('environment')->andReturn('production'); $app->shouldReceive('call')->andReturnUsing(fn (callable $callback) => $callback()); - Carbon::setTestNow(Carbon::now()->startOfDay()->addHours(9)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->startOfDay()->addHours(9)); $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); $this->assertFalse($event->unlessBetween('8:00', '10:00')->filtersPass($app)); diff --git a/tests/Console/Scheduling/FrequencyTest.php b/tests/Console/Scheduling/FrequencyTest.php index 098a9d623..dd3e9c6a6 100644 --- a/tests/Console/Scheduling/FrequencyTest.php +++ b/tests/Console/Scheduling/FrequencyTest.php @@ -6,7 +6,7 @@ use Hypervel\Console\Scheduling\Event; use Hypervel\Console\Scheduling\EventMutex; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -124,7 +124,7 @@ public function testMonthlyOn() public function testLastDayOfMonth() { - Carbon::setTestNow('2020-10-10 10:10:10'); + CarbonImmutable::setTestNow('2020-10-10 10:10:10'); $this->assertSame('0 0 31 * *', $this->event->lastDayOfMonth()->getExpression()); } diff --git a/tests/Console/Scheduling/ScheduleRunCommandTest.php b/tests/Console/Scheduling/ScheduleRunCommandTest.php index 7a97b2de8..a5c53a30c 100644 --- a/tests/Console/Scheduling/ScheduleRunCommandTest.php +++ b/tests/Console/Scheduling/ScheduleRunCommandTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Console\Scheduling; +use Carbon\CarbonInterface; use Hypervel\Console\Commands\ScheduleRunCommand; use Hypervel\Console\Events\ScheduledBackgroundTaskFinished; use Hypervel\Console\Events\ScheduledTaskFailed; @@ -20,9 +21,13 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Engine\Channel; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; +use Hypervel\Support\Facades\Date; +use Hypervel\Support\Sleep; use Hypervel\Testbench\TestCase; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use ReflectionMethod; use ReflectionProperty; use RuntimeException; @@ -140,7 +145,7 @@ public function testBackgroundTaskStillDispatchesBackgroundFinishedOnFailure() $this->assertSame($exception, $this->dispatched[1]->exception); } - public function testSkippedNonRepeatableTaskIsOnlyEvaluatedOncePerMinute() + public function testSkippedNonRepeatableTaskIsOnlyEvaluatedOncePerMinute(): void { $eventMutex = m::mock(EventMutex::class); @@ -150,10 +155,10 @@ public function testSkippedNonRepeatableTaskIsOnlyEvaluatedOncePerMinute() $callbackEvent->when(false); $command = $this->makeCommand(); - $startedAt = Carbon::parse('2026-05-28 12:34:00'); + $startedAt = CarbonImmutable::parse('2026-05-28 12:34:00'); $this->invokeRunEvents($command, [$callbackEvent], $startedAt); - $this->invokeRunEvents($command, [$callbackEvent], $startedAt->copy()->addSeconds(30)); + $this->invokeRunEvents($command, [$callbackEvent], $startedAt->addSeconds(30)); $this->assertCount(1, $this->dispatched); $this->assertInstanceOf(ScheduledTaskSkipped::class, $this->dispatched[0]); @@ -233,7 +238,7 @@ public function testTaskMarkedEvenWhenPausedRunsWhileSchedulerIsPaused() $this->assertInstanceOf(ScheduledTaskFinished::class, $this->dispatched[1]); } - public function testPausedNonRepeatableTaskIsOnlyEvaluatedOncePerMinute() + public function testPausedNonRepeatableTaskIsOnlyEvaluatedOncePerMinute(): void { $eventMutex = m::mock(EventMutex::class); @@ -248,17 +253,17 @@ public function testPausedNonRepeatableTaskIsOnlyEvaluatedOncePerMinute() ->andReturnTrue(); $command = $this->makeCommand($cache); - $startedAt = Carbon::parse('2026-05-28 12:34:00'); + $startedAt = CarbonImmutable::parse('2026-05-28 12:34:00'); $this->invokeRunEvents($command, [$callbackEvent], $startedAt); - $this->invokeRunEvents($command, [$callbackEvent], $startedAt->copy()->addSeconds(30)); + $this->invokeRunEvents($command, [$callbackEvent], $startedAt->addSeconds(30)); $this->assertCount(1, $this->dispatched); $this->assertInstanceOf(ScheduledTaskSkipped::class, $this->dispatched[0]); $this->assertSame($callbackEvent, $this->dispatched[0]->task); } - public function testNonRepeatableEventOnlyRunsOncePerMinute() + public function testNonRepeatableEventOnlyRunsOncePerMinute(): void { $runCount = 0; @@ -272,16 +277,16 @@ public function testNonRepeatableEventOnlyRunsOncePerMinute() }); $command = $this->makeCommand(); - $startedAt = Carbon::parse('2026-05-28 12:34:00'); + $startedAt = CarbonImmutable::parse('2026-05-28 12:34:00'); $this->invokeRunEvents($command, [$callbackEvent], $startedAt); $this->assertSame(1, $runCount); $this->assertNotNull($callbackEvent->lastChecked); - $this->invokeRunEvents($command, [$callbackEvent], $startedAt->copy()->addSeconds(30)); + $this->invokeRunEvents($command, [$callbackEvent], $startedAt->addSeconds(30)); $this->assertSame(1, $runCount); - $this->invokeRunEvents($command, [$callbackEvent], $startedAt->copy()->addMinute()); + $this->invokeRunEvents($command, [$callbackEvent], $startedAt->addMinute()); $this->assertSame(2, $runCount); } @@ -312,6 +317,58 @@ public function testRepeatableEventIsThrottledByLastChecked() $this->assertSame(1, $runCount); } + #[DataProvider('dateClassProvider')] + public function testRepeatEventsPreservesOriginalStartForSingleServerMutex(string $dateClass): void + { + Date::use($dateClass); + Date::setTestNow('2026-05-28 12:34:00'); + Sleep::fake(); + + $startedAt = Date::now()->startOfMinute(); + $expectedStartedAt = $startedAt->format('Y-m-d H:i:s'); + $eventMutex = m::mock(EventMutex::class); + $callbackEvent = new CallbackEvent($eventMutex, function () use ($startedAt): int { + Date::setTestNow($startedAt->copy()->addMinute()); + + return 0; + }); + $callbackEvent->name('single-server-repeat')->onOneServer(); + $callbackEvent->repeatSeconds = 1; + $callbackEvent->lastChecked = $startedAt->copy()->subSecond(); + + $schedule = m::mock(Schedule::class); + $schedule->shouldReceive('serverShouldRun') + ->once() + ->withArgs(function (Event $event, CarbonInterface $time) use ($callbackEvent, $expectedStartedAt): bool { + $this->assertSame($callbackEvent, $event); + $this->assertSame($expectedStartedAt, $time->format('Y-m-d H:i:s')); + + return true; + }) + ->andReturnTrue(); + + $command = $this->makeCommand(); + (new ReflectionProperty($command, 'schedule'))->setValue($command, $schedule); + (new ReflectionProperty($command, 'startedAt'))->setValue($command, $startedAt); + + $this->invokeRepeatEvents($command, [$callbackEvent]); + + $this->assertSame($dateClass, $startedAt::class); + $this->assertSame($expectedStartedAt, $startedAt->format('Y-m-d H:i:s')); + Sleep::assertSleptTimes(1); + } + + /** + * Provide mutable and immutable Date factory classes. + */ + public static function dateClassProvider(): array + { + return [ + 'immutable default' => [CarbonImmutable::class], + 'mutable opt-out' => [Carbon::class], + ]; + } + public function testConcurrentFinishesUseRunLocalExitCodeForSuccessAndFailureCallbacks() { $eventMutex = m::mock(EventMutex::class); @@ -449,10 +506,19 @@ protected function makeCommand(?Cache $cache = null): ScheduleRunCommand /** * Invoke the protected runEvents method. */ - protected function invokeRunEvents(ScheduleRunCommand $command, array $events, ?Carbon $startedAt = null): void + protected function invokeRunEvents(ScheduleRunCommand $command, array $events, ?CarbonInterface $startedAt = null): void { $method = new ReflectionMethod($command, 'runEvents'); - $method->invoke($command, new Collection($events), $startedAt ?? Carbon::now()); + $method->invoke($command, new Collection($events), $startedAt ?? Date::now()); + } + + /** + * Invoke the protected repeatEvents method. + */ + protected function invokeRepeatEvents(ScheduleRunCommand $command, array $events): void + { + $method = new ReflectionMethod($command, 'repeatEvents'); + $method->invoke($command, new Collection($events)); } /** diff --git a/tests/Console/Scheduling/ScheduleRunContextPropagationTest.php b/tests/Console/Scheduling/ScheduleRunContextPropagationTest.php index 791c150f8..ef7c05dfd 100644 --- a/tests/Console/Scheduling/ScheduleRunContextPropagationTest.php +++ b/tests/Console/Scheduling/ScheduleRunContextPropagationTest.php @@ -16,7 +16,7 @@ use Hypervel\Coroutine\Concurrent; use Hypervel\Engine\Channel; use Hypervel\Log\Context\Repository as ContextRepository; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Testbench\TestCase; use Mockery as m; @@ -189,6 +189,6 @@ protected function makeCommand(): ScheduleRunCommand protected function invokeRunEvents(ScheduleRunCommand $command, array $events): void { $method = new ReflectionMethod($command, 'runEvents'); - $method->invoke($command, new Collection($events), Carbon::now()); + $method->invoke($command, new Collection($events), CarbonImmutable::now()); } } diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index c87248e68..1af092a7e 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -17,7 +17,7 @@ use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Foundation\Application; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; use Mockery\MockInterface; @@ -112,15 +112,15 @@ public function testDueEventsAtUsesGivenTime() $app->shouldReceive('environment')->andReturn('production'); try { - Carbon::setTestNow(Carbon::parse('2026-05-29 13:00:00')); + CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-05-29 13:00:00')); $schedule = new Schedule; $schedule->command('reports:generate')->dailyAt('13:00'); - self::assertCount(0, $schedule->dueEventsAt($app, Carbon::parse('2026-05-29 12:59:59'))); - self::assertCount(1, $schedule->dueEventsAt($app, Carbon::parse('2026-05-29 13:00:00'))); + self::assertCount(0, $schedule->dueEventsAt($app, CarbonImmutable::parse('2026-05-29 12:59:59'))); + self::assertCount(1, $schedule->dueEventsAt($app, CarbonImmutable::parse('2026-05-29 13:00:00'))); } finally { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } } diff --git a/tests/Integration/Console/CommandSchedulingTest.php b/tests/Integration/Console/CommandSchedulingTest.php index cf27a60a6..d7feb73e0 100644 --- a/tests/Integration/Console/CommandSchedulingTest.php +++ b/tests/Integration/Console/CommandSchedulingTest.php @@ -14,7 +14,7 @@ use Hypervel\Console\Scheduling\SchedulingMutex; use Hypervel\Container\Container; use Hypervel\Contracts\Cache\Factory; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Artisan; use Hypervel\Testbench\TestCase; use UnitEnum; @@ -25,7 +25,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow(now()->startOfYear()); + CarbonImmutable::setTestNow(now()->startOfYear()); CommandSchedulingTestCommand::$log = []; diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index a8c7bf26f..9df94dc45 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -4,10 +4,11 @@ namespace Hypervel\Tests\Integration\Console\Scheduling\ScheduleGroupTest; +use Carbon\CarbonInterface; use Hypervel\Console\Scheduling\Event; use Hypervel\Console\Scheduling\Schedule as ScheduleClass; use Hypervel\Contracts\Queue\ShouldQueue; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schedule; use Hypervel\Testbench\TestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -162,9 +163,9 @@ public static function groupAttributes(): array } #[DataProvider('scheduleTestCases')] - public function testGroupedScheduleExecution($time, $expected, $description) + public function testGroupedScheduleExecution(CarbonInterface $time, array $expected, string $description): void { - Carbon::setTestNow($time); + CarbonImmutable::setTestNow($time); $app = app(); Schedule::days([1, 2, 3, 4, 5, 6])->group(function () { @@ -188,11 +189,11 @@ public function testGroupedScheduleExecution($time, $expected, $description) } } - public static function scheduleTestCases() + public static function scheduleTestCases(): array { return [ [ - Carbon::create(2024, 1, 1, 7, 30), + CarbonImmutable::create(2024, 1, 1, 7, 30), [ 'Task 1' => true, 'Task 2' => true, @@ -201,7 +202,7 @@ public static function scheduleTestCases() 'Tasks at 07:30', ], [ - Carbon::create(2024, 1, 1, 8, 5), + CarbonImmutable::create(2024, 1, 1, 8, 5), [ 'Task 1' => false, 'Task 2' => false, diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 14f922640..304f96b7a 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -7,7 +7,7 @@ use Hypervel\Console\Command; use Hypervel\Console\Commands\ScheduleListCommand; use Hypervel\Console\Scheduling\Schedule; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Artisan; use Hypervel\Support\ProcessUtils; use Hypervel\Testbench\TestCase; @@ -20,7 +20,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2023-01-01'); + CarbonImmutable::setTestNow('2023-01-01'); ScheduleListCommand::resolveTerminalWidthUsing(fn () => 80); $this->schedule = $this->app->make(Schedule::class); diff --git a/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php index e42fbe0d3..0294e4c9e 100644 --- a/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php @@ -7,7 +7,7 @@ use Hypervel\Console\Command; use Hypervel\Console\Commands\ScheduleTestCommand; use Hypervel\Console\Scheduling\Schedule; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Artisan; use Hypervel\Testbench\TestCase; @@ -19,7 +19,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow(now()->startOfYear()); + CarbonImmutable::setTestNow(now()->startOfYear()); $this->schedule = $this->app->make(Schedule::class); diff --git a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php index 0f40487cc..7c1378ab4 100644 --- a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php +++ b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php @@ -14,7 +14,7 @@ use Hypervel\Container\Container; use Hypervel\Contracts\Cache\Factory; use Hypervel\Contracts\Cache\Repository as CacheRepository; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Sleep; use Hypervel\Testbench\TestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -55,9 +55,9 @@ public function store(UnitEnum|string|null $name = null): Repository $this->schedule = $this->app->make(Schedule::class); } - public function testItDoesntWaitForSubMinuteEventsWhenNothingIsScheduled() + public function testItDoesntWaitForSubMinuteEventsWhenNothingIsScheduled(): void { - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); Sleep::fake(); $this->artisan('schedule:run', ['--once' => true]) @@ -66,13 +66,13 @@ public function testItDoesntWaitForSubMinuteEventsWhenNothingIsScheduled() Sleep::assertNeverSlept(); } - public function testItDoesntWaitForSubMinuteEventsWhenNoneAreScheduled() + public function testItDoesntWaitForSubMinuteEventsWhenNoneAreScheduled(): void { $this->schedule ->call(fn () => true) ->everyMinute(); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); Sleep::fake(); $this->artisan('schedule:run', ['--once' => true]) @@ -82,16 +82,16 @@ public function testItDoesntWaitForSubMinuteEventsWhenNoneAreScheduled() } #[DataProvider('frequencyProvider')] - public function testItRunsSubMinuteCallbacks(string $frequency, int $expectedRuns) + public function testItRunsSubMinuteCallbacks(string $frequency, int $expectedRuns): void { $runs = 0; $this->schedule->call(function () use (&$runs) { ++$runs; })->{$frequency}(); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); Sleep::fake(); - Sleep::whenFakingSleep(fn ($duration) => Carbon::setTestNow(now()->add($duration))); + Sleep::whenFakingSleep(fn ($duration) => CarbonImmutable::setTestNow(now()->add($duration))); $this->artisan('schedule:run', ['--once' => true]) ->expectsOutputToContain('Running [Callback]'); @@ -113,7 +113,7 @@ public static function frequencyProvider(): array ]; } - public function testItRunsMultipleSubMinuteCallbacks() + public function testItRunsMultipleSubMinuteCallbacks(): void { $everySecondRuns = 0; $this->schedule->call(function () use (&$everySecondRuns) { @@ -125,9 +125,9 @@ public function testItRunsMultipleSubMinuteCallbacks() ++$everyThirtySecondsRuns; })->everyThirtySeconds(); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); Sleep::fake(); - Sleep::whenFakingSleep(fn ($duration) => Carbon::setTestNow(now()->add($duration))); + Sleep::whenFakingSleep(fn ($duration) => CarbonImmutable::setTestNow(now()->add($duration))); $this->artisan('schedule:run', ['--once' => true]) ->expectsOutputToContain('Running [Callback]'); @@ -137,18 +137,18 @@ public function testItRunsMultipleSubMinuteCallbacks() $this->assertEquals(2, $everyThirtySecondsRuns); } - public function testSubMinuteSchedulingCanBeInterrupted() + public function testSubMinuteSchedulingCanBeInterrupted(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { ++$runs; })->everySecond(); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); $startedAt = now(); Sleep::fake(); Sleep::whenFakingSleep(function ($duration) use ($startedAt) { - Carbon::setTestNow(now()->add($duration)); + CarbonImmutable::setTestNow(now()->add($duration)); if ($startedAt->diffInSeconds() >= 30) { $this->artisan('schedule:interrupt') @@ -164,7 +164,7 @@ public function testSubMinuteSchedulingCanBeInterrupted() $this->assertEquals(30, $startedAt->diffInSeconds(now())); } - public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceMaintenanceModeIsEnabled() + public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceMaintenanceModeIsEnabled(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { @@ -173,11 +173,11 @@ public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceMaintenanceModeI config(['app.maintenance.driver' => 'cache']); config(['app.maintenance.store' => 'array']); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); $startedAt = now(); Sleep::fake(); Sleep::whenFakingSleep(function ($duration) use ($startedAt) { - Carbon::setTestNow(now()->add($duration)); + CarbonImmutable::setTestNow(now()->add($duration)); if ($startedAt->diffInSeconds() >= 30 && ! $this->app->isDownForMaintenance()) { $this->artisan('down'); @@ -195,7 +195,7 @@ public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceMaintenanceModeI $this->assertEquals(30, $runs); } - public function testSubMinuteEventsCanBeRunInMaintenanceMode() + public function testSubMinuteEventsCanBeRunInMaintenanceMode(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { @@ -204,11 +204,11 @@ public function testSubMinuteEventsCanBeRunInMaintenanceMode() config(['app.maintenance.driver' => 'cache']); config(['app.maintenance.store' => 'array']); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); $startedAt = now(); Sleep::fake(); Sleep::whenFakingSleep(function ($duration) use ($startedAt) { - Carbon::setTestNow(now()->add($duration)); + CarbonImmutable::setTestNow(now()->add($duration)); if (now()->diffInSeconds($startedAt) >= 30 && ! $this->app->isDownForMaintenance()) { $this->artisan('down'); @@ -222,19 +222,19 @@ public function testSubMinuteEventsCanBeRunInMaintenanceMode() $this->assertEquals(60, $runs); } - public function testSubMinuteEventsCanBeRunWhenScheduleIsPaused() + public function testSubMinuteEventsCanBeRunWhenScheduleIsPaused(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { ++$runs; })->everySecond()->evenWhenPaused(); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); $startedAt = now(); $cache = $this->app->make(CacheRepository::class); Sleep::fake(); Sleep::whenFakingSleep(function ($duration) use ($startedAt, $cache) { - Carbon::setTestNow(now()->add($duration)); + CarbonImmutable::setTestNow(now()->add($duration)); if ($startedAt->diffInSeconds() >= 30 && ! $cache->get('hypervel:schedule:paused', false)) { $this->artisan('schedule:pause') @@ -249,19 +249,19 @@ public function testSubMinuteEventsCanBeRunWhenScheduleIsPaused() $this->assertEquals(60, $runs); } - public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceScheduleIsPaused() + public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceScheduleIsPaused(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { ++$runs; })->everySecond(); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); $startedAt = now(); $cache = $this->app->make(CacheRepository::class); Sleep::fake(); Sleep::whenFakingSleep(function ($duration) use ($startedAt, $cache) { - Carbon::setTestNow(now()->add($duration)); + CarbonImmutable::setTestNow(now()->add($duration)); if ($startedAt->diffInSeconds() >= 30 && ! $cache->get('hypervel:schedule:paused', false)) { $this->artisan('schedule:pause') @@ -276,16 +276,16 @@ public function testSubMinuteEventsStopForTheRestOfTheMinuteOnceScheduleIsPaused $this->assertEquals(30, $runs); } - public function testSubMinuteSchedulingRespectsFilters() + public function testSubMinuteSchedulingRespectsFilters(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { ++$runs; })->everySecond()->when(fn () => now()->second % 2 === 0); - Carbon::setTestNow(now()->startOfMinute()); + CarbonImmutable::setTestNow(now()->startOfMinute()); Sleep::fake(); - Sleep::whenFakingSleep(fn ($duration) => Carbon::setTestNow(now()->add($duration))); + Sleep::whenFakingSleep(fn ($duration) => CarbonImmutable::setTestNow(now()->add($duration))); $this->artisan('schedule:run', ['--once' => true]) ->expectsOutputToContain('Running [Callback]'); @@ -294,7 +294,7 @@ public function testSubMinuteSchedulingRespectsFilters() $this->assertEquals(30, $runs); } - public function testSubMinuteSchedulingCanRunOnOneServer() + public function testSubMinuteSchedulingCanRunOnOneServer(): void { $runs = 0; $this->schedule->call(function () use (&$runs) { @@ -302,9 +302,9 @@ public function testSubMinuteSchedulingCanRunOnOneServer() })->everySecond()->name('test')->onOneServer(); $startedAt = now()->startOfMinute(); - Carbon::setTestNow($startedAt); + CarbonImmutable::setTestNow($startedAt); Sleep::fake(); - Sleep::whenFakingSleep(fn ($duration) => Carbon::setTestNow(now()->add($duration))); + Sleep::whenFakingSleep(fn ($duration) => CarbonImmutable::setTestNow(now()->add($duration))); $this->app->instance(Schedule::class, clone $this->schedule); $this->artisan('schedule:run', ['--once' => true]) @@ -314,7 +314,7 @@ public function testSubMinuteSchedulingCanRunOnOneServer() $this->assertEquals(60, $runs); // Fake a second server running at the same minute. - Carbon::setTestNow($startedAt); + CarbonImmutable::setTestNow($startedAt); $this->app->instance(Schedule::class, clone $this->schedule); $this->artisan('schedule:run', ['--once' => true]) diff --git a/tests/Integration/Foundation/Configuration/WithScheduleTest.php b/tests/Integration/Foundation/Configuration/WithScheduleTest.php index 9deae840a..63d2e780e 100644 --- a/tests/Integration/Foundation/Configuration/WithScheduleTest.php +++ b/tests/Integration/Foundation/Configuration/WithScheduleTest.php @@ -7,7 +7,7 @@ use Hypervel\Console\Commands\ScheduleListCommand; use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Foundation\Application; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; class WithScheduleTest extends TestCase @@ -16,7 +16,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2023-01-01'); + CarbonImmutable::setTestNow('2023-01-01'); ScheduleListCommand::resolveTerminalWidthUsing(fn () => 80); } From 4989a4ac08019ff6405bdec3765db1aa72fc97ba Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:15:58 +0000 Subject: [PATCH 11/34] feat(database): make Eloquent dates immutable by default Create Eloquent date casts, date query boundaries, and factory timestamps through the immutable Hypervel date model while preserving DateFactory configurability for ordinary date and datetime casts. Return the canonical Hypervel immutable class for explicit immutable casts, capture modifier results, retain intentional mutable serialization conversions, and parse formatted values through Carbon rather than incomplete format preflight helpers. Migrate database unit and integration expectations across model casts, timestamps, soft deletes, relations, factories, query builders, locks, cache storage, and all supported database drivers. --- .../src/Concerns/BuildsWhereDateClauses.php | 6 +- .../src/Eloquent/Concerns/HasAttributes.php | 15 +- .../src/Eloquent/Factories/Factory.php | 4 +- ...EloquentBelongsToManyCreateOrFirstTest.php | 4 +- ...uentBelongsToManySyncTouchesParentTest.php | 6 +- ...tabaseEloquentBuilderCreateOrFirstTest.php | 4 +- .../Database/DatabaseEloquentBuilderTest.php | 19 +- .../Database/DatabaseEloquentFactoryTest.php | 22 +- ...tabaseEloquentHasManyCreateOrFirstTest.php | 4 +- ...loquentHasManyThroughCreateOrFirstTest.php | 6 +- .../DatabaseEloquentIntegrationTest.php | 97 +++--- .../DatabaseEloquentIrregularPluralTest.php | 6 +- tests/Database/DatabaseEloquentModelTest.php | 57 ++-- .../Database/DatabaseEloquentRelationTest.php | 6 +- ...baseEloquentSoftDeletesIntegrationTest.php | 10 +- .../DatabaseEloquentTimestampsTest.php | 16 +- tests/Database/DatabaseQueryBuilderTest.php | 22 +- tests/Database/DatabaseSoftDeletingTest.php | 8 +- .../DatabaseSoftDeletingTraitTest.php | 10 +- .../Eloquent/Concerns/DateFactoryTest.php | 307 +++--------------- .../Eloquent/Factories/FactoryTest.php | 18 +- tests/Database/QueryDurationThresholdTest.php | 9 +- .../Database/DatabaseCacheStoreTest.php | 6 +- ...abaseEloquentModelAttributeCastingTest.php | 4 +- .../Integration/Database/DatabaseLockTest.php | 8 +- .../Database/Eloquent/CastsTest.php | 21 +- .../Database/EloquentBelongsToManyTest.php | 24 +- .../EloquentEagerLoadingLimitTest.php | 42 +-- .../Database/EloquentModelDateCastingTest.php | 21 +- .../EloquentModelImmutableDateCastingTest.php | 29 +- .../Database/EloquentModelTest.php | 4 +- .../Database/EloquentMorphManyTest.php | 8 +- .../Database/MariaDb/EloquentCastTest.php | 38 +-- .../Database/MySql/EloquentCastTest.php | 38 +-- .../Integration/Database/QueryBuilderTest.php | 30 +- 35 files changed, 365 insertions(+), 564 deletions(-) diff --git a/src/database/src/Concerns/BuildsWhereDateClauses.php b/src/database/src/Concerns/BuildsWhereDateClauses.php index f3702a2a6..385ec5fc1 100644 --- a/src/database/src/Concerns/BuildsWhereDateClauses.php +++ b/src/database/src/Concerns/BuildsWhereDateClauses.php @@ -4,8 +4,8 @@ namespace Hypervel\Database\Concerns; -use Carbon\Carbon; use Hypervel\Support\Arr; +use Hypervel\Support\CarbonImmutable; trait BuildsWhereDateClauses { @@ -97,7 +97,7 @@ public function orWhereNowOrFuture(array|string $columns): static protected function wherePastOrFuture(array|string $columns, string $operator, string $boolean): static { $type = 'Basic'; - $value = Carbon::now(); + $value = CarbonImmutable::now(); foreach (Arr::wrap($columns) as $column) { $this->wheres[] = compact('type', 'column', 'boolean', 'operator', 'value'); @@ -215,7 +215,7 @@ public function orWhereTodayOrAfter(array|string $columns): static */ protected function whereTodayBeforeOrAfter(array|string $columns, string $operator, string $boolean): static { - $value = Carbon::today()->format('Y-m-d'); + $value = CarbonImmutable::today()->format('Y-m-d'); foreach (Arr::wrap($columns) as $column) { $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); diff --git a/src/database/src/Eloquent/Concerns/HasAttributes.php b/src/database/src/Eloquent/Concerns/HasAttributes.php index 44a3ffce9..6745dc3fb 100644 --- a/src/database/src/Eloquent/Concerns/HasAttributes.php +++ b/src/database/src/Eloquent/Concerns/HasAttributes.php @@ -8,8 +8,8 @@ use Brick\Math\BigDecimal; use Brick\Math\Exception\MathException as BrickMathException; use Brick\Math\RoundingMode; -use Carbon\CarbonImmutable; use Carbon\CarbonInterface; +use Carbon\Exceptions\InvalidFormatException; use DateTimeImmutable; use DateTimeInterface; use Hypervel\Contracts\Database\Eloquent\Castable; @@ -35,6 +35,7 @@ use Hypervel\Database\LazyLoadingViolationException; use Hypervel\Support\Arr; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Exceptions\MathException; @@ -1397,8 +1398,6 @@ protected function asDecimal(float|string $value, int $decimals): string /** * Return a timestamp as DateTime object with time set to 00:00:00. - * - * @return \Hypervel\Support\Carbon */ protected function asDate(mixed $value): CarbonInterface { @@ -1407,8 +1406,6 @@ protected function asDate(mixed $value): CarbonInterface /** * Return a timestamp as DateTime object. - * - * @return \Hypervel\Support\Carbon */ protected function asDateTime(mixed $value): CarbonInterface { @@ -1440,7 +1437,7 @@ protected function asDateTime(mixed $value): CarbonInterface // Carbon instances from that format. Again, this provides for simple date // fields on the database, while still supporting Carbonized conversion. if ($this->isStandardDateFormat($value)) { - return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay()); + return Date::parse($value)->startOfDay(); } $format = $this->getDateFormat(); @@ -1450,12 +1447,12 @@ protected function asDateTime(mixed $value): CarbonInterface // that is returned back out to the developers after we convert it here. try { $date = Date::createFromFormat($format, $value); - // @phpstan-ignore catch.neverThrown (defensive: some Carbon versions/configs may throw) - } catch (InvalidArgumentException) { + // @phpstan-ignore catch.neverThrown (the Date facade's magic dispatch hides Carbon's @throws from analysis) + } catch (InvalidFormatException) { $date = null; } - return $date ?: Date::parse($value); + return $date ?? Date::parse($value); } /** diff --git a/src/database/src/Eloquent/Factories/Factory.php b/src/database/src/Eloquent/Factories/Factory.php index 65403f137..85e21b46f 100644 --- a/src/database/src/Eloquent/Factories/Factory.php +++ b/src/database/src/Eloquent/Factories/Factory.php @@ -11,7 +11,7 @@ use Hypervel\Database\Eloquent\Collection as EloquentCollection; use Hypervel\Database\Eloquent\Factories\Attributes\UseModel; use Hypervel\Database\Eloquent\Model; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Enumerable; use Hypervel\Support\Str; @@ -1013,7 +1013,7 @@ public function __call(string $method, array $parameters): mixed if ($method === 'trashed' && $this->modelName()::isSoftDeletable()) { return $this->state([ - $this->newModel()->getDeletedAtColumn() => $parameters[0] ?? Carbon::now()->subDay(), + $this->newModel()->getDeletedAtColumn() => $parameters[0] ?? CarbonImmutable::now()->subDay(), ]); } diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php index 19e02aff8..eecf74e6c 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -12,7 +12,7 @@ use Hypervel\Database\Eloquent\Relations\BelongsToMany; use Hypervel\Database\Query\Builder as BaseBuilder; use Hypervel\Database\UniqueConstraintViolationException; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; use PDO; @@ -23,7 +23,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2023-01-01 00:00:00'); + CarbonImmutable::setTestNow('2023-01-01 00:00:00'); } public function testCreateOrFirstMethodCreatesNewRelated(): void diff --git a/tests/Database/DatabaseEloquentBelongsToManySyncTouchesParentTest.php b/tests/Database/DatabaseEloquentBelongsToManySyncTouchesParentTest.php index 71b6bebeb..1d3384e15 100644 --- a/tests/Database/DatabaseEloquentBelongsToManySyncTouchesParentTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManySyncTouchesParentTest.php @@ -7,7 +7,7 @@ use Hypervel\Database\Capsule\Manager as DB; use Hypervel\Database\Eloquent\Model as Eloquent; use Hypervel\Database\Eloquent\Relations\Pivot as EloquentPivot; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; class DatabaseEloquentBelongsToManySyncTouchesParentTest extends TestCase @@ -81,12 +81,12 @@ public function testSyncWithDetachedValuesShouldTouch() { $this->seedData(); - Carbon::setTestNow('2021-07-19 10:13:14'); + CarbonImmutable::setTestNow('2021-07-19 10:13:14'); $article = Article::create(['id' => 1, 'title' => 'uuid title']); $article->users()->sync([1, 2, 3]); $this->assertSame('2021-07-19 10:13:14', $article->updated_at->format('Y-m-d H:i:s')); - Carbon::setTestNow('2021-07-20 19:13:14'); + CarbonImmutable::setTestNow('2021-07-20 19:13:14'); $result = $article->users()->sync([1, 2]); $this->assertCount(1, collect($result['detached'])); $this->assertSame('3', (string) collect($result['detached'])->first()); diff --git a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php index 7651e0ef6..b66c8463a 100755 --- a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php @@ -11,7 +11,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Database\Query\Expression; use Hypervel\Database\UniqueConstraintViolationException; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; use PDO; @@ -22,7 +22,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2023-01-01 00:00:00'); + CarbonImmutable::setTestNow('2023-01-01 00:00:00'); } public function testCreateOrFirstMethodCreatesNewRecord(): void diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index ae21016ab..0178bf09a 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -20,7 +20,7 @@ use Hypervel\Database\Query\Expression; use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Processors\Processor; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Testbench\TestCase; use Mockery as m; @@ -2555,7 +2555,7 @@ public function testOldestWithColumn() public function testUpdate() { - Carbon::setTestNow($now = '2017-10-10 10:10:10'); + CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); @@ -2621,7 +2621,7 @@ public function testUpdateWithoutTimestamp() public function testUpdateWithAlias() { - Carbon::setTestNow($now = '2017-10-10 10:10:10'); + CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); @@ -2639,7 +2639,7 @@ public function testUpdateWithAlias() public function testUpdateWithAliasWithQualifiedTimestampValue() { - Carbon::setTestNow($now = '2017-10-10 10:10:10'); + CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); @@ -2654,12 +2654,12 @@ public function testUpdateWithAliasWithQualifiedTimestampValue() $result = $builder->from('table as alias')->update(['foo' => 'bar', 'alias.updated_at' => null]); $this->assertEquals(1, $result); - Carbon::setTestNow(null); + CarbonImmutable::setTestNow(null); } public function testUpsert() { - Carbon::setTestNow($now = '2017-10-10 10:10:10'); + CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); $query = m::mock(BaseBuilder::class); $query->shouldReceive('from')->with('foo_table')->andReturnSelf(); @@ -2682,7 +2682,7 @@ public function testUpsert() public function testTouch() { - Carbon::setTestNow($now = '2017-10-10 10:10:10'); + CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); $query = m::mock(BaseBuilder::class); $query->shouldReceive('from')->with('foo_table')->andReturnSelf(); @@ -2701,7 +2701,7 @@ public function testTouch() public function testTouchWithCustomColumn() { - Carbon::setTestNow($now = '2017-10-10 10:10:10'); + CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); $query = m::mock(BaseBuilder::class); $query->shouldReceive('from')->with('foo_table')->andReturnSelf(); @@ -2984,8 +2984,7 @@ public function __construct(array $attributes) protected function asDateTime(mixed $value): \Carbon\CarbonInterface { - // Return a mock Carbon that stringifies to 'date_' prefix for test assertion - return Carbon::parse('date_' . $value); + return CarbonImmutable::parse('date_' . $value); } } diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index d9f7fb858..5ae0a328c 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -5,7 +5,6 @@ namespace Hypervel\Tests\Database\DatabaseEloquentFactoryTest; use BadMethodCallException; -use Carbon\Carbon; use Faker\Generator; use Hypervel\Container\Container; use Hypervel\Contracts\Foundation\Application; @@ -20,6 +19,7 @@ use Hypervel\Database\Eloquent\Model as Eloquent; use Hypervel\Database\Eloquent\Relations\Pivot; use Hypervel\Database\Eloquent\SoftDeletes; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Tests\Database\Fixtures\Models\Money\Price; use Hypervel\Tests\TestCase; @@ -827,29 +827,29 @@ public function testFactoryCanConditionallyExecuteCode() public function testDynamicTrashedStateForSoftdeletesModels() { - $now = Carbon::create(2020, 6, 7, 8, 9); - Carbon::setTestNow($now); + $now = CarbonImmutable::create(2020, 6, 7, 8, 9); + CarbonImmutable::setTestNow($now); $post = PostFactory::new()->trashed()->create(); $this->assertTrue($post->deleted_at->equalTo($now->subDay())); - $deleted_at = Carbon::create(2020, 1, 2, 3, 4, 5); + $deleted_at = CarbonImmutable::create(2020, 1, 2, 3, 4, 5); $post = PostFactory::new()->trashed($deleted_at)->create(); $this->assertTrue($deleted_at->equalTo($post->deleted_at)); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testDynamicTrashedStateRespectsExistingState() { - $now = Carbon::create(2020, 6, 7, 8, 9); - Carbon::setTestNow($now); + $now = CarbonImmutable::create(2020, 6, 7, 8, 9); + CarbonImmutable::setTestNow($now); $comment = CommentFactory::new()->trashed()->create(); $this->assertTrue($comment->deleted_at->equalTo($now->subWeek())); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testDynamicTrashedStateThrowsExceptionWhenNotASoftdeletesModel() @@ -1146,8 +1146,8 @@ public function testFactoryCanInsertWithArrayCasts() $users = DB::table('users')->get(); foreach ($users as $user) { $this->assertEquals(['rtj'], json_decode($user->options, true)); - $createdAt = Carbon::parse($user->created_at); - $updatedAt = Carbon::parse($user->updated_at); + $createdAt = CarbonImmutable::parse($user->created_at); + $updatedAt = CarbonImmutable::parse($user->updated_at); $this->assertEquals($updatedAt, $createdAt); } } @@ -1303,7 +1303,7 @@ public function definition(): array public function trashed() { return $this->state([ - 'deleted_at' => Carbon::now()->subWeek(), + 'deleted_at' => CarbonImmutable::now()->subWeek(), ]); } } diff --git a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php index 8382753ba..c61990d86 100755 --- a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php @@ -11,7 +11,7 @@ use Hypervel\Database\Eloquent\Relations\HasMany; use Hypervel\Database\Query\Builder; use Hypervel\Database\UniqueConstraintViolationException; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; use PDO; @@ -22,7 +22,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2023-01-01 00:00:00'); + CarbonImmutable::setTestNow('2023-01-01 00:00:00'); } public function testCreateOrFirstMethodCreatesNewRecord(): void diff --git a/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php index 7a3bd9b26..9e892283f 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php @@ -11,7 +11,7 @@ use Hypervel\Database\Eloquent\Relations\HasManyThrough; use Hypervel\Database\Query\Builder; use Hypervel\Database\UniqueConstraintViolationException; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; use PDO; @@ -22,12 +22,12 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2023-01-01 00:00:00'); + CarbonImmutable::setTestNow('2023-01-01 00:00:00'); } protected function tearDown(): void { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); parent::tearDown(); } diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index f5e0c466e..2354a84ff 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Database\DatabaseEloquentIntegrationTest; -use DateTime; use DateTimeInterface; use Exception; use Hypervel\Database\Capsule\Manager as DB; @@ -26,7 +25,7 @@ use Hypervel\Pagination\Cursor; use Hypervel\Pagination\CursorPaginator; use Hypervel\Pagination\LengthAwarePaginator; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; @@ -1286,8 +1285,8 @@ public function testHasOnSelfReferencingBelongsToRelationship() public function testAggregatedValuesOfDatetimeField() { User::insert([ - ['id' => 1, 'email' => 'test1@test.test', 'created_at' => '2016-08-10 09:21:00', 'updated_at' => Carbon::now()], - ['id' => 2, 'email' => 'test2@test.test', 'created_at' => '2016-08-01 12:00:00', 'updated_at' => Carbon::now()], + ['id' => 1, 'email' => 'test1@test.test', 'created_at' => '2016-08-10 09:21:00', 'updated_at' => CarbonImmutable::now()], + ['id' => 2, 'email' => 'test2@test.test', 'created_at' => '2016-08-01 12:00:00', 'updated_at' => CarbonImmutable::now()], ]); $this->assertSame('2016-08-10 09:21:00', User::max('created_at')); @@ -1998,10 +1997,10 @@ public function testIsAfterRetrievingTheSameModel() public function testFreshMethodOnModel() { - $now = Carbon::now()->startOfSecond(); + $now = CarbonImmutable::now()->startOfSecond(); $nowSerialized = $now->toJSON(); $nowWithFractionsSerialized = $now->toJSON(); - Carbon::setTestNow($now); + CarbonImmutable::setTestNow($now); $storedUser1 = User::create([ 'id' => 1, @@ -2159,7 +2158,7 @@ public function testTimestampsUsingOldSqlServerDateFormatFallbackToDefaultParsin $this->assertFalse(Date::hasFormat('2017-11-14 08:23:19.734', $model->getDateFormat())); } - public function testSpecialFormats() + public function testSpecialFormats(): void { $model = new User; $model->setDateFormat('!Y-d-m \Y'); @@ -2178,6 +2177,14 @@ public function testSpecialFormats() $date = $model->getAttribute('updated_at'); $this->assertSame('2020-09-11 00:00:00.000000', $date->format('Y-m-d H:i:s.u'), 'the date should respect the whole format'); + $model->setDateFormat('!Y-m-d+'); + $model->setRawAttributes([ + 'updated_at' => '2020-09-11 trailing data', + ]); + + $date = $model->getAttribute('updated_at'); + $this->assertSame('2020-09-11 00:00:00.000000', $date->format('Y-m-d H:i:s.u'), 'the date should allow trailing data when the format does'); + $model->setDateFormat('Y d m|*'); $model->setRawAttributes([ 'updated_at' => '2020 11 09 foo', @@ -2189,7 +2196,7 @@ public function testSpecialFormats() public function testUpdatingChildModelTouchesParent() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['name' => 'Parent Post', 'user_id' => 1]); @@ -2197,7 +2204,7 @@ public function testUpdatingChildModelTouchesParent() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); $post->update(['name' => 'Updated']); @@ -2207,7 +2214,7 @@ public function testUpdatingChildModelTouchesParent() public function testMultiLevelTouchingWorks() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2215,7 +2222,7 @@ public function testMultiLevelTouchingWorks() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingComment::create(['content' => 'Comment content', 'post_id' => 1]); @@ -2225,7 +2232,7 @@ public function testMultiLevelTouchingWorks() public function testDeletingChildModelTouchesParentTimestamps() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['name' => 'Parent Post', 'user_id' => 1]); @@ -2233,7 +2240,7 @@ public function testDeletingChildModelTouchesParentTimestamps() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); $post->delete(); @@ -2242,7 +2249,7 @@ public function testDeletingChildModelTouchesParentTimestamps() public function testTouchingChildModelUpdatesParentsTimestamps() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2250,7 +2257,7 @@ public function testTouchingChildModelUpdatesParentsTimestamps() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); $post->touch(); @@ -2260,7 +2267,7 @@ public function testTouchingChildModelUpdatesParentsTimestamps() public function testTouchingChildModelRespectsParentNoTouching() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2268,7 +2275,7 @@ public function testTouchingChildModelRespectsParentNoTouching() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingUser::withoutTouching(function () use ($post) { $post->touch(); @@ -2287,7 +2294,7 @@ public function testTouchingChildModelRespectsParentNoTouching() public function testUpdatingChildPostRespectsNoTouchingDefinition() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['name' => 'Parent Post', 'user_id' => 1]); @@ -2295,7 +2302,7 @@ public function testUpdatingChildPostRespectsNoTouchingDefinition() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingUser::withoutTouching(function () use ($post) { $post->update(['name' => 'Updated']); @@ -2307,7 +2314,7 @@ public function testUpdatingChildPostRespectsNoTouchingDefinition() public function testUpdatingModelInTheDisabledScopeTouchesItsOwnTimestamps() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['name' => 'Parent Post', 'user_id' => 1]); @@ -2315,7 +2322,7 @@ public function testUpdatingModelInTheDisabledScopeTouchesItsOwnTimestamps() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); Model::withoutTouching(function () use ($post) { $post->update(['name' => 'Updated']); @@ -2327,7 +2334,7 @@ public function testUpdatingModelInTheDisabledScopeTouchesItsOwnTimestamps() public function testDeletingChildModelRespectsTheNoTouchingRule() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['name' => 'Parent Post', 'user_id' => 1]); @@ -2335,7 +2342,7 @@ public function testDeletingChildModelRespectsTheNoTouchingRule() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingUser::withoutTouching(function () use ($post) { $post->delete(); @@ -2346,7 +2353,7 @@ public function testDeletingChildModelRespectsTheNoTouchingRule() public function testRespectedMultiLevelTouchingChain() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2354,7 +2361,7 @@ public function testRespectedMultiLevelTouchingChain() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingUser::withoutTouching(function () { TouchingComment::create(['content' => 'Comment content', 'post_id' => 1]); @@ -2366,7 +2373,7 @@ public function testRespectedMultiLevelTouchingChain() public function testTouchesGreatParentEvenWhenParentIsInNoTouchScope() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2374,7 +2381,7 @@ public function testTouchesGreatParentEvenWhenParentIsInNoTouchScope() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingPost::withoutTouching(function () { TouchingComment::create(['content' => 'Comment content', 'post_id' => 1]); @@ -2386,7 +2393,7 @@ public function testTouchesGreatParentEvenWhenParentIsInNoTouchScope() public function testCanNestCallsOfNoTouching() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2394,7 +2401,7 @@ public function testCanNestCallsOfNoTouching() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); TouchingUser::withoutTouching(function () { TouchingPost::withoutTouching(function () { @@ -2408,7 +2415,7 @@ public function testCanNestCallsOfNoTouching() public function testCanPassArrayOfModelsToIgnore() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $user = TouchingUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); $post = TouchingPost::create(['id' => 1, 'name' => 'Parent Post', 'user_id' => 1]); @@ -2416,7 +2423,7 @@ public function testCanPassArrayOfModelsToIgnore() $this->assertTrue($before->isSameDay($user->updated_at)); $this->assertTrue($before->isSameDay($post->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); Model::withoutTouchingOn([TouchingUser::class, TouchingPost::class], function () { TouchingComment::create(['content' => 'Comment content', 'post_id' => 1]); @@ -2506,7 +2513,7 @@ public function testMorphPivotsCanBeRefreshed() public function testTouchingChaperonedChildModelUpdatesParentTimestamps() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); $one = TouchingCategory::create(['id' => 1, 'name' => 'One']); $two = $one->children()->create(['id' => 2, 'name' => 'Two']); @@ -2514,7 +2521,7 @@ public function testTouchingChaperonedChildModelUpdatesParentTimestamps() $this->assertTrue($before->isSameDay($one->updated_at)); $this->assertTrue($before->isSameDay($two->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); $two->touch(); @@ -2524,7 +2531,7 @@ public function testTouchingChaperonedChildModelUpdatesParentTimestamps() public function testTouchingBiDirectionalChaperonedModelUpdatesAllRelatedTimestamps() { - $before = Carbon::now(); + $before = CarbonImmutable::now(); TouchingCategory::insert([ ['id' => 1, 'name' => 'One', 'parent_id' => null, 'created_at' => $before, 'updated_at' => $before], @@ -2542,7 +2549,7 @@ public function testTouchingBiDirectionalChaperonedModelUpdatesAllRelatedTimesta $this->assertTrue($before->isSameDay($three->updated_at)); $this->assertTrue($before->isSameDay($four->updated_at)); - Carbon::setTestNow($future = $before->copy()->addDays(3)); + CarbonImmutable::setTestNow($future = $before->addDays(3)); // Touch a random model and check that all of the others have been updated $models = tap([$one, $two, $three, $four], shuffle(...)); @@ -2559,15 +2566,15 @@ public function testTouchingBiDirectionalChaperonedModelUpdatesAllRelatedTimesta } } - public function testCanFillAndInsert() + public function testCanFillAndInsert(): void { DB::enableQueryLog(); - Carbon::setTestNow('2025-03-15T07:32:00Z'); + CarbonImmutable::setTestNow('2025-03-15T07:32:00Z'); $this->assertTrue(User::fillAndInsert([ ['email' => 'taylor@laravel.com', 'birthday' => null], - ['email' => 'nuno@laravel.com', 'birthday' => new Carbon('1980-01-01')], - ['email' => 'tim@laravel.com', 'birthday' => '1987-11-01', 'created_at' => '2025-01-02T02:00:55', 'updated_at' => Carbon::parse('2025-02-19T11:41:13')], + ['email' => 'nuno@laravel.com', 'birthday' => new CarbonImmutable('1980-01-01')], + ['email' => 'tim@laravel.com', 'birthday' => '1987-11-01', 'created_at' => '2025-01-02T02:00:55', 'updated_at' => CarbonImmutable::parse('2025-02-19T11:41:13')], ])); $this->assertCount(1, DB::getQueryLog()); @@ -2575,17 +2582,17 @@ public function testCanFillAndInsert() $this->assertCount(3, $users = User::get()); $users->take(2)->each(function (User $user) { - $this->assertEquals(Carbon::parse('2025-03-15T07:32:00Z'), $user->created_at); - $this->assertEquals(Carbon::parse('2025-03-15T07:32:00Z'), $user->updated_at); + $this->assertEquals(CarbonImmutable::parse('2025-03-15T07:32:00Z'), $user->created_at); + $this->assertEquals(CarbonImmutable::parse('2025-03-15T07:32:00Z'), $user->updated_at); }); $tim = $users->firstWhere('email', 'tim@laravel.com'); - $this->assertEquals(Carbon::parse('2025-01-02T02:00:55'), $tim->created_at); - $this->assertEquals(Carbon::parse('2025-02-19T11:41:13'), $tim->updated_at); + $this->assertEquals(CarbonImmutable::parse('2025-01-02T02:00:55'), $tim->created_at); + $this->assertEquals(CarbonImmutable::parse('2025-02-19T11:41:13'), $tim->updated_at); $this->assertNull($users[0]->birthday); - $this->assertInstanceOf(DateTime::class, $users[1]->birthday); - $this->assertInstanceOf(DateTime::class, $users[2]->birthday); + $this->assertSame(CarbonImmutable::class, $users[1]->birthday::class); + $this->assertSame(CarbonImmutable::class, $users[2]->birthday::class); $this->assertEquals('1987-11-01', $users[2]->birthday->format('Y-m-d')); DB::flushQueryLog(); diff --git a/tests/Database/DatabaseEloquentIrregularPluralTest.php b/tests/Database/DatabaseEloquentIrregularPluralTest.php index bec4a873a..4c6743f79 100644 --- a/tests/Database/DatabaseEloquentIrregularPluralTest.php +++ b/tests/Database/DatabaseEloquentIrregularPluralTest.php @@ -6,7 +6,7 @@ use Hypervel\Database\Capsule\Manager as DB; use Hypervel\Database\Eloquent\Model; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; class DatabaseEloquentIrregularPluralTest extends TestCase @@ -82,7 +82,7 @@ public function testItPluralizesTheTableName() public function testItTouchesTheParentWithAnIrregularPlural() { - Carbon::setTestNow('2018-05-01 12:13:14'); + CarbonImmutable::setTestNow('2018-05-01 12:13:14'); IrregularPluralHuman::create(['email' => 'taylorotwell@gmail.com']); @@ -94,7 +94,7 @@ public function testItTouchesTheParentWithAnIrregularPlural() $tokenIds = IrregularPluralToken::pluck('id'); - Carbon::setTestNow('2018-05-01 15:16:17'); + CarbonImmutable::setTestNow('2018-05-01 15:16:17'); $human->irregularPluralTokens()->sync($tokenIds); diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index a6bab0474..f85318a43 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Database\DatabaseEloquentModelTest; +use Carbon\CarbonInterface; use DateTime; use DateTimeImmutable; use DateTimeInterface; @@ -52,7 +53,7 @@ use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Processors\Processor; use Hypervel\Events\Dispatcher as EventDispatcher; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\ClassInvoker; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Fluent; @@ -937,7 +938,7 @@ public function testUpdateUsesOldPrimaryKey() $this->assertTrue($model->save()); } - public function testTimestampsAreReturnedAsObjects() + public function testTimestampsAreReturnedAsObjects(): void { $model = new class extends DateModelStub { public function getDateFormat(): string @@ -950,11 +951,11 @@ public function getDateFormat(): string 'updated_at' => '2012-12-05', ]); - $this->assertInstanceOf(Carbon::class, $model->created_at); - $this->assertInstanceOf(Carbon::class, $model->updated_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); + $this->assertSame(CarbonImmutable::class, $model->updated_at::class); } - public function testTimestampsAreReturnedAsObjectsFromPlainDatesAndTimestamps() + public function testTimestampsAreReturnedAsObjectsFromPlainDatesAndTimestamps(): void { $model = new class extends DateModelStub { public function getDateFormat(): string @@ -967,15 +968,15 @@ public function getDateFormat(): string 'updated_at' => $this->currentTime(), ]); - $this->assertInstanceOf(Carbon::class, $model->created_at); - $this->assertInstanceOf(Carbon::class, $model->updated_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); + $this->assertSame(CarbonImmutable::class, $model->updated_at::class); } - public function testTimestampsAreReturnedAsObjectsOnCreate() + public function testTimestampsAreReturnedAsObjectsOnCreate(): void { $timestamps = [ - 'created_at' => Carbon::now(), - 'updated_at' => Carbon::now(), + 'created_at' => CarbonImmutable::now(), + 'updated_at' => CarbonImmutable::now(), ]; $model = new DateModelStub; Model::setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class)); @@ -983,15 +984,15 @@ public function testTimestampsAreReturnedAsObjectsOnCreate() $mockConnection->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class)); $grammar->shouldReceive('getDateFormat')->andReturn('Y-m-d H:i:s'); $instance = $model->newInstance($timestamps); - $this->assertInstanceOf(Carbon::class, $instance->updated_at); - $this->assertInstanceOf(Carbon::class, $instance->created_at); + $this->assertSame(CarbonImmutable::class, $instance->updated_at::class); + $this->assertSame(CarbonImmutable::class, $instance->created_at::class); } public function testDateTimeAttributesReturnNullIfSetToNull() { $timestamps = [ - 'created_at' => Carbon::now(), - 'updated_at' => Carbon::now(), + 'created_at' => CarbonImmutable::now(), + 'updated_at' => CarbonImmutable::now(), ]; $model = new DateModelStub; Model::setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class)); @@ -1004,30 +1005,30 @@ public function testDateTimeAttributesReturnNullIfSetToNull() $this->assertNull($instance->created_at); } - public function testTimestampsAreCreatedFromStringsAndIntegers() + public function testTimestampsAreCreatedFromStringsAndIntegers(): void { $model = new DateModelStub; $model->created_at = '2013-05-22 00:00:00'; - $this->assertInstanceOf(Carbon::class, $model->created_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); $model = new DateModelStub; $model->created_at = $this->currentTime(); - $this->assertInstanceOf(Carbon::class, $model->created_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); $model = new DateModelStub; $model->created_at = 0; - $this->assertInstanceOf(Carbon::class, $model->created_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); $model = new DateModelStub; $model->created_at = '2012-01-01'; - $this->assertInstanceOf(Carbon::class, $model->created_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); } - public function testFromDateTime() + public function testFromDateTime(): void { $model = new ModelStub; - $value = Carbon::parse('2015-04-17 22:59:01'); + $value = CarbonImmutable::parse('2015-04-17 22:59:01'); $this->assertSame('2015-04-17 22:59:01', $model->fromDateTime($value)); $value = new DateTime('2015-04-17 22:59:01'); @@ -1055,7 +1056,7 @@ public function testFromDateTime() $this->assertNull($model->fromDateTime(null)); } - public function testFromDateTimeMilliseconds() + public function testFromDateTimeMilliseconds(): void { $model = new class extends DateModelStub { public function getDateFormat(): string @@ -1067,7 +1068,7 @@ public function getDateFormat(): string 'created_at' => '2012-12-04 22:59.32130', ]); - $this->assertInstanceOf(Carbon::class, $model->created_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); $this->assertSame('22:30:59.321000', $model->created_at->format('H:i:s.u')); } @@ -2679,7 +2680,7 @@ public function testRelationshipTouchOwnersIsNotPropagatedIfNoRelationshipResult $model->touchOwners(); } - public function testModelAttributesAreCastedWhenPresentInCastsPropertyOrCastsMethod() + public function testModelAttributesAreCastedWhenPresentInCastsPropertyOrCastsMethod(): void { $model = new CastingStub; $model->setDateFormat('Y-m-d H:i:s'); @@ -2717,8 +2718,8 @@ public function testModelAttributesAreCastedWhenPresentInCastsPropertyOrCastsMet $this->assertSame('{"foo":"bar"}', $model->jsonAttributeValue()); $this->assertEquals(['こんにちは' => '世界'], $model->jsonAttributeWithUnicode); $this->assertSame('{"こんにちは":"世界"}', $model->jsonAttributeWithUnicodeValue()); - $this->assertInstanceOf(Carbon::class, $model->dateAttribute); - $this->assertInstanceOf(Carbon::class, $model->datetimeAttribute); + $this->assertSame(CarbonImmutable::class, $model->dateAttribute::class); + $this->assertSame(CarbonImmutable::class, $model->datetimeAttribute::class); $this->assertInstanceOf(BaseCollection::class, $model->collectionAttribute); $this->assertInstanceOf(CustomCollection::class, $model->asCustomCollectionAttribute); $this->assertSame('1969-07-20', $model->dateAttribute->toDateString()); @@ -3329,7 +3330,7 @@ public function testScopesMethod() 'published', 'category' => 'Laravel', 'framework' => ['Laravel', '5.3'], - 'date' => Carbon::now(), + 'date' => CarbonImmutable::now(), ]; $this->assertInstanceOf(Builder::class, $model->scopes($scopes)); @@ -4186,7 +4187,7 @@ public function scopeFramework(Builder $builder, $framework, $version) $this->scopesCalled['framework'] = [$framework, $version]; } - public function scopeDate(Builder $builder, Carbon $date) + public function scopeDate(Builder $builder, CarbonInterface $date) { $this->scopesCalled['date'] = $date; } diff --git a/tests/Database/DatabaseEloquentRelationTest.php b/tests/Database/DatabaseEloquentRelationTest.php index 41aa2137d..a3bf612ef 100755 --- a/tests/Database/DatabaseEloquentRelationTest.php +++ b/tests/Database/DatabaseEloquentRelationTest.php @@ -16,7 +16,7 @@ use Hypervel\Database\Query\Builder as QueryBuilder; use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Processors\Processor; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -53,7 +53,7 @@ public function testTouchMethodUpdatesRelatedTimestamps() $relation = new HasOne($builder, $parent, 'foreign_key', 'id'); $related->shouldReceive('getTable')->andReturn('table'); $related->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at'); - $now = Carbon::now(); + $now = CarbonImmutable::now(); $related->shouldReceive('freshTimestampString')->andReturn($now); $builder->shouldReceive('update')->once()->with(['updated_at' => $now])->andReturn(1); @@ -126,7 +126,7 @@ public function testCanDisableTouchingForSpecificModel() $anotherBuilder->shouldReceive('where'); $anotherBuilder->shouldReceive('withoutGlobalScopes')->andReturnSelf(); $anotherRelation = new HasOne($anotherBuilder, $anotherParent, 'foreign_key', 'id'); - $now = Carbon::now(); + $now = CarbonImmutable::now(); $anotherRelated->shouldReceive('freshTimestampString')->andReturn($now); $anotherBuilder->shouldReceive('update')->once()->with(['updated_at' => $now])->andReturn(1); diff --git a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php index fc2f829a2..4d2a04b49 100644 --- a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php +++ b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php @@ -13,7 +13,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Pagination\CursorPaginator; use Hypervel\Pagination\Paginator; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -185,11 +185,11 @@ public function testWithTrashedAcceptsAnArgument() $this->assertCount(2, User::withTrashed(true)->get()); } - public function testDeleteSetsDeletedColumn() + public function testDeleteSetsDeletedColumn(): void { $this->createUsers(); - $this->assertInstanceOf(Carbon::class, User::withTrashed()->find(1)->deleted_at); + $this->assertSame(CarbonImmutable::class, User::withTrashed()->find(1)->deleted_at::class); $this->assertNull(User::find(2)->deleted_at); } @@ -397,9 +397,9 @@ public function testCreateOrFirst() /** * @throws Exception */ - public function testUpdateModelAfterSoftDeleting() + public function testUpdateModelAfterSoftDeleting(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $this->createUsers(); /** @var User $userModel */ diff --git a/tests/Database/DatabaseEloquentTimestampsTest.php b/tests/Database/DatabaseEloquentTimestampsTest.php index 679655eb7..86ad74b09 100644 --- a/tests/Database/DatabaseEloquentTimestampsTest.php +++ b/tests/Database/DatabaseEloquentTimestampsTest.php @@ -6,7 +6,7 @@ use Hypervel\Database\Capsule\Manager as DB; use Hypervel\Database\Eloquent\Model as Eloquent; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use RuntimeException; @@ -70,7 +70,7 @@ protected function tearDown(): void */ public function testUserWithCreatedAtAndUpdatedAt() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $user = UserWithCreatedAndUpdated::create([ 'email' => 'test@test.com', @@ -82,7 +82,7 @@ public function testUserWithCreatedAtAndUpdatedAt() public function testUserWithCreatedAt() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $user = UserWithCreated::create([ 'email' => 'test@test.com', @@ -93,7 +93,7 @@ public function testUserWithCreatedAt() public function testUserWithUpdatedAt() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $user = UserWithUpdated::create([ 'email' => 'test@test.com', @@ -104,9 +104,9 @@ public function testUserWithUpdatedAt() public function testWithoutTimestamp() { - Carbon::setTestNow($now = Carbon::now()->setYear(1995)->startOfYear()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()->setYear(1995)->startOfYear()); $user = UserWithCreatedAndUpdated::create(['email' => 'foo@example.com']); - Carbon::setTestNow(Carbon::now()->addHour()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addHour()); $this->assertTrue($user->usesTimestamps()); @@ -130,9 +130,9 @@ public function testWithoutTimestamp() public function testWithoutTimestampWhenAlreadyIgnoringTimestamps() { - Carbon::setTestNow($now = Carbon::now()->setYear(1995)->startOfYear()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()->setYear(1995)->startOfYear()); $user = UserWithCreatedAndUpdated::create(['email' => 'foo@example.com']); - Carbon::setTestNow(Carbon::now()->addHour()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addHour()); $user->timestamps = false; diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index f56d495d4..8ce382e6b 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -5,7 +5,6 @@ namespace Hypervel\Tests\Database; use BadMethodCallException; -use Carbon\Carbon; use Closure; use DateTime; use Hypervel\Contracts\Database\Query\ConditionExpression; @@ -27,6 +26,7 @@ use Hypervel\Pagination\Cursor; use Hypervel\Pagination\CursorPaginator; use Hypervel\Pagination\LengthAwarePaginator; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Tests\Database\Fixtures\Enums\Bar; use Hypervel\Tests\TestCase; @@ -637,9 +637,9 @@ public function testWhereTimePostgres() public function testWherePast() { - Carbon::setTestNow('2022-04-20 23:45:06.123456'); + CarbonImmutable::setTestNow('2022-04-20 23:45:06.123456'); - $testDate = Carbon::create('2022-04-20 23:45:06.123456'); + $testDate = CarbonImmutable::create('2022-04-20 23:45:06.123456'); $builder = $this->getBuilder(); $builder->select('*')->from('posts')->wherePast('published_at'); @@ -654,9 +654,9 @@ public function testWherePast() public function testWherePastUsesArray() { - Carbon::setTestNow('2022-04-20 12:34:56.123456'); + CarbonImmutable::setTestNow('2022-04-20 12:34:56.123456'); - $testDate = Carbon::create('2022-04-20 12:34:56.123456'); + $testDate = CarbonImmutable::create('2022-04-20 12:34:56.123456'); $builder = $this->getBuilder(); $builder->select('*')->from('posts')->wherePast(['published_at', 'held_at']); @@ -671,7 +671,7 @@ public function testWherePastUsesArray() public function testWhereTodayMySQL() { - Carbon::setTestNow('2022-04-20 12:34:56.123456'); + CarbonImmutable::setTestNow('2022-04-20 12:34:56.123456'); $builder = $this->getMySqlBuilder(); $builder->select('*')->from('posts')->whereToday('published_at'); @@ -686,7 +686,7 @@ public function testWhereTodayMySQL() public function testPassingArrayToWhereTodayMySQL() { - Carbon::setTestNow('2022-04-20 12:34:56.123456'); + CarbonImmutable::setTestNow('2022-04-20 12:34:56.123456'); $builder = $this->getMySqlBuilder(); $builder->select('*')->from('posts')->whereToday(['published_at', 'held_at']); @@ -701,9 +701,9 @@ public function testPassingArrayToWhereTodayMySQL() public function testWhereFuture() { - Carbon::setTestNow('2022-04-22 21:01:23.123456'); + CarbonImmutable::setTestNow('2022-04-22 21:01:23.123456'); - $testDate = Carbon::create('2022-04-22 21:01:23.123456'); + $testDate = CarbonImmutable::create('2022-04-22 21:01:23.123456'); $builder = $this->getBuilder(); $builder->select('*')->from('posts')->whereFuture('published_at'); @@ -718,9 +718,9 @@ public function testWhereFuture() public function testPassingArrayToWhereFuture() { - Carbon::setTestNow('2022-04-22 01:23:45.123456'); + CarbonImmutable::setTestNow('2022-04-22 01:23:45.123456'); - $testDate = Carbon::create('2022-04-22 01:23:45.123456'); + $testDate = CarbonImmutable::create('2022-04-22 01:23:45.123456'); $builder = $this->getBuilder(); $builder->select('*')->from('posts')->whereFuture(['published_at', 'held_at']); diff --git a/tests/Database/DatabaseSoftDeletingTest.php b/tests/Database/DatabaseSoftDeletingTest.php index cc20f7d54..0075013bd 100644 --- a/tests/Database/DatabaseSoftDeletingTest.php +++ b/tests/Database/DatabaseSoftDeletingTest.php @@ -6,7 +6,7 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\SoftDeletes; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; class DatabaseSoftDeletingTest extends TestCase @@ -19,12 +19,12 @@ public function testDeletedAtIsAddedToCastsAsDefaultType() $this->assertSame('datetime', $model->getCasts()['deleted_at']); } - public function testDeletedAtIsCastToCarbonInstance() + public function testDeletedAtIsCastToCarbonImmutableByDefault(): void { - $expected = Carbon::createFromFormat('Y-m-d H:i:s', '2018-12-29 13:59:39'); + $expected = CarbonImmutable::createFromFormat('Y-m-d H:i:s', '2018-12-29 13:59:39'); $model = new SoftDeletingModel(['deleted_at' => $expected->format('Y-m-d H:i:s')]); - $this->assertInstanceOf(Carbon::class, $model->deleted_at); + $this->assertSame(CarbonImmutable::class, $model->deleted_at::class); $this->assertTrue($expected->eq($model->deleted_at)); } diff --git a/tests/Database/DatabaseSoftDeletingTraitTest.php b/tests/Database/DatabaseSoftDeletingTraitTest.php index 0e19d41f3..70bedcac8 100644 --- a/tests/Database/DatabaseSoftDeletingTraitTest.php +++ b/tests/Database/DatabaseSoftDeletingTraitTest.php @@ -5,14 +5,14 @@ namespace Hypervel\Tests\Database\DatabaseSoftDeletingTraitTest; use Hypervel\Database\Eloquent\SoftDeletes; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; use stdClass; class DatabaseSoftDeletingTraitTest extends TestCase { - public function testDeleteSetsSoftDeletedColumn() + public function testDeleteSetsSoftDeletedColumn(): void { $model = m::mock(Stub::class); $model->makePartial(); @@ -29,7 +29,7 @@ public function testDeleteSetsSoftDeletedColumn() $model->shouldReceive('usesTimestamps')->once()->andReturn(true); $model->delete(); - $this->assertInstanceOf(Carbon::class, $model->deleted_at); + $this->assertSame(CarbonImmutable::class, $model->deleted_at::class); } public function testRestore() @@ -96,9 +96,9 @@ public function fireModelEvent() { } - public function freshTimestamp() + public function freshTimestamp(): CarbonImmutable { - return Carbon::now(); + return CarbonImmutable::now(); } public function fromDateTime() diff --git a/tests/Database/Eloquent/Concerns/DateFactoryTest.php b/tests/Database/Eloquent/Concerns/DateFactoryTest.php index ad343bee5..1f4e38e5f 100644 --- a/tests/Database/Eloquent/Concerns/DateFactoryTest.php +++ b/tests/Database/Eloquent/Concerns/DateFactoryTest.php @@ -4,307 +4,97 @@ namespace Hypervel\Tests\Database\Eloquent\Concerns; -use Carbon\Carbon; -use Carbon\CarbonImmutable; -use Carbon\CarbonInterface; +use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; use DateTime; use DateTimeImmutable; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\MorphPivot; use Hypervel\Database\Eloquent\Relations\Pivot; -use Hypervel\Support\DateFactory; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -/** - * Tests that Date::use() configuration is respected throughout the framework. - * - * When Date::use(CarbonImmutable::class) is called, all date-related operations - * should return CarbonImmutable instances instead of mutable Carbon instances. - */ class DateFactoryTest extends TestCase { - // ========================================== - // Date Facade Tests - // ========================================== - - public function testDateFacadeReturnsDefaultCarbonByDefault(): void - { - $date = Date::now(); - - $this->assertInstanceOf(Carbon::class, $date); - $this->assertNotInstanceOf(CarbonImmutable::class, $date); - } - - public function testDateFacadeReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); - - $date = Date::now(); - - $this->assertInstanceOf(CarbonImmutable::class, $date); - } - - public function testDateFacadeParseReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); - - $date = Date::parse('2024-01-15 10:30:00'); - - $this->assertInstanceOf(CarbonImmutable::class, $date); - } - - public function testDateFacadeCreateFromTimestampReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); - - $date = Date::createFromTimestamp(1705312200); - - $this->assertInstanceOf(CarbonImmutable::class, $date); - } - - public function testDateFacadeCreateFromFormatReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); - - $date = Date::createFromFormat('Y-m-d', '2024-01-15'); - - $this->assertInstanceOf(CarbonImmutable::class, $date); - } - - public function testDateFacadeInstanceReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); - - $carbon = Carbon::now(); - $date = Date::instance($carbon); - - $this->assertInstanceOf(CarbonImmutable::class, $date); - } - - public function testDateFacadeUseDefaultResetsToMutableCarbon(): void - { - Date::use(CarbonImmutable::class); - $this->assertInstanceOf(CarbonImmutable::class, Date::now()); - - DateFactory::useDefault(); - - $this->assertInstanceOf(Carbon::class, Date::now()); - $this->assertNotInstanceOf(CarbonImmutable::class, Date::now()); - } - - // ========================================== - // Model freshTimestamp() Tests - // ========================================== - - public function testModelFreshTimestampReturnsDefaultCarbonByDefault(): void - { - $model = new DateFactoryTestModel; - - $timestamp = $model->freshTimestamp(); - - $this->assertInstanceOf(CarbonInterface::class, $timestamp); - $this->assertInstanceOf(Carbon::class, $timestamp); - $this->assertNotInstanceOf(CarbonImmutable::class, $timestamp); - } - - public function testModelFreshTimestampReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - $timestamp = $model->freshTimestamp(); - - $this->assertInstanceOf(CarbonImmutable::class, $timestamp); - } - - // ========================================== - // Model asDateTime() Tests - // ========================================== - - public function testAsDateTimeReturnsCarbonImmutableFromCarbonInstance(): void - { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - $model->setRawAttributes(['published_at' => Carbon::parse('2024-01-15 10:30:00')]); - - $date = $model->published_at; - - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); - } - - public function testAsDateTimeReturnsCarbonImmutableFromDateTimeInterface(): void - { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - $model->setRawAttributes(['published_at' => new DateTime('2024-01-15 10:30:00')]); - - $date = $model->published_at; - - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); - } - - public function testAsDateTimeReturnsCarbonImmutableFromDateTimeImmutable(): void + #[DataProvider('dateInputProvider')] + public function testDatetimeCastUsesImmutableDefault(mixed $input, string $expected): void { - Date::use(CarbonImmutable::class); - $model = new DateFactoryTestModel; - $model->setRawAttributes(['published_at' => new DateTimeImmutable('2024-01-15 10:30:00')]); + $model->setRawAttributes(['published_at' => $input]); $date = $model->published_at; - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); + $this->assertSame(CarbonImmutable::class, $date::class); + $this->assertSame($expected, $date->format('Y-m-d H:i:s')); } - public function testAsDateTimeReturnsCarbonImmutableFromTimestamp(): void + public static function dateInputProvider(): array { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - // 1705312200 = 2024-01-15 10:30:00 UTC - $model->setRawAttributes(['published_at' => 1705312200]); - - $date = $model->published_at; - - $this->assertInstanceOf(CarbonImmutable::class, $date); + return [ + 'base mutable Carbon' => [BaseCarbon::parse('2024-01-15 10:30:00'), '2024-01-15 10:30:00'], + 'base immutable Carbon' => [BaseCarbonImmutable::parse('2024-01-15 10:30:00'), '2024-01-15 10:30:00'], + 'native mutable' => [new DateTime('2024-01-15 10:30:00'), '2024-01-15 10:30:00'], + 'native immutable' => [new DateTimeImmutable('2024-01-15 10:30:00'), '2024-01-15 10:30:00'], + 'timestamp' => [1705314600, '2024-01-15 10:30:00'], + 'database format' => ['2024-01-15 10:30:00', '2024-01-15 10:30:00'], + 'parse fallback' => ['January 15, 2024 10:30:00', '2024-01-15 10:30:00'], + ]; } - public function testAsDateTimeReturnsCarbonImmutableFromStandardDateFormat(): void + public function testDateAndDatetimeCastsRespectMutableOptOut(): void { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - $model->setRawAttributes(['published_at' => '2024-01-15']); - - $date = $model->published_at; - - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15', $date->format('Y-m-d')); - // Standard date format should start at beginning of day - $this->assertSame('00:00:00', $date->format('H:i:s')); - } - - public function testAsDateTimeReturnsCarbonImmutableFromString(): void - { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - $model->setRawAttributes(['published_at' => '2024-01-15 10:30:00']); - - $date = $model->published_at; - - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); - } - - // ========================================== - // Model Date Cast Tests - // ========================================== - - public function testDateCastReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); + Date::use(Carbon::class); $model = new DateFactoryDateCastModel; - $model->setRawAttributes(['event_date' => '2024-01-15']); - - $date = $model->event_date; + $model->setRawAttributes([ + 'event_date' => '2024-01-15', + 'event_datetime' => '2024-01-15 10:30:00', + ]); - $this->assertInstanceOf(CarbonImmutable::class, $date); - // Date cast should return start of day - $this->assertSame('00:00:00', $date->format('H:i:s')); + $this->assertSame(Carbon::class, $model->event_date::class); + $this->assertSame(Carbon::class, $model->event_datetime::class); + $this->assertSame('00:00:00', $model->event_date->format('H:i:s')); } - public function testDatetimeCastReturnsCarbonImmutableWhenConfigured(): void + public function testImmutableCastsRemainHypervelImmutableDuringMutableOptOut(): void { - Date::use(CarbonImmutable::class); + Date::use(Carbon::class); $model = new DateFactoryDateCastModel; - $model->setRawAttributes(['event_datetime' => '2024-01-15 10:30:00']); - - $date = $model->event_datetime; + $model->setRawAttributes([ + 'immutable_event_date' => '2024-01-15', + 'immutable_event_datetime' => '2024-01-15 10:30:00', + ]); - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); + $this->assertSame(CarbonImmutable::class, $model->immutable_event_date::class); + $this->assertSame(CarbonImmutable::class, $model->immutable_event_datetime::class); } - // ========================================== - // Pivot Model Tests - // ========================================== - - public function testPivotFreshTimestampReturnsCarbonImmutableWhenConfigured(): void + public function testFreshTimestampsUseConfiguredFactory(): void { - Date::use(CarbonImmutable::class); - - $pivot = new DateFactoryTestPivot; - $timestamp = $pivot->freshTimestamp(); - - $this->assertInstanceOf(CarbonImmutable::class, $timestamp); - } + $model = new DateFactoryTestModel; - public function testMorphPivotFreshTimestampReturnsCarbonImmutableWhenConfigured(): void - { - Date::use(CarbonImmutable::class); + $this->assertSame(CarbonImmutable::class, $model->freshTimestamp()::class); + $this->assertSame(CarbonImmutable::class, (new DateFactoryTestPivot)->freshTimestamp()::class); + $this->assertSame(CarbonImmutable::class, (new DateFactoryTestMorphPivot)->freshTimestamp()::class); - $pivot = new DateFactoryTestMorphPivot; - $timestamp = $pivot->freshTimestamp(); + Date::use(Carbon::class); - $this->assertInstanceOf(CarbonImmutable::class, $timestamp); + $this->assertSame(Carbon::class, $model->freshTimestamp()::class); } - // ========================================== - // Edge Cases - // ========================================== - public function testAsDateTimeWithNullReturnsNull(): void { - Date::use(CarbonImmutable::class); - $model = new DateFactoryTestModel; $model->setRawAttributes(['published_at' => null]); - // The $dates array functionality will still return null for null values $this->assertNull($model->published_at); } - - public function testAsDateTimeHandlesCarbonImmutableInstanceDirectly(): void - { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryTestModel; - $immutable = CarbonImmutable::parse('2024-01-15 10:30:00'); - $model->setRawAttributes(['published_at' => $immutable]); - - $date = $model->published_at; - - $this->assertInstanceOf(CarbonImmutable::class, $date); - $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); - } - - public function testMultipleDateFieldsAllReturnCarbonImmutable(): void - { - Date::use(CarbonImmutable::class); - - $model = new DateFactoryMultipleDatesModel; - $model->setRawAttributes([ - 'created_at' => '2024-01-15 08:00:00', - 'updated_at' => '2024-01-15 09:00:00', - 'published_at' => '2024-01-15 10:00:00', - ]); - - $this->assertInstanceOf(CarbonImmutable::class, $model->created_at); - $this->assertInstanceOf(CarbonImmutable::class, $model->updated_at); - $this->assertInstanceOf(CarbonImmutable::class, $model->published_at); - } } -// Test Model Classes - class DateFactoryTestModel extends Model { protected ?string $table = 'test_models'; @@ -319,16 +109,11 @@ class DateFactoryDateCastModel extends Model protected array $casts = [ 'event_date' => 'date', 'event_datetime' => 'datetime', + 'immutable_event_date' => 'immutable_date', + 'immutable_event_datetime' => 'immutable_datetime', ]; } -class DateFactoryMultipleDatesModel extends Model -{ - protected ?string $table = 'test_models'; - - protected array $casts = ['published_at' => 'datetime']; -} - class DateFactoryTestPivot extends Pivot { protected ?string $table = 'test_pivots'; diff --git a/tests/Database/Eloquent/Factories/FactoryTest.php b/tests/Database/Eloquent/Factories/FactoryTest.php index 5610a7547..58f5dfeeb 100644 --- a/tests/Database/Eloquent/Factories/FactoryTest.php +++ b/tests/Database/Eloquent/Factories/FactoryTest.php @@ -5,7 +5,6 @@ namespace Hypervel\Tests\Database\Eloquent\Factories; use BadMethodCallException; -use Carbon\Carbon; use Hypervel\Contracts\Foundation\Application; use Hypervel\Database\Eloquent\Attributes\UseFactory; use Hypervel\Database\Eloquent\Collection; @@ -17,6 +16,7 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\SoftDeletes; use Hypervel\Foundation\Testing\RefreshDatabase; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Database\Fixtures\Models\Money\Price; use ReflectionClass; @@ -610,29 +610,29 @@ public function testFactoryCanConditionallyExecuteCode() public function testDynamicTrashedStateForSoftdeletesModels() { - $now = Carbon::create(2020, 6, 7, 8, 9); - Carbon::setTestNow($now); + $now = CarbonImmutable::create(2020, 6, 7, 8, 9); + CarbonImmutable::setTestNow($now); $post = FactoryTestPostFactory::new()->trashed()->create(); $this->assertTrue($post->deleted_at->equalTo($now->subDay())); - $deleted_at = Carbon::create(2020, 1, 2, 3, 4, 5); + $deleted_at = CarbonImmutable::create(2020, 1, 2, 3, 4, 5); $post = FactoryTestPostFactory::new()->trashed($deleted_at)->create(); $this->assertTrue($deleted_at->equalTo($post->deleted_at)); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testDynamicTrashedStateRespectsExistingState() { - $now = Carbon::create(2020, 6, 7, 8, 9); - Carbon::setTestNow($now); + $now = CarbonImmutable::create(2020, 6, 7, 8, 9); + CarbonImmutable::setTestNow($now); $comment = FactoryTestCommentFactory::new()->trashed()->create(); $this->assertTrue($comment->deleted_at->equalTo($now->subWeek())); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testDynamicTrashedStateThrowsExceptionWhenNotASoftdeletesModel() @@ -945,7 +945,7 @@ public function definition(): array public function trashed() { return $this->state([ - 'deleted_at' => Carbon::now()->subWeek(), + 'deleted_at' => CarbonImmutable::now()->subWeek(), ]); } } diff --git a/tests/Database/QueryDurationThresholdTest.php b/tests/Database/QueryDurationThresholdTest.php index fd5545d4b..ba524d26d 100644 --- a/tests/Database/QueryDurationThresholdTest.php +++ b/tests/Database/QueryDurationThresholdTest.php @@ -8,16 +8,13 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Database\Connection; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use PDO; class QueryDurationThresholdTest extends TestCase { - /** - * @var \Illuminate\Support\Carbon - */ - protected $now; + protected ?CarbonImmutable $now = null; public function testElapsedQueryTimeUsesTheMonotonicClock(): void { @@ -81,7 +78,7 @@ public function testItIsOnlyCalledOnceWhenHandlerRunsAnotherQuery() public function testItIsOnlyCalledOnceWhenGivenDateTime() { - Carbon::setTestNow($this->now = Carbon::create(2017, 6, 27, 13, 14, 15, 'UTC')); + CarbonImmutable::setTestNow($this->now = CarbonImmutable::create(2017, 6, 27, 13, 14, 15, 'UTC')); $connection = new Connection(new PDO('sqlite::memory:'), '', '', ['name' => 'sqlite']); $connection->setEventDispatcher($this->app->make(Dispatcher::class)); diff --git a/tests/Integration/Database/DatabaseCacheStoreTest.php b/tests/Integration/Database/DatabaseCacheStoreTest.php index 71898abd1..db0cb6ca3 100644 --- a/tests/Integration/Database/DatabaseCacheStoreTest.php +++ b/tests/Integration/Database/DatabaseCacheStoreTest.php @@ -7,7 +7,7 @@ use Hypervel\Cache\DatabaseStore; use Hypervel\Cache\Repository; use Hypervel\Database\SQLiteConnection; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Cache; use Hypervel\Support\Facades\DB; use Hypervel\Testbench\Attributes\WithMigration; @@ -361,7 +361,7 @@ protected function insertToCacheTable(string $key, mixed $value, int $ttl = 60): [ 'key' => $this->withCachePrefix($key), 'value' => serialize($value), - 'expiration' => Carbon::now()->addSeconds($ttl)->getTimestamp(), + 'expiration' => CarbonImmutable::now()->addSeconds($ttl)->getTimestamp(), ] ); } @@ -373,7 +373,7 @@ protected function insertToLocksTable(string $key, string $owner, int $ttl = 60) [ 'key' => $this->withCachePrefix($key), 'owner' => $owner, - 'expiration' => Carbon::now()->addSeconds($ttl)->getTimestamp(), + 'expiration' => CarbonImmutable::now()->addSeconds($ttl)->getTimestamp(), ] ); } diff --git a/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php index 1e0a01575..59a5b14d7 100644 --- a/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php @@ -7,7 +7,7 @@ use Hypervel\Database\Eloquent\Casts\Attribute; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Schema\Blueprint; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Support\Facades\Schema; use Hypervel\Support\Str; @@ -394,7 +394,7 @@ public function birthdayAt(): Attribute { return new Attribute( function ($value) { - return Carbon::parse($value); + return CarbonImmutable::parse($value); }, function ($value) { return $value->format('Y-m-d'); diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php index b877e0a10..8f93d7eb8 100644 --- a/tests/Integration/Database/DatabaseLockTest.php +++ b/tests/Integration/Database/DatabaseLockTest.php @@ -9,7 +9,7 @@ use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\Query\Builder; use Hypervel\Database\QueryException; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Cache; use Hypervel\Support\Facades\DB; use Hypervel\Testbench\Attributes\WithMigration; @@ -60,7 +60,7 @@ public function testExpiredLockCanBeRetrieved(): void { $lock = Cache::driver('database')->lock('foo'); $this->assertTrue($lock->get()); - DB::table('cache_locks')->update(['expiration' => Carbon::now()->subDay()->getTimestamp()]); + DB::table('cache_locks')->update(['expiration' => CarbonImmutable::now()->subDay()->getTimestamp()]); $otherLock = Cache::driver('database')->lock('foo'); $this->assertTrue($otherLock->get()); @@ -88,7 +88,7 @@ public function testExpiredLockIsNotLocked(): void $lock->get(); $this->assertTrue($lock->isLocked()); - DB::table('cache_locks')->update(['expiration' => Carbon::now()->subDay()->getTimestamp()]); + DB::table('cache_locks')->update(['expiration' => CarbonImmutable::now()->subDay()->getTimestamp()]); $this->assertFalse($lock->isLocked()); } @@ -133,7 +133,7 @@ public function testExpiredLockCannotBeRefreshedByPreviousOwner(): void $lock = Cache::driver('database')->lock('foo', 10); $this->assertTrue($lock->get()); - DB::table('cache_locks')->update(['expiration' => Carbon::now()->subDay()->getTimestamp()]); + DB::table('cache_locks')->update(['expiration' => CarbonImmutable::now()->subDay()->getTimestamp()]); $this->assertFalse($lock->refresh(20)); } diff --git a/tests/Integration/Database/Eloquent/CastsTest.php b/tests/Integration/Database/Eloquent/CastsTest.php index 58fe1fecd..1f9b429c9 100644 --- a/tests/Integration/Database/Eloquent/CastsTest.php +++ b/tests/Integration/Database/Eloquent/CastsTest.php @@ -5,12 +5,11 @@ namespace Hypervel\Tests\Integration\Database\Eloquent; use ArrayObject; -use Carbon\Carbon; -use Carbon\CarbonInterface; use Hypervel\Database\Eloquent\Casts\AsArrayObject; use Hypervel\Database\Eloquent\Casts\AsCollection; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Schema\Blueprint; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Schema; use Hypervel\Tests\Integration\Database\DatabaseTestCase; @@ -114,22 +113,22 @@ public function testCollectionCast(): void public function testDatetimeCast(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); $model = CastModel::create(['name' => 'Test', 'published_at' => $now]); - $this->assertInstanceOf(CarbonInterface::class, $model->published_at); + $this->assertSame(CarbonImmutable::class, $model->published_at::class); $retrieved = CastModel::find($model->id); - $this->assertInstanceOf(CarbonInterface::class, $retrieved->published_at); + $this->assertSame(CarbonImmutable::class, $retrieved->published_at::class); $this->assertSame($now->format('Y-m-d H:i:s'), $retrieved->published_at->format('Y-m-d H:i:s')); } public function testDateCast(): void { - $date = Carbon::parse('1990-05-15'); + $date = CarbonImmutable::parse('1990-05-15'); $model = CastModel::create(['name' => 'Test', 'birth_date' => $date]); - $this->assertInstanceOf(CarbonInterface::class, $model->birth_date); + $this->assertSame(CarbonImmutable::class, $model->birth_date::class); $retrieved = CastModel::find($model->id); $this->assertSame('1990-05-15', $retrieved->birth_date->format('Y-m-d')); @@ -139,7 +138,7 @@ public function testDatetimeCastFromString(): void { $model = CastModel::create(['name' => 'Test', 'published_at' => '2024-01-15 10:30:00']); - $this->assertInstanceOf(CarbonInterface::class, $model->published_at); + $this->assertSame(CarbonImmutable::class, $model->published_at::class); $this->assertSame('2024-01-15', $model->published_at->format('Y-m-d')); $this->assertSame('10:30:00', $model->published_at->format('H:i:s')); } @@ -148,8 +147,8 @@ public function testTimestampsCast(): void { $model = CastModel::create(['name' => 'Test']); - $this->assertInstanceOf(CarbonInterface::class, $model->created_at); - $this->assertInstanceOf(CarbonInterface::class, $model->updated_at); + $this->assertSame(CarbonImmutable::class, $model->created_at::class); + $this->assertSame(CarbonImmutable::class, $model->updated_at::class); } public function testEnumCast(): void @@ -197,7 +196,7 @@ public function testMassAssignmentWithCasts(): void $this->assertIsFloat($model->price); $this->assertIsBool($model->is_active); $this->assertIsArray($model->metadata); - $this->assertInstanceOf(CarbonInterface::class, $model->published_at); + $this->assertSame(CarbonImmutable::class, $model->published_at::class); } public function testArrayObjectCast(): void diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index 39d0fcf30..0644761af 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -10,7 +10,7 @@ use Hypervel\Database\Eloquent\Relations\Pivot; use Hypervel\Database\RecordsNotFoundException; use Hypervel\Database\Schema\Blueprint; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Schema; use Hypervel\Support\Str; @@ -76,7 +76,7 @@ protected function afterRefreshingDatabase(): void public function testBasicCreateAndRetrieve() { - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $post = Post::create(['title' => Str::random()]); @@ -141,7 +141,7 @@ public function testRefreshOnOtherModelWorks() public function testCustomPivotClass() { - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $post = Post::create(['title' => Str::random()]); @@ -171,7 +171,7 @@ public function testCustomPivotClass() public function testCustomPivotClassUsingSync() { - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $post = Post::create(['title' => Str::random()]); @@ -196,7 +196,7 @@ public function testCustomPivotClassUsingSync() public function testCustomPivotClassUsingUpdateExistingPivot() { - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $post = Post::create(['title' => Str::random()]); $tag = TagWithCustomPivot::create(['name' => Str::random()]); @@ -223,7 +223,7 @@ public function testCustomPivotClassUsingUpdateExistingPivot() public function testCustomPivotClassUpdatesTimestamps() { - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $post = Post::create(['title' => Str::random()]); $tag = TagWithCustomPivot::create(['name' => Str::random()]); @@ -236,7 +236,7 @@ public function testCustomPivotClassUpdatesTimestamps() ], ]); - Carbon::setTestNow('2017-10-10 10:10:20'); // +10 seconds + CarbonImmutable::setTestNow('2017-10-10 10:10:20'); // +10 seconds $this->assertEquals( 1, @@ -972,7 +972,7 @@ public function testTouchingParent() $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString()); - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $tag->update(['name' => $tag->name]); $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString()); @@ -990,7 +990,7 @@ public function testTouchingRelatedModelsOnSync() $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString()); $this->assertNotSame('2017-10-10 10:10:10', $tag->fresh()->updated_at->toDateTimeString()); - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $tag->posts()->sync([$post->id]); @@ -1007,7 +1007,7 @@ public function testNoTouchingHappensIfNotConfigured() $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString()); $this->assertNotSame('2017-10-10 10:10:10', $tag->fresh()->updated_at->toDateTimeString()); - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $tag->posts()->sync([$post->id]); @@ -1048,7 +1048,7 @@ public function testCanTouchRelatedModels() ['post_id' => $post->id, 'tag_id' => 3, 'flag' => ''], ]); - Carbon::setTestNow('2017-10-10 10:10:10'); + CarbonImmutable::setTestNow('2017-10-10 10:10:10'); $post->tags()->touch(); @@ -1667,7 +1667,7 @@ class PostTagPivot extends Pivot public function getCreatedAtAttribute(mixed $value): string { - return Carbon::parse($value)->format('U'); + return CarbonImmutable::parse($value)->format('U'); } } diff --git a/tests/Integration/Database/EloquentEagerLoadingLimitTest.php b/tests/Integration/Database/EloquentEagerLoadingLimitTest.php index 10f46a5b0..9df129eb0 100644 --- a/tests/Integration/Database/EloquentEagerLoadingLimitTest.php +++ b/tests/Integration/Database/EloquentEagerLoadingLimitTest.php @@ -9,7 +9,7 @@ use Hypervel\Database\Eloquent\Relations\HasMany; use Hypervel\Database\Eloquent\Relations\HasManyThrough; use Hypervel\Database\Schema\Blueprint; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Schema; use Hypervel\Tests\Integration\Database\DatabaseTestCase; @@ -47,26 +47,26 @@ protected function afterRefreshingDatabase(): void User::create(); User::create(); - Post::create(['user_id' => 1, 'created_at' => new Carbon('2024-01-01 00:00:01')]); - Post::create(['user_id' => 1, 'created_at' => new Carbon('2024-01-01 00:00:02')]); - Post::create(['user_id' => 1, 'created_at' => new Carbon('2024-01-01 00:00:03')]); - Post::create(['user_id' => 2, 'created_at' => new Carbon('2024-01-01 00:00:04')]); - Post::create(['user_id' => 2, 'created_at' => new Carbon('2024-01-01 00:00:05')]); - Post::create(['user_id' => 2, 'created_at' => new Carbon('2024-01-01 00:00:06')]); - - Comment::create(['post_id' => 1, 'created_at' => new Carbon('2024-01-01 00:00:01')]); - Comment::create(['post_id' => 2, 'created_at' => new Carbon('2024-01-01 00:00:02')]); - Comment::create(['post_id' => 3, 'created_at' => new Carbon('2024-01-01 00:00:03')]); - Comment::create(['post_id' => 4, 'created_at' => new Carbon('2024-01-01 00:00:04')]); - Comment::create(['post_id' => 5, 'created_at' => new Carbon('2024-01-01 00:00:05')]); - Comment::create(['post_id' => 6, 'created_at' => new Carbon('2024-01-01 00:00:06')]); - - Role::create(['created_at' => new Carbon('2024-01-01 00:00:01')]); - Role::create(['created_at' => new Carbon('2024-01-01 00:00:02')]); - Role::create(['created_at' => new Carbon('2024-01-01 00:00:03')]); - Role::create(['created_at' => new Carbon('2024-01-01 00:00:04')]); - Role::create(['created_at' => new Carbon('2024-01-01 00:00:05')]); - Role::create(['created_at' => new Carbon('2024-01-01 00:00:06')]); + Post::create(['user_id' => 1, 'created_at' => new CarbonImmutable('2024-01-01 00:00:01')]); + Post::create(['user_id' => 1, 'created_at' => new CarbonImmutable('2024-01-01 00:00:02')]); + Post::create(['user_id' => 1, 'created_at' => new CarbonImmutable('2024-01-01 00:00:03')]); + Post::create(['user_id' => 2, 'created_at' => new CarbonImmutable('2024-01-01 00:00:04')]); + Post::create(['user_id' => 2, 'created_at' => new CarbonImmutable('2024-01-01 00:00:05')]); + Post::create(['user_id' => 2, 'created_at' => new CarbonImmutable('2024-01-01 00:00:06')]); + + Comment::create(['post_id' => 1, 'created_at' => new CarbonImmutable('2024-01-01 00:00:01')]); + Comment::create(['post_id' => 2, 'created_at' => new CarbonImmutable('2024-01-01 00:00:02')]); + Comment::create(['post_id' => 3, 'created_at' => new CarbonImmutable('2024-01-01 00:00:03')]); + Comment::create(['post_id' => 4, 'created_at' => new CarbonImmutable('2024-01-01 00:00:04')]); + Comment::create(['post_id' => 5, 'created_at' => new CarbonImmutable('2024-01-01 00:00:05')]); + Comment::create(['post_id' => 6, 'created_at' => new CarbonImmutable('2024-01-01 00:00:06')]); + + Role::create(['created_at' => new CarbonImmutable('2024-01-01 00:00:01')]); + Role::create(['created_at' => new CarbonImmutable('2024-01-01 00:00:02')]); + Role::create(['created_at' => new CarbonImmutable('2024-01-01 00:00:03')]); + Role::create(['created_at' => new CarbonImmutable('2024-01-01 00:00:04')]); + Role::create(['created_at' => new CarbonImmutable('2024-01-01 00:00:05')]); + Role::create(['created_at' => new CarbonImmutable('2024-01-01 00:00:06')]); DB::table('role_user')->insert([ ['role_id' => 1, 'user_id' => 1], diff --git a/tests/Integration/Database/EloquentModelDateCastingTest.php b/tests/Integration/Database/EloquentModelDateCastingTest.php index f23ad5ad7..c416e397c 100644 --- a/tests/Integration/Database/EloquentModelDateCastingTest.php +++ b/tests/Integration/Database/EloquentModelDateCastingTest.php @@ -4,10 +4,11 @@ namespace Hypervel\Tests\Integration\Database\EloquentModelDateCastingTest; -use Carbon\Carbon; -use Carbon\CarbonImmutable; +use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Schema\Blueprint; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; use Hypervel\Tests\Integration\Database\DatabaseTestCase; @@ -33,7 +34,7 @@ protected function afterRefreshingDatabase(): void }); } - public function testDatesAreCustomCastable() + public function testDatesAreCustomCastable(): void { $user = TestModel1::create([ 'date_field' => '2019-10-01', @@ -42,8 +43,8 @@ public function testDatesAreCustomCastable() $this->assertSame('2019-10', $user->toArray()['date_field']); $this->assertSame('2019-10 10:15', $user->toArray()['datetime_field']); - $this->assertInstanceOf(Carbon::class, $user->date_field); - $this->assertInstanceOf(Carbon::class, $user->datetime_field); + $this->assertSame(CarbonImmutable::class, $user->date_field::class); + $this->assertSame(CarbonImmutable::class, $user->datetime_field::class); } public function testDatesFormattedAttributeBindings() @@ -85,7 +86,7 @@ public function testDatesFormattedArrayAndJson() $this->assertSame(json_encode($expected), $user->toJson()); } - public function testCustomDateCastsAreComparedAsDatesForCarbonInstances() + public function testCustomDateCastsAreComparedAsDatesForCarbonInstances(): void { $user = TestModel1::create([ 'date_field' => '2019-10-01', @@ -94,10 +95,10 @@ public function testCustomDateCastsAreComparedAsDatesForCarbonInstances() 'immutable_datetime_field' => '2019-10-01 10:15:20', ]); - $user->date_field = new Carbon('2019-10-01'); - $user->datetime_field = new Carbon('2019-10-01 10:15:20'); - $user->immutable_date_field = new CarbonImmutable('2019-10-01'); - $user->immutable_datetime_field = new CarbonImmutable('2019-10-01 10:15:20'); + $user->date_field = new BaseCarbon('2019-10-01'); + $user->datetime_field = new BaseCarbon('2019-10-01 10:15:20'); + $user->immutable_date_field = new BaseCarbonImmutable('2019-10-01'); + $user->immutable_datetime_field = new BaseCarbonImmutable('2019-10-01 10:15:20'); $this->assertArrayNotHasKey('date_field', $user->getDirty()); $this->assertArrayNotHasKey('datetime_field', $user->getDirty()); diff --git a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php index 3fd919d86..54c9240cf 100644 --- a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php +++ b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php @@ -4,9 +4,11 @@ namespace Hypervel\Tests\Integration\Database\EloquentModelImmutableDateCastingTest; -use Carbon\CarbonImmutable; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Schema\Blueprint; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Support\Facades\Schema; use Hypervel\Tests\Integration\Database\DatabaseTestCase; @@ -21,7 +23,7 @@ protected function afterRefreshingDatabase(): void }); } - public function testDatesAreImmutableCastable() + public function testDatesAreImmutableCastable(): void { $model = TestModelImmutable::create([ 'date_field' => '2019-10-01', @@ -30,11 +32,11 @@ public function testDatesAreImmutableCastable() $this->assertSame('2019-10-01T00:00:00.000000Z', $model->toArray()['date_field']); $this->assertSame('2019-10-01T10:15:20.000000Z', $model->toArray()['datetime_field']); - $this->assertInstanceOf(CarbonImmutable::class, $model->date_field); - $this->assertInstanceOf(CarbonImmutable::class, $model->datetime_field); + $this->assertSame(CarbonImmutable::class, $model->date_field::class); + $this->assertSame(CarbonImmutable::class, $model->datetime_field::class); } - public function testDatesAreImmutableAndCustomCastable() + public function testDatesAreImmutableAndCustomCastable(): void { $model = TestModelCustomImmutable::create([ 'date_field' => '2019-10-01', @@ -43,8 +45,21 @@ public function testDatesAreImmutableAndCustomCastable() $this->assertSame('2019-10', $model->toArray()['date_field']); $this->assertSame('2019-10 10:15', $model->toArray()['datetime_field']); - $this->assertInstanceOf(CarbonImmutable::class, $model->date_field); - $this->assertInstanceOf(CarbonImmutable::class, $model->datetime_field); + $this->assertSame(CarbonImmutable::class, $model->date_field::class); + $this->assertSame(CarbonImmutable::class, $model->datetime_field::class); + } + + public function testImmutableCastsIgnoreTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + + $model = TestModelImmutable::create([ + 'date_field' => '2019-10-01', + 'datetime_field' => '2019-10-01 10:15:20', + ]); + + $this->assertSame(CarbonImmutable::class, $model->date_field::class); + $this->assertSame(CarbonImmutable::class, $model->datetime_field::class); } } diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index 876d01e69..9223d88db 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -6,7 +6,7 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Schema\Blueprint; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; use Hypervel\Support\Str; @@ -33,7 +33,7 @@ public function testUserCanUpdateNullableDate() ]); $user->fill([ - 'nullable_date' => $now = Carbon::now(), + 'nullable_date' => $now = CarbonImmutable::now(), ]); $this->assertTrue($user->isDirty('nullable_date')); diff --git a/tests/Integration/Database/EloquentMorphManyTest.php b/tests/Integration/Database/EloquentMorphManyTest.php index 901a22bbc..e9d936d2b 100644 --- a/tests/Integration/Database/EloquentMorphManyTest.php +++ b/tests/Integration/Database/EloquentMorphManyTest.php @@ -9,7 +9,7 @@ use Hypervel\Database\Eloquent\Relations\MorphOne; use Hypervel\Database\Eloquent\Relations\MorphTo; use Hypervel\Database\Schema\Blueprint; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; use Hypervel\Support\Str; use Hypervel\Tests\Integration\Database\DatabaseTestCase; @@ -59,13 +59,13 @@ public function testCanMorphOne() { $post = Post::create(['title' => 'Your favorite book by C.S. Lewis']); - Carbon::setTestNow('1990-02-02 12:00:00'); + CarbonImmutable::setTestNow('1990-02-02 12:00:00'); $oldestComment = tap((new Comment(['name' => 'The Allegory Of Love']))->commentable()->associate($post))->save(); - Carbon::setTestNow('2000-07-02 09:00:00'); + CarbonImmutable::setTestNow('2000-07-02 09:00:00'); tap((new Comment(['name' => 'The Screwtape Letters']))->commentable()->associate($post))->save(); - Carbon::setTestNow('2022-01-01 00:00:00'); + CarbonImmutable::setTestNow('2022-01-01 00:00:00'); $latestComment = tap((new Comment(['name' => 'The Silver Chair']))->commentable()->associate($post))->save(); $this->assertInstanceOf(MorphOne::class, $post->comments()->one()); diff --git a/tests/Integration/Database/MariaDb/EloquentCastTest.php b/tests/Integration/Database/MariaDb/EloquentCastTest.php index 7595b9b8e..0f6cce94c 100644 --- a/tests/Integration/Database/MariaDb/EloquentCastTest.php +++ b/tests/Integration/Database/MariaDb/EloquentCastTest.php @@ -7,7 +7,7 @@ use Hypervel\Contracts\Database\Eloquent\CastsAttributes; use Hypervel\Database\Eloquent\Casts\Attribute; use Hypervel\Database\Eloquent\Model; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; class EloquentCastTest extends MariaDbTestCase @@ -36,9 +36,9 @@ protected function destroyDatabaseMigrations(): void Schema::drop('users'); } - public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() + public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): void { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $createdAt = now()->timestamp; $castUser = UserWithIntTimestampsViaCasts::create([ @@ -79,9 +79,9 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() $this->assertSame($createdAt, $mutatorUser->fresh()->updated_at->timestamp); } - public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() + public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $createdAt = now()->timestamp; $castUser = UserWithIntTimestampsViaCasts::create([ @@ -101,7 +101,7 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); - Carbon::setTestNow(now()->addSecond()); + CarbonImmutable::setTestNow(now()->addSecond()); $updatedAt = now()->timestamp; $castUser->update([ @@ -125,9 +125,9 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->timestamp); } - public function testItCastTimestampsUpdatedByAMutator() + public function testItCastTimestampsUpdatedByAMutator(): void { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $mutatorUser = UserWithUpdatedAtViaMutator::create([ 'email' => fake()->unique()->email, @@ -135,7 +135,7 @@ public function testItCastTimestampsUpdatedByAMutator() $this->assertNull($mutatorUser->updated_at); - Carbon::setTestNow(now()->addSecond()); + CarbonImmutable::setTestNow(now()->addSecond()); $updatedAt = now()->timestamp; $mutatorUser->update([ @@ -163,12 +163,12 @@ class UnixTimeStampToCarbon implements CastsAttributes { public function get(Model $model, string $key, mixed $value, array $attributes): mixed { - return Carbon::parse($value); + return CarbonImmutable::parse($value); } public function set(Model $model, string $key, mixed $value, array $attributes): mixed { - return Carbon::parse($value)->timestamp; + return CarbonImmutable::parse($value)->timestamp; } } @@ -181,16 +181,16 @@ class UserWithIntTimestampsViaAttribute extends Model protected function updatedAt(): Attribute { return Attribute::make( - get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + get: fn ($value) => CarbonImmutable::parse($value), + set: fn ($value) => CarbonImmutable::parse($value)->timestamp, ); } protected function createdAt(): Attribute { return Attribute::make( - get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + get: fn ($value) => CarbonImmutable::parse($value), + set: fn ($value) => CarbonImmutable::parse($value)->timestamp, ); } } @@ -203,22 +203,22 @@ class UserWithIntTimestampsViaMutator extends Model protected function getUpdatedAtAttribute($value) { - return Carbon::parse($value); + return CarbonImmutable::parse($value); } protected function setUpdatedAtAttribute($value) { - $this->attributes['updated_at'] = Carbon::parse($value)->timestamp; + $this->attributes['updated_at'] = CarbonImmutable::parse($value)->timestamp; } protected function getCreatedAtAttribute($value) { - return Carbon::parse($value); + return CarbonImmutable::parse($value); } protected function setCreatedAtAttribute($value) { - $this->attributes['created_at'] = Carbon::parse($value)->timestamp; + $this->attributes['created_at'] = CarbonImmutable::parse($value)->timestamp; } } diff --git a/tests/Integration/Database/MySql/EloquentCastTest.php b/tests/Integration/Database/MySql/EloquentCastTest.php index 8f01590fd..29aec5e18 100644 --- a/tests/Integration/Database/MySql/EloquentCastTest.php +++ b/tests/Integration/Database/MySql/EloquentCastTest.php @@ -7,7 +7,7 @@ use Hypervel\Contracts\Database\Eloquent\CastsAttributes; use Hypervel\Database\Eloquent\Casts\Attribute; use Hypervel\Database\Eloquent\Model; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; class EloquentCastTest extends MySqlTestCase @@ -36,9 +36,9 @@ protected function destroyDatabaseMigrations(): void Schema::drop('users'); } - public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() + public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): void { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $createdAt = now()->timestamp; $castUser = UserWithIntTimestampsViaCasts::create([ @@ -79,9 +79,9 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() $this->assertSame($createdAt, $mutatorUser->fresh()->updated_at->timestamp); } - public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() + public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $createdAt = now()->timestamp; $castUser = UserWithIntTimestampsViaCasts::create([ @@ -101,7 +101,7 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); - Carbon::setTestNow(now()->addSecond()); + CarbonImmutable::setTestNow(now()->addSecond()); $updatedAt = now()->timestamp; $castUser->update([ @@ -125,9 +125,9 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->timestamp); } - public function testItCastTimestampsUpdatedByAMutator() + public function testItCastTimestampsUpdatedByAMutator(): void { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $mutatorUser = UserWithUpdatedAtViaMutator::create([ 'email' => fake()->unique()->email, @@ -135,7 +135,7 @@ public function testItCastTimestampsUpdatedByAMutator() $this->assertNull($mutatorUser->updated_at); - Carbon::setTestNow(now()->addSecond()); + CarbonImmutable::setTestNow(now()->addSecond()); $updatedAt = now()->timestamp; $mutatorUser->update([ @@ -163,12 +163,12 @@ class UnixTimeStampToCarbon implements CastsAttributes { public function get(Model $model, string $key, mixed $value, array $attributes): mixed { - return Carbon::parse($value); + return CarbonImmutable::parse($value); } public function set(Model $model, string $key, mixed $value, array $attributes): mixed { - return Carbon::parse($value)->timestamp; + return CarbonImmutable::parse($value)->timestamp; } } @@ -181,16 +181,16 @@ class UserWithIntTimestampsViaAttribute extends Model protected function updatedAt(): Attribute { return Attribute::make( - get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + get: fn ($value) => CarbonImmutable::parse($value), + set: fn ($value) => CarbonImmutable::parse($value)->timestamp, ); } protected function createdAt(): Attribute { return Attribute::make( - get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + get: fn ($value) => CarbonImmutable::parse($value), + set: fn ($value) => CarbonImmutable::parse($value)->timestamp, ); } } @@ -203,22 +203,22 @@ class UserWithIntTimestampsViaMutator extends Model protected function getUpdatedAtAttribute($value) { - return Carbon::parse($value); + return CarbonImmutable::parse($value); } protected function setUpdatedAtAttribute($value) { - $this->attributes['updated_at'] = Carbon::parse($value)->timestamp; + $this->attributes['updated_at'] = CarbonImmutable::parse($value)->timestamp; } protected function getCreatedAtAttribute($value) { - return Carbon::parse($value); + return CarbonImmutable::parse($value); } protected function setCreatedAtAttribute($value) { - $this->attributes['created_at'] = Carbon::parse($value)->timestamp; + $this->attributes['created_at'] = CarbonImmutable::parse($value)->timestamp; } } diff --git a/tests/Integration/Database/QueryBuilderTest.php b/tests/Integration/Database/QueryBuilderTest.php index 856710dc0..90ee0649a 100644 --- a/tests/Integration/Database/QueryBuilderTest.php +++ b/tests/Integration/Database/QueryBuilderTest.php @@ -8,7 +8,7 @@ use Hypervel\Database\MultipleRecordsFoundException; use Hypervel\Database\RecordsNotFoundException; use Hypervel\Database\Schema\Blueprint; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Schema; use Hypervel\Testbench\Attributes\DefineEnvironment; @@ -28,8 +28,8 @@ protected function afterRefreshingDatabase(): void }); DB::table('posts')->insert([ - ['title' => 'Foo Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2017-11-12 13:14:15')], - ['title' => 'Bar Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2018-01-02 03:04:05')], + ['title' => 'Foo Post', 'content' => 'Lorem Ipsum.', 'created_at' => new CarbonImmutable('2017-11-12 13:14:15')], + ['title' => 'Bar Post', 'content' => 'Lorem Ipsum.', 'created_at' => new CarbonImmutable('2018-01-02 03:04:05')], ]); } @@ -167,7 +167,7 @@ public function testSoleWithParameters() public function testSoleFailsForMultipleRecords() { DB::table('posts')->insert([ - ['title' => 'Foo Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2017-11-12 13:14:15')], + ['title' => 'Foo Post', 'content' => 'Lorem Ipsum.', 'created_at' => new CarbonImmutable('2017-11-12 13:14:15')], ]); $this->expectExceptionObject(new MultipleRecordsFoundException(2)); @@ -289,7 +289,7 @@ public function testWhereNotInputStringParameter() $this->assertSame('Bar Post', $results[0]->title); DB::table('posts')->insert([ - ['title' => 'Baz Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2017-11-12 13:14:15')], + ['title' => 'Baz Post', 'content' => 'Lorem Ipsum.', 'created_at' => new CarbonImmutable('2017-11-12 13:14:15')], ]); $results = DB::table('posts')->whereNot('title', 'Foo Post')->whereNot('title', 'Bar Post')->get(); @@ -308,7 +308,7 @@ public function testOrWhereNot() public function testWhereDate() { $this->assertSame(1, DB::table('posts')->whereDate('created_at', '2018-01-02')->count()); - $this->assertSame(1, DB::table('posts')->whereDate('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(1, DB::table('posts')->whereDate('created_at', new CarbonImmutable('2018-01-02'))->count()); } #[DefineEnvironment('defineEnvironmentWouldThrowsPDOException')] @@ -331,7 +331,7 @@ public function testWhereDateWithInvalidOperator() public function testOrWhereDate() { $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDate('created_at', '2018-01-02')->count()); - $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDate('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDate('created_at', new CarbonImmutable('2018-01-02'))->count()); } #[DefineEnvironment('defineEnvironmentWouldThrowsPDOException')] @@ -361,7 +361,7 @@ public function testWhereDay() { $this->assertSame(1, DB::table('posts')->whereDay('created_at', '02')->count()); $this->assertSame(1, DB::table('posts')->whereDay('created_at', 2)->count()); - $this->assertSame(1, DB::table('posts')->whereDay('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(1, DB::table('posts')->whereDay('created_at', new CarbonImmutable('2018-01-02'))->count()); } public function testWhereDayWithInvalidOperator() @@ -384,7 +384,7 @@ public function testOrWhereDay() { $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDay('created_at', '02')->count()); $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDay('created_at', 2)->count()); - $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDay('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereDay('created_at', new CarbonImmutable('2018-01-02'))->count()); } public function testOrWhereDayWithInvalidOperator() @@ -413,7 +413,7 @@ public function testWhereMonth() { $this->assertSame(1, DB::table('posts')->whereMonth('created_at', '01')->count()); $this->assertSame(1, DB::table('posts')->whereMonth('created_at', 1)->count()); - $this->assertSame(1, DB::table('posts')->whereMonth('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(1, DB::table('posts')->whereMonth('created_at', new CarbonImmutable('2018-01-02'))->count()); } public function testWhereMonthWithInvalidOperator() @@ -436,7 +436,7 @@ public function testOrWhereMonth() { $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereMonth('created_at', '01')->count()); $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereMonth('created_at', 1)->count()); - $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereMonth('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereMonth('created_at', new CarbonImmutable('2018-01-02'))->count()); } public function testOrWhereMonthWithInvalidOperator() @@ -465,7 +465,7 @@ public function testWhereYear() { $this->assertSame(1, DB::table('posts')->whereYear('created_at', '2018')->count()); $this->assertSame(1, DB::table('posts')->whereYear('created_at', 2018)->count()); - $this->assertSame(1, DB::table('posts')->whereYear('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(1, DB::table('posts')->whereYear('created_at', new CarbonImmutable('2018-01-02'))->count()); } #[DefineEnvironment('defineEnvironmentWouldThrowsPDOException')] @@ -489,7 +489,7 @@ public function testOrWhereYear() { $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereYear('created_at', '2018')->count()); $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereYear('created_at', 2018)->count()); - $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereYear('created_at', new Carbon('2018-01-02'))->count()); + $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereYear('created_at', new CarbonImmutable('2018-01-02'))->count()); } #[DefineEnvironment('defineEnvironmentWouldThrowsPDOException')] @@ -518,7 +518,7 @@ public function testOrWhereYearWithInvalidOperator() public function testWhereTime() { $this->assertSame(1, DB::table('posts')->whereTime('created_at', '03:04:05')->count()); - $this->assertSame(1, DB::table('posts')->whereTime('created_at', new Carbon('2018-01-02 03:04:05'))->count()); + $this->assertSame(1, DB::table('posts')->whereTime('created_at', new CarbonImmutable('2018-01-02 03:04:05'))->count()); } #[DefineEnvironment('defineEnvironmentWouldThrowsPDOException')] @@ -541,7 +541,7 @@ public function testWhereTimeWithInvalidOperator() public function testOrWhereTime() { $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereTime('created_at', '03:04:05')->count()); - $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereTime('created_at', new Carbon('2018-01-02 03:04:05'))->count()); + $this->assertSame(2, DB::table('posts')->where('id', 1)->orWhereTime('created_at', new CarbonImmutable('2018-01-02 03:04:05'))->count()); } #[DefineEnvironment('defineEnvironmentWouldThrowsPDOException')] From 7181562ee9ce68747eafd46eb49fdf7196c380ab Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:16:08 +0000 Subject: [PATCH 12/34] fix(validation): accept immutable parsed dates Widen the internal date parsing contracts from native mutable DateTime to DateTimeInterface so validator fallback parsing can return the configured immutable Carbon implementation. Retain native DateTime format results while allowing DateFactory parsing for after, before, and sibling date-format comparisons. Add regressions for immutable fallback results and update date-rule fixtures to capture immutable modifier values. --- .../src/Concerns/ValidatesAttributes.php | 4 +- tests/Validation/ValidationDateRuleTest.php | 48 ++++++++--------- tests/Validation/ValidationValidatorTest.php | 53 +++++++++++++------ 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/validation/src/Concerns/ValidatesAttributes.php b/src/validation/src/Concerns/ValidatesAttributes.php index 6929afaed..2cad6053d 100644 --- a/src/validation/src/Concerns/ValidatesAttributes.php +++ b/src/validation/src/Concerns/ValidatesAttributes.php @@ -263,7 +263,7 @@ protected function checkDateTimeOrder(string $format, string $first, string $sec /** * Get a DateTime instance from a string. */ - protected function getDateTimeWithOptionalFormat(string $format, string $value): ?DateTime + protected function getDateTimeWithOptionalFormat(string $format, string $value): ?DateTimeInterface { if ($date = DateTime::createFromFormat('!' . $format, $value)) { return $date; @@ -275,7 +275,7 @@ protected function getDateTimeWithOptionalFormat(string $format, string $value): /** * Get a DateTime instance from a string with no format. */ - protected function getDateTime(DateTimeInterface|string $value): ?DateTime + protected function getDateTime(DateTimeInterface|string $value): ?DateTimeInterface { try { return @Date::parse($value) ?: null; // @phpstan-ignore ternary.alwaysTrue (Date::parse() PHPDoc claims non-null but can fail at runtime) diff --git a/tests/Validation/ValidationDateRuleTest.php b/tests/Validation/ValidationDateRuleTest.php index 4aa556274..abaca620e 100644 --- a/tests/Validation/ValidationDateRuleTest.php +++ b/tests/Validation/ValidationDateRuleTest.php @@ -4,17 +4,17 @@ namespace Hypervel\Tests\Validation; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Tests\TestCase; use Hypervel\Translation\ArrayLoader; use Hypervel\Translation\Translator; use Hypervel\Validation\Rule; use Hypervel\Validation\Rules\Date; use Hypervel\Validation\Validator; -use PHPUnit\Framework\TestCase; class ValidationDateRuleTest extends TestCase { - public function testDefaultDateRule() + public function testDefaultDateRule(): void { $rule = Rule::date(); $this->assertEquals('date', (string) $rule); @@ -23,13 +23,13 @@ public function testDefaultDateRule() $this->assertSame('date', (string) $rule); } - public function testDateFormatRule() + public function testDateFormatRule(): void { $rule = Rule::date()->format('d/m/Y'); $this->assertEquals('date_format:d/m/Y', (string) $rule); } - public function testAfterTodayRule() + public function testAfterTodayRule(): void { $rule = Rule::date()->afterToday(); $this->assertEquals('date|after:today', (string) $rule); @@ -38,7 +38,7 @@ public function testAfterTodayRule() $this->assertEquals('date|after_or_equal:today', (string) $rule); } - public function testBeforeTodayRule() + public function testBeforeTodayRule(): void { $rule = Rule::date()->beforeToday(); $this->assertEquals('date|before:today', (string) $rule); @@ -47,58 +47,58 @@ public function testBeforeTodayRule() $this->assertEquals('date|before_or_equal:today', (string) $rule); } - public function testAfterSpecificDateRule() + public function testAfterSpecificDateRule(): void { - $rule = Rule::date()->after(Carbon::parse('2024-01-01')); + $rule = Rule::date()->after(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date|after:2024-01-01', (string) $rule); - $rule = Rule::date()->format('d/m/Y')->after(Carbon::parse('2024-01-01')); + $rule = Rule::date()->format('d/m/Y')->after(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date_format:d/m/Y|after:01/01/2024', (string) $rule); } - public function testBeforeSpecificDateRule() + public function testBeforeSpecificDateRule(): void { - $rule = Rule::date()->before(Carbon::parse('2024-01-01')); + $rule = Rule::date()->before(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date|before:2024-01-01', (string) $rule); - $rule = Rule::date()->format('d/m/Y')->before(Carbon::parse('2024-01-01')); + $rule = Rule::date()->format('d/m/Y')->before(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date_format:d/m/Y|before:01/01/2024', (string) $rule); } - public function testAfterOrEqualSpecificDateRule() + public function testAfterOrEqualSpecificDateRule(): void { - $rule = Rule::date()->afterOrEqual(Carbon::parse('2024-01-01')); + $rule = Rule::date()->afterOrEqual(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date|after_or_equal:2024-01-01', (string) $rule); - $rule = Rule::date()->format('d/m/Y')->afterOrEqual(Carbon::parse('2024-01-01')); + $rule = Rule::date()->format('d/m/Y')->afterOrEqual(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date_format:d/m/Y|after_or_equal:01/01/2024', (string) $rule); } - public function testBeforeOrEqualSpecificDateRule() + public function testBeforeOrEqualSpecificDateRule(): void { - $rule = Rule::date()->beforeOrEqual(Carbon::parse('2024-01-01')); + $rule = Rule::date()->beforeOrEqual(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date|before_or_equal:2024-01-01', (string) $rule); - $rule = Rule::date()->format('d/m/Y')->beforeOrEqual(Carbon::parse('2024-01-01')); + $rule = Rule::date()->format('d/m/Y')->beforeOrEqual(CarbonImmutable::parse('2024-01-01')); $this->assertEquals('date_format:d/m/Y|before_or_equal:01/01/2024', (string) $rule); } - public function testBetweenDatesRule() + public function testBetweenDatesRule(): void { - $rule = Rule::date()->between(Carbon::parse('2024-01-01'), Carbon::parse('2024-02-01')); + $rule = Rule::date()->between(CarbonImmutable::parse('2024-01-01'), CarbonImmutable::parse('2024-02-01')); $this->assertEquals('date|after:2024-01-01|before:2024-02-01', (string) $rule); - $rule = Rule::date()->format('d/m/Y')->between(Carbon::parse('2024-01-01'), Carbon::parse('2024-02-01')); + $rule = Rule::date()->format('d/m/Y')->between(CarbonImmutable::parse('2024-01-01'), CarbonImmutable::parse('2024-02-01')); $this->assertEquals('date_format:d/m/Y|after:01/01/2024|before:01/02/2024', (string) $rule); } - public function testBetweenOrEqualDatesRule() + public function testBetweenOrEqualDatesRule(): void { $rule = Rule::date()->betweenOrEqual('2024-01-01', '2024-02-01'); $this->assertEquals('date|after_or_equal:2024-01-01|before_or_equal:2024-02-01', (string) $rule); } - public function testChainedRules() + public function testChainedRules(): void { $rule = Rule::date('Y-m-d H:i:s') ->format('Y-m-d') @@ -117,7 +117,7 @@ public function testChainedRules() $this->assertSame('date_format:Y-m-d|after:2024-01-01', (string) $rule); } - public function testDateValidation() + public function testDateValidation(): void { $trans = new Translator(new ArrayLoader, 'en'); diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 8808ae79a..909dbce23 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -23,9 +23,10 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Http\UploadedFile; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Exceptions\MathException; use Hypervel\Support\Stringable; +use Hypervel\Tests\TestCase; use Hypervel\Translation\ArrayLoader; use Hypervel\Translation\Translator; use Hypervel\Validation\DatabasePresenceVerifierInterface; @@ -41,7 +42,6 @@ use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; -use PHPUnit\Framework\TestCase; use RuntimeException; use SplFileInfo; use stdClass; @@ -6287,17 +6287,17 @@ public function testValidateDateAndFormat() $this->assertTrue($v->passes()); } - public function testDateEquals() + public function testDateEquals(): void { date_default_timezone_set('UTC'); $trans = $this->getArrayTranslator(); $v = new Validator($trans, ['x' => '2000-01-01'], ['x' => 'date_equals:2000-01-01']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['x' => new Carbon('2000-01-01')], ['x' => 'date_equals:2000-01-01']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2000-01-01')], ['x' => 'date_equals:2000-01-01']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['x' => new Carbon('2000-01-01')], ['x' => 'date_equals:2001-01-01']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2000-01-01')], ['x' => 'date_equals:2001-01-01']); $this->assertTrue($v->fails()); $v = new Validator($trans, ['start' => new DateTime('2000-01-01'), 'ends' => new DateTime('2000-01-01')], ['ends' => 'date_equals:start']); @@ -6349,11 +6349,11 @@ public function testDateEquals() $this->assertTrue($v->fails()); } - public function testDateEqualsRespectsCarbonTestNowWhenParameterIsRelative() + public function testDateEqualsRespectsCarbonTestNowWhenParameterIsRelative(): void { date_default_timezone_set('UTC'); $trans = $this->getArrayTranslator(); - Carbon::setTestNow(new Carbon('2018-01-01')); + CarbonImmutable::setTestNow(new CarbonImmutable('2018-01-01')); $v = new Validator($trans, ['x' => '2018-01-01 00:00:00'], ['x' => 'date_equals:now']); $this->assertTrue($v->passes()); @@ -6385,17 +6385,17 @@ public function testDateEqualsRespectsCarbonTestNowWhenParameterIsRelative() $v = new Validator($trans, ['x' => new DateTime('2018-01-01')], ['x' => 'date_equals:tomorrow']); $this->assertTrue($v->fails()); - $v = new Validator($trans, ['x' => new Carbon('2018-01-01')], ['x' => 'date_equals:today|after:yesterday|before:tomorrow']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2018-01-01')], ['x' => 'date_equals:today|after:yesterday|before:tomorrow']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['x' => new Carbon('2018-01-01')], ['x' => 'date_equals:yesterday']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2018-01-01')], ['x' => 'date_equals:yesterday']); $this->assertTrue($v->fails()); - $v = new Validator($trans, ['x' => new Carbon('2018-01-01')], ['x' => 'date_equals:tomorrow']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2018-01-01')], ['x' => 'date_equals:tomorrow']); $this->assertTrue($v->fails()); } - public function testBeforeAndAfter() + public function testBeforeAndAfter(): void { date_default_timezone_set('UTC'); $trans = $this->getArrayTranslator(); @@ -6405,10 +6405,10 @@ public function testBeforeAndAfter() $v = new Validator($trans, ['x' => ['2000-01-01']], ['x' => 'Before:2012-01-01']); $this->assertFalse($v->passes()); - $v = new Validator($trans, ['x' => new Carbon('2000-01-01')], ['x' => 'Before:2012-01-01']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2000-01-01')], ['x' => 'Before:2012-01-01']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['x' => [new Carbon('2000-01-01')]], ['x' => 'Before:2012-01-01']); + $v = new Validator($trans, ['x' => [new CarbonImmutable('2000-01-01')]], ['x' => 'Before:2012-01-01']); $this->assertFalse($v->passes()); $v = new Validator($trans, ['x' => '2012-01-01'], ['x' => 'After:2000-01-01']); @@ -6417,10 +6417,10 @@ public function testBeforeAndAfter() $v = new Validator($trans, ['x' => ['2012-01-01']], ['x' => 'After:2000-01-01']); $this->assertFalse($v->passes()); - $v = new Validator($trans, ['x' => new Carbon('2012-01-01')], ['x' => 'After:2000-01-01']); + $v = new Validator($trans, ['x' => new CarbonImmutable('2012-01-01')], ['x' => 'After:2000-01-01']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['x' => [new Carbon('2012-01-01')]], ['x' => 'After:2000-01-01']); + $v = new Validator($trans, ['x' => [new CarbonImmutable('2012-01-01')]], ['x' => 'After:2000-01-01']); $this->assertFalse($v->passes()); $v = new Validator($trans, ['start' => '2012-01-01', 'ends' => '2013-01-01'], ['start' => 'After:2000-01-01', 'ends' => 'After:start']); @@ -6438,7 +6438,7 @@ public function testBeforeAndAfter() $v = new Validator($trans, ['x' => new DateTime('2000-01-01')], ['x' => 'Before:2012-01-01']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['start' => new DateTime('2012-01-01'), 'ends' => new Carbon('2013-01-01')], ['start' => 'Before:ends', 'ends' => 'After:start']); + $v = new Validator($trans, ['start' => new DateTime('2012-01-01'), 'ends' => new CarbonImmutable('2013-01-01')], ['start' => 'Before:ends', 'ends' => 'After:start']); $this->assertTrue($v->passes()); $v = new Validator($trans, ['start' => '2012-01-01', 'ends' => new DateTime('2013-01-01')], ['start' => 'Before:ends', 'ends' => 'After:start']); @@ -6481,7 +6481,26 @@ public function testBeforeAndAfter() $this->assertTrue($v->fails()); } - public function testBeforeAndAfterWithFormat() + public function testBeforeAndAfterAcceptImmutableFallbackDates(): void + { + $translator = $this->getArrayTranslator(); + + $validator = new Validator( + $translator, + ['date' => date('Y-m-d')], + ['date' => 'date_format:Y-m-d|after:yesterday|before:tomorrow'] + ); + $this->assertTrue($validator->passes()); + + $validator = new Validator( + $translator, + ['start' => date('d/m/Y'), 'end' => 'tomorrow'], + ['start' => 'date_format:d/m/Y|before:end'] + ); + $this->assertTrue($validator->passes()); + } + + public function testBeforeAndAfterWithFormat(): void { date_default_timezone_set('UTC'); $trans = $this->getArrayTranslator(); From 4bfb275e909ea05b57e8af8e5bfb3224fd33df49 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:16:19 +0000 Subject: [PATCH 13/34] fix(queue): accept immutable mail and notification retries Return DateTimeInterface from queued mailable and notification retryUntil wrappers so application retry deadlines may use Hypervel CarbonImmutable without native return-type failures. Preserve nullable property and method-based retry contracts and keep locale-sensitive mail and notification fixtures aligned with immutable test dates. Cover both queued object types with immutable retry deadlines while retaining existing queue serialization behavior. --- src/mail/src/SendQueuedMailable.php | 4 +-- .../src/SendQueuedNotifications.php | 4 +-- .../Mail/Fixtures/timestamp.blade.php | 2 +- .../Mail/SendingMailWithLocaleTest.php | 28 +++++++-------- .../SendingNotificationsWithLocaleTest.php | 34 +++++++++---------- tests/Mail/MailableQueuedTest.php | 18 ++++++++++ ...NotificationSendQueuedNotificationTest.php | 25 ++++++++++++++ 7 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/mail/src/SendQueuedMailable.php b/src/mail/src/SendQueuedMailable.php index ec13789ad..16faffc68 100644 --- a/src/mail/src/SendQueuedMailable.php +++ b/src/mail/src/SendQueuedMailable.php @@ -4,7 +4,7 @@ namespace Hypervel\Mail; -use DateTime; +use DateTimeInterface; use Hypervel\Bus\Queueable; use Hypervel\Contracts\Mail\Factory as MailFactory; use Hypervel\Contracts\Mail\Mailable as MailableContract; @@ -93,7 +93,7 @@ public function backoff(): mixed /** * Determine the time at which the job should timeout. */ - public function retryUntil(): ?DateTime + public function retryUntil(): ?DateTimeInterface { if (! method_exists($this->mailable, 'retryUntil') && ! isset($this->mailable->retryUntil)) { return null; diff --git a/src/notifications/src/SendQueuedNotifications.php b/src/notifications/src/SendQueuedNotifications.php index ff29e83e9..a63088f28 100644 --- a/src/notifications/src/SendQueuedNotifications.php +++ b/src/notifications/src/SendQueuedNotifications.php @@ -4,7 +4,7 @@ namespace Hypervel\Notifications; -use DateTime; +use DateTimeInterface; use Hypervel\Bus\Queueable; use Hypervel\Contracts\Queue\ShouldBeEncrypted; use Hypervel\Contracts\Queue\ShouldQueue; @@ -149,7 +149,7 @@ public function backoff(): mixed /** * Determine the time at which the job should timeout. */ - public function retryUntil(): ?DateTime + public function retryUntil(): ?DateTimeInterface { if (! method_exists($this->notification, 'retryUntil') && ! isset($this->notification->retryUntil)) { return null; diff --git a/tests/Integration/Mail/Fixtures/timestamp.blade.php b/tests/Integration/Mail/Fixtures/timestamp.blade.php index 564bfe651..f66319f4c 100644 --- a/tests/Integration/Mail/Fixtures/timestamp.blade.php +++ b/tests/Integration/Mail/Fixtures/timestamp.blade.php @@ -1 +1 @@ -{{__('nom')}} {{ Hypervel\Support\Carbon::tomorrow()->diffForHumans() }} +{{__('nom')}} {{ Hypervel\Support\CarbonImmutable::tomorrow()->diffForHumans() }} diff --git a/tests/Integration/Mail/SendingMailWithLocaleTest.php b/tests/Integration/Mail/SendingMailWithLocaleTest.php index 4eda7a539..c8a678572 100644 --- a/tests/Integration/Mail/SendingMailWithLocaleTest.php +++ b/tests/Integration/Mail/SendingMailWithLocaleTest.php @@ -8,7 +8,7 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Foundation\Events\LocaleUpdated; use Hypervel\Mail\Mailable; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Support\Facades\Mail; use Hypervel\Testbench\TestCase; @@ -41,7 +41,7 @@ protected function defineEnvironment($app): void ]); } - public function testMailIsSentWithDefaultLocale() + public function testMailIsSentWithDefaultLocale(): void { Mail::to('test@mail.com')->send(new SendingLocaleTestMail); @@ -51,7 +51,7 @@ public function testMailIsSentWithDefaultLocale() ); } - public function testMailIsSentWithSelectedLocale() + public function testMailIsSentWithSelectedLocale(): void { Mail::to('test@mail.com')->locale('ar')->send(new SendingLocaleTestMail); @@ -61,7 +61,7 @@ public function testMailIsSentWithSelectedLocale() ); } - public function testMailIsSentWithLocaleFromMailable() + public function testMailIsSentWithLocaleFromMailable(): void { $mailable = new SendingLocaleTestMail; $mailable->locale('ar'); @@ -74,12 +74,12 @@ public function testMailIsSentWithLocaleFromMailable() ); } - public function testMailIsSentWithLocaleUpdatedListenersCalled() + public function testMailIsSentWithLocaleUpdatedListenersCalled(): void { - Carbon::setTestNow('2018-04-01'); + CarbonImmutable::setTestNow('2018-04-01'); Event::listen(LocaleUpdated::class, function ($event) { - Carbon::setLocale($event->locale); + CarbonImmutable::setLocale($event->locale); }); Mail::to('test@mail.com')->locale('es')->send(new SendingLocaleTimestampTestMail); @@ -89,12 +89,12 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled() $this->app->make('mailer')->getSymfonyTransport()->messages()[0]->toString() ); - $this->assertSame('en', Carbon::getLocale()); + $this->assertSame('en', CarbonImmutable::getLocale()); - Carbon::setTestNow(null); + CarbonImmutable::setTestNow(null); } - public function testLocaleIsSentWithModelPreferredLocale() + public function testLocaleIsSentWithModelPreferredLocale(): void { $recipient = new SendingLocaleTestEmailLocaleUser([ 'email' => 'test@mail.com', @@ -114,7 +114,7 @@ public function testLocaleIsSentWithModelPreferredLocale() $this->assertSame($recipient->email_locale, $mailable->locale); } - public function testLocaleIsSentWithSelectedLocaleOverridingModelPreferredLocale() + public function testLocaleIsSentWithSelectedLocaleOverridingModelPreferredLocale(): void { $recipient = new SendingLocaleTestEmailLocaleUser([ 'email' => 'test@mail.com', @@ -129,7 +129,7 @@ public function testLocaleIsSentWithSelectedLocaleOverridingModelPreferredLocale ); } - public function testLocaleIsSentWithModelPreferredLocaleWillIgnorePreferredLocaleOfTheCcRecipient() + public function testLocaleIsSentWithModelPreferredLocaleWillIgnorePreferredLocaleOfTheCcRecipient(): void { $toRecipient = new SendingLocaleTestEmailLocaleUser([ 'email' => 'test@mail.com', @@ -149,7 +149,7 @@ public function testLocaleIsSentWithModelPreferredLocaleWillIgnorePreferredLocal ); } - public function testLocaleIsNotSentWithModelPreferredLocaleWhenThereAreMultipleRecipients() + public function testLocaleIsNotSentWithModelPreferredLocaleWhenThereAreMultipleRecipients(): void { $recipients = [ new SendingLocaleTestEmailLocaleUser([ @@ -170,7 +170,7 @@ public function testLocaleIsNotSentWithModelPreferredLocaleWhenThereAreMultipleR ); } - public function testLocaleIsSetBackToDefaultAfterMailSent() + public function testLocaleIsSetBackToDefaultAfterMailSent(): void { Mail::to('test@mail.com')->locale('ar')->send(new SendingLocaleTestMail); Mail::to('test@mail.com')->send(new SendingLocaleTestMail); diff --git a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php index 828dfb7b0..736667875 100644 --- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php +++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php @@ -14,7 +14,7 @@ use Hypervel\Notifications\Messages\MailMessage; use Hypervel\Notifications\Notifiable; use Hypervel\Notifications\Notification; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Support\Facades\Notification as NotificationFacade; use Hypervel\Support\Facades\Schema; @@ -54,7 +54,7 @@ protected function setUp(): void }); } - public function testMailIsSentWithDefaultLocale() + public function testMailIsSentWithDefaultLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ 'email' => 'taylor@laravel.com', @@ -69,7 +69,7 @@ public function testMailIsSentWithDefaultLocale() ); } - public function testMailIsSentWithFacadeSelectedLocale() + public function testMailIsSentWithFacadeSelectedLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ 'email' => 'taylor@laravel.com', @@ -84,7 +84,7 @@ public function testMailIsSentWithFacadeSelectedLocale() ); } - public function testMailIsSentWithNotificationSelectedLocale() + public function testMailIsSentWithNotificationSelectedLocale(): void { $users = [ NotifiableLocalizedUser::forceCreate([ @@ -110,7 +110,7 @@ public function testMailIsSentWithNotificationSelectedLocale() ); } - public function testMailableIsSentWithSelectedLocale() + public function testMailableIsSentWithSelectedLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ 'email' => 'taylor@laravel.com', @@ -125,12 +125,12 @@ public function testMailableIsSentWithSelectedLocale() ); } - public function testMailIsSentWithLocaleUpdatedListenersCalled() + public function testMailIsSentWithLocaleUpdatedListenersCalled(): void { - Carbon::setTestNow('2018-07-25'); + CarbonImmutable::setTestNow('2018-07-25'); Event::listen(LocaleUpdated::class, function ($event) { - Carbon::setLocale($event->locale); + CarbonImmutable::setLocale($event->locale); }); $user = NotifiableLocalizedUser::forceCreate([ @@ -152,12 +152,12 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled() $this->assertTrue($this->app->isLocale('en')); - $this->assertSame('en', Carbon::getLocale()); + $this->assertSame('en', CarbonImmutable::getLocale()); - Carbon::setTestNow(null); + CarbonImmutable::setTestNow(null); } - public function testLocaleIsSentWithNotifiablePreferredLocale() + public function testLocaleIsSentWithNotifiablePreferredLocale(): void { $recipient = new NotifiableEmailLocalePreferredUser([ 'email' => 'test@mail.com', @@ -172,7 +172,7 @@ public function testLocaleIsSentWithNotifiablePreferredLocale() ); } - public function testLocaleIsSentWithNotifiablePreferredLocaleForMultipleRecipients() + public function testLocaleIsSentWithNotifiablePreferredLocaleForMultipleRecipients(): void { $recipients = [ new NotifiableEmailLocalePreferredUser([ @@ -207,7 +207,7 @@ public function testLocaleIsSentWithNotifiablePreferredLocaleForMultipleRecipien ); } - public function testLocaleIsSentWithNotificationSelectedLocaleOverridingNotifiablePreferredLocale() + public function testLocaleIsSentWithNotificationSelectedLocaleOverridingNotifiablePreferredLocale(): void { $recipient = new NotifiableEmailLocalePreferredUser([ 'email' => 'test@mail.com', @@ -224,7 +224,7 @@ public function testLocaleIsSentWithNotificationSelectedLocaleOverridingNotifiab ); } - public function testLocaleIsSentWithFacadeSelectedLocaleOverridingNotifiablePreferredLocale() + public function testLocaleIsSentWithFacadeSelectedLocaleOverridingNotifiablePreferredLocale(): void { $recipient = new NotifiableEmailLocalePreferredUser([ 'email' => 'test@mail.com', @@ -269,16 +269,16 @@ public function preferredLocale(): ?string class GreetingMailNotification extends Notification { - public function via($notifiable) + public function via(mixed $notifiable): array { return [MailChannel::class]; } - public function toMail($notifiable) + public function toMail(mixed $notifiable): MailMessage { return (new MailMessage) ->greeting(__('hi')) - ->line(Carbon::tomorrow()->diffForHumans()); + ->line(CarbonImmutable::tomorrow()->diffForHumans()); } } diff --git a/tests/Mail/MailableQueuedTest.php b/tests/Mail/MailableQueuedTest.php index 09b9e139e..9ad77ed51 100644 --- a/tests/Mail/MailableQueuedTest.php +++ b/tests/Mail/MailableQueuedTest.php @@ -14,6 +14,7 @@ use Hypervel\Queue\Attributes\Connection; use Hypervel\Queue\Attributes\Delay; use Hypervel\Queue\Attributes\Queue as QueueAttribute; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Testing\Fakes\QueueFake; use Hypervel\Testbench\TestCase; use Hypervel\View\Factory; @@ -220,6 +221,18 @@ public function testDelayedMailablePreservesZeroQueueAndDefaultsEmptyQueue(): vo $defaultQueue->assertPushedOn(null, SendQueuedMailable::class); } + public function testQueuedMailableAcceptsImmutableRetryUntilProperty(): void + { + $retryUntil = CarbonImmutable::parse('2026-07-23 12:34:56'); + $mailable = new MailableQueueableStubWithRetryUntil; + $mailable->retryUntil = $retryUntil; + + $this->assertSame( + $retryUntil, + (new SendQueuedMailable($mailable))->retryUntil() + ); + } + public function testQueuedMailableForwardsDeduplicationIdMethodToQueueJob() { $queueFake = new QueueFake($this->app); @@ -317,6 +330,11 @@ public function deduplicationId($payload, $queue) } } +class MailableQueueableStubWithRetryUntil extends MailableQueueableStub +{ + public CarbonImmutable $retryUntil; +} + #[Connection('redis')] #[QueueAttribute('mail-queue')] class MailableQueueableStubWithQueueAndConnectionAttributes extends MailableQueueableStub diff --git a/tests/Notifications/NotificationSendQueuedNotificationTest.php b/tests/Notifications/NotificationSendQueuedNotificationTest.php index 2618ae946..5e15574c1 100644 --- a/tests/Notifications/NotificationSendQueuedNotificationTest.php +++ b/tests/Notifications/NotificationSendQueuedNotificationTest.php @@ -11,6 +11,7 @@ use Hypervel\Notifications\Notifiable; use Hypervel\Notifications\Notification; use Hypervel\Notifications\SendQueuedNotifications; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Tests\TestCase; use Mockery as m; @@ -63,6 +64,17 @@ public function testNotificationCanSetMaxExceptions() $this->assertEquals(23, $job->maxExceptions); } + + public function testNotificationAcceptsImmutableRetryUntilMethod(): void + { + $retryUntil = CarbonImmutable::parse('2026-07-23 12:34:56'); + $notification = new TestNotificationWithRetryUntil($retryUntil); + + $this->assertSame( + $retryUntil, + (new SendQueuedNotifications('notifiable', $notification))->retryUntil() + ); + } } class NotifiableUser extends Model @@ -77,3 +89,16 @@ class NotifiableUser extends Model class TestNotification extends Notification { } + +class TestNotificationWithRetryUntil extends Notification +{ + public function __construct( + private CarbonImmutable $retryUntil + ) { + } + + public function retryUntil(): CarbonImmutable + { + return $this->retryUntil; + } +} From 0313636f6d2c7c30a679a64558f8ff33290299fa Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:16:29 +0000 Subject: [PATCH 14/34] refactor(auth): use immutable authentication deadlines Use Hypervel CarbonImmutable for verification-link expiry and password-token repository deadlines so framework-owned authentication timestamps cannot be mutated after construction. Preserve existing expiry calculations, storage formats, and comparison behavior while capturing all modifier results explicitly. Update database and cache token repository coverage, verification-related middleware fixtures, and exact date expectations. --- src/auth/src/Notifications/VerifyEmail.php | 4 +- .../src/Passwords/CacheTokenRepository.php | 8 +-- .../src/Passwords/DatabaseTokenRepository.php | 10 ++-- .../Auth/AuthDatabaseTokenRepositoryTest.php | 42 +++++++--------- tests/Auth/CacheTokenRepositoryTest.php | 50 +++++++------------ tests/Auth/RequirePasswordMiddlewareTest.php | 42 ++++++++-------- 6 files changed, 69 insertions(+), 87 deletions(-) diff --git a/src/auth/src/Notifications/VerifyEmail.php b/src/auth/src/Notifications/VerifyEmail.php index cc251a5a0..418ac1d66 100644 --- a/src/auth/src/Notifications/VerifyEmail.php +++ b/src/auth/src/Notifications/VerifyEmail.php @@ -7,7 +7,7 @@ use Closure; use Hypervel\Notifications\Messages\MailMessage; use Hypervel\Notifications\Notification; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Config; use Hypervel\Support\Facades\Lang; use Hypervel\Support\Facades\URL; @@ -73,7 +73,7 @@ protected function verificationUrl(mixed $notifiable): string return URL::temporarySignedRoute( 'verification.verify', - Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), + CarbonImmutable::now()->addMinutes(Config::get('auth.verification.expire', 60)), [ 'id' => $notifiable->getKey(), 'hash' => sha1($notifiable->getEmailForVerification()), diff --git a/src/auth/src/Passwords/CacheTokenRepository.php b/src/auth/src/Passwords/CacheTokenRepository.php index 92693a882..a1dab9d11 100644 --- a/src/auth/src/Passwords/CacheTokenRepository.php +++ b/src/auth/src/Passwords/CacheTokenRepository.php @@ -7,7 +7,7 @@ use Hypervel\Cache\Repository; use Hypervel\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Hypervel\Contracts\Hashing\Hasher as HasherContract; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use SensitiveParameter; @@ -41,7 +41,7 @@ public function create(CanResetPasswordContract $user): string $this->cache->put( $this->cacheKey($user), - [$this->hasher->make($token), Carbon::now()->format($this->format)], + [$this->hasher->make($token), CarbonImmutable::now()->format($this->format)], $this->expires, ); @@ -65,7 +65,7 @@ public function exists(CanResetPasswordContract $user, #[SensitiveParameter] str */ protected function tokenExpired(string $createdAt): bool { - return Carbon::createFromFormat($this->format, $createdAt)->addSeconds($this->expires)->isPast(); + return CarbonImmutable::createFromFormat($this->format, $createdAt)->addSeconds($this->expires)->isPast(); } /** @@ -87,7 +87,7 @@ protected function tokenRecentlyCreated(string $createdAt): bool return false; } - return Carbon::createFromFormat($this->format, $createdAt)->addSeconds( + return CarbonImmutable::createFromFormat($this->format, $createdAt)->addSeconds( $this->throttle )->isFuture(); } diff --git a/src/auth/src/Passwords/DatabaseTokenRepository.php b/src/auth/src/Passwords/DatabaseTokenRepository.php index 02e8fb9d7..6941637db 100755 --- a/src/auth/src/Passwords/DatabaseTokenRepository.php +++ b/src/auth/src/Passwords/DatabaseTokenRepository.php @@ -8,7 +8,7 @@ use Hypervel\Contracts\Hashing\Hasher as HasherContract; use Hypervel\Database\ConnectionInterface; use Hypervel\Database\Query\Builder; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use SensitiveParameter; @@ -62,7 +62,7 @@ protected function deleteExisting(CanResetPasswordContract $user): int */ protected function getPayload(string $email, #[SensitiveParameter] string $token): array { - return ['email' => $email, 'token' => $this->hasher->make($token), 'created_at' => new Carbon]; + return ['email' => $email, 'token' => $this->hasher->make($token), 'created_at' => new CarbonImmutable]; } /** @@ -85,7 +85,7 @@ public function exists(CanResetPasswordContract $user, #[SensitiveParameter] str */ protected function tokenExpired(string $createdAt): bool { - return Carbon::parse($createdAt)->addSeconds($this->expires)->isPast(); + return CarbonImmutable::parse($createdAt)->addSeconds($this->expires)->isPast(); } /** @@ -110,7 +110,7 @@ protected function tokenRecentlyCreated(string $createdAt): bool return false; } - return Carbon::parse($createdAt)->addSeconds( + return CarbonImmutable::parse($createdAt)->addSeconds( $this->throttle )->isFuture(); } @@ -128,7 +128,7 @@ public function delete(CanResetPasswordContract $user): void */ public function deleteExpired(): void { - $expiredAt = Carbon::now()->subSeconds($this->expires); + $expiredAt = CarbonImmutable::now()->subSeconds($this->expires); $this->getTable()->where('created_at', '<', $expiredAt)->delete(); } diff --git a/tests/Auth/AuthDatabaseTokenRepositoryTest.php b/tests/Auth/AuthDatabaseTokenRepositoryTest.php index 63918d7ad..c10124387 100755 --- a/tests/Auth/AuthDatabaseTokenRepositoryTest.php +++ b/tests/Auth/AuthDatabaseTokenRepositoryTest.php @@ -9,13 +9,13 @@ use Hypervel\Contracts\Hashing\Hasher; use Hypervel\Database\ConnectionInterface; use Hypervel\Database\Query\Builder; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; class AuthDatabaseTokenRepositoryTest extends TestCase { - public function testCreateInsertsNewRecordIntoTable() + public function testCreateInsertsNewRecordIntoTable(): void { $repo = $this->getRepo(); $repo->getHasher()->shouldReceive('make')->once()->andReturn('hashed-token'); @@ -32,7 +32,7 @@ public function testCreateInsertsNewRecordIntoTable() $this->assertGreaterThan(1, strlen($results)); } - public function testExistReturnsFalseIfNoRowFoundForUser() + public function testExistReturnsFalseIfNoRowFoundForUser(): void { $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); @@ -44,12 +44,12 @@ public function testExistReturnsFalseIfNoRowFoundForUser() $this->assertFalse($repo->exists($user, 'token')); } - public function testExistReturnsFalseIfRecordIsExpired() + public function testExistReturnsFalseIfRecordIsExpired(): void { $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = Carbon::now()->subSeconds(300000)->toDateTimeString(); + $date = CarbonImmutable::now()->subSeconds(300000)->toDateTimeString(); $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); @@ -57,13 +57,13 @@ public function testExistReturnsFalseIfRecordIsExpired() $this->assertFalse($repo->exists($user, 'token')); } - public function testExistReturnsTrueIfValidRecordExists() + public function testExistReturnsTrueIfValidRecordExists(): void { $repo = $this->getRepo(); $repo->getHasher()->shouldReceive('check')->once()->with('token', 'hashed-token')->andReturn(true); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = Carbon::now()->subMinutes(10)->toDateTimeString(); + $date = CarbonImmutable::now()->subMinutes(10)->toDateTimeString(); $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); @@ -71,13 +71,13 @@ public function testExistReturnsTrueIfValidRecordExists() $this->assertTrue($repo->exists($user, 'token')); } - public function testExistReturnsFalseIfInvalidToken() + public function testExistReturnsFalseIfInvalidToken(): void { $repo = $this->getRepo(); $repo->getHasher()->shouldReceive('check')->once()->with('wrong-token', 'hashed-token')->andReturn(false); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = Carbon::now()->subMinutes(10)->toDateTimeString(); + $date = CarbonImmutable::now()->subMinutes(10)->toDateTimeString(); $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); @@ -85,7 +85,7 @@ public function testExistReturnsFalseIfInvalidToken() $this->assertFalse($repo->exists($user, 'wrong-token')); } - public function testRecentlyCreatedReturnsFalseIfNoRowFoundForUser() + public function testRecentlyCreatedReturnsFalseIfNoRowFoundForUser(): void { $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); @@ -97,41 +97,37 @@ public function testRecentlyCreatedReturnsFalseIfNoRowFoundForUser() $this->assertFalse($repo->recentlyCreatedToken($user)); } - public function testRecentlyCreatedReturnsTrueIfRecordIsRecentlyCreated() + public function testRecentlyCreatedReturnsTrueIfRecordIsRecentlyCreated(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = Carbon::now()->subSeconds(59)->toDateTimeString(); + $date = CarbonImmutable::now()->subSeconds(59)->toDateTimeString(); $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); $this->assertTrue($repo->recentlyCreatedToken($user)); - - Carbon::setTestNow(); } - public function testRecentlyCreatedReturnsFalseIfValidRecordExists() + public function testRecentlyCreatedReturnsFalseIfValidRecordExists(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = Carbon::now()->subSeconds(61)->toDateTimeString(); + $date = CarbonImmutable::now()->subSeconds(61)->toDateTimeString(); $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); $this->assertFalse($repo->recentlyCreatedToken($user)); - - Carbon::setTestNow(); } - public function testDeleteMethodDeletesByToken() + public function testDeleteMethodDeletesByToken(): void { $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); @@ -143,7 +139,7 @@ public function testDeleteMethodDeletesByToken() $repo->delete($user); } - public function testDeleteExpiredMethodDeletesExpiredTokens() + public function testDeleteExpiredMethodDeletesExpiredTokens(): void { $repo = $this->getRepo(); $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); @@ -153,7 +149,7 @@ public function testDeleteExpiredMethodDeletesExpiredTokens() $repo->deleteExpired(); } - protected function getRepo() + protected function getRepo(): DatabaseTokenRepository { return new DatabaseTokenRepository( m::mock(ConnectionInterface::class), diff --git a/tests/Auth/CacheTokenRepositoryTest.php b/tests/Auth/CacheTokenRepositoryTest.php index a0f62406b..2edf63f72 100644 --- a/tests/Auth/CacheTokenRepositoryTest.php +++ b/tests/Auth/CacheTokenRepositoryTest.php @@ -8,15 +8,15 @@ use Hypervel\Cache\Repository; use Hypervel\Contracts\Auth\CanResetPassword; use Hypervel\Contracts\Hashing\Hasher; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; class CacheTokenRepositoryTest extends TestCase { - public function testCreateStoresHashedTokenAndReturnsPlainToken() + public function testCreateStoresHashedTokenAndReturnsPlainToken(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -41,13 +41,11 @@ public function testCreateStoresHashedTokenAndReturnsPlainToken() $this->assertIsString($token); $this->assertSame(64, strlen($token)); // SHA-256 hash is 64 hex chars - - Carbon::setTestNow(); } - public function testExistsReturnsTrueForValidToken() + public function testExistsReturnsTrueForValidToken(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -63,13 +61,11 @@ public function testExistsReturnsTrueForValidToken() $repository = new CacheTokenRepository($cache, $hasher, 'test-hash-key', 3600); $this->assertTrue($repository->exists($user, 'plain_token')); - - Carbon::setTestNow(); } - public function testExistsReturnsFalseForExpiredToken() + public function testExistsReturnsFalseForExpiredToken(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -84,13 +80,11 @@ public function testExistsReturnsFalseForExpiredToken() $repository = new CacheTokenRepository($cache, $hasher, 'test-hash-key', 3600); $this->assertFalse($repository->exists($user, 'plain_token')); - - Carbon::setTestNow(); } - public function testExistsReturnsFalseForInvalidHash() + public function testExistsReturnsFalseForInvalidHash(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -106,13 +100,11 @@ public function testExistsReturnsFalseForInvalidHash() $repository = new CacheTokenRepository($cache, $hasher, 'test-hash-key', 3600); $this->assertFalse($repository->exists($user, 'wrong_token')); - - Carbon::setTestNow(); } - public function testRecentlyCreatedTokenReturnsTrueWithinThrottle() + public function testRecentlyCreatedTokenReturnsTrueWithinThrottle(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -128,13 +120,11 @@ public function testRecentlyCreatedTokenReturnsTrueWithinThrottle() $repository = new CacheTokenRepository($cache, $hasher, 'test-hash-key', 3600, 60); $this->assertTrue($repository->recentlyCreatedToken($user)); - - Carbon::setTestNow(); } - public function testRecentlyCreatedTokenReturnsFalseAfterThrottle() + public function testRecentlyCreatedTokenReturnsFalseAfterThrottle(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -150,13 +140,11 @@ public function testRecentlyCreatedTokenReturnsFalseAfterThrottle() $repository = new CacheTokenRepository($cache, $hasher, 'test-hash-key', 3600, 60); $this->assertFalse($repository->recentlyCreatedToken($user)); - - Carbon::setTestNow(); } - public function testRecentlyCreatedTokenReturnsFalseWhenThrottleIsZero() + public function testRecentlyCreatedTokenReturnsFalseWhenThrottleIsZero(): void { - Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2024, 1, 1, 12, 0, 0)); $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -172,11 +160,9 @@ public function testRecentlyCreatedTokenReturnsFalseWhenThrottleIsZero() $repository = new CacheTokenRepository($cache, $hasher, 'test-hash-key', 3600, 0); $this->assertFalse($repository->recentlyCreatedToken($user)); - - Carbon::setTestNow(); } - public function testDeleteRemovesFromCache() + public function testDeleteRemovesFromCache(): void { $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -190,7 +176,7 @@ public function testDeleteRemovesFromCache() $repository->delete($user); } - public function testCacheKeyHashesEmail() + public function testCacheKeyHashesEmail(): void { $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); @@ -202,7 +188,7 @@ public function testCacheKeyHashesEmail() $this->assertSame(hash('sha256', 'foo@bar.com'), $repository->cacheKey($user)); } - public function testDeleteExpiredIsNoOp() + public function testDeleteExpiredIsNoOp(): void { $cache = m::mock(Repository::class); $hasher = m::mock(Hasher::class); diff --git a/tests/Auth/RequirePasswordMiddlewareTest.php b/tests/Auth/RequirePasswordMiddlewareTest.php index a74e0c5cb..40349f4f2 100644 --- a/tests/Auth/RequirePasswordMiddlewareTest.php +++ b/tests/Auth/RequirePasswordMiddlewareTest.php @@ -14,7 +14,7 @@ use Hypervel\Http\Middleware\PrefersJsonResponses; use Hypervel\Http\RedirectResponse; use Hypervel\Http\Request; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; use Symfony\Component\HttpFoundation\Response; @@ -36,7 +36,7 @@ public function testUsingGeneratesCorrectMiddlewareString(): void public function testPassesThroughWhenPasswordConfirmationIsFresh(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(1000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(1000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -53,12 +53,12 @@ public function testPassesThroughWhenPasswordConfirmationIsFresh(): void $this->assertSame($expectedResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testReturnsJson423WhenStaleAndRequestExpectsJson(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(20000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(20000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -83,12 +83,12 @@ public function testReturnsJson423WhenStaleAndRequestExpectsJson(): void $this->assertSame($jsonResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testPreferredJsonResponsesTurnWildcardRequestsIntoJsonResponses(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(20000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(20000)); try { $session = m::mock(Session::class); @@ -121,13 +121,13 @@ public function testPreferredJsonResponsesTurnWildcardRequestsIntoJsonResponses( $this->assertSame($jsonResponse, $response); } finally { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } } public function testPreferredJsonResponsesPreserveExplicitHtmlRedirects(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(20000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(20000)); try { $session = m::mock(Session::class); @@ -164,13 +164,13 @@ public function testPreferredJsonResponsesPreserveExplicitHtmlRedirects(): void $this->assertSame($redirectResponse, $response); } finally { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } } public function testRedirectsWhenStaleAndRequestDoesNotExpectJson(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(20000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(20000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -198,12 +198,12 @@ public function testRedirectsWhenStaleAndRequestDoesNotExpectJson(): void $this->assertSame($redirectResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testCustomRouteIsUsed(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(20000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(20000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -235,12 +235,12 @@ public function testCustomRouteIsUsed(): void $this->assertSame($redirectResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testCustomTimeoutIsHonored(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(1000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(1000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -276,12 +276,12 @@ public function testCustomTimeoutIsHonored(): void ); $this->assertSame($jsonResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testConfirmationIsScopedToCurrentGuard(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(20000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(20000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -303,12 +303,12 @@ public function testConfirmationIsScopedToCurrentGuard(): void $this->assertSame($jsonResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testPerGuardTimeoutIsHonored(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(1000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(1000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -345,12 +345,12 @@ public function testPerGuardTimeoutIsHonored(): void $result = $middleware->handle($request, fn () => $expectedResponse); $this->assertSame($expectedResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testRouteParameterOverridesPerGuardTimeout(): void { - Carbon::setTestNow(Carbon::createFromTimestamp(1000)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromTimestamp(1000)); $session = m::mock(Session::class); $session->shouldReceive('get') @@ -382,7 +382,7 @@ public function testRouteParameterOverridesPerGuardTimeout(): void $this->assertSame($jsonResponse, $result); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } private function middleware( From 4aef2e250e2769ec4fa78eccf944e29ae1143513 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:16:39 +0000 Subject: [PATCH 15/34] refactor(cache): make expiry records immutable Store cache item and lock expiry values as exact Hypervel CarbonImmutable instances across array, worker-array, repository, session, stack, and Swoole stores. Keep public DateInterval and timestamp behavior unchanged while preventing shared worker records from being shifted by later modifier calls. Update cache unit and integration suites for immutable record shapes, TTL handling, locks, tags, memoization, sessions, database stores, Redis, and Swoole timers. --- src/cache/src/AbstractArrayStore.php | 6 +- src/cache/src/ArrayLock.php | 10 +-- src/cache/src/ArrayStore.php | 8 +- src/cache/src/Repository.php | 10 +-- src/cache/src/SessionStore.php | 6 +- src/cache/src/StackStore.php | 6 +- src/cache/src/SwooleStore.php | 6 +- src/cache/src/WorkerArrayStore.php | 8 +- tests/Cache/CacheArrayStoreTest.php | 82 +++++++++---------- tests/Cache/CacheDatabaseLockTest.php | 14 ++-- tests/Cache/CacheDatabaseStoreTest.php | 8 +- tests/Cache/CacheFileStoreTest.php | 16 ++-- tests/Cache/CacheMemoizedStoreTest.php | 8 +- tests/Cache/CacheRepositoryTest.php | 40 ++++----- tests/Cache/CacheSessionStoreTest.php | 30 +++---- tests/Cache/CacheStackStoreTest.php | 34 ++++---- tests/Cache/CacheSwooleStoreIntervalTest.php | 62 +++++++------- tests/Cache/CacheSwooleStoreTest.php | 48 +++++------ tests/Cache/CacheWorkerArrayStoreTest.php | 18 ++-- .../Operations/AllTag/FlushStaleTest.php | 6 +- tests/Cache/Redis/RedisCacheTestCase.php | 4 +- .../Redis/TtlHandlingIntegrationTest.php | 6 +- tests/Integration/Cache/RepositoryTest.php | 44 +++++----- 23 files changed, 240 insertions(+), 240 deletions(-) diff --git a/src/cache/src/AbstractArrayStore.php b/src/cache/src/AbstractArrayStore.php index d233edc06..7b49fe9ff 100644 --- a/src/cache/src/AbstractArrayStore.php +++ b/src/cache/src/AbstractArrayStore.php @@ -6,7 +6,7 @@ use Hypervel\Contracts\Cache\CanFlushLocks; use Hypervel\Contracts\Cache\LockProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use RuntimeException; @@ -226,14 +226,14 @@ public function hasSeparateLockStore(): bool /** * Get the lock record for the given name. * - * @return null|array{owner: ?string, expiresAt: ?Carbon} + * @return null|array{owner: ?string, expiresAt: ?CarbonImmutable} */ abstract public function getLockRecord(string $name): ?array; /** * Store the lock record for the given name. * - * @param array{owner: ?string, expiresAt: ?Carbon} $record + * @param array{owner: ?string, expiresAt: ?CarbonImmutable} $record */ abstract public function putLockRecord(string $name, array $record): void; diff --git a/src/cache/src/ArrayLock.php b/src/cache/src/ArrayLock.php index 06620745a..48db320df 100644 --- a/src/cache/src/ArrayLock.php +++ b/src/cache/src/ArrayLock.php @@ -5,7 +5,7 @@ namespace Hypervel\Cache; use Hypervel\Contracts\Cache\RefreshableLock; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use InvalidArgumentException; class ArrayLock extends Lock implements RefreshableLock @@ -31,7 +31,7 @@ public function __construct(AbstractArrayStore $store, string $name, int $second public function acquire(): bool { $record = $this->store->getLockRecord($this->name); - $expiration = $record['expiresAt'] ?? Carbon::now()->addSecond(); + $expiration = $record['expiresAt'] ?? CarbonImmutable::now()->addSecond(); if ($record !== null && $expiration->isFuture()) { return false; @@ -40,7 +40,7 @@ public function acquire(): bool // WorkerArrayStore shares this check/write path across coroutines; keep it non-yielding. $this->store->putLockRecord($this->name, [ 'owner' => $this->owner, - 'expiresAt' => $this->seconds === 0 ? null : Carbon::now()->addSeconds($this->seconds), + 'expiresAt' => $this->seconds === 0 ? null : CarbonImmutable::now()->addSeconds($this->seconds), ]); return true; @@ -111,7 +111,7 @@ public function refresh(?int $seconds = null): bool return false; } - $record['expiresAt'] = Carbon::now()->addSeconds($seconds); + $record['expiresAt'] = CarbonImmutable::now()->addSeconds($seconds); $this->store->putLockRecord($this->name, $record); return true; @@ -138,6 +138,6 @@ public function getRemainingLifetime(): ?float return null; } - return (float) Carbon::now()->diffInSeconds($expiresAt); + return (float) CarbonImmutable::now()->diffInSeconds($expiresAt); } } diff --git a/src/cache/src/ArrayStore.php b/src/cache/src/ArrayStore.php index 437fa3cf1..e762b75fe 100644 --- a/src/cache/src/ArrayStore.php +++ b/src/cache/src/ArrayStore.php @@ -5,7 +5,7 @@ namespace Hypervel\Cache; use Hypervel\Context\CoroutineContext; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; class ArrayStore extends AbstractArrayStore { @@ -109,7 +109,7 @@ protected function getCacheItems(): array /** * Get the lock record for the given name. * - * @return null|array{owner: ?string, expiresAt: ?Carbon} + * @return null|array{owner: ?string, expiresAt: ?CarbonImmutable} */ public function getLockRecord(string $name): ?array { @@ -119,7 +119,7 @@ public function getLockRecord(string $name): ?array /** * Store the lock record for the given name. * - * @param array{owner: ?string, expiresAt: ?Carbon} $record + * @param array{owner: ?string, expiresAt: ?CarbonImmutable} $record */ public function putLockRecord(string $name, array $record): void { @@ -152,7 +152,7 @@ public function clearLockRecords(): void /** * Get all lock records. * - * @return array + * @return array */ protected function getLockRecords(): array { diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 376bf6564..38a0ad7bf 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -35,7 +35,7 @@ use Hypervel\Contracts\Cache\Repository as CacheContract; use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Events\Dispatcher; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; @@ -623,13 +623,13 @@ public function flexible(UnitEnum|string $key, array $ttl, mixed $callback, ?arr $this->putMany([ $key => $stored, - $markerKey => Carbon::now()->getTimestamp(), + $markerKey => CarbonImmutable::now()->getTimestamp(), ], $ttl[1]); return NullSentinel::unwrap($stored); } - if (($created + $this->getSeconds($ttl[0])) > Carbon::now()->getTimestamp()) { + if (($created + $this->getSeconds($ttl[0])) > CarbonImmutable::now()->getTimestamp()) { return NullSentinel::unwrap($value); } @@ -647,7 +647,7 @@ public function flexible(UnitEnum|string $key, array $ttl, mixed $callback, ?arr $this->putMany([ $key => value($callback), - $markerKey => Carbon::now()->getTimestamp(), + $markerKey => CarbonImmutable::now()->getTimestamp(), ], $ttl[1]); }); }; @@ -1033,7 +1033,7 @@ protected function getSeconds(DateInterval|DateTimeInterface|int $ttl): int if ($duration instanceof DateTimeInterface) { $duration = (int) ceil( - Carbon::now()->diffInMilliseconds($duration, false) / 1000 + CarbonImmutable::now()->diffInMilliseconds($duration, false) / 1000 ); } diff --git a/src/cache/src/SessionStore.php b/src/cache/src/SessionStore.php index b5ced9aae..bd267f3bf 100644 --- a/src/cache/src/SessionStore.php +++ b/src/cache/src/SessionStore.php @@ -6,7 +6,7 @@ use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Session\Session; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; class SessionStore implements Store @@ -60,7 +60,7 @@ public function get(string $key): mixed */ protected function isExpired(int|float $expiresAt): bool { - return $expiresAt > 0 && (Carbon::now()->getPreciseTimestamp(3) / 1000) >= $expiresAt; + return $expiresAt > 0 && (CarbonImmutable::now()->getPreciseTimestamp(3) / 1000) >= $expiresAt; } /** @@ -81,7 +81,7 @@ public function put(string $key, mixed $value, int $seconds): bool */ protected function toTimestamp(int $seconds): float { - return $seconds > 0 ? (Carbon::now()->getPreciseTimestamp(3) / 1000) + $seconds : 0.0; + return $seconds > 0 ? (CarbonImmutable::now()->getPreciseTimestamp(3) / 1000) + $seconds : 0.0; } /** diff --git a/src/cache/src/StackStore.php b/src/cache/src/StackStore.php index 5a6205a4f..32d51ba2d 100644 --- a/src/cache/src/StackStore.php +++ b/src/cache/src/StackStore.php @@ -10,7 +10,7 @@ use Hypervel\Contracts\Cache\Lock; use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Cache\Store; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use InvalidArgumentException; class StackStore extends TaggableStore implements CanFlushLocks, LockProvider @@ -319,7 +319,7 @@ protected function putToStore(StackStoreProxy $store, string $key, array $record return $store->forever($key, $record); } - $currentTimestamp = Carbon::now()->getTimestamp(); + $currentTimestamp = CarbonImmutable::now()->getTimestamp(); $value = $record['value']; $expiration = $record['expiration'] ?? $currentTimestamp + $record['ttl']; $ttl = $record['ttl'] ?? $record['expiration'] - $currentTimestamp; @@ -355,7 +355,7 @@ protected function putToStoreTagged(StackStoreProxy $proxy, array $tags, string return $store->tags($tags)->put($key, $record, $proxyTtl); } - $currentTimestamp = Carbon::now()->getTimestamp(); + $currentTimestamp = CarbonImmutable::now()->getTimestamp(); $value = $record['value']; $expiration = $record['expiration'] ?? $currentTimestamp + $record['ttl']; $ttl = $record['ttl'] ?? $record['expiration'] - $currentTimestamp; diff --git a/src/cache/src/SwooleStore.php b/src/cache/src/SwooleStore.php index c2831f90a..4a078e54d 100644 --- a/src/cache/src/SwooleStore.php +++ b/src/cache/src/SwooleStore.php @@ -10,7 +10,7 @@ use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Debug\ExceptionHandler; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use InvalidArgumentException; use Laravel\SerializableClosure\SerializableClosure; use RuntimeException; @@ -548,8 +548,8 @@ protected function evictRecordsIfNeeded(): void */ protected function getCurrentTimestamp(): float { - return Carbon::hasTestNow() - ? Carbon::now()->getPreciseTimestamp(6) / 1000000 + return CarbonImmutable::hasTestNow() + ? CarbonImmutable::now()->getPreciseTimestamp(6) / 1000000 : microtime(true); } diff --git a/src/cache/src/WorkerArrayStore.php b/src/cache/src/WorkerArrayStore.php index 74c12a592..bcd6c566d 100644 --- a/src/cache/src/WorkerArrayStore.php +++ b/src/cache/src/WorkerArrayStore.php @@ -4,7 +4,7 @@ namespace Hypervel\Cache; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; class WorkerArrayStore extends AbstractArrayStore { @@ -18,7 +18,7 @@ class WorkerArrayStore extends AbstractArrayStore /** * The array of locks. * - * @var array + * @var array */ protected array $locks = []; @@ -77,7 +77,7 @@ protected function getCacheItems(): array /** * Get the lock record for the given name. * - * @return null|array{owner: ?string, expiresAt: ?Carbon} + * @return null|array{owner: ?string, expiresAt: ?CarbonImmutable} */ public function getLockRecord(string $name): ?array { @@ -87,7 +87,7 @@ public function getLockRecord(string $name): ?array /** * Store the lock record for the given name. * - * @param array{owner: ?string, expiresAt: ?Carbon} $record + * @param array{owner: ?string, expiresAt: ?CarbonImmutable} $record */ public function putLockRecord(string $name, array $record): void { diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 571aeef4e..fa758e70f 100644 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -6,7 +6,7 @@ use Hypervel\Cache\ArrayStore; use Hypervel\Contracts\Cache\RefreshableLock; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use InvalidArgumentException; use stdClass; @@ -25,13 +25,13 @@ public function testCacheTtl(): void { $store = new ArrayStore; - Carbon::setTestNow('2000-01-01 00:00:00.500'); // 500 milliseconds past + CarbonImmutable::setTestNow('2000-01-01 00:00:00.500'); // 500 milliseconds past $store->put('hello', 'world', 1); - Carbon::setTestNow('2000-01-01 00:00:01.499'); // progress 0.999 seconds + CarbonImmutable::setTestNow('2000-01-01 00:00:01.499'); // progress 0.999 seconds $this->assertSame('world', $store->get('hello')); - Carbon::setTestNow('2000-01-01 00:00:01.500'); // progress 0.001 seconds. 1 second since putting into cache. + CarbonImmutable::setTestNow('2000-01-01 00:00:01.500'); // progress 0.001 seconds. 1 second since putting into cache. $this->assertNull($store->get('hello')); } @@ -55,12 +55,12 @@ public function testMultipleItemsCanBeSetAndRetrieved(): void public function testItemsCanExpire(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $store->put('foo', 'bar', 10); - Carbon::setTestNow(Carbon::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); $result = $store->get('foo'); $this->assertNull($result); @@ -70,12 +70,12 @@ public function testTouchExtendsTtl(): void { $store = new ArrayStore; - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store->put('key', 'value', 30); $store->touch('key', 60); - Carbon::setTestNow($now->addSeconds(45)); + CarbonImmutable::setTestNow($now->addSeconds(45)); $this->assertSame('value', $store->get('key')); } @@ -134,18 +134,18 @@ public function testNonExistingKeysCanBeIncremented(): void $this->assertEquals(1, $store->get('foo')); // Will be there forever - Carbon::setTestNow(Carbon::now()->addYears(10)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addYears(10)); $this->assertEquals(1, $store->get('foo')); } public function testExpiredKeysAreIncrementedLikeNonExistingKeys(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $store->put('foo', 999, 10); - Carbon::setTestNow(Carbon::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); $result = $store->increment('foo'); $this->assertEquals(1, $result); @@ -216,19 +216,19 @@ public function testCannotAcquireLockTwice(): void public function testCanAcquireLockAgainAfterExpiry(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - Carbon::setTestNow(Carbon::now()->addSeconds(10)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)); $this->assertTrue($lock->acquire()); } public function testExpiredLockIsNotLockedOrOwned(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); @@ -237,7 +237,7 @@ public function testExpiredLockIsNotLockedOrOwned(): void $this->assertTrue($lock->isLocked()); $this->assertTrue($lock->isOwnedByCurrentProcess()); - Carbon::setTestNow($now->copy()->addSeconds(10)); + CarbonImmutable::setTestNow($now->addSeconds(10)); $this->assertFalse($lock->isLocked()); $this->assertFalse($lock->isOwnedByCurrentProcess()); @@ -246,12 +246,12 @@ public function testExpiredLockIsNotLockedOrOwned(): void public function testLockExpirationLowerBoundary(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - Carbon::setTestNow(Carbon::now()->addSeconds(10)->subMicrosecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->subMicrosecond()); $this->assertFalse($lock->acquire()); } @@ -261,7 +261,7 @@ public function testLockWithNoExpirationNeverExpires(): void $store = new ArrayStore; $lock = $store->lock('foo'); $lock->acquire(); - Carbon::setTestNow(Carbon::now()->addYears(100)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addYears(100)); $this->assertFalse($lock->acquire()); } @@ -288,13 +288,13 @@ public function testAnotherOwnerCannotReleaseLock(): void public function testExpiredLockCannotBeReleasedByOldOwner(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - Carbon::setTestNow($now->copy()->addSeconds(10)); + CarbonImmutable::setTestNow($now->addSeconds(10)); $this->assertFalse($lock->release()); } @@ -385,13 +385,13 @@ public function testRestoringNonExistingLockDoesNotOwnAnything(): void public function testCanGetAll(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore(false); $store->put('foo', 'bar', 10); $this->assertEquals([ - 'foo' => ['value' => 'bar', 'expiresAt' => Carbon::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000], + 'foo' => ['value' => 'bar', 'expiresAt' => CarbonImmutable::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000], ], $store->all()); } @@ -409,7 +409,7 @@ public function testSeparateArrayStoreInstancesDoNotShareContextData(): void public function testAllOnlyReturnsCurrentStoreContextData(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $first = new ArrayStore; $second = new ArrayStore; @@ -429,27 +429,27 @@ public function testAllOnlyReturnsCurrentStoreContextData(): void public function testCanGetAllWhenSerialized(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore(true); $store->put('foo', 'bar', 10); $this->assertEquals([ - 'foo' => ['value' => 'bar', 'expiresAt' => $expiresAt = (Carbon::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000)], + 'foo' => ['value' => 'bar', 'expiresAt' => $expiresAt = (CarbonImmutable::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000)], ], $store->all()); // Now let's put a serializable value in there $store->forget('foo'); - $store->put('foo', Carbon::now(), 10); + $store->put('foo', CarbonImmutable::now(), 10); $this->assertEquals([ 'foo' => [ - 'value' => Carbon::now(), + 'value' => CarbonImmutable::now(), 'expiresAt' => $expiresAt, ], ], $store->all()); $this->assertEquals( - serialize(Carbon::now()), + serialize(CarbonImmutable::now()), $store->all(false)['foo']['value'] ); } @@ -464,27 +464,27 @@ public function testLockImplementsRefreshableLock(): void public function testRefreshExtendsLockExpiration(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - Carbon::setTestNow(Carbon::now()->addSeconds(5)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(5)); $this->assertTrue($lock->refresh()); // Lock should now expire 10 seconds from now, not 5 - Carbon::setTestNow(Carbon::now()->addSeconds(9)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(9)); $this->assertFalse($store->lock('foo', 10)->acquire()); - Carbon::setTestNow(Carbon::now()->addSeconds(2)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(2)); $this->assertTrue($store->lock('foo', 10)->acquire()); } public function testRefreshWithCustomTtl(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); @@ -493,10 +493,10 @@ public function testRefreshWithCustomTtl(): void $this->assertTrue($lock->refresh(30)); // Lock should now expire 30 seconds from now - Carbon::setTestNow(Carbon::now()->addSeconds(29)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(29)); $this->assertFalse($store->lock('foo', 10)->acquire()); - Carbon::setTestNow(Carbon::now()->addSeconds(2)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(2)); $this->assertTrue($store->lock('foo', 10)->acquire()); } @@ -520,13 +520,13 @@ public function testRefreshReturnsFalseWhenNotOwned(): void public function testRefreshReturnsFalseWhenExpired(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - Carbon::setTestNow($now->copy()->addSeconds(10)); + CarbonImmutable::setTestNow($now->addSeconds(10)); $this->assertFalse($lock->refresh()); } @@ -591,7 +591,7 @@ public function testRefreshWithInvalidTtlThrowsEvenWhenNotOwned(): void public function testGetRemainingLifetimeReturnsSeconds(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); @@ -599,7 +599,7 @@ public function testGetRemainingLifetimeReturnsSeconds(): void $this->assertSame(10.0, $lock->getRemainingLifetime()); - Carbon::setTestNow(Carbon::now()->addSeconds(3)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(3)); $this->assertSame(7.0, $lock->getRemainingLifetime()); } @@ -622,13 +622,13 @@ public function testGetRemainingLifetimeReturnsNullForInfiniteLock(): void public function testGetRemainingLifetimeReturnsNullWhenExpired(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - Carbon::setTestNow(Carbon::now()->addSeconds(15)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(15)); $this->assertNull($lock->getRemainingLifetime()); } } diff --git a/tests/Cache/CacheDatabaseLockTest.php b/tests/Cache/CacheDatabaseLockTest.php index d13d54a81..de287a22f 100644 --- a/tests/Cache/CacheDatabaseLockTest.php +++ b/tests/Cache/CacheDatabaseLockTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Cache; -use Carbon\Carbon; use Exception; use Hypervel\Cache\DatabaseLock; use Hypervel\Contracts\Cache\RefreshableLock; @@ -12,6 +11,7 @@ use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\Query\Builder; use Hypervel\Database\QueryException; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; @@ -163,7 +163,7 @@ public function testLockCanBeForceReleased(): void public function testLockWithDefaultTimeout(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$lock, $table] = $this->getLock(seconds: 0); @@ -186,7 +186,7 @@ public function testLockImplementsRefreshableLock(): void public function testRefreshExtendsLockExpiration(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$lock, $table] = $this->getLock(); $owner = $lock->owner(); @@ -204,7 +204,7 @@ public function testRefreshExtendsLockExpiration(): void public function testRefreshWithCustomTtl(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$lock, $table] = $this->getLock(); $owner = $lock->owner(); @@ -235,7 +235,7 @@ public function testRefreshReturnsFalseWhenNotOwned(): void public function testRefreshOnDefaultTimeoutLockReappliesDefaultTimeout(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$lock, $table] = $this->getLock(seconds: 0); $owner = $lock->owner(); @@ -286,7 +286,7 @@ public function testRefreshWithNegativeSecondsThrowsException(): void public function testGetRemainingLifetimeReturnsSeconds(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$lock, $table] = $this->getLock(); @@ -312,7 +312,7 @@ public function testGetRemainingLifetimeReturnsNullWhenLockDoesNotExist(): void public function testGetRemainingLifetimeReturnsNullWhenExpired(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$lock, $table] = $this->getLock(); diff --git a/tests/Cache/CacheDatabaseStoreTest.php b/tests/Cache/CacheDatabaseStoreTest.php index 7cf6cfd33..e6d4089b7 100644 --- a/tests/Cache/CacheDatabaseStoreTest.php +++ b/tests/Cache/CacheDatabaseStoreTest.php @@ -4,13 +4,13 @@ namespace Hypervel\Tests\Cache; -use Carbon\Carbon; use Hypervel\Cache\DatabaseStore; use Hypervel\Database\ConnectionInterface; use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\PostgresConnection; use Hypervel\Database\Query\Builder; use Hypervel\Database\SQLiteConnection; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Tests\TestCase; use Mockery as m; @@ -28,7 +28,7 @@ public function testNullIsReturnedWhenItemNotFound() public function testNullIsReturnedAndItemDeletedWhenItemIsExpired() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$store, $table] = $this->getStore(); @@ -109,7 +109,7 @@ public function testManyReturnsMultipleItems() public function testExpiredItemsAreRemovedOnRetrieval() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$store, $table] = $this->getStore(); @@ -426,7 +426,7 @@ public function testSupportsFlushingLocksRequiresSeparateLockTable() public function testPruneExpiredRemovesExpiredEntries() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); [$store, $table] = $this->getStore(); $table->shouldReceive('where')->once()->with('expiration', '<=', $now->getTimestamp())->andReturn($table); diff --git a/tests/Cache/CacheFileStoreTest.php b/tests/Cache/CacheFileStoreTest.php index 9bc7082bb..f7ce613e8 100644 --- a/tests/Cache/CacheFileStoreTest.php +++ b/tests/Cache/CacheFileStoreTest.php @@ -9,7 +9,7 @@ use Hypervel\Contracts\Filesystem\FileNotFoundException; use Hypervel\Filesystem\Filesystem; use Hypervel\Filesystem\LockableFile; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Testing\ParallelTesting; use Hypervel\Tests\TestCase; @@ -114,7 +114,7 @@ public function testTouchExtendsTtl() $files = $this->mockFilesystem(); $store = $this->getMockBuilder(FileStore::class)->onlyMethods(['expiration', 'get', 'getPayload'])->setConstructorArgs([$files, __DIR__])->getMock(); - $now = Carbon::now(); + $now = CarbonImmutable::now(); $key = 'foo'; $content = 'Hello World'; @@ -125,11 +125,11 @@ public function testTouchExtendsTtl() $store->expects($this->once()) ->method('expiration') ->with($this->equalTo($ttl)) - ->willReturn($now->clone()->addSeconds($ttl)->getTimestamp()); + ->willReturn($now->addSeconds($ttl)->getTimestamp()); $store->expects($this->once()) ->method('getPayload') ->with($key) - ->willReturn(['data' => $content, 'expiration' => $now->clone()->addSeconds($ttl)->getTimestamp()]); + ->willReturn(['data' => $content, 'expiration' => $now->addSeconds($ttl)->getTimestamp()]); $files->expects($this->once()) ->method('put') ->with( @@ -250,11 +250,11 @@ public function testForeversAreNotRemovedOnIncrement() public function testIncrementExpiredKeys() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $filePath = $this->getCachePath('foo'); $files = $this->mockFilesystem(); - $now = Carbon::now()->getTimestamp(); + $now = CarbonImmutable::now()->getTimestamp(); $initialValue = ($now - 10) . serialize(77); $valueAfterIncrement = '9999999999' . serialize(3); $store = new FileStore($files, __DIR__); @@ -328,10 +328,10 @@ public function testIncrementNonExistentKeys() public function testIncrementDoesNotExtendCacheLife() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $files = $this->mockFilesystem(); - $expiration = Carbon::now()->addSeconds(50)->getTimestamp(); + $expiration = CarbonImmutable::now()->addSeconds(50)->getTimestamp(); $initialValue = $expiration . serialize(1); $valueAfterIncrement = $expiration . serialize(2); $store = new FileStore($files, __DIR__); diff --git a/tests/Cache/CacheMemoizedStoreTest.php b/tests/Cache/CacheMemoizedStoreTest.php index 0167cd593..3581b76ed 100644 --- a/tests/Cache/CacheMemoizedStoreTest.php +++ b/tests/Cache/CacheMemoizedStoreTest.php @@ -14,22 +14,22 @@ use Hypervel\Cache\StackStore; use Hypervel\Cache\StackStoreProxy; use Hypervel\Contracts\Events\Dispatcher; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; class CacheMemoizedStoreTest extends TestCase { - public function testTouchExtendsTtl() + public function testTouchExtendsTtl(): void { $store = new MemoizedStore('test', new Repository(new ArrayStore)); - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store->put('foo', 'bar', 30); $store->touch('foo', 60); - Carbon::setTestNow($now->addSeconds(45)); + CarbonImmutable::setTestNow($now->addSeconds(45)); $this->assertSame('bar', $store->get('foo')); } diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index a940a598f..05a4985ca 100644 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -31,7 +31,7 @@ use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Filesystem\Filesystem; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; @@ -43,7 +43,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow(Carbon::parse($this->getTestDate())); + CarbonImmutable::setTestNow(CarbonImmutable::parse($this->getTestDate())); } public function testGetReturnsValueFromCache() @@ -126,17 +126,17 @@ public function testRememberMethodCallsPutAndReturnsDefault() /* * Use Carbon object... */ - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->times(2)->andReturn(null); $repo->getStore()->shouldReceive('put')->once()->with('foo', 'bar', 602); $repo->getStore()->shouldReceive('put')->once()->with('baz', 'qux', 598); - $result = $repo->remember('foo', Carbon::now()->addMinutes(10)->addSeconds(2), function () { + $result = $repo->remember('foo', CarbonImmutable::now()->addMinutes(10)->addSeconds(2), function () { return 'bar'; }); $this->assertSame('bar', $result); - $result = $repo->remember('baz', Carbon::now()->addMinutes(10)->subSeconds(2), function () { + $result = $repo->remember('baz', CarbonImmutable::now()->addMinutes(10)->subSeconds(2), function () { return 'qux'; }); $this->assertSame('qux', $result); @@ -284,7 +284,7 @@ public function testFlexibleNullableStoresSentinelWhenCallbackReturnsNull() public function testFlexibleNullableReturnsNullOnFreshSentinelHit() { $repo = $this->getRepository(); - $now = Carbon::now()->getTimestamp(); + $now = CarbonImmutable::now()->getTimestamp(); $repo->getStore() ->shouldReceive('many') @@ -303,7 +303,7 @@ public function testFlexibleNullableReturnsNullOnFreshSentinelHit() public function testFlexibleNullableReturnsValueOnFreshValueHit() { $repo = $this->getRepository(); - $now = Carbon::now()->getTimestamp(); + $now = CarbonImmutable::now()->getTimestamp(); $repo->getStore() ->shouldReceive('many') @@ -439,7 +439,7 @@ public function testRememberNullableReRunsCallbackAfterTtlExpiry() $repo->rememberNullable('k', 60, fn () => null); - Carbon::setTestNow(Carbon::now()->addSeconds(61)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(61)); $invoked = false; $result = $repo->rememberNullable('k', 60, function () use (&$invoked) { @@ -524,9 +524,9 @@ public function testPutWithDatetimeInPastOrZeroSecondsRemovesOldItem() $repo = $this->getRepository(); $repo->getStore()->shouldReceive('put')->never(); $repo->getStore()->shouldReceive('forget')->twice()->andReturn(true); - $result = $repo->put('foo', 'bar', Carbon::now()->subMinutes(10)); + $result = $repo->put('foo', 'bar', CarbonImmutable::now()->subMinutes(10)); $this->assertTrue($result); - $result = $repo->put('foo', 'bar', Carbon::now()); + $result = $repo->put('foo', 'bar', CarbonImmutable::now()); $this->assertTrue($result); } @@ -575,14 +575,14 @@ public function testAddMethodCanAcceptDateTimeInterface() $withAddStore = m::mock(RedisStore::class); $withAddStore->shouldReceive('add')->once()->with('k', 'v', 61)->andReturn(true); $repository = new Repository($withAddStore); - $this->assertTrue($repository->add('k', 'v', Carbon::now()->addSeconds(61))); + $this->assertTrue($repository->add('k', 'v', CarbonImmutable::now()->addSeconds(61))); $noAddStore = m::mock(ArrayStore::class); $this->assertFalse(method_exists(ArrayStore::class, 'add'), 'This store should not have add method on it.'); $noAddStore->shouldReceive('get')->once()->with('k')->andReturn(null); $noAddStore->shouldReceive('put')->once()->with('k', 'v', 62)->andReturn(true); $repository = new Repository($noAddStore); - $this->assertTrue($repository->add('k', 'v', Carbon::now()->addSeconds(62))); + $this->assertTrue($repository->add('k', 'v', CarbonImmutable::now()->addSeconds(62))); } public function testAddWithNullTTLRemembersItemForever() @@ -597,9 +597,9 @@ public function testAddWithDatetimeInPastOrZeroSecondsReturnsImmediately() { $repo = $this->getRepository(); $repo->getStore()->shouldReceive('add', 'get', 'put')->never(); - $result = $repo->add('foo', 'bar', Carbon::now()->subMinutes(10)); + $result = $repo->add('foo', 'bar', CarbonImmutable::now()->subMinutes(10)); $this->assertFalse($result); - $result = $repo->add('foo', 'bar', Carbon::now()); + $result = $repo->add('foo', 'bar', CarbonImmutable::now()); $this->assertFalse($result); $result = $repo->add('foo', 'bar', -1); $this->assertFalse($result); @@ -608,7 +608,7 @@ public function testAddWithDatetimeInPastOrZeroSecondsReturnsImmediately() #[DataProvider('dataProviderTestGetSeconds')] public function testGetSeconds($duration) { - Carbon::setTestNow(Carbon::parse($this->getTestDate())); + CarbonImmutable::setTestNow(CarbonImmutable::parse($this->getTestDate())); $repo = $this->getRepository(); $repo->getStore()->shouldReceive('put')->once()->with($key = 'foo', $value = 'bar', 300); @@ -619,10 +619,10 @@ public function testGetSeconds($duration) public static function dataProviderTestGetSeconds() { - Carbon::setTestNow(Carbon::parse(self::getTestDate())); + CarbonImmutable::setTestNow(CarbonImmutable::parse(self::getTestDate())); return [ - [Carbon::parse(self::getTestDate())->addMinutes(5)], + [CarbonImmutable::parse(self::getTestDate())->addMinutes(5)], [(new DateTime(self::getTestDate()))->modify('+5 minutes')], [(new DateTimeImmutable(self::getTestDate()))->modify('+5 minutes')], [new DateInterval('PT5M')], @@ -632,11 +632,11 @@ public static function dataProviderTestGetSeconds() public function testGetSecondsCeilsSubSecondTtl() { - Carbon::setTestNow(Carbon::parse($this->getTestDate())); + CarbonImmutable::setTestNow(CarbonImmutable::parse($this->getTestDate())); $repo = $this->getRepository(); $repo->getStore()->shouldReceive('put')->once()->with('foo', 'bar', 1); - $repo->put('foo', 'bar', Carbon::parse($this->getTestDate())->addMilliseconds(400)); + $repo->put('foo', 'bar', CarbonImmutable::parse($this->getTestDate())->addMilliseconds(400)); $this->assertTrue(true); } @@ -916,7 +916,7 @@ public function testTouchWithEnumKeyProxiesResolvedKeyToStore() public function testTouchWithDatetimeTtlCorrectlyProxiesToStore() { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->with('key')->andReturn('bar'); diff --git a/tests/Cache/CacheSessionStoreTest.php b/tests/Cache/CacheSessionStoreTest.php index e7c2efe05..365904d1d 100755 --- a/tests/Cache/CacheSessionStoreTest.php +++ b/tests/Cache/CacheSessionStoreTest.php @@ -7,7 +7,7 @@ use Hypervel\Cache\SessionStore; use Hypervel\Session\ArraySessionHandler; use Hypervel\Session\Store; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use stdClass; @@ -25,13 +25,13 @@ public function testCacheTtl() { $store = new SessionStore(self::getSession()); - Carbon::setTestNow('2000-01-01 00:00:00.500'); // 500 milliseconds past + CarbonImmutable::setTestNow('2000-01-01 00:00:00.500'); // 500 milliseconds past $store->put('hello', 'world', 1); - Carbon::setTestNow('2000-01-01 00:00:01.499'); // progress 0.999 seconds + CarbonImmutable::setTestNow('2000-01-01 00:00:01.499'); // progress 0.999 seconds $this->assertSame('world', $store->get('hello')); - Carbon::setTestNow('2000-01-01 00:00:01.500'); // progress 0.001 seconds. 1 second since putting into cache. + CarbonImmutable::setTestNow('2000-01-01 00:00:01.500'); // progress 0.001 seconds. 1 second since putting into cache. $this->assertNull($store->get('hello')); } @@ -55,12 +55,12 @@ public function testMultipleItemsCanBeSetAndRetrieved() public function testItemsCanExpire() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); - Carbon::setTestNow(Carbon::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); $result = $store->get('foo'); $this->assertNull($result); @@ -68,21 +68,21 @@ public function testItemsCanExpire() public function testTouchExtendsTtl() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); // Move time forward and touch to extend TTL - Carbon::setTestNow(Carbon::now()->addSeconds(5)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(5)); $this->assertTrue($store->touch('foo', 60)); // Value should still exist past the original expiry - Carbon::setTestNow(Carbon::now()->addSeconds(10)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)); $this->assertSame('bar', $store->get('foo')); // Value should expire after the new TTL - Carbon::setTestNow(Carbon::now()->addSeconds(50)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(50)); $this->assertNull($store->get('foo')); } @@ -143,18 +143,18 @@ public function testNonExistingKeysCanBeIncremented() $this->assertEquals(1, $store->get('foo')); // Will be there forever - Carbon::setTestNow(Carbon::now()->addYears(10)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addYears(10)); $this->assertEquals(1, $store->get('foo')); } public function testExpiredKeysAreIncrementedLikeNonExistingKeys() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 999, 10); - Carbon::setTestNow(Carbon::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); $result = $store->increment('foo'); $this->assertEquals(1, $result); @@ -222,13 +222,13 @@ public function testValuesAreStoredByReference() public function testCanGetAll() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); $this->assertEquals([ - 'foo' => ['value' => 'bar', 'expiresAt' => Carbon::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000], + 'foo' => ['value' => 'bar', 'expiresAt' => CarbonImmutable::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000], ], $store->all()); } diff --git a/tests/Cache/CacheStackStoreTest.php b/tests/Cache/CacheStackStoreTest.php index fc340840f..2d4317b7b 100644 --- a/tests/Cache/CacheStackStoreTest.php +++ b/tests/Cache/CacheStackStoreTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Cache; -use Carbon\Carbon; use Hypervel\Cache\ArrayStore; use Hypervel\Cache\NullSentinel; use Hypervel\Cache\RedisStore; @@ -12,6 +11,7 @@ use Hypervel\Cache\StackStore; use Hypervel\Cache\StackStoreProxy; use Hypervel\Cache\SwooleStore; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; @@ -31,7 +31,7 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2000-01-01 12:34:56.123456'); + CarbonImmutable::setTestNow('2000-01-01 12:34:56.123456'); } public function testRetrieveItemFromStoreStacked() @@ -41,7 +41,7 @@ public function testRetrieveItemFromStoreStacked() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $this->swoole->shouldReceive('get')->once()->with($key)->andReturn(null); @@ -66,10 +66,10 @@ public function testPutWithCorrectTTL() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); - Carbon::setTestNow(Carbon::now()->addSeconds(50)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(50)); $this->swoole->shouldReceive('get')->once()->with($key)->andReturn(null); $this->redis->shouldReceive('get')->once()->with($key)->andReturn($record); @@ -85,7 +85,7 @@ public function testAvoidRedundantCall() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $this->swoole->shouldReceive('get')->once()->with($key)->andReturn($record); @@ -137,7 +137,7 @@ public function testPutItemToStoreStacked() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $this->swoole->shouldReceive('put')->once()->with($key, $record, $ttl)->andReturn(true); @@ -153,7 +153,7 @@ public function testPutItemToStoreFailed() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $this->swoole->shouldReceive('put')->once()->with($key, $record, $ttl)->andReturn(false); @@ -168,7 +168,7 @@ public function testPutItemToStoreFailedAndRollback() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $this->swoole->shouldReceive('put')->once()->with($key, $record, $ttl)->andReturn(true); @@ -193,7 +193,7 @@ public function testPutMany() $this->createStores(); $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $this->swoole->shouldReceive('put')->once()->with('foo', ['value' => 'bar', 'expiration' => $expiration], $ttl)->andReturn(true); $this->redis->shouldReceive('put')->once()->with('foo', ['value' => 'bar', 'expiration' => $expiration], $ttl)->andReturn(true); @@ -215,7 +215,7 @@ public function testPutManyReturnsFalseForFailedKeyAndAttemptsLaterKeys() $this->createStores(); $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $this->swoole->shouldReceive('put')->once()->with('first', ['value' => 'one', 'expiration' => $expiration], $ttl)->andReturn(true); $this->redis->shouldReceive('put')->once()->with('first', ['value' => 'one', 'expiration' => $expiration], $ttl)->andReturn(true); @@ -255,7 +255,7 @@ public function testIncrementWithTTL() $key = 'foo'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $this->swoole->shouldReceive('get')->once()->with($key)->andReturn(['value' => 1, 'expiration' => $expiration]); $this->swoole->shouldReceive('put')->once()->with($key, ['value' => 2, 'expiration' => $expiration], $ttl)->andReturn(true); @@ -288,7 +288,7 @@ public function testDecrementWithTTL() $key = 'foo'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $this->swoole->shouldReceive('get')->once()->with($key)->andReturn(['value' => 2, 'expiration' => $expiration]); $this->swoole->shouldReceive('put')->once()->with($key, ['value' => 1, 'expiration' => $expiration], $ttl)->andReturn(true); @@ -381,7 +381,7 @@ public function testThreeStores() $key = 'foo'; $value = 'bar'; $ttl = 100; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $array->shouldReceive('get')->once()->with($key)->andReturn($record); @@ -442,7 +442,7 @@ public function testProxyMaxTTL() $value = 'bar'; $ttl = 100; $maxTTL = 3; - $expiration = Carbon::now()->getTimestamp() + $ttl; + $expiration = CarbonImmutable::now()->getTimestamp() + $ttl; $record = compact('value', 'expiration'); $store = new StackStore([ @@ -481,7 +481,7 @@ public function testProxyMaxTTLWithForever() public function testTouchPropagatesThroughAllLayers() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $this->createStores(); $key = 'foo'; @@ -507,7 +507,7 @@ public function testTouchReturnsFalseWhenKeyDoesNotExist() public function testTouchProxyCapsMaxTTL() { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); /** @var MockInterface|SwooleStore $swoole */ $swoole = m::mock(SwooleStore::class); diff --git a/tests/Cache/CacheSwooleStoreIntervalTest.php b/tests/Cache/CacheSwooleStoreIntervalTest.php index e22f26029..c5b5237d9 100644 --- a/tests/Cache/CacheSwooleStoreIntervalTest.php +++ b/tests/Cache/CacheSwooleStoreIntervalTest.php @@ -4,13 +4,13 @@ namespace Hypervel\Tests\Cache; -use Carbon\Carbon; use Hypervel\Cache\SwooleStore; use Hypervel\Cache\SwooleTableManager; use Hypervel\Cache\SwooleTableState; use Hypervel\Container\Container; use Hypervel\Contracts\Debug\ExceptionHandler; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testing\ParallelTesting; use Hypervel\Tests\TestCase; use Laravel\SerializableClosure\SerializableClosure; @@ -114,7 +114,7 @@ public function testIntervalRegistrationIsIdempotentForLocalAndSharedIndexes(): public function testIntervalReregistrationPreservesLastRefreshTimestamp(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -129,7 +129,7 @@ public function testIntervalReregistrationPreservesLastRefreshTimestamp(): void $metadataKey = $this->metadataKey($store, 'foo'); $lastRefreshedAt = $this->metadata($state, $metadataKey)['lastRefreshedAt']; - Carbon::setTestNow('2000-01-01 00:00:01'); + CarbonImmutable::setTestNow('2000-01-01 00:00:01'); $store->interval('foo', fn () => 999, 5); @@ -143,7 +143,7 @@ public function testIntervalReregistrationPreservesLastRefreshTimestamp(): void public function testIntervalReregistrationPreservesFreshRefreshClaim(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -171,7 +171,7 @@ public function testIntervalReregistrationPreservesFreshRefreshClaim(): void public function testIntervalReregistrationUpdatesResolverAndRefreshIntervalForFutureRefreshes(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -180,13 +180,13 @@ public function testIntervalReregistrationUpdatesResolverAndRefreshIntervalForFu $store->refreshIntervalCaches(); $this->assertSame('first', $store->get('foo')); - Carbon::setTestNow('2000-01-01 00:00:01'); + CarbonImmutable::setTestNow('2000-01-01 00:00:01'); $store->interval('foo', fn () => 'second', 2); $store->refreshIntervalCaches(); $this->assertSame('first', $store->get('foo')); - Carbon::setTestNow('2000-01-01 00:00:02'); + CarbonImmutable::setTestNow('2000-01-01 00:00:02'); $store->refreshIntervalCaches(); @@ -279,29 +279,29 @@ public function testNullReturningIntervalResolverStoresLivePublicRow(): void public function testRefreshOnlyRunsWhenIntervalIsDue(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $workerStore = $this->createStore($state); $refresherStore = $this->createStore($state); - $workerStore->interval('foo', fn () => Carbon::now()->getTimestamp(), 5); + $workerStore->interval('foo', fn () => CarbonImmutable::now()->getTimestamp(), 5); $refresherStore->refreshIntervalCaches(); - $this->assertSame(Carbon::now()->getTimestamp(), $refresherStore->get('foo')); + $this->assertSame(CarbonImmutable::now()->getTimestamp(), $refresherStore->get('foo')); - Carbon::setTestNow('2000-01-01 00:00:04'); + CarbonImmutable::setTestNow('2000-01-01 00:00:04'); $refresherStore->refreshIntervalCaches(); - $this->assertSame(Carbon::parse('2000-01-01 00:00:00')->getTimestamp(), $refresherStore->get('foo')); + $this->assertSame(CarbonImmutable::parse('2000-01-01 00:00:00')->getTimestamp(), $refresherStore->get('foo')); - Carbon::setTestNow('2000-01-01 00:00:06'); + CarbonImmutable::setTestNow('2000-01-01 00:00:06'); $refresherStore->refreshIntervalCaches(); - $this->assertSame(Carbon::now()->getTimestamp(), $refresherStore->get('foo')); + $this->assertSame(CarbonImmutable::now()->getTimestamp(), $refresherStore->get('foo')); } public function testSuccessfulRefreshUpdatesMetadata(): void { - Carbon::setTestNow('2000-01-01 00:00:00.123456'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00.123456'); $state = $this->createState(); $store = $this->createStore($state); @@ -318,12 +318,12 @@ public function testSuccessfulRefreshUpdatesMetadata(): void public function testSlowSuccessfulRefreshUsesCommitTimestampForCadence(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); $store->interval('foo', function () { - Carbon::setTestNow('2000-01-01 00:00:03.123456'); + CarbonImmutable::setTestNow('2000-01-01 00:00:03.123456'); return 'bar'; }, 5); @@ -333,7 +333,7 @@ public function testSlowSuccessfulRefreshUsesCommitTimestampForCadence(): void $store->refreshIntervalCaches(); $this->assertSame( - Carbon::parse('2000-01-01 00:00:03.123456')->getPreciseTimestamp(6) / 1000000, + CarbonImmutable::parse('2000-01-01 00:00:03.123456')->getPreciseTimestamp(6) / 1000000, $this->metadata($state, $metadataKey)['lastRefreshedAt'] ); $this->assertSame('bar', $store->get('foo')); @@ -341,7 +341,7 @@ public function testSlowSuccessfulRefreshUsesCommitTimestampForCadence(): void public function testFreshRefreshClaimPreventsOverlappingRefresh(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -364,7 +364,7 @@ public function testFreshRefreshClaimPreventsOverlappingRefresh(): void public function testStaleRefreshClaimCanBeReclaimed(): void { - Carbon::setTestNow('2000-01-01 00:05:01'); + CarbonImmutable::setTestNow('2000-01-01 00:05:01'); $state = $this->createState(); $workerStore = $this->createStore($state); @@ -393,7 +393,7 @@ public function testRefreshIntervalUsesDoubledIntervalWhenItExceedsClaimTimeout( public function testStaleRefresherCannotOverwriteNewerCommittedValue(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $workerStore = $this->createStore($state); @@ -404,7 +404,7 @@ public function testStaleRefresherCannotOverwriteNewerCommittedValue(): void ++IntervalReentryProbe::$attempts; if (IntervalReentryProbe::$attempts === 1) { - Carbon::setTestNow('2000-01-01 00:05:01'); + CarbonImmutable::setTestNow('2000-01-01 00:05:01'); IntervalReentryProbe::$refresherStore->refreshIntervalCaches(); return 'A'; @@ -423,14 +423,14 @@ public function testStaleRefresherCannotOverwriteNewerCommittedValue(): void $this->assertSame('B', $workerStore->get('foo')); $this->assertNull($metadata['refreshingAt']); $this->assertSame( - Carbon::parse('2000-01-01 00:05:01')->getPreciseTimestamp(6) / 1000000, + CarbonImmutable::parse('2000-01-01 00:05:01')->getPreciseTimestamp(6) / 1000000, $metadata['lastRefreshedAt'] ); } public function testLostClaimBeforeCommitDoesNotWriteResolverResult(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -470,7 +470,7 @@ public function testCompletionAndClaimClearingDoNotOverwriteNewerClaim(): void public function testSameInstanceFallbackDuringFreshClaimReturnsNullWithoutRunningResolver(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -491,7 +491,7 @@ public function testSameInstanceFallbackDuringFreshClaimReturnsNullWithoutRunnin public function testFlushPreservesMetadataAndIndexRowsForRefresh(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $workerStore = $this->createStore($state); @@ -506,7 +506,7 @@ public function testFlushPreservesMetadataAndIndexRowsForRefresh(): void $this->assertNotFalse($state->table()->get($this->metadataKey($workerStore, 'foo'))); $this->assertNotFalse($state->table()->get($this->indexKey($workerStore, $this->metadataKey($workerStore, 'foo')))); - Carbon::setTestNow('2000-01-01 00:00:05'); + CarbonImmutable::setTestNow('2000-01-01 00:00:05'); $refresherStore->refreshIntervalCaches(); @@ -530,7 +530,7 @@ public function testGenericMissDoesNotConsultSharedIntervalIndex(): void public function testIndexRowsAreTouchedDuringRefreshDiscovery(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -539,7 +539,7 @@ public function testIndexRowsAreTouchedDuringRefreshDiscovery(): void $indexKey = $this->indexKey($store, $this->metadataKey($store, 'foo')); $before = $state->table()->get($indexKey)['expiration']; - Carbon::setTestNow('2000-01-01 00:00:10'); + CarbonImmutable::setTestNow('2000-01-01 00:00:10'); $store->refreshIntervalCaches(); @@ -548,7 +548,7 @@ public function testIndexRowsAreTouchedDuringRefreshDiscovery(): void public function testStaleCleanupAndEvictionSkipIntervalControlRows(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(rows: 8); $store = $this->createStore( @@ -811,7 +811,7 @@ private function rewriteRowExpiration(SwooleTableState $state, string $key, floa private function currentTimestamp(): float { - return Carbon::now()->getPreciseTimestamp(6) / 1000000; + return CarbonImmutable::now()->getPreciseTimestamp(6) / 1000000; } } diff --git a/tests/Cache/CacheSwooleStoreTest.php b/tests/Cache/CacheSwooleStoreTest.php index 9155dd7b8..a32b6ecd1 100644 --- a/tests/Cache/CacheSwooleStoreTest.php +++ b/tests/Cache/CacheSwooleStoreTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Cache; -use Carbon\Carbon; use Hypervel\Cache\Exceptions\ValueTooLargeForColumnException; use Hypervel\Cache\NullSentinel; use Hypervel\Cache\Repository; @@ -15,6 +14,7 @@ use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Config\Repository as ConfigRepository; use Hypervel\Contracts\Container\Container; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Tests\TestCase; use InvalidArgumentException; @@ -181,7 +181,7 @@ public function testAddPreservesLiveRow(): void public function testAddDoesNotUpdateHitMetadata(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state, SwooleStore::EVICTION_POLICY_LRU); @@ -191,7 +191,7 @@ public function testAddDoesNotUpdateHitMetadata(): void 'used_count' => 7, ]); - Carbon::setTestNow('2000-01-01 00:01:00'); + CarbonImmutable::setTestNow('2000-01-01 00:01:00'); $this->assertFalse($store->add('foo', 'baz', 5)); @@ -239,7 +239,7 @@ public function testGetUnderNoEvictionPolicyDoesNotUpdateHitMetadata(): void public function testGetUnderLruPolicyUpdatesOnlyLastUsedAt(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state, SwooleStore::EVICTION_POLICY_LRU); @@ -250,7 +250,7 @@ public function testGetUnderLruPolicyUpdatesOnlyLastUsedAt(): void 'used_count' => 7, ]); - Carbon::setTestNow('2000-01-01 00:01:00.123456'); + CarbonImmutable::setTestNow('2000-01-01 00:01:00.123456'); $this->assertSame('bar', $store->get('foo')); @@ -264,7 +264,7 @@ public function testGetUnderLruPolicyUpdatesOnlyLastUsedAt(): void public function testGetUnderLfuPolicyUpdatesOnlyUsedCount(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state, SwooleStore::EVICTION_POLICY_LFU); @@ -394,7 +394,7 @@ public function testIntervalsCanBeRefreshed(): void $this->assertTrue(is_string($first = $store->get('foo'))); - Carbon::setTestNow(now()->addMinutes(1)); + CarbonImmutable::setTestNow(now()->addMinutes(1)); $store->refreshIntervalCaches(); @@ -454,13 +454,13 @@ public function testExpiredAtWithMicrosecond(): void { $store = $this->createStore(); - Carbon::setTestNow('2000-01-01 00:00:00.500000'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00.500000'); $store->put('foo', 'bar', 1); - Carbon::setTestNow('2000-01-01 00:00:01.499999'); + CarbonImmutable::setTestNow('2000-01-01 00:00:01.499999'); $this->assertSame('bar', $store->get('foo')); - Carbon::setTestNow('2000-01-01 00:00:01.500000'); + CarbonImmutable::setTestNow('2000-01-01 00:00:01.500000'); $this->assertNull($store->get('foo')); } @@ -490,7 +490,7 @@ public function testEvictRecordsFlushesStaleRecordsWhenCalledDirectly(): void public function testEvictRecordsFlushesRecordsExpiringAtCurrentTimestamp(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); @@ -642,7 +642,7 @@ public function testEvictionCandidateDoesNotDeleteRowMutatedByIncrement(): void public function testEvictionCandidateDoesNotDeleteRowMutatedByLruHit(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = new SwooleStoreEvictionProbe($state, 0.05, SwooleStore::EVICTION_POLICY_LRU, 0.05); @@ -654,7 +654,7 @@ public function testEvictionCandidateDoesNotDeleteRowMutatedByLruHit(): void ]); $fingerprint = $store->fingerprintFor($state->table()->get($tableKey)); - Carbon::setTestNow('2000-01-01 00:01:00'); + CarbonImmutable::setTestNow('2000-01-01 00:01:00'); $this->assertSame('bar', $store->get('foo')); @@ -682,14 +682,14 @@ public function testEvictionCandidateDoesNotDeleteRowMutatedByLfuHit(): void public function testEvictRecordsPrunesExpiredLockRows(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $state = $this->createState(); $store = $this->createStore($state); $this->assertTrue($store->lock('expired', 1)->acquire()); - Carbon::setTestNow('2000-01-01 00:00:02'); + CarbonImmutable::setTestNow('2000-01-01 00:00:02'); $store->evictRecords(); @@ -698,7 +698,7 @@ public function testEvictRecordsPrunesExpiredLockRows(): void public function testTouchPreservesValueAndChangesExpiration(): void { - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $state = $this->createState(); $store = $this->createStore($state); @@ -706,7 +706,7 @@ public function testTouchPreservesValueAndChangesExpiration(): void $store->put('foo', 'bar', 30); $store->touch('foo', 60); - Carbon::setTestNow($now->addSeconds(45)); + CarbonImmutable::setTestNow($now->addSeconds(45)); $this->assertSame('bar', $store->get('foo')); } @@ -806,13 +806,13 @@ public function testLockAcquireSucceedsOnceWhileLive(): void public function testExpiredLocksCanBeAcquiredByNewOwner(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $store = $this->createStore(); $this->assertTrue($store->lock('foo', 1, 'owner-1')->acquire()); - Carbon::setTestNow('2000-01-01 00:00:02'); + CarbonImmutable::setTestNow('2000-01-01 00:00:02'); $this->assertTrue($store->lock('foo', 60, 'owner-2')->acquire()); } @@ -849,14 +849,14 @@ public function testRestoreLockUsesSuppliedOwner(): void public function testRefreshExtendsLiveOwnedLock(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $store = $this->createStore(); $lock = $store->lock('foo', 10, 'owner-1'); $this->assertTrue($lock->acquire()); - Carbon::setTestNow('2000-01-01 00:00:05'); + CarbonImmutable::setTestNow('2000-01-01 00:00:05'); $this->assertTrue($lock->refresh(20)); $this->assertSame(20.0, $lock->getRemainingLifetime()); @@ -882,7 +882,7 @@ public function testRefreshRejectsExplicitNonPositiveTtl(): void public function testGetRemainingLifetimeReturnsNullForMissingExpiredAndPermanentLocks(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $store = $this->createStore(); @@ -895,7 +895,7 @@ public function testGetRemainingLifetimeReturnsNullForMissingExpiredAndPermanent $expiring = $store->lock('expiring', 1); $this->assertTrue($expiring->acquire()); - Carbon::setTestNow('2000-01-01 00:00:02'); + CarbonImmutable::setTestNow('2000-01-01 00:00:02'); $this->assertNull($expiring->getRemainingLifetime()); } @@ -953,7 +953,7 @@ private function getLogicalRow(SwooleTableState $state, SwooleStore $store, stri private function getCurrentTimestamp(): float { - return Carbon::now()->getPreciseTimestamp(6) / 1000000; + return CarbonImmutable::now()->getPreciseTimestamp(6) / 1000000; } } diff --git a/tests/Cache/CacheWorkerArrayStoreTest.php b/tests/Cache/CacheWorkerArrayStoreTest.php index b0a8e4560..718c9a27a 100644 --- a/tests/Cache/CacheWorkerArrayStoreTest.php +++ b/tests/Cache/CacheWorkerArrayStoreTest.php @@ -6,7 +6,7 @@ use Hypervel\Cache\WorkerArrayStore; use Hypervel\Engine\Channel; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use stdClass; @@ -24,21 +24,21 @@ public function testItemsCanBeSetAndRetrieved(): void public function testItemsCanExpire(): void { - Carbon::setTestNow('2000-01-01 00:00:00.500'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00.500'); $store = new WorkerArrayStore; $store->put('hello', 'world', 1); - Carbon::setTestNow('2000-01-01 00:00:01.499'); + CarbonImmutable::setTestNow('2000-01-01 00:00:01.499'); $this->assertSame('world', $store->get('hello')); - Carbon::setTestNow('2000-01-01 00:00:01.500'); + CarbonImmutable::setTestNow('2000-01-01 00:00:01.500'); $this->assertNull($store->get('hello')); } public function testSerializedValuesCanBeRetrievedRaw(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new WorkerArrayStore(true); $object = new stdClass; @@ -65,19 +65,19 @@ public function testTouchExtendsTtl(): void { $store = new WorkerArrayStore; - Carbon::setTestNow($now = Carbon::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store->put('key', 'value', 30); $store->touch('key', 60); - Carbon::setTestNow($now->addSeconds(45)); + CarbonImmutable::setTestNow($now->addSeconds(45)); $this->assertSame('value', $store->get('key')); } public function testLocksCanBeRestoredRefreshedAndMeasured(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $store = new WorkerArrayStore; $lock = $store->lock('foo', 10); @@ -88,7 +88,7 @@ public function testLocksCanBeRestoredRefreshedAndMeasured(): void $this->assertTrue($restoredLock->isOwnedByCurrentProcess()); $this->assertSame(10.0, $restoredLock->getRemainingLifetime()); - Carbon::setTestNow(Carbon::now()->addSeconds(5)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(5)); $this->assertTrue($restoredLock->refresh(30)); $this->assertSame(30.0, $restoredLock->getRemainingLifetime()); diff --git a/tests/Cache/Redis/Operations/AllTag/FlushStaleTest.php b/tests/Cache/Redis/Operations/AllTag/FlushStaleTest.php index 3d258ad0f..7a82d2d16 100644 --- a/tests/Cache/Redis/Operations/AllTag/FlushStaleTest.php +++ b/tests/Cache/Redis/Operations/AllTag/FlushStaleTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Cache\Redis\Operations\AllTag; -use Carbon\Carbon; use Hypervel\Cache\Redis\Operations\AllTag\FlushStale; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\Cache\Redis\RedisCacheTestCase; use Mockery as m; @@ -111,8 +111,8 @@ public function testFlushStaleEntriesUsesCorrectPrefix(): void public function testFlushStaleEntriesUsesCurrentTimestampAsUpperBound(): void { // Set a specific time so we can verify the timestamp - Carbon::setTestNow('2025-06-15 12:30:45'); - $expectedTimestamp = (string) Carbon::now()->getTimestamp(); + CarbonImmutable::setTestNow('2025-06-15 12:30:45'); + $expectedTimestamp = (string) CarbonImmutable::now()->getTimestamp(); $connection = $this->mockConnection(); diff --git a/tests/Cache/Redis/RedisCacheTestCase.php b/tests/Cache/Redis/RedisCacheTestCase.php index dd24e141e..7f5d8a453 100644 --- a/tests/Cache/Redis/RedisCacheTestCase.php +++ b/tests/Cache/Redis/RedisCacheTestCase.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Cache\Redis; -use Carbon\Carbon; use Hypervel\Cache\RedisStore; use Hypervel\Contracts\Redis\Factory as RedisFactory; use Hypervel\Redis\PhpRedisClusterConnection; @@ -13,6 +12,7 @@ use Hypervel\Redis\Pool\RedisPool; use Hypervel\Redis\RedisConnection; use Hypervel\Redis\RedisProxy; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Redis\Fixtures\FakeRedisClient; use Hypervel\Tests\Redis\Fixtures\PhpRedisConnectionStub; @@ -55,7 +55,7 @@ protected function setUp(): void parent::setUp(); // Fixed time for tag tests - ZSET scores use timestamps - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); } /** diff --git a/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php b/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php index b16858e4d..726b4b6be 100644 --- a/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php +++ b/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php @@ -4,10 +4,10 @@ namespace Hypervel\Tests\Integration\Cache\Redis; -use Carbon\Carbon; use DateInterval; use Hypervel\Cache\NullSentinel; use Hypervel\Cache\TagMode; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Cache; /** @@ -69,7 +69,7 @@ public function testAllModeHandlesDateTimeTtl(): void { $this->setTagMode(TagMode::All); - $expires = Carbon::now()->addSeconds(60); + $expires = CarbonImmutable::now()->addSeconds(60); Cache::tags(['datetime_ttl'])->put('datetime_key', 'datetime_value', $expires); $this->assertSame('datetime_value', Cache::tags(['datetime_ttl'])->get('datetime_key')); @@ -86,7 +86,7 @@ public function testAnyModeHandlesDateTimeTtl(): void { $this->setTagMode(TagMode::Any); - $expires = Carbon::now()->addSeconds(60); + $expires = CarbonImmutable::now()->addSeconds(60); Cache::tags(['datetime_ttl'])->put('datetime_key', 'datetime_value', $expires); $this->assertSame('datetime_value', Cache::get('datetime_key')); diff --git a/tests/Integration/Cache/RepositoryTest.php b/tests/Integration/Cache/RepositoryTest.php index 8d1448e04..64d56fd58 100644 --- a/tests/Integration/Cache/RepositoryTest.php +++ b/tests/Integration/Cache/RepositoryTest.php @@ -7,7 +7,7 @@ use Hypervel\Cache\Events\KeyWritten; use Hypervel\Cache\NullSentinel; use Hypervel\Foundation\Testing\LazilyRefreshDatabase; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Cache; use Hypervel\Support\Facades\Event; use Hypervel\Testbench\Attributes\WithMigration; @@ -18,9 +18,9 @@ class RepositoryTest extends TestCase { use LazilyRefreshDatabase; - public function testStaleWhileRevalidate() + public function testStaleWhileRevalidate(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $cache = Cache::driver('array'); $count = 0; @@ -43,7 +43,7 @@ public function testStaleWhileRevalidate() $this->assertSame(1, $cache->get('foo')); $this->assertSame(946684800, $cache->get('hypervel:cache:flexible:created:foo')); - Carbon::setTestNow(now()->addSeconds(11)); + CarbonImmutable::setTestNow(now()->addSeconds(11)); // Cache is now "stale". The stored value should be used and a deferred // callback should be registered to refresh the cache. @@ -83,7 +83,7 @@ public function testStaleWhileRevalidate() $this->assertSame(946684811, $cache->get('hypervel:cache:flexible:created:foo')); // Let's now progress time beyond the stale TTL... - Carbon::setTestNow(now()->addSeconds(21)); + CarbonImmutable::setTestNow(now()->addSeconds(21)); // Now the values should have left the cache. We should refresh. $value = $cache->flexible('foo', [10, 20], function () use (&$count) { @@ -97,7 +97,7 @@ public function testStaleWhileRevalidate() // Now lets see what happens when another request, job, or command is // also trying to refresh the same key at the same time. Will push past // the "fresh" TTL and register a deferred callback. - Carbon::setTestNow(now()->addSeconds(11)); + CarbonImmutable::setTestNow(now()->addSeconds(11)); $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); @@ -130,7 +130,7 @@ public function testStaleWhileRevalidate() // The last thing is to check that we don't refresh the cache in the // deferred callback if another thread has already done the work for us. // We will make the cache stale... - Carbon::setTestNow(now()->addSeconds(11)); + CarbonImmutable::setTestNow(now()->addSeconds(11)); $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); @@ -157,7 +157,7 @@ public function testStaleWhileRevalidate() $this->assertSame(946684863, $cache->get('hypervel:cache:flexible:created:foo')); } - public function testItHandlesStrayTtlKeyAfterMainKeyIsForgotten() + public function testItHandlesStrayTtlKeyAfterMainKeyIsForgotten(): void { $cache = Cache::driver('array'); $count = 0; @@ -182,7 +182,7 @@ public function testItHandlesStrayTtlKeyAfterMainKeyIsForgotten() $this->assertSame(2, $count); } - public function testItImplicitlyClearsTtlKeysFromDatabaseCache() + public function testItImplicitlyClearsTtlKeysFromDatabaseCache(): void { $this->freezeTime(); $cache = Cache::driver('database'); @@ -211,7 +211,7 @@ public function testItImplicitlyClearsTtlKeysFromDatabaseCache() $this->assertTrue($cache->missing('hypervel:cache:flexible:created:count')); } - public function testItImplicitlyClearsTtlKeysFromFileDriver() + public function testItImplicitlyClearsTtlKeysFromFileDriver(): void { $this->freezeTime(); $cache = Cache::driver('file'); @@ -241,7 +241,7 @@ public function testItImplicitlyClearsTtlKeysFromFileDriver() $this->assertTrue($cache->missing('hypervel:cache:flexible:created:count')); } - public function testItCanAlwaysDefer() + public function testItCanAlwaysDefer(): void { $this->freezeTime(); $cache = Cache::driver('array'); @@ -255,7 +255,7 @@ public function testItCanAlwaysDefer() // First call to flexible() should not defer $this->assertCount(0, defer()); - Carbon::setTestNow(now()->addSeconds(11)); + CarbonImmutable::setTestNow(now()->addSeconds(11)); // Second callback should defer with always now true $cache->flexible('foo', [10, 20], function () use (&$count) { @@ -266,7 +266,7 @@ public function testItCanAlwaysDefer() $this->assertTrue(defer()->first()->always); } - public function testItRoundsDateTimeValuesToAccountForTimePassedDuringScriptExecution() + public function testItRoundsDateTimeValuesToAccountForTimePassedDuringScriptExecution(): void { // do not freeze time as this test depends on time progressing duration execution. $cache = Cache::driver('array'); @@ -283,7 +283,7 @@ public function testItRoundsDateTimeValuesToAccountForTimePassedDuringScriptExec $this->assertSame(1, $events[0]->seconds); } - public function testFakedCacheEventsAreStillRecordedWithoutRealListeners() + public function testFakedCacheEventsAreStillRecordedWithoutRealListeners(): void { Event::fake([KeyWritten::class]); @@ -296,7 +296,7 @@ public function testFakedCacheEventsAreStillRecordedWithoutRealListeners() }); } - public function testWorksWithEnumKey() + public function testWorksWithEnumKey(): void { $cache = Cache::driver('array'); @@ -345,7 +345,7 @@ public function testWorksWithEnumKey() $this->assertSame(['foo' => 'default', 'qux' => 'default'], $cache->getMultiple([TestCacheKey::Foo, TestCacheKey::Qux], 'default')); } - public function testRememberNullableRoundTripsThroughDefaultStore() + public function testRememberNullableRoundTripsThroughDefaultStore(): void { $cache = Cache::driver('array'); @@ -364,7 +364,7 @@ public function testRememberNullableRoundTripsThroughDefaultStore() $this->assertSame(1, $count); } - public function testHasReturnsFalseForCachedNullSentinelViaRealStore() + public function testHasReturnsFalseForCachedNullSentinelViaRealStore(): void { $cache = Cache::driver('array'); @@ -375,7 +375,7 @@ public function testHasReturnsFalseForCachedNullSentinelViaRealStore() $this->assertTrue($cache->missing('k')); } - public function testPutOverwritesCachedNullSentinelEndToEnd() + public function testPutOverwritesCachedNullSentinelEndToEnd(): void { $cache = Cache::driver('array'); @@ -386,7 +386,7 @@ public function testPutOverwritesCachedNullSentinelEndToEnd() $this->assertTrue($cache->has('k')); } - public function testTtlExpiryOnSentinelStoredKeyReRunsCallback() + public function testTtlExpiryOnSentinelStoredKeyReRunsCallback(): void { $this->freezeTime(); $cache = Cache::driver('array'); @@ -405,9 +405,9 @@ public function testTtlExpiryOnSentinelStoredKeyReRunsCallback() $this->assertTrue($invoked); } - public function testFlexibleNullableStaleHitUnwrapsAndTriggersRefresh() + public function testFlexibleNullableStaleHitUnwrapsAndTriggersRefresh(): void { - Carbon::setTestNow('2000-01-01 00:00:00'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00'); $cache = Cache::driver('array'); $count = 0; @@ -423,7 +423,7 @@ public function testFlexibleNullableStaleHitUnwrapsAndTriggersRefresh() // Advance past the fresh TTL. Next call returns the stale sentinel (unwrapped) // and registers a deferred refresh. - Carbon::setTestNow(now()->addSeconds(11)); + CarbonImmutable::setTestNow(now()->addSeconds(11)); $value = $cache->flexibleNullable('foo', [10, 20], function () use (&$count) { ++$count; return null; From a1f6cacf42e716245ae53f0fac9b11c56886e1b3 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:16:52 +0000 Subject: [PATCH 16/34] refactor(bus): canonicalize immutable batch dates Use Hypervel CarbonImmutable for batch creation, cancellation, completion, pruning, debounce locks, batchable state, and batch repository fakes. Preserve serialized repository data and public batch behavior while ensuring framework-held timestamps use one canonical immutable class. Update batch, cancellation, debounce, retry-command, and fake-repository coverage to assert exact Hypervel immutable values. --- src/bus/src/Batch.php | 2 +- src/bus/src/BatchFactory.php | 2 +- src/bus/src/Batchable.php | 2 +- src/bus/src/DatabaseBatchRepository.php | 2 +- src/bus/src/DebounceLock.php | 2 +- src/support/src/Testing/Fakes/BatchFake.php | 2 +- .../src/Testing/Fakes/BatchRepositoryFake.php | 2 +- tests/Bus/BusBatchTest.php | 12 ++++++------ tests/Bus/BusBatchableTest.php | 5 +++-- tests/Integration/Queue/DebouncedJobTest.php | 2 +- tests/Integration/Queue/SkipIfBatchCancelledTest.php | 4 ++-- tests/Queue/RetryBatchCommandTest.php | 6 +++--- 12 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/bus/src/Batch.php b/src/bus/src/Batch.php index b2f669de5..643ed9ee6 100644 --- a/src/bus/src/Batch.php +++ b/src/bus/src/Batch.php @@ -4,7 +4,6 @@ namespace Hypervel\Bus; -use Carbon\CarbonImmutable; use Closure; use Hypervel\Bus\Events\BatchCanceled; use Hypervel\Bus\Events\BatchFinished; @@ -15,6 +14,7 @@ use Hypervel\Contracts\Support\Arrayable; use Hypervel\Queue\CallQueuedClosure; use Hypervel\Support\Arr; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Enumerable; use JsonSerializable; diff --git a/src/bus/src/BatchFactory.php b/src/bus/src/BatchFactory.php index c75c414a9..45fcf90cc 100644 --- a/src/bus/src/BatchFactory.php +++ b/src/bus/src/BatchFactory.php @@ -4,8 +4,8 @@ namespace Hypervel\Bus; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Queue\Factory as QueueFactory; +use Hypervel\Support\CarbonImmutable; class BatchFactory { diff --git a/src/bus/src/Batchable.php b/src/bus/src/Batchable.php index 359a84e98..bb90b12f0 100644 --- a/src/bus/src/Batchable.php +++ b/src/bus/src/Batchable.php @@ -4,8 +4,8 @@ namespace Hypervel\Bus; -use Carbon\CarbonImmutable; use Hypervel\Container\Container; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Support\Testing\Fakes\BatchFake; diff --git a/src/bus/src/DatabaseBatchRepository.php b/src/bus/src/DatabaseBatchRepository.php index 26767ffc4..ec4c30f80 100644 --- a/src/bus/src/DatabaseBatchRepository.php +++ b/src/bus/src/DatabaseBatchRepository.php @@ -4,13 +4,13 @@ namespace Hypervel\Bus; -use Carbon\CarbonImmutable; use Closure; use DateTimeInterface; use Hypervel\Database\ConnectionInterface; use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\PostgresConnection; use Hypervel\Database\Query\Expression; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use RuntimeException; use Throwable; diff --git a/src/bus/src/DebounceLock.php b/src/bus/src/DebounceLock.php index 1f10413a0..dd55931be 100644 --- a/src/bus/src/DebounceLock.php +++ b/src/bus/src/DebounceLock.php @@ -4,10 +4,10 @@ namespace Hypervel\Bus; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Cache\Repository as Cache; use Hypervel\Queue\Attributes\DebounceFor; use Hypervel\Queue\Attributes\ReadsQueueAttributes; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; class DebounceLock diff --git a/src/support/src/Testing/Fakes/BatchFake.php b/src/support/src/Testing/Fakes/BatchFake.php index 8a6e5128d..3b6b5832d 100644 --- a/src/support/src/Testing/Fakes/BatchFake.php +++ b/src/support/src/Testing/Fakes/BatchFake.php @@ -4,9 +4,9 @@ namespace Hypervel\Support\Testing\Fakes; -use Carbon\CarbonImmutable; use Hypervel\Bus\Batch; use Hypervel\Bus\UpdatedBatchJobCounts; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Enumerable; use Throwable; diff --git a/src/support/src/Testing/Fakes/BatchRepositoryFake.php b/src/support/src/Testing/Fakes/BatchRepositoryFake.php index 32dc2d5ac..38c6130df 100644 --- a/src/support/src/Testing/Fakes/BatchRepositoryFake.php +++ b/src/support/src/Testing/Fakes/BatchRepositoryFake.php @@ -4,12 +4,12 @@ namespace Hypervel\Support\Testing\Fakes; -use Carbon\CarbonImmutable; use Closure; use Hypervel\Bus\Batch; use Hypervel\Bus\BatchRepository; use Hypervel\Bus\PendingBatch; use Hypervel\Bus\UpdatedBatchJobCounts; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; class BatchRepositoryFake implements BatchRepository diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 57afbf357..ab81bd372 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Bus; -use Carbon\CarbonImmutable; use Hypervel\Bus\Batch; use Hypervel\Bus\Batchable; use Hypervel\Bus\BatchFactory; @@ -24,6 +23,7 @@ use Hypervel\Foundation\Bus\Dispatchable; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Queue\CallQueuedClosure; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Bus; use Hypervel\Support\Facades\Queue; @@ -109,7 +109,7 @@ public function testBatchRepositoryAppliesAZeroBeforeCursor(): void $this->assertSame([], $repository->get(50, '0')); } - public function testJobsCanBeAddedToTheBatch() + public function testJobsCanBeAddedToTheBatch(): void { $queue = m::mock(Factory::class); @@ -143,7 +143,7 @@ public function testJobsCanBeAddedToTheBatch() $this->assertEquals(3, $batch->totalJobs); $this->assertEquals(3, $batch->pendingJobs); $this->assertIsString($job->batchId); - $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); + $this->assertSame(CarbonImmutable::class, $batch->createdAt::class); } public function testJobsCanBeAddedToPendingBatch() @@ -566,7 +566,7 @@ public function testDeletedBatchIgnoresLateJobResultsAndCallbacks(): void $this->assertSame(0, $_SERVER['__catch.count']); } - public function testBatchStateCanBeInspected() + public function testBatchStateCanBeInspected(): void { $queue = m::mock(Factory::class); @@ -606,7 +606,7 @@ public function testBatchStateCanBeInspected() $this->assertIsString(json_encode($batch)); } - public function testChainCanBeAddedToBatch() + public function testChainCanBeAddedToBatch(): void { $queue = m::mock(Factory::class); @@ -639,7 +639,7 @@ public function testChainCanBeAddedToBatch() $this->assertIsString($chainHeadJob->batchId); $this->assertIsString($secondJob->batchId); $this->assertIsString($thirdJob->batchId); - $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); + $this->assertSame(CarbonImmutable::class, $batch->createdAt::class); } public function testChainedJobsPreserveTheirRoutesWhenTheBatchHasNone(): void diff --git a/tests/Bus/BusBatchableTest.php b/tests/Bus/BusBatchableTest.php index b0880d365..85216b02d 100644 --- a/tests/Bus/BusBatchableTest.php +++ b/tests/Bus/BusBatchableTest.php @@ -4,11 +4,11 @@ namespace Hypervel\Tests\Bus; -use Carbon\CarbonImmutable; use Hypervel\Bus\Batch; use Hypervel\Bus\Batchable; use Hypervel\Bus\BatchRepository; use Hypervel\Container\Container; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Testing\Fakes\BatchFake; use Hypervel\Tests\TestCase; use Mockery as m; @@ -36,7 +36,7 @@ public function testBatchMayBeRetrieved() Container::setInstance(null); } - public function testWithFakeBatchSetsAndReturnsFake() + public function testWithFakeBatchSetsAndReturnsFake(): void { $job = new class { use Batchable; @@ -50,6 +50,7 @@ public function testWithFakeBatchSetsAndReturnsFake() $this->assertSame('test-batch-id', $job->batch()->id); $this->assertSame('test-batch-name', $job->batch()->name); $this->assertSame(3, $job->batch()->totalJobs); + $this->assertSame(CarbonImmutable::class, $job->batch()->createdAt::class); } public function testZeroBatchIdMayBeRetrievedAndFaked(): void diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php index 917491458..8e0167625 100644 --- a/tests/Integration/Queue/DebouncedJobTest.php +++ b/tests/Integration/Queue/DebouncedJobTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Integration\Queue\DebouncedJobTest; -use Carbon\CarbonImmutable; use Exception; use Hypervel\Bus\DebounceLock; use Hypervel\Bus\Queueable; @@ -18,6 +17,7 @@ use Hypervel\Queue\Attributes\DebounceFor; use Hypervel\Queue\Events\JobDebounced; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Cache as CacheFacade; use Hypervel\Support\Facades\Event; use Hypervel\Support\Facades\Queue; diff --git a/tests/Integration/Queue/SkipIfBatchCancelledTest.php b/tests/Integration/Queue/SkipIfBatchCancelledTest.php index 8a5a21dbf..5654c0188 100644 --- a/tests/Integration/Queue/SkipIfBatchCancelledTest.php +++ b/tests/Integration/Queue/SkipIfBatchCancelledTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Integration\Queue\SkipIfBatchCancelledTest; -use Carbon\CarbonImmutable; use Hypervel\Bus\Batchable; use Hypervel\Bus\Dispatcher; use Hypervel\Bus\Queueable; @@ -12,12 +11,13 @@ use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Middleware\SkipIfBatchCancelled; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; class SkipIfBatchCancelledTest extends TestCase { - public function testJobsAreSkippedOnceBatchIsCancelled() + public function testJobsAreSkippedOnceBatchIsCancelled(): void { [$beforeCancelled] = (new SkipCancelledBatchableTestJob)->withFakeBatch(); [$afterCancelled] = (new SkipCancelledBatchableTestJob)->withFakeBatch( diff --git a/tests/Queue/RetryBatchCommandTest.php b/tests/Queue/RetryBatchCommandTest.php index 5f2935438..5cc00711d 100644 --- a/tests/Queue/RetryBatchCommandTest.php +++ b/tests/Queue/RetryBatchCommandTest.php @@ -4,11 +4,11 @@ namespace Hypervel\Tests\Queue; -use Carbon\CarbonImmutable; use Hypervel\Bus\Batch; use Hypervel\Bus\BatchRepository; use Hypervel\Contracts\Queue\Factory as QueueFactory; use Hypervel\Queue\Console\RetryBatchCommand; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; use Symfony\Component\Console\Input\ArrayInput; @@ -16,7 +16,7 @@ class RetryBatchCommandTest extends TestCase { - public function testItDoesNotFallThroughWhenBatchCannotBeFound() + public function testItDoesNotFallThroughWhenBatchCannotBeFound(): void { $repo = m::mock(BatchRepository::class); $repo->shouldReceive('find')->once()->with('missing-batch')->andReturn(null); @@ -33,7 +33,7 @@ public function testItDoesNotFallThroughWhenBatchCannotBeFound() $this->assertStringContainsString('Unable to find a batch with ID [missing-batch].', $output->fetch()); } - public function testItDoesNotRetryWhenBatchHasNoFailedJobs() + public function testItDoesNotRetryWhenBatchHasNoFailedJobs(): void { $batch = new Batch( m::mock(QueueFactory::class), From da0cc8c09e7f847dba7ae2c8842a32194f502d04 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:17:04 +0000 Subject: [PATCH 17/34] refactor(queue): use immutable worker deadlines Use Hypervel CarbonImmutable for queue availability, retry, pruning, worker timeout, and command timestamps while retaining DateTimeInterface at user-configurable scheduling boundaries. Capture deadline modifiers explicitly and keep queue payload timestamps, database storage, pause and resume behavior, and driver protocols unchanged. Migrate queue unit and integration coverage across database, Redis, SQS, Beanstalkd, deferred execution, rate limiting, throttling, chaining, failed jobs, work commands, and worker behavior. --- src/queue/src/Console/PruneBatchesCommand.php | 8 +-- .../src/Console/PruneFailedJobsCommand.php | 4 +- src/queue/src/Console/WorkCommand.php | 8 +-- src/queue/src/DatabaseQueue.php | 4 +- src/queue/src/Queue.php | 6 +- src/queue/src/Worker.php | 8 +-- tests/Integration/Queue/JobChainingTest.php | 6 +- tests/Integration/Queue/RateLimitedTest.php | 40 ++++++------ .../Queue/ThrottlesExceptionsTest.php | 48 +++++++-------- .../ThrottlesExceptionsWithRedisTest.php | 12 ++-- tests/Integration/Queue/WorkCommandTest.php | 14 ++--- tests/Queue/DatabaseFailedJobProviderTest.php | 28 ++++----- .../DatabaseUuidFailedJobProviderTest.php | 18 +++--- tests/Queue/FileFailedJobProviderTest.php | 61 ++++++++++++------- tests/Queue/QueueBackgroundQueueTest.php | 22 +++---- tests/Queue/QueueBeanstalkdQueueTest.php | 16 ++--- .../QueueDatabaseQueueIntegrationTest.php | 48 +++++++-------- tests/Queue/QueueDatabaseQueueUnitTest.php | 16 ++--- tests/Queue/QueueDeferredQueueTest.php | 22 +++---- tests/Queue/QueuePauseResumeTest.php | 14 ++--- tests/Queue/QueueRedisQueueTest.php | 42 ++++++------- tests/Queue/QueueSqsQueueTest.php | 6 +- tests/Queue/QueueWorkerTest.php | 16 ++--- 23 files changed, 243 insertions(+), 224 deletions(-) diff --git a/src/queue/src/Console/PruneBatchesCommand.php b/src/queue/src/Console/PruneBatchesCommand.php index 8aaef1645..44f856e30 100644 --- a/src/queue/src/Console/PruneBatchesCommand.php +++ b/src/queue/src/Console/PruneBatchesCommand.php @@ -8,7 +8,7 @@ use Hypervel\Bus\DatabaseBatchRepository; use Hypervel\Bus\PrunableBatchRepository; use Hypervel\Console\Command; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'queue:prune-batches')] @@ -37,7 +37,7 @@ public function handle() $count = 0; if ($repository instanceof PrunableBatchRepository) { - $count = $repository->prune(Carbon::now()->subHours((int) $this->option('hours'))); + $count = $repository->prune(CarbonImmutable::now()->subHours((int) $this->option('hours'))); } $this->info("{$count} entries deleted."); @@ -46,7 +46,7 @@ public function handle() $count = 0; if ($repository instanceof DatabaseBatchRepository) { - $count = $repository->pruneUnfinished(Carbon::now()->subHours((int) $this->option('unfinished'))); + $count = $repository->pruneUnfinished(CarbonImmutable::now()->subHours((int) $this->option('unfinished'))); } $this->info("{$count} unfinished entries deleted."); @@ -56,7 +56,7 @@ public function handle() $count = 0; if ($repository instanceof DatabaseBatchRepository) { - $count = $repository->pruneCancelled(Carbon::now()->subHours((int) $this->option('cancelled'))); + $count = $repository->pruneCancelled(CarbonImmutable::now()->subHours((int) $this->option('cancelled'))); } $this->info("{$count} cancelled entries deleted."); diff --git a/src/queue/src/Console/PruneFailedJobsCommand.php b/src/queue/src/Console/PruneFailedJobsCommand.php index a074af855..ff2c922bc 100644 --- a/src/queue/src/Console/PruneFailedJobsCommand.php +++ b/src/queue/src/Console/PruneFailedJobsCommand.php @@ -7,7 +7,7 @@ use Hypervel\Console\Command; use Hypervel\Queue\Failed\FailedJobProviderInterface; use Hypervel\Queue\Failed\PrunableFailedJobProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'queue:prune-failed')] @@ -32,7 +32,7 @@ public function handle(): ?int $failer = $this->hypervel->make(FailedJobProviderInterface::class); if ($failer instanceof PrunableFailedJobProvider) { - $count = $failer->prune(Carbon::now()->subHours((int) $this->option('hours'))); + $count = $failer->prune(CarbonImmutable::now()->subHours((int) $this->option('hours'))); } else { $this->error('The [' . class_basename($failer) . '] failed job storage driver does not support pruning.'); diff --git a/src/queue/src/Console/WorkCommand.php b/src/queue/src/Console/WorkCommand.php index 1cc2292e0..e865b9229 100644 --- a/src/queue/src/Console/WorkCommand.php +++ b/src/queue/src/Console/WorkCommand.php @@ -17,7 +17,7 @@ use Hypervel\Queue\Failed\FailedJobProviderInterface; use Hypervel\Queue\Worker; use Hypervel\Queue\WorkerOptions; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Str; use Symfony\Component\Console\Attribute\AsCommand; @@ -294,17 +294,17 @@ protected function writeOutputAsJson(Job $job, $status, ?Throwable $exception = /** * Get the current date / time. */ - protected function now(): Carbon + protected function now(): CarbonImmutable { $queueTimezone = $this->config->get('queue.output_timezone'); if ($queueTimezone && $queueTimezone !== $this->config->get('app.timezone') ) { - return Carbon::now()->setTimezone($queueTimezone); + return CarbonImmutable::now()->setTimezone($queueTimezone); } - return Carbon::now(); + return CarbonImmutable::now(); } /** diff --git a/src/queue/src/DatabaseQueue.php b/src/queue/src/DatabaseQueue.php index 63c6729ee..69b31ac5b 100644 --- a/src/queue/src/DatabaseQueue.php +++ b/src/queue/src/DatabaseQueue.php @@ -14,7 +14,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Queue\Jobs\DatabaseJob; use Hypervel\Queue\Jobs\DatabaseJobRecord; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; use PDO; @@ -278,7 +278,7 @@ protected function isAvailable(Builder $query): void */ protected function isReservedButExpired(Builder $query): void { - $expiration = Carbon::now()->subSeconds($this->retryAfter)->getTimestamp(); + $expiration = CarbonImmutable::now()->subSeconds($this->retryAfter)->getTimestamp(); $query->orWhere(function ($query) use ($expiration) { $query->where('reserved_at', '<=', $expiration); diff --git a/src/queue/src/Queue.php b/src/queue/src/Queue.php index 20c531c95..b597754dd 100644 --- a/src/queue/src/Queue.php +++ b/src/queue/src/Queue.php @@ -26,7 +26,7 @@ use Hypervel\Queue\Attributes\Tries; use Hypervel\Queue\Events\JobQueued; use Hypervel\Queue\Events\JobQueueing; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Context; use Hypervel\Support\InteractsWithTime; @@ -162,7 +162,7 @@ protected function createObjectPayload(object $job, ?string $queue): array 'command' => $job, 'batchId' => $job->batchId ?? null, ], - 'createdAt' => Carbon::now()->getTimestamp(), + 'createdAt' => CarbonImmutable::now()->getTimestamp(), ]; $uniqueJobMetadata = UniqueJobPayloadContext::consume($job); @@ -279,7 +279,7 @@ protected function createStringPayload(array|string $job, ?string $queue, mixed 'backoff' => null, 'timeout' => null, 'data' => $data, - 'createdAt' => Carbon::now()->getTimestamp(), + 'createdAt' => CarbonImmutable::now()->getTimestamp(), ]); } diff --git a/src/queue/src/Worker.php b/src/queue/src/Worker.php index b92c7fc3c..7c745a73e 100644 --- a/src/queue/src/Worker.php +++ b/src/queue/src/Worker.php @@ -30,7 +30,7 @@ use Hypervel\Queue\Events\WorkerResuming; use Hypervel\Queue\Events\WorkerStarting; use Hypervel\Queue\Events\WorkerStopping; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use RuntimeException; use Throwable; @@ -681,7 +681,7 @@ protected function markJobAsFailedIfAlreadyExceedsMaxAttempts(string $connection $retryUntil = $job->retryUntil(); - if ($retryUntil && Carbon::now()->getTimestamp() <= $retryUntil) { + if ($retryUntil && CarbonImmutable::now()->getTimestamp() <= $retryUntil) { return; } @@ -701,7 +701,7 @@ protected function markJobAsFailedIfWillExceedMaxAttempts(string $connectionName { $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; - if ($job->retryUntil() && $job->retryUntil() <= Carbon::now()->getTimestamp()) { + if ($job->retryUntil() && $job->retryUntil() <= CarbonImmutable::now()->getTimestamp()) { $this->failJob($job, $e); } @@ -724,7 +724,7 @@ protected function markJobAsFailedIfWillExceedMaxExceptions(string $connectionNa /* @phpstan-ignore-next-line */ if (! $this->cache->get('job-exceptions:' . $uuid)) { /* @phpstan-ignore-next-line */ - $this->cache->put('job-exceptions:' . $uuid, 0, Carbon::now()->addDay()); + $this->cache->put('job-exceptions:' . $uuid, 0, CarbonImmutable::now()->addDay()); } /* @phpstan-ignore-next-line */ diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index bdfe998dd..2c6a5d573 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Integration\Queue\JobChainingTest; +use Carbon\CarbonInterface; use Exception; use Hypervel\Bus\Batchable; use Hypervel\Bus\PendingBatch; @@ -13,7 +14,6 @@ use Hypervel\Foundation\Bus\PendingChain; use Hypervel\Foundation\Testing\DatabaseMigrations; use Hypervel\Queue\InteractsWithQueue; -use Hypervel\Support\Carbon; use Hypervel\Support\Facades\Bus; use Hypervel\Support\Facades\Queue; use Hypervel\Testbench\Attributes\WithMigration; @@ -941,7 +941,7 @@ class JobChainAddingExistingJob implements ShouldQueue use InteractsWithQueue; use Queueable; - public static ?Carbon $ranAt = null; + public static ?CarbonInterface $ranAt = null; public function handle() { @@ -955,7 +955,7 @@ class JobChainAddingAddedJob implements ShouldQueue use InteractsWithQueue; use Queueable; - public static ?Carbon $ranAt = null; + public static ?CarbonInterface $ranAt = null; public function handle() { diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index 26c660b5f..3b848018a 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -16,13 +16,13 @@ use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Middleware\RateLimited; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; class RateLimitedTest extends TestCase { - public function testUnlimitedJobsAreExecuted() + public function testUnlimitedJobsAreExecuted(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -34,7 +34,7 @@ public function testUnlimitedJobsAreExecuted() $this->assertJobRanSuccessfully(RateLimitedTestJob::class); } - public function testUnlimitedJobsAreExecutedUsingBackedEnum() + public function testUnlimitedJobsAreExecutedUsingBackedEnum(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -46,7 +46,7 @@ public function testUnlimitedJobsAreExecutedUsingBackedEnum() $this->assertJobRanSuccessfully(RateLimitedTestJobUsingBackedEnum::class); } - public function testUnlimitedJobsAreExecutedUsingUnitEnum() + public function testUnlimitedJobsAreExecutedUsingUnitEnum(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -58,7 +58,7 @@ public function testUnlimitedJobsAreExecutedUsingUnitEnum() $this->assertJobRanSuccessfully(RateLimitedTestJobUsingUnitEnum::class); } - public function testRateLimitedJobsAreNotExecutedOnLimitReached2() + public function testRateLimitedJobsAreNotExecutedOnLimitReached2(): void { $cache = m::mock(Cache::class); $cache->shouldReceive('get')->andReturn(0, 1, null); @@ -97,7 +97,7 @@ public function testRateLimitedJobsAreNotExecutedOnLimitReached2() $this->assertFalse(RateLimitedTestJob::$handled); } - public function testRateLimitedJobsAreNotExecutedOnLimitReached() + public function testRateLimitedJobsAreNotExecutedOnLimitReached(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -109,7 +109,7 @@ public function testRateLimitedJobsAreNotExecutedOnLimitReached() $this->assertJobWasReleased(RateLimitedTestJob::class); } - public function testRateLimitedJobsCanBeSkippedOnLimitReached() + public function testRateLimitedJobsCanBeSkippedOnLimitReached(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -121,7 +121,7 @@ public function testRateLimitedJobsCanBeSkippedOnLimitReached() $this->assertJobWasSkipped(RateLimitedDontReleaseTestJob::class); } - public function testJobsCanHaveConditionalRateLimits() + public function testJobsCanHaveConditionalRateLimits(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -140,7 +140,7 @@ public function testJobsCanHaveConditionalRateLimits() $this->assertJobWasReleased(NonAdminTestJob::class); } - public function testRateLimitedJobsCanBeSkippedOnLimitReachedAndReleasedAfter() + public function testRateLimitedJobsCanBeSkippedOnLimitReachedAndReleasedAfter(): void { $rateLimiter = $this->app->make(RateLimiter::class); @@ -152,7 +152,7 @@ public function testRateLimitedJobsCanBeSkippedOnLimitReachedAndReleasedAfter() $this->assertJobWasReleasedAfter(RateLimitedReleaseAfterTestJob::class, 60); } - public function testMiddlewareSerialization() + public function testMiddlewareSerialization(): void { $rateLimited = new RateLimited('limiterName'); $rateLimited->shouldRelease = false; @@ -244,7 +244,7 @@ protected function assertJobWasSkipped(string $class): void $this->assertFalse($class::$handled); } - public function testItCanLimitPerMinute() + public function testItCanLimitPerMinute(): void { Container::getInstance()->instance(RateLimiter::class, $limiter = new RateLimiter(new Repository(new ArrayStore))); $limiter->for('test', fn () => Limit::perMinute(3)); @@ -260,34 +260,34 @@ public function release() $middleware = new RateLimited('test'); - Carbon::setTestNow('2000-00-00 00:00:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.000'); for ($i = 0; $i < 3; ++$i) { $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); $this->assertFalse($job->released); - Carbon::setTestNow(now()->addSeconds(1)); + CarbonImmutable::setTestNow(now()->addSeconds(1)); } $result = $middleware->handle($job = $jobFactory(), $next); $this->assertNull($result); $this->assertTrue($job->released); - Carbon::setTestNow('2000-00-00 00:00:59.999'); + CarbonImmutable::setTestNow('2000-00-00 00:00:59.999'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertNull($result); $this->assertTrue($job->released); - Carbon::setTestNow('2000-00-00 00:01:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:01:00.000'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); $this->assertFalse($job->released); } - public function testItCanLimitPerSecond() + public function testItCanLimitPerSecond(): void { Container::getInstance()->instance(RateLimiter::class, $limiter = new RateLimiter(new Repository(new ArrayStore))); $limiter->for('test', fn () => Limit::perSecond(3)); @@ -303,27 +303,27 @@ public function release() $middleware = new RateLimited('test'); - Carbon::setTestNow('2000-00-00 00:00:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.000'); for ($i = 0; $i < 3; ++$i) { $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); $this->assertFalse($job->released); - Carbon::setTestNow(now()->addMilliseconds(100)); + CarbonImmutable::setTestNow(now()->addMilliseconds(100)); } $result = $middleware->handle($job = $jobFactory(), $next); $this->assertNull($result); $this->assertTrue($job->released); - Carbon::setTestNow('2000-00-00 00:00:00.999'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.999'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertNull($result); $this->assertTrue($job->released); - Carbon::setTestNow('2000-00-00 00:00:01.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:01.000'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index 28049c266..276575e10 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -13,28 +13,28 @@ use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Middleware\ThrottlesExceptions; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Mockery as m; use RuntimeException; class ThrottlesExceptionsTest extends TestCase { - public function testCircuitIsOpenedForJobErrors() + public function testCircuitIsOpenedForJobErrors(): void { $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); } - public function testCircuitStaysClosedForSuccessfulJobs() + public function testCircuitStaysClosedForSuccessfulJobs(): void { $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); } - public function testCircuitResetsAfterSuccess() + public function testCircuitResetsAfterSuccess(): void { $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); @@ -43,12 +43,12 @@ public function testCircuitResetsAfterSuccess() $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); } - public function testCircuitCanSkipJob() + public function testCircuitCanSkipJob(): void { $this->assertJobWasDeleted(CircuitBreakerSkipJob::class); } - public function testCircuitCanFailJob() + public function testCircuitCanFailJob(): void { $this->assertJobWasFailed(CircuitBreakerFailedJob::class); } @@ -157,7 +157,7 @@ protected function assertJobRanSuccessfully($class): void $this->assertTrue($class::$handled); } - public function testItCanLimitPerMinute() + public function testItCanLimitPerMinute(): void { $jobFactory = fn () => new class { public $released = false; @@ -179,7 +179,7 @@ public function release() $middleware = new ThrottlesExceptions(3, 60); - Carbon::setTestNow('2000-00-00 00:00:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.000'); for ($i = 0; $i < 3; ++$i) { $result = $middleware->handle($job = $jobFactory(), $next); @@ -187,7 +187,7 @@ public function release() $this->assertTrue($job->released); $this->assertTrue($job->handled); - Carbon::setTestNow(now()->addSeconds(1)); + CarbonImmutable::setTestNow(now()->addSeconds(1)); } $result = $middleware->handle($job = $jobFactory(), $next); @@ -195,14 +195,14 @@ public function release() $this->assertTrue($job->released); $this->assertFalse($job->handled); - Carbon::setTestNow('2000-00-00 00:00:59.999'); + CarbonImmutable::setTestNow('2000-00-00 00:00:59.999'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); $this->assertTrue($job->released); $this->assertFalse($job->handled); - Carbon::setTestNow('2000-00-00 00:01:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:01:00.000'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); @@ -210,7 +210,7 @@ public function release() $this->assertTrue($job->handled); } - public function testItCanLimitPerSecond() + public function testItCanLimitPerSecond(): void { $jobFactory = fn () => new class { public $released = false; @@ -232,7 +232,7 @@ public function release() $middleware = new ThrottlesExceptions(3, 1); - Carbon::setTestNow('2000-00-00 00:00:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.000'); for ($i = 0; $i < 3; ++$i) { $result = $middleware->handle($job = $jobFactory(), $next); @@ -240,7 +240,7 @@ public function release() $this->assertTrue($job->released); $this->assertTrue($job->handled); - Carbon::setTestNow(now()->addMilliseconds(100)); + CarbonImmutable::setTestNow(now()->addMilliseconds(100)); } $result = $middleware->handle($job = $jobFactory(), $next); @@ -248,14 +248,14 @@ public function release() $this->assertTrue($job->released); $this->assertFalse($job->handled); - Carbon::setTestNow('2000-00-00 00:00:00.999'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.999'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); $this->assertTrue($job->released); $this->assertFalse($job->handled); - Carbon::setTestNow('2000-00-00 00:00:01.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:01.000'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); @@ -263,7 +263,7 @@ public function release() $this->assertTrue($job->handled); } - public function testLimitingWithDefaultValues() + public function testLimitingWithDefaultValues(): void { $jobFactory = fn () => new class { public $released = false; @@ -285,7 +285,7 @@ public function release() $middleware = new ThrottlesExceptions; - Carbon::setTestNow('2000-00-00 00:00:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:00:00.000'); for ($i = 0; $i < 10; ++$i) { $result = $middleware->handle($job = $jobFactory(), $next); @@ -293,7 +293,7 @@ public function release() $this->assertTrue($job->released); $this->assertTrue($job->handled); - Carbon::setTestNow(now()->addSeconds(1)); + CarbonImmutable::setTestNow(now()->addSeconds(1)); } $result = $middleware->handle($job = $jobFactory(), $next); @@ -301,14 +301,14 @@ public function release() $this->assertTrue($job->released); $this->assertFalse($job->handled); - Carbon::setTestNow('2000-00-00 00:09:59.999'); + CarbonImmutable::setTestNow('2000-00-00 00:09:59.999'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); $this->assertTrue($job->released); $this->assertFalse($job->handled); - Carbon::setTestNow('2000-00-00 00:10:00.000'); + CarbonImmutable::setTestNow('2000-00-00 00:10:00.000'); $result = $middleware->handle($job = $jobFactory(), $next); $this->assertSame($job, $result); @@ -316,7 +316,7 @@ public function release() $this->assertTrue($job->handled); } - public function testReportingExceptions() + public function testReportingExceptions(): void { $this->spy(ExceptionHandler::class) ->shouldReceive('report') @@ -345,7 +345,7 @@ public function release() $middleware->handle($job, $next); } - public function testUsesJobClassNameForCacheKey() + public function testUsesJobClassNameForCacheKey(): void { $rateLimiter = $this->mock(RateLimiter::class); @@ -381,7 +381,7 @@ public function release() $this->assertTrue($job->released); } - public function testUsesDisplayNameForCacheKeyWhenAvailable() + public function testUsesDisplayNameForCacheKeyWhenAvailable(): void { $rateLimiter = $this->mock(RateLimiter::class); diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php index 25b050830..4c5fa0d7f 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -13,7 +13,7 @@ use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Middleware\ThrottlesExceptionsWithRedis; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; use Mockery as m; @@ -29,24 +29,24 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); } - public function testCircuitIsOpenedForJobErrors() + public function testCircuitIsOpenedForJobErrors(): void { $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); } - public function testCircuitStaysClosedForSuccessfulJobs() + public function testCircuitStaysClosedForSuccessfulJobs(): void { $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key = Str::random()); $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); } - public function testCircuitResetsAfterSuccess() + public function testCircuitResetsAfterSuccess(): void { $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); @@ -114,7 +114,7 @@ protected function assertJobRanSuccessfully($class, string $key): void $this->assertTrue($class::$handled); } - public function testReportingExceptions() + public function testReportingExceptions(): void { $this->spy(ExceptionHandler::class) ->shouldReceive('report') diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 4a8653849..494eb06e3 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -12,7 +12,7 @@ use Hypervel\Foundation\Bus\Dispatchable; use Hypervel\Foundation\Testing\DatabaseMigrations; use Hypervel\Queue\Worker; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Artisan; use Hypervel\Support\Facades\Exceptions; use Hypervel\Support\Facades\Queue; @@ -135,10 +135,10 @@ public function testOnceDoesNotRunInMaintenanceModeUnlessForced() } } - public function testRunTimestampOutputWithDefaultAppTimezone() + public function testRunTimestampOutputWithDefaultAppTimezone(): void { // queue.output_timezone not set at all - $this->travelTo(Carbon::create(2023, 1, 18, 10, 10, 11)); + $this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11)); Queue::push(new FirstJob); $this->artisan('queue:work', [ @@ -148,11 +148,11 @@ public function testRunTimestampOutputWithDefaultAppTimezone() ->assertExitCode(0); } - public function testRunTimestampOutputWithDifferentLogTimezone() + public function testRunTimestampOutputWithDifferentLogTimezone(): void { $this->app['config']->set('queue.output_timezone', 'Europe/Helsinki'); - $this->travelTo(Carbon::create(2023, 1, 18, 10, 10, 11)); + $this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11)); Queue::push(new FirstJob); $this->artisan('queue:work', [ @@ -162,11 +162,11 @@ public function testRunTimestampOutputWithDifferentLogTimezone() ->assertExitCode(0); } - public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault() + public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault(): void { $this->app['config']->set('queue.output_timezone', 'UTC'); - $this->travelTo(Carbon::create(2023, 1, 18, 10, 10, 11)); + $this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11)); Queue::push(new FirstJob); $this->artisan('queue:work', [ diff --git a/tests/Queue/DatabaseFailedJobProviderTest.php b/tests/Queue/DatabaseFailedJobProviderTest.php index b9af8f995..4ddc0ce1c 100644 --- a/tests/Queue/DatabaseFailedJobProviderTest.php +++ b/tests/Queue/DatabaseFailedJobProviderTest.php @@ -8,7 +8,7 @@ use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Queue\Failed\DatabaseFailedJobProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; @@ -84,34 +84,34 @@ public function testCanRemoveFailedJobsById() $this->assertSame(0, $this->failedJobsTable()->count()); } - public function testCanPruneFailedJobs() + public function testCanPruneFailedJobs(): void { - Carbon::setTestNow(Carbon::createFromDate(2024, 4, 28)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromDate(2024, 4, 28)); - $this->createFailedJobsRecord(['failed_at' => Carbon::createFromDate(2024, 4, 24)]); - $this->createFailedJobsRecord(['failed_at' => Carbon::createFromDate(2024, 4, 26)]); + $this->createFailedJobsRecord(['failed_at' => CarbonImmutable::createFromDate(2024, 4, 24)]); + $this->createFailedJobsRecord(['failed_at' => CarbonImmutable::createFromDate(2024, 4, 26)]); - $this->provider->prune(Carbon::createFromDate(2024, 4, 23)); + $this->provider->prune(CarbonImmutable::createFromDate(2024, 4, 23)); $this->assertSame(2, $this->failedJobsTable()->count()); - $this->provider->prune(Carbon::createFromDate(2024, 4, 25)); + $this->provider->prune(CarbonImmutable::createFromDate(2024, 4, 25)); $this->assertSame(1, $this->failedJobsTable()->count()); - $this->provider->prune(Carbon::createFromDate(2024, 4, 30)); + $this->provider->prune(CarbonImmutable::createFromDate(2024, 4, 30)); $this->assertSame(0, $this->failedJobsTable()->count()); } - public function testCanPruneFailedJobsWithRelativeHoursAndMinutes() + public function testCanPruneFailedJobsWithRelativeHoursAndMinutes(): void { - Carbon::setTestNow(Carbon::create(2025, 8, 24, 12, 0, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2025, 8, 24, 12, 0, 0)); - $this->createFailedJobsRecord(['failed_at' => Carbon::create(2025, 8, 24, 11, 45, 0)]); - $this->createFailedJobsRecord(['failed_at' => Carbon::create(2025, 8, 24, 13, 0, 0)]); + $this->createFailedJobsRecord(['failed_at' => CarbonImmutable::create(2025, 8, 24, 11, 45, 0)]); + $this->createFailedJobsRecord(['failed_at' => CarbonImmutable::create(2025, 8, 24, 13, 0, 0)]); - $this->provider->prune(Carbon::create(2025, 8, 24, 11, 45, 0)); + $this->provider->prune(CarbonImmutable::create(2025, 8, 24, 11, 45, 0)); $this->assertSame(2, $this->failedJobsTable()->count()); - $this->provider->prune(Carbon::create(2025, 8, 24, 14, 0, 0)); + $this->provider->prune(CarbonImmutable::create(2025, 8, 24, 14, 0, 0)); $this->assertSame(0, $this->failedJobsTable()->count()); } diff --git a/tests/Queue/DatabaseUuidFailedJobProviderTest.php b/tests/Queue/DatabaseUuidFailedJobProviderTest.php index 7e0e4f025..4af2b02c3 100644 --- a/tests/Queue/DatabaseUuidFailedJobProviderTest.php +++ b/tests/Queue/DatabaseUuidFailedJobProviderTest.php @@ -7,7 +7,7 @@ use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Queue\Failed\DatabaseUuidFailedJobProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; use RuntimeException; @@ -104,34 +104,34 @@ public function testRemovingAllFailedJobs() $this->assertEmpty($this->provider->all()); } - public function testPruningFailedJobs() + public function testPruningFailedJobs(): void { - Carbon::setTestNow(Carbon::createFromDate(2024, 4, 28)); + CarbonImmutable::setTestNow(CarbonImmutable::createFromDate(2024, 4, 28)); $this->provider->log('connection-1', 'queue-1', json_encode(['uuid' => 'uuid-1']), new RuntimeException); $this->provider->log('connection-2', 'queue-2', json_encode(['uuid' => 'uuid-2']), new RuntimeException); - $this->provider->prune(Carbon::createFromDate(2024, 4, 26)); + $this->provider->prune(CarbonImmutable::createFromDate(2024, 4, 26)); $this->assertCount(2, $this->provider->all()); - $this->provider->prune(Carbon::createFromDate(2024, 4, 30)); + $this->provider->prune(CarbonImmutable::createFromDate(2024, 4, 30)); $this->assertEmpty($this->provider->all()); } - public function testPruningFailedJobsWithRelativeHoursAndMinutes() + public function testPruningFailedJobsWithRelativeHoursAndMinutes(): void { - Carbon::setTestNow(Carbon::create(2025, 8, 24, 12, 30, 0)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2025, 8, 24, 12, 30, 0)); $this->provider->log('connection-1', 'queue-1', json_encode(['uuid' => 'uuid-1']), new RuntimeException); $this->provider->log('connection-2', 'queue-2', json_encode(['uuid' => 'uuid-2']), new RuntimeException); - $this->provider->prune(Carbon::create(2025, 8, 24, 12, 30, 0)); + $this->provider->prune(CarbonImmutable::create(2025, 8, 24, 12, 30, 0)); $this->assertCount(2, $this->provider->all()); - $this->provider->prune(Carbon::create(2025, 8, 24, 13, 0, 0)); + $this->provider->prune(CarbonImmutable::create(2025, 8, 24, 13, 0, 0)); $this->assertEmpty($this->provider->all()); } diff --git a/tests/Queue/FileFailedJobProviderTest.php b/tests/Queue/FileFailedJobProviderTest.php index 4a43c02ac..f635233b8 100644 --- a/tests/Queue/FileFailedJobProviderTest.php +++ b/tests/Queue/FileFailedJobProviderTest.php @@ -5,24 +5,42 @@ namespace Hypervel\Tests\Queue; use Exception; +use Hypervel\Filesystem\Filesystem; use Hypervel\Queue\Failed\FileFailedJobProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; -use PHPUnit\Framework\TestCase; +use Hypervel\Testing\ParallelTesting; +use Hypervel\Tests\TestCase; class FileFailedJobProviderTest extends TestCase { - protected $path; + protected string $tempDirectory; - protected $provider; + protected string $path; + + protected FileFailedJobProvider $provider; + + protected Filesystem $filesystem; protected function setUp(): void { - $this->path = @tempnam('tmp', 'file_failed_job_provider_test'); + parent::setUp(); + + $this->filesystem = new Filesystem; + $this->tempDirectory = ParallelTesting::tempDir('FileFailedJobProviderTest'); + mkdir($this->tempDirectory, 0777, true); + $this->path = $this->tempDirectory . '/failed-jobs.json'; $this->provider = new FileFailedJobProvider($this->path); } - public function testCanLogFailedJobs() + protected function tearDown(): void + { + $this->filesystem->deleteDirectory($this->tempDirectory); + + parent::tearDown(); + } + + public function testCanLogFailedJobs(): void { [$uuid, $exception] = $this->logFailedJob(); @@ -41,10 +59,10 @@ public function testCanLogFailedJobs() ], $failedJobs); } - public function testCanRetrieveAllFailedJobs() + public function testCanRetrieveAllFailedJobs(): void { try { - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); [$uuidOne, $exceptionOne] = $this->logFailedJob(); [$uuidTwo, $exceptionTwo] = $this->logFailedJob(); @@ -72,11 +90,11 @@ public function testCanRetrieveAllFailedJobs() ], ], $failedJobs); } finally { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } } - public function testCanFindFailedJobs() + public function testCanFindFailedJobs(): void { [$uuid, $exception] = $this->logFailedJob(); @@ -93,7 +111,7 @@ public function testCanFindFailedJobs() ], $failedJob); } - public function testNullIsReturnedIfJobNotFound() + public function testNullIsReturnedIfJobNotFound(): void { $uuid = Str::uuid(); @@ -102,7 +120,7 @@ public function testNullIsReturnedIfJobNotFound() $this->assertNull($failedJob); } - public function testCanForgetFailedJobs() + public function testCanForgetFailedJobs(): void { [$uuid] = $this->logFailedJob(); @@ -113,7 +131,7 @@ public function testCanForgetFailedJobs() $this->assertNull($failedJob); } - public function testCanFlushFailedJobs() + public function testCanFlushFailedJobs(): void { $this->logFailedJob(); $this->logFailedJob(); @@ -125,7 +143,7 @@ public function testCanFlushFailedJobs() $this->assertEmpty($failedJobs); } - public function testCanPruneFailedJobs() + public function testCanPruneFailedJobs(): void { $this->logFailedJob(); $this->logFailedJob(); @@ -142,7 +160,7 @@ public function testCanPruneFailedJobs() $this->assertCount(2, $failedJobs); } - public function testCanPruneFailedJobsWithRelativeHours() + public function testCanPruneFailedJobsWithRelativeHours(): void { $this->logFailedJob(); $this->logFailedJob(); @@ -159,14 +177,14 @@ public function testCanPruneFailedJobsWithRelativeHours() $this->assertCount(2, $failedJobs); } - public function testEmptyFailedJobsByDefault() + public function testEmptyFailedJobsByDefault(): void { $failedJobs = $this->provider->all(); $this->assertEmpty($failedJobs); } - public function testJobsCanBeCounted() + public function testJobsCanBeCounted(): void { $this->assertSame(0, $this->provider->count()); @@ -178,7 +196,7 @@ public function testJobsCanBeCounted() $this->assertSame(3, $this->provider->count()); } - public function testJobsCanBeCountedByConnection() + public function testJobsCanBeCountedByConnection(): void { $this->logFailedJob('connection-1', 'default'); $this->logFailedJob('connection-2', 'default'); @@ -190,7 +208,7 @@ public function testJobsCanBeCountedByConnection() $this->assertSame(1, $this->provider->count('connection-2')); } - public function testJobsCanBeCountedByQueue() + public function testJobsCanBeCountedByQueue(): void { $this->logFailedJob('database', 'queue-1'); $this->logFailedJob('database', 'queue-2'); @@ -202,7 +220,7 @@ public function testJobsCanBeCountedByQueue() $this->assertSame(1, $this->provider->count(queue: 'queue-2')); } - public function testJobsCanBeCountedByQueueAndConnection() + public function testJobsCanBeCountedByQueueAndConnection(): void { $this->logFailedJob('connection-1', 'queue-99'); $this->logFailedJob('connection-1', 'queue-99'); @@ -216,7 +234,8 @@ public function testJobsCanBeCountedByQueueAndConnection() $this->assertSame(2, $this->provider->count('connection-2', 'queue-1')); } - public function logFailedJob($connection = 'connection', $queue = 'queue') + /** @return array{string, Exception} */ + public function logFailedJob(string $connection = 'connection', string $queue = 'queue'): array { $uuid = Str::uuid(); diff --git a/tests/Queue/QueueBackgroundQueueTest.php b/tests/Queue/QueueBackgroundQueueTest.php index bf80af5ba..4ebd06797 100644 --- a/tests/Queue/QueueBackgroundQueueTest.php +++ b/tests/Queue/QueueBackgroundQueueTest.php @@ -16,7 +16,7 @@ use Hypervel\Queue\BackgroundQueue; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Jobs\SyncJob; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; use RuntimeException; @@ -256,7 +256,7 @@ public function testPushSnapshotsAfterCommitJobWhenTheTransactionCommits(): void public function testLaterWithDateInterval() { - Carbon::setTestNow('2024-01-01 12:00:00'); + CarbonImmutable::setTestNow('2024-01-01 12:00:00'); $timer = m::mock(Timer::class); $timer->shouldReceive('after') @@ -278,12 +278,12 @@ public function testLaterWithDateInterval() $this->assertInstanceOf(SyncJob::class, $_SERVER['__background.later.test'][0]); $this->assertEquals(['baz' => 'qux'], $_SERVER['__background.later.test'][1]); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } - public function testLaterWithDateTime() + public function testLaterWithDateTime(): void { - Carbon::setTestNow('2024-01-01 12:00:00'); + CarbonImmutable::setTestNow('2024-01-01 12:00:00'); $timer = m::mock(Timer::class); $timer->shouldReceive('after') @@ -300,12 +300,12 @@ public function testLaterWithDateTime() unset($_SERVER['__background.later.test']); - run(fn () => $background->later(Carbon::parse('2024-01-01 12:00:15'), BackgroundQueueLaterTestHandler::class, ['test' => 'data'])); + run(fn () => $background->later(CarbonImmutable::parse('2024-01-01 12:00:15'), BackgroundQueueLaterTestHandler::class, ['test' => 'data'])); $this->assertInstanceOf(SyncJob::class, $_SERVER['__background.later.test'][0]); $this->assertEquals(['test' => 'data'], $_SERVER['__background.later.test'][1]); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testLaterAddsTransactionCallbackForAfterCommitJobs() @@ -433,9 +433,9 @@ public function testLaterClampsNegativeIntegerDelay() run(fn () => $background->later(-5, BackgroundQueueLaterTestHandler::class)); } - public function testLaterClampsPastDateTimeInterface() + public function testLaterClampsPastDateTimeInterface(): void { - Carbon::setTestNow('2024-01-01 12:00:00'); + CarbonImmutable::setTestNow('2024-01-01 12:00:00'); $timer = m::mock(Timer::class); $timer->shouldReceive('after')->once()->with(0.0, m::type('Closure'))->andReturn(1); @@ -444,9 +444,9 @@ public function testLaterClampsPastDateTimeInterface() $background->setConnectionName('background'); $background->setContainer($this->getContainer()); - run(fn () => $background->later(Carbon::parse('2024-01-01 11:59:50'), BackgroundQueueLaterTestHandler::class)); + run(fn () => $background->later(CarbonImmutable::parse('2024-01-01 11:59:50'), BackgroundQueueLaterTestHandler::class)); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testLaterFailedJobGetsHandledWhenAnExceptionIsThrown() diff --git a/tests/Queue/QueueBeanstalkdQueueTest.php b/tests/Queue/QueueBeanstalkdQueueTest.php index adae13ea3..32a017318 100644 --- a/tests/Queue/QueueBeanstalkdQueueTest.php +++ b/tests/Queue/QueueBeanstalkdQueueTest.php @@ -7,8 +7,9 @@ use Hypervel\Contracts\Container\Container; use Hypervel\Queue\BeanstalkdQueue; use Hypervel\Queue\Jobs\BeanstalkdJob; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; +use Hypervel\Tests\TestCase; use Mockery as m; use Pheanstalk\Contract\JobIdInterface; use Pheanstalk\Contract\PheanstalkManagerInterface; @@ -18,7 +19,6 @@ use Pheanstalk\Values\Job; use Pheanstalk\Values\TubeList; use Pheanstalk\Values\TubeName; -use PHPUnit\Framework\TestCase; class QueueBeanstalkdQueueTest extends TestCase { @@ -41,10 +41,10 @@ public function testQueueNamesPreserveZeroAndDefaultEmptyString(): void $this->assertSame('0', $this->queue->getQueue('0')); } - public function testPushProperlyPushesJobOntoBeanstalkd() + public function testPushProperlyPushesJobOntoBeanstalkd(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = Str::uuid(); @@ -62,10 +62,10 @@ public function testPushProperlyPushesJobOntoBeanstalkd() $this->container->shouldHaveReceived('bound')->with('events')->times(4); } - public function testDelayedPushProperlyPushesJobOntoBeanstalkd() + public function testDelayedPushProperlyPushesJobOntoBeanstalkd(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = Str::uuid(); diff --git a/tests/Queue/QueueDatabaseQueueIntegrationTest.php b/tests/Queue/QueueDatabaseQueueIntegrationTest.php index e8321ca62..151054142 100644 --- a/tests/Queue/QueueDatabaseQueueIntegrationTest.php +++ b/tests/Queue/QueueDatabaseQueueIntegrationTest.php @@ -10,7 +10,7 @@ use Hypervel\Queue\DatabaseQueue; use Hypervel\Queue\Events\JobQueued; use Hypervel\Queue\Events\JobQueueing; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; @@ -55,7 +55,7 @@ protected function connection(): ConnectionInterface /** * Test that jobs that are not reserved and have an available_at value less then now, are popped. */ - public function testAvailableAndUnReservedJobsArePopped() + public function testAvailableAndUnReservedJobsArePopped(): void { $this->connection() ->table('jobs') @@ -65,8 +65,8 @@ public function testAvailableAndUnReservedJobsArePopped() 'payload' => 'mock_payload', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->subSeconds(1)->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->subSeconds(1)->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ]); $poppedJob = $this->queue->pop($mockQueueName); @@ -77,7 +77,7 @@ public function testAvailableAndUnReservedJobsArePopped() /** * Test that when jobs are popped, the attempts attribute is incremented. */ - public function testPoppedJobsIncrementAttempts() + public function testPoppedJobsIncrementAttempts(): void { $job = [ 'id' => 1, @@ -85,8 +85,8 @@ public function testPoppedJobsIncrementAttempts() 'payload' => 'mock_payload', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->subSeconds(1)->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->subSeconds(1)->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ]; $this->connection()->table('jobs')->insert($job); @@ -102,7 +102,7 @@ public function testPoppedJobsIncrementAttempts() /** * Test that the queue can be cleared. */ - public function testThatQueueCanBeCleared() + public function testThatQueueCanBeCleared(): void { $this->connection() ->table('jobs') @@ -111,17 +111,17 @@ public function testThatQueueCanBeCleared() 'queue' => $mock_queue_name = 'mock_queue_name', 'payload' => 'mock_payload', 'attempts' => 0, - 'reserved_at' => Carbon::now()->addDay()->getTimestamp(), - 'available_at' => Carbon::now()->subDay()->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'reserved_at' => CarbonImmutable::now()->addDay()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->subDay()->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ], [ 'id' => 2, 'queue' => $mock_queue_name, 'payload' => 'mock_payload 2', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->subSeconds(1)->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->subSeconds(1)->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ]]); $this->assertEquals(2, $this->queue->clear($mock_queue_name)); @@ -131,7 +131,7 @@ public function testThatQueueCanBeCleared() /** * Test that jobs that are not reserved and have an available_at value in the future, are not popped. */ - public function testUnavailableJobsAreNotPopped() + public function testUnavailableJobsAreNotPopped(): void { $this->connection() ->table('jobs') @@ -141,8 +141,8 @@ public function testUnavailableJobsAreNotPopped() 'payload' => 'mock_payload', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->addSeconds(60)->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->addSeconds(60)->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ]); $poppedJob = $this->queue->pop($mock_queue_name); @@ -153,7 +153,7 @@ public function testUnavailableJobsAreNotPopped() /** * Test that jobs that are reserved and have expired are popped. */ - public function testThatReservedAndExpiredJobsArePopped() + public function testThatReservedAndExpiredJobsArePopped(): void { $this->connection() ->table('jobs') @@ -162,9 +162,9 @@ public function testThatReservedAndExpiredJobsArePopped() 'queue' => $mock_queue_name = 'mock_queue_name', 'payload' => 'mock_payload', 'attempts' => 0, - 'reserved_at' => Carbon::now()->subDay()->getTimestamp(), - 'available_at' => Carbon::now()->addDay()->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'reserved_at' => CarbonImmutable::now()->subDay()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->addDay()->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ]); $poppedJob = $this->queue->pop($mock_queue_name); @@ -175,7 +175,7 @@ public function testThatReservedAndExpiredJobsArePopped() /** * Test that jobs that are reserved and not expired and available are not popped. */ - public function testThatReservedJobsAreNotPopped() + public function testThatReservedJobsAreNotPopped(): void { $this->connection() ->table('jobs') @@ -184,9 +184,9 @@ public function testThatReservedJobsAreNotPopped() 'queue' => $mock_queue_name = 'mock_queue_name', 'payload' => 'mock_payload', 'attempts' => 0, - 'reserved_at' => Carbon::now()->addDay()->getTimestamp(), - 'available_at' => Carbon::now()->subDay()->getTimestamp(), - 'created_at' => Carbon::now()->getTimestamp(), + 'reserved_at' => CarbonImmutable::now()->addDay()->getTimestamp(), + 'available_at' => CarbonImmutable::now()->subDay()->getTimestamp(), + 'created_at' => CarbonImmutable::now()->getTimestamp(), ]); $poppedJob = $this->queue->pop($mock_queue_name); diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 94a5bcb3b..8ca973f43 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -13,11 +13,11 @@ use Hypervel\Database\Query\Builder; use Hypervel\Queue\DatabaseQueue; use Hypervel\Queue\Queue; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; +use Hypervel\Tests\TestCase; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; @@ -83,10 +83,10 @@ public static function pushJobsDataProvider() ]; } - public function testDelayedPushProperlyPushesJobOntoDatabase() + public function testDelayedPushProperlyPushesJobOntoDatabase(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = Str::uuid(); @@ -180,10 +180,10 @@ public function testFailureToCreatePayloadFromArray() ]); } - public function testBulkBatchPushesOntoDatabase() + public function testBulkBatchPushesOntoDatabase(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = Str::uuid(); diff --git a/tests/Queue/QueueDeferredQueueTest.php b/tests/Queue/QueueDeferredQueueTest.php index 095be46ff..7fc508451 100644 --- a/tests/Queue/QueueDeferredQueueTest.php +++ b/tests/Queue/QueueDeferredQueueTest.php @@ -16,7 +16,7 @@ use Hypervel\Queue\DeferredQueue; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Jobs\SyncJob; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -177,7 +177,7 @@ public function testLaterSchedulesJobWithDelay() public function testLaterWithDateInterval() { - Carbon::setTestNow('2024-01-01 12:00:00'); + CarbonImmutable::setTestNow('2024-01-01 12:00:00'); $timer = m::mock(Timer::class); $timer->shouldReceive('after') @@ -199,12 +199,12 @@ public function testLaterWithDateInterval() $this->assertInstanceOf(SyncJob::class, $_SERVER['__deferred.later.test'][0]); $this->assertEquals(['baz' => 'qux'], $_SERVER['__deferred.later.test'][1]); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } - public function testLaterWithDateTime() + public function testLaterWithDateTime(): void { - Carbon::setTestNow('2024-01-01 12:00:00'); + CarbonImmutable::setTestNow('2024-01-01 12:00:00'); $timer = m::mock(Timer::class); $timer->shouldReceive('after') @@ -221,12 +221,12 @@ public function testLaterWithDateTime() unset($_SERVER['__deferred.later.test']); - run(fn () => $deferred->later(Carbon::parse('2024-01-01 12:00:15'), DeferredQueueLaterTestHandler::class, ['test' => 'data'])); + run(fn () => $deferred->later(CarbonImmutable::parse('2024-01-01 12:00:15'), DeferredQueueLaterTestHandler::class, ['test' => 'data'])); $this->assertInstanceOf(SyncJob::class, $_SERVER['__deferred.later.test'][0]); $this->assertEquals(['test' => 'data'], $_SERVER['__deferred.later.test'][1]); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testLaterAddsTransactionCallbackForAfterCommitJobs() @@ -354,9 +354,9 @@ public function testLaterClampsNegativeIntegerDelay() run(fn () => $deferred->later(-5, DeferredQueueLaterTestHandler::class)); } - public function testLaterClampsPastDateTimeInterface() + public function testLaterClampsPastDateTimeInterface(): void { - Carbon::setTestNow('2024-01-01 12:00:00'); + CarbonImmutable::setTestNow('2024-01-01 12:00:00'); $timer = m::mock(Timer::class); $timer->shouldReceive('after')->once()->with(0.0, m::type('Closure'))->andReturn(1); @@ -365,9 +365,9 @@ public function testLaterClampsPastDateTimeInterface() $deferred->setConnectionName('deferred'); $deferred->setContainer($this->getContainer()); - run(fn () => $deferred->later(Carbon::parse('2024-01-01 11:59:50'), DeferredQueueLaterTestHandler::class)); + run(fn () => $deferred->later(CarbonImmutable::parse('2024-01-01 11:59:50'), DeferredQueueLaterTestHandler::class)); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testLaterFailedJobGetsHandledWhenAnExceptionIsThrown() diff --git a/tests/Queue/QueuePauseResumeTest.php b/tests/Queue/QueuePauseResumeTest.php index bce9a6c8a..65e285310 100644 --- a/tests/Queue/QueuePauseResumeTest.php +++ b/tests/Queue/QueuePauseResumeTest.php @@ -14,7 +14,7 @@ use Hypervel\Queue\Events\QueuePaused; use Hypervel\Queue\Events\QueueResumed; use Hypervel\Queue\QueueManager; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; class QueuePauseResumeTest extends TestCase @@ -66,25 +66,25 @@ public function testPauseQueueWithConnection() $this->assertTrue($this->manager->isPaused('redis', 'default')); } - public function testPauseQueueWithTTL() + public function testPauseQueueWithTTL(): void { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); $this->manager->pauseFor('redis', 'default', 30); $this->assertTrue($this->manager->isPaused('redis', 'default')); - Carbon::setTestNow(Carbon::now()->addMinute()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addMinute()); $this->assertFalse($this->manager->isPaused('redis', 'default')); } - public function testPauseQueueIndefinitely() + public function testPauseQueueIndefinitely(): void { - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); $this->manager->pause('redis', 'default'); $this->assertTrue($this->manager->isPaused('redis', 'default')); - Carbon::setTestNow(Carbon::now()->addYear()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addYear()); $this->assertTrue($this->manager->isPaused('redis', 'default')); } diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index b12b55480..0b92a9ce6 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -14,7 +14,7 @@ use Hypervel\Queue\Queue; use Hypervel\Queue\RedisQueue; use Hypervel\Redis\RedisProxy; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Tests\TestCase; use Mockery as m; @@ -24,8 +24,8 @@ class QueueRedisQueueTest extends TestCase { public function testPushProperlyPushesJobOntoRedis(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Redis::class), 'default', 'default'])->getMock(); @@ -44,8 +44,8 @@ public function testPushProperlyPushesJobOntoRedis(): void public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Redis::class), 'default', 'default'])->getMock(); @@ -70,8 +70,8 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook(): void public function testJobQueueingAndQueuedEventsAreSkippedWhenNoListenersAreRegistered(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Redis::class), 'default', 'default'])->getMock(); @@ -98,8 +98,8 @@ public function testJobQueueingAndQueuedEventsAreSkippedWhenNoListenersAreRegist public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Redis::class), 'default', 'default'])->getMock(); @@ -128,8 +128,8 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook(): vo public function testDelayedPushProperlyPushesJobOntoRedis(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['availableAt', 'getRandomId'])->setConstructorArgs([$redis = m::mock(Redis::class), 'default', 'default'])->getMock(); @@ -156,11 +156,11 @@ public function testDelayedPushProperlyPushesJobOntoRedis(): void public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); - $date = Carbon::now(); + $date = CarbonImmutable::now()->addSeconds(5); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['availableAt', 'getRandomId'])->setConstructorArgs([$redis = m::mock(Redis::class), 'default', 'default'])->getMock(); $queue->setContainer($container = m::spy(Container::class)); $queue->setConnectionName('default'); @@ -178,7 +178,7 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis(): void ); $redis->shouldReceive('connection')->twice()->andReturn($redisProxy); - $queue->later($date->addSeconds(5), 'foo', ['data']); + $queue->later($date, 'foo', ['data']); $container->shouldHaveReceived('bound')->with('events')->twice(); } @@ -258,8 +258,8 @@ public function testGetRedisKeyWrapsInvalidHashTagsOnCluster(): void public function testPushUsesClusterSafeRedisKeyForLuaScript(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class) @@ -287,8 +287,8 @@ public function testPushUsesClusterSafeRedisKeyForLuaScript(): void public function testPushPassesLogicalQueueToPayloadCallbacksOnCluster(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class) @@ -323,8 +323,8 @@ public function testPushPassesLogicalQueueToPayloadCallbacksOnCluster(): void public function testLaterUsesClusterSafeRedisKeyForDelayedSet(): void { - $now = Carbon::now(); - Carbon::setTestNow($now); + $now = CarbonImmutable::now(); + CarbonImmutable::setTestNow($now); $uuid = $this->mockUuid(); $queue = $this->getMockBuilder(RedisQueue::class) diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 2cf3431ef..68642b339 100644 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -13,7 +13,7 @@ use Hypervel\Queue\Jobs\SqsJob; use Hypervel\Queue\QueueRoutes; use Hypervel\Queue\SqsQueue; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; use Hypervel\Tests\Queue\Fixtures\FakeSqsJob; use Hypervel\Tests\Queue\Fixtures\FakeSqsJobWithDeduplication; @@ -162,9 +162,9 @@ public function testPopProperlyHandlesEmptyMessage() $this->assertNull($result); } - public function testDelayedPushWithDateTimeProperlyPushesJobOntoSqs() + public function testDelayedPushWithDateTimeProperlyPushesJobOntoSqs(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'secondsUntil', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); $queue->setContainer($container = m::spy(ContainerContract::class)); $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->queueName, $this->mockedData)->willReturn($this->mockedPayload); diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index 1057ce3b4..582cc061f 100644 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -38,7 +38,7 @@ use Hypervel\Queue\Worker; use Hypervel\Queue\WorkerOptions; use Hypervel\Queue\WorkerStopReason; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; use RuntimeException; @@ -448,7 +448,7 @@ public function testJobIsNotReleasedIfItHasExceededMaxAttempts() $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); } - public function testJobIsNotReleasedIfItHasExpired() + public function testJobIsNotReleasedIfItHasExpired(): void { $e = new RuntimeException; @@ -463,8 +463,8 @@ public function testJobIsNotReleasedIfItHasExpired() $job->attempts = 0; - Carbon::setTestNow( - Carbon::now()->addSeconds(1) + CarbonImmutable::setTestNow( + CarbonImmutable::now()->addSeconds(1) ); $worker = $this->getWorker('default', ['queue' => [$job]]); @@ -497,18 +497,18 @@ public function testJobIsFailedIfItHasAlreadyExceededMaxAttempts() $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); } - public function testJobIsFailedIfItHasAlreadyExpired() + public function testJobIsFailedIfItHasAlreadyExpired(): void { $job = new WorkerFakeJob(function ($job) { ++$job->attempts; }); - $job->retryUntil = Carbon::now()->addSeconds(2)->getTimestamp(); + $job->retryUntil = CarbonImmutable::now()->addSeconds(2)->getTimestamp(); $job->attempts = 1; - Carbon::setTestNow( - Carbon::now()->addSeconds(3) + CarbonImmutable::setTestNow( + CarbonImmutable::now()->addSeconds(3) ); $worker = $this->getWorker('default', ['queue' => [$job]]); From 55b91929e2d8557ff90b5bb73b8805c52f36c585 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:17:17 +0000 Subject: [PATCH 18/34] refactor(horizon): use canonical immutable timestamps Replace base CarbonImmutable values with Hypervel CarbonImmutable throughout Horizon job retries, trimming, monitoring, supervisors, process pools, metrics, and Redis repositories. Keep Redis payloads and Horizon public behavior stable while ensuring repository hydration and worker-held timestamps retain the Hypervel helper surface and immutable semantics. Update feature coverage for retrieval, metrics, wait monitoring, process repositories, queue processing, supervisors, trimming, and worker processes. --- src/horizon/src/Jobs/RetryFailedJob.php | 2 +- src/horizon/src/Listeners/MonitorWaitTimes.php | 2 +- src/horizon/src/Listeners/TrimFailedJobs.php | 2 +- src/horizon/src/Listeners/TrimMonitoredJobs.php | 2 +- src/horizon/src/Listeners/TrimRecentJobs.php | 2 +- src/horizon/src/MasterSupervisor.php | 2 +- src/horizon/src/ProcessPool.php | 2 +- src/horizon/src/Repositories/RedisJobRepository.php | 4 ++-- .../src/Repositories/RedisMasterSupervisorRepository.php | 2 +- src/horizon/src/Repositories/RedisMetricsRepository.php | 2 +- src/horizon/src/Repositories/RedisProcessRepository.php | 2 +- src/horizon/src/Repositories/RedisSupervisorRepository.php | 2 +- src/horizon/src/Supervisor.php | 2 +- src/horizon/src/WorkerProcess.php | 2 +- tests/Integration/Horizon/Feature/JobRetrievalTest.php | 2 +- tests/Integration/Horizon/Feature/MetricsTest.php | 2 +- tests/Integration/Horizon/Feature/MonitorWaitTimesTest.php | 2 +- tests/Integration/Horizon/Feature/ProcessRepositoryTest.php | 2 +- tests/Integration/Horizon/Feature/QueueProcessingTest.php | 2 +- tests/Integration/Horizon/Feature/SupervisorTest.php | 2 +- tests/Integration/Horizon/Feature/TrimMonitoredJobsTest.php | 2 +- tests/Integration/Horizon/Feature/TrimRecentJobsTest.php | 2 +- tests/Integration/Horizon/Feature/WorkerProcessTest.php | 2 +- 23 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/horizon/src/Jobs/RetryFailedJob.php b/src/horizon/src/Jobs/RetryFailedJob.php index 6849b71f9..92e456d2b 100644 --- a/src/horizon/src/Jobs/RetryFailedJob.php +++ b/src/horizon/src/Jobs/RetryFailedJob.php @@ -4,9 +4,9 @@ namespace Hypervel\Horizon\Jobs; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Queue\Factory as Queue; use Hypervel\Horizon\Contracts\JobRepository; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; class RetryFailedJob diff --git a/src/horizon/src/Listeners/MonitorWaitTimes.php b/src/horizon/src/Listeners/MonitorWaitTimes.php index 426368e98..f432d7411 100644 --- a/src/horizon/src/Listeners/MonitorWaitTimes.php +++ b/src/horizon/src/Listeners/MonitorWaitTimes.php @@ -4,10 +4,10 @@ namespace Hypervel\Horizon\Listeners; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\MetricsRepository; use Hypervel\Horizon\Events\LongWaitDetected; use Hypervel\Horizon\WaitTimeCalculator; +use Hypervel\Support\CarbonImmutable; class MonitorWaitTimes { diff --git a/src/horizon/src/Listeners/TrimFailedJobs.php b/src/horizon/src/Listeners/TrimFailedJobs.php index e166ef4ce..4cccf8787 100644 --- a/src/horizon/src/Listeners/TrimFailedJobs.php +++ b/src/horizon/src/Listeners/TrimFailedJobs.php @@ -4,9 +4,9 @@ namespace Hypervel\Horizon\Listeners; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\Events\MasterSupervisorLooped; +use Hypervel\Support\CarbonImmutable; class TrimFailedJobs { diff --git a/src/horizon/src/Listeners/TrimMonitoredJobs.php b/src/horizon/src/Listeners/TrimMonitoredJobs.php index d30e635dc..0f6de5578 100644 --- a/src/horizon/src/Listeners/TrimMonitoredJobs.php +++ b/src/horizon/src/Listeners/TrimMonitoredJobs.php @@ -4,9 +4,9 @@ namespace Hypervel\Horizon\Listeners; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\Events\MasterSupervisorLooped; +use Hypervel\Support\CarbonImmutable; class TrimMonitoredJobs { diff --git a/src/horizon/src/Listeners/TrimRecentJobs.php b/src/horizon/src/Listeners/TrimRecentJobs.php index 3abeb3ff6..395354b9c 100644 --- a/src/horizon/src/Listeners/TrimRecentJobs.php +++ b/src/horizon/src/Listeners/TrimRecentJobs.php @@ -4,9 +4,9 @@ namespace Hypervel\Horizon\Listeners; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\Events\MasterSupervisorLooped; +use Hypervel\Support\CarbonImmutable; class TrimRecentJobs { diff --git a/src/horizon/src/MasterSupervisor.php b/src/horizon/src/MasterSupervisor.php index cb8753948..5cba8324d 100644 --- a/src/horizon/src/MasterSupervisor.php +++ b/src/horizon/src/MasterSupervisor.php @@ -4,7 +4,6 @@ namespace Hypervel\Horizon; -use Carbon\CarbonImmutable; use Closure; use Exception; use Hypervel\Contracts\Cache\Factory as CacheFactory; @@ -16,6 +15,7 @@ use Hypervel\Horizon\Contracts\SupervisorRepository; use Hypervel\Horizon\Contracts\Terminable; use Hypervel\Horizon\Events\MasterSupervisorLooped; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Throwable; diff --git a/src/horizon/src/ProcessPool.php b/src/horizon/src/ProcessPool.php index 637ed3bb6..192bdc7cb 100644 --- a/src/horizon/src/ProcessPool.php +++ b/src/horizon/src/ProcessPool.php @@ -4,9 +4,9 @@ namespace Hypervel\Horizon; -use Carbon\CarbonImmutable; use Closure; use Countable; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Symfony\Component\Process\Process; diff --git a/src/horizon/src/Repositories/RedisJobRepository.php b/src/horizon/src/Repositories/RedisJobRepository.php index ac1c446a6..947c4606a 100644 --- a/src/horizon/src/Repositories/RedisJobRepository.php +++ b/src/horizon/src/Repositories/RedisJobRepository.php @@ -4,13 +4,13 @@ namespace Hypervel\Horizon\Repositories; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Redis\Factory as Redis; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\JobPayload; use Hypervel\Horizon\LuaScripts; use Hypervel\Redis\RedisProxy; use Hypervel\Support\Arr; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use stdClass; use Throwable; @@ -596,7 +596,7 @@ public function storeRetryReference(string $id, string $retryId): void public function deleteFailed(string $id): int { /* @phpstan-ignore-next-line */ - return $this->connection()->zrem('failed_jobs', $id) != 1 + return $this->connection()->zrem('failed_jobs', $id) !== 1 ? 0 : $this->connection()->del($id); } diff --git a/src/horizon/src/Repositories/RedisMasterSupervisorRepository.php b/src/horizon/src/Repositories/RedisMasterSupervisorRepository.php index b8e3f240b..122adde9a 100644 --- a/src/horizon/src/Repositories/RedisMasterSupervisorRepository.php +++ b/src/horizon/src/Repositories/RedisMasterSupervisorRepository.php @@ -4,13 +4,13 @@ namespace Hypervel\Horizon\Repositories; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Redis\Factory as Redis; use Hypervel\Horizon\Contracts\MasterSupervisorRepository; use Hypervel\Horizon\Contracts\SupervisorRepository; use Hypervel\Horizon\MasterSupervisor; use Hypervel\Redis\RedisProxy; use Hypervel\Support\Arr; +use Hypervel\Support\CarbonImmutable; use stdClass; class RedisMasterSupervisorRepository implements MasterSupervisorRepository diff --git a/src/horizon/src/Repositories/RedisMetricsRepository.php b/src/horizon/src/Repositories/RedisMetricsRepository.php index 8b5cc4fa0..36c8b02a6 100644 --- a/src/horizon/src/Repositories/RedisMetricsRepository.php +++ b/src/horizon/src/Repositories/RedisMetricsRepository.php @@ -4,7 +4,6 @@ namespace Hypervel\Horizon\Repositories; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Redis\Factory as Redis; use Hypervel\Horizon\Contracts\MetricsRepository; use Hypervel\Horizon\Lock; @@ -12,6 +11,7 @@ use Hypervel\Horizon\WaitTimeCalculator; use Hypervel\Redis\PhpRedis; use Hypervel\Redis\RedisProxy; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; class RedisMetricsRepository implements MetricsRepository diff --git a/src/horizon/src/Repositories/RedisProcessRepository.php b/src/horizon/src/Repositories/RedisProcessRepository.php index 12a3b7ef8..eb610c8a8 100644 --- a/src/horizon/src/Repositories/RedisProcessRepository.php +++ b/src/horizon/src/Repositories/RedisProcessRepository.php @@ -4,10 +4,10 @@ namespace Hypervel\Horizon\Repositories; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Redis\Factory as Redis; use Hypervel\Horizon\Contracts\ProcessRepository; use Hypervel\Redis\RedisProxy; +use Hypervel\Support\CarbonImmutable; class RedisProcessRepository implements ProcessRepository { diff --git a/src/horizon/src/Repositories/RedisSupervisorRepository.php b/src/horizon/src/Repositories/RedisSupervisorRepository.php index 11e85a360..41421f3f9 100644 --- a/src/horizon/src/Repositories/RedisSupervisorRepository.php +++ b/src/horizon/src/Repositories/RedisSupervisorRepository.php @@ -4,12 +4,12 @@ namespace Hypervel\Horizon\Repositories; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Redis\Factory as Redis; use Hypervel\Horizon\Contracts\SupervisorRepository; use Hypervel\Horizon\Supervisor; use Hypervel\Redis\RedisProxy; use Hypervel\Support\Arr; +use Hypervel\Support\CarbonImmutable; use stdClass; class RedisSupervisorRepository implements SupervisorRepository diff --git a/src/horizon/src/Supervisor.php b/src/horizon/src/Supervisor.php index a1cf2072a..f980add57 100644 --- a/src/horizon/src/Supervisor.php +++ b/src/horizon/src/Supervisor.php @@ -4,7 +4,6 @@ namespace Hypervel\Horizon; -use Carbon\CarbonImmutable; use Closure; use Exception; use Hypervel\Contracts\Cache\Factory as CacheFactory; @@ -15,6 +14,7 @@ use Hypervel\Horizon\Contracts\SupervisorRepository; use Hypervel\Horizon\Contracts\Terminable; use Hypervel\Horizon\Events\SupervisorLooped; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Throwable; diff --git a/src/horizon/src/WorkerProcess.php b/src/horizon/src/WorkerProcess.php index 266db82c9..c2142ae8b 100644 --- a/src/horizon/src/WorkerProcess.php +++ b/src/horizon/src/WorkerProcess.php @@ -4,11 +4,11 @@ namespace Hypervel\Horizon; -use Carbon\CarbonImmutable; use Closure; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Horizon\Events\UnableToLaunchProcess; use Hypervel\Horizon\Events\WorkerProcessRestarting; +use Hypervel\Support\CarbonImmutable; use RuntimeException; use Symfony\Component\Process\Exception\ExceptionInterface; use Symfony\Component\Process\Process; diff --git a/tests/Integration/Horizon/Feature/JobRetrievalTest.php b/tests/Integration/Horizon/Feature/JobRetrievalTest.php index 28f287cd1..789ed23f7 100644 --- a/tests/Integration/Horizon/Feature/JobRetrievalTest.php +++ b/tests/Integration/Horizon/Feature/JobRetrievalTest.php @@ -4,9 +4,9 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\JobPayload; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Queue; use Hypervel\Support\Facades\Redis; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; diff --git a/tests/Integration/Horizon/Feature/MetricsTest.php b/tests/Integration/Horizon/Feature/MetricsTest.php index bb6bbe724..af5667c22 100644 --- a/tests/Integration/Horizon/Feature/MetricsTest.php +++ b/tests/Integration/Horizon/Feature/MetricsTest.php @@ -4,9 +4,9 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\MetricsRepository; use Hypervel\Horizon\Stopwatch; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Queue; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; use Mockery as m; diff --git a/tests/Integration/Horizon/Feature/MonitorWaitTimesTest.php b/tests/Integration/Horizon/Feature/MonitorWaitTimesTest.php index b3bfe20b1..0fd2530a3 100644 --- a/tests/Integration/Horizon/Feature/MonitorWaitTimesTest.php +++ b/tests/Integration/Horizon/Feature/MonitorWaitTimesTest.php @@ -4,11 +4,11 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\MetricsRepository; use Hypervel\Horizon\Events\LongWaitDetected; use Hypervel\Horizon\Listeners\MonitorWaitTimes; use Hypervel\Horizon\WaitTimeCalculator; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; use Mockery as m; diff --git a/tests/Integration/Horizon/Feature/ProcessRepositoryTest.php b/tests/Integration/Horizon/Feature/ProcessRepositoryTest.php index 25a807c8c..3091923ad 100644 --- a/tests/Integration/Horizon/Feature/ProcessRepositoryTest.php +++ b/tests/Integration/Horizon/Feature/ProcessRepositoryTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\ProcessRepository; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; class ProcessRepositoryTest extends IntegrationTestCase diff --git a/tests/Integration/Horizon/Feature/QueueProcessingTest.php b/tests/Integration/Horizon/Feature/QueueProcessingTest.php index d3e7c1057..831456dd5 100644 --- a/tests/Integration/Horizon/Feature/QueueProcessingTest.php +++ b/tests/Integration/Horizon/Feature/QueueProcessingTest.php @@ -4,10 +4,10 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\Events\JobReserved; use Hypervel\Horizon\Events\JobsMigrated; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Support\Facades\Queue; use Hypervel\Support\Facades\Redis; diff --git a/tests/Integration/Horizon/Feature/SupervisorTest.php b/tests/Integration/Horizon/Feature/SupervisorTest.php index 00021c385..a3f7fd975 100644 --- a/tests/Integration/Horizon/Feature/SupervisorTest.php +++ b/tests/Integration/Horizon/Feature/SupervisorTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Exception; use Hypervel\Contracts\Debug\ExceptionHandler; use Hypervel\Horizon\AutoScaler; @@ -21,6 +20,7 @@ use Hypervel\Horizon\SystemProcessCounter; use Hypervel\Horizon\WorkerCommandString; use Hypervel\Horizon\WorkerProcess; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Support\Facades\Queue; use Hypervel\Support\Facades\Redis; diff --git a/tests/Integration/Horizon/Feature/TrimMonitoredJobsTest.php b/tests/Integration/Horizon/Feature/TrimMonitoredJobsTest.php index 66aca8b8b..3734edacd 100644 --- a/tests/Integration/Horizon/Feature/TrimMonitoredJobsTest.php +++ b/tests/Integration/Horizon/Feature/TrimMonitoredJobsTest.php @@ -4,11 +4,11 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\Events\MasterSupervisorLooped; use Hypervel\Horizon\Listeners\TrimMonitoredJobs; use Hypervel\Horizon\MasterSupervisor; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; use Mockery as m; diff --git a/tests/Integration/Horizon/Feature/TrimRecentJobsTest.php b/tests/Integration/Horizon/Feature/TrimRecentJobsTest.php index 0f4ca0215..822af56e6 100644 --- a/tests/Integration/Horizon/Feature/TrimRecentJobsTest.php +++ b/tests/Integration/Horizon/Feature/TrimRecentJobsTest.php @@ -4,11 +4,11 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Hypervel\Horizon\Contracts\JobRepository; use Hypervel\Horizon\Events\MasterSupervisorLooped; use Hypervel\Horizon\Listeners\TrimRecentJobs; use Hypervel\Horizon\MasterSupervisor; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; use Mockery as m; diff --git a/tests/Integration/Horizon/Feature/WorkerProcessTest.php b/tests/Integration/Horizon/Feature/WorkerProcessTest.php index 88c31c9bb..9a7c30471 100644 --- a/tests/Integration/Horizon/Feature/WorkerProcessTest.php +++ b/tests/Integration/Horizon/Feature/WorkerProcessTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Integration\Horizon\Feature; -use Carbon\CarbonImmutable; use Closure; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Horizon\Events\UnableToLaunchProcess; @@ -14,6 +13,7 @@ use Hypervel\Horizon\SupervisorProcess; use Hypervel\Horizon\WorkerProcess; use Hypervel\Queue\Worker as QueueWorker; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use Hypervel\Tests\Integration\Horizon\IntegrationTestCase; use Mockery as m; From b072d83c7a591156b078b238d869b9b457b4e208 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:17:25 +0000 Subject: [PATCH 19/34] refactor(session): make session expiry immutable Use Hypervel CarbonImmutable for database and file session expiration and for session middleware lifetime calculations. Preserve existing garbage collection, cookie lifetime, storage, and route-lock behavior while capturing every modifier result that must persist. Update array, file, and database session handler coverage to use immutable clocks and exact expiry expectations. --- src/session/src/DatabaseSessionHandler.php | 4 +- src/session/src/FileSessionHandler.php | 4 +- src/session/src/Middleware/StartSession.php | 9 ++--- .../Session/DatabaseSessionHandlerTest.php | 14 +++---- tests/Session/ArraySessionHandlerTest.php | 37 ++++++++++--------- tests/Session/FileSessionHandlerTest.php | 18 ++++----- 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/session/src/DatabaseSessionHandler.php b/src/session/src/DatabaseSessionHandler.php index 0be7a8c79..56b560c8a 100644 --- a/src/session/src/DatabaseSessionHandler.php +++ b/src/session/src/DatabaseSessionHandler.php @@ -13,7 +13,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Database\QueryException; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use SessionHandlerInterface; @@ -81,7 +81,7 @@ public function read(string $sessionId): false|string protected function expired(object $session): bool { return isset($session->last_activity) - && $session->last_activity < Carbon::now()->subMinutes($this->minutes)->getTimestamp(); + && $session->last_activity < CarbonImmutable::now()->subMinutes($this->minutes)->getTimestamp(); } public function write(string $sessionId, string $data): bool diff --git a/src/session/src/FileSessionHandler.php b/src/session/src/FileSessionHandler.php index 9ae78e8ef..d575cf67c 100644 --- a/src/session/src/FileSessionHandler.php +++ b/src/session/src/FileSessionHandler.php @@ -6,7 +6,7 @@ use Hypervel\Contracts\Filesystem\FileNotFoundException; use Hypervel\Filesystem\Filesystem; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use SessionHandlerInterface; use Symfony\Component\Finder\Finder; @@ -39,7 +39,7 @@ public function close(): bool public function read(string $sessionId): false|string { if ($this->files->isFile($path = $this->path . '/' . $sessionId) - && $this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp() + && $this->files->lastModified($path) >= CarbonImmutable::now()->subMinutes($this->minutes)->getTimestamp() ) { try { return $this->files->sharedGet($path); diff --git a/src/session/src/Middleware/StartSession.php b/src/session/src/Middleware/StartSession.php index ca180eb12..a1377c76f 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -15,8 +15,7 @@ use Hypervel\Routing\Route; use Hypervel\Session\SessionManager; use Hypervel\Session\Store; -use Hypervel\Support\Carbon; -use Hypervel\Support\Facades\Date; +use Hypervel\Support\CarbonImmutable; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; use Throwable; @@ -279,9 +278,9 @@ protected function getCookieExpirationDate(): DateTimeInterface|int { $expiresOnClose = $this->manager->getSessionConfig()['expire_on_close']; - return $expiresOnClose ? 0 : Date::instance( - Carbon::now()->addSeconds($this->getSessionLifetimeInSeconds()) - ); + return $expiresOnClose + ? 0 + : CarbonImmutable::now()->addSeconds($this->getSessionLifetimeInSeconds()); } /** diff --git a/tests/Integration/Session/DatabaseSessionHandlerTest.php b/tests/Integration/Session/DatabaseSessionHandlerTest.php index a893f4a6d..9473e32d4 100644 --- a/tests/Integration/Session/DatabaseSessionHandlerTest.php +++ b/tests/Integration/Session/DatabaseSessionHandlerTest.php @@ -7,14 +7,14 @@ use Hypervel\Context\RequestContext; use Hypervel\Http\Request; use Hypervel\Session\DatabaseSessionHandler; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\Attributes\WithMigration; use Hypervel\Tests\Integration\Database\DatabaseTestCase; #[WithMigration('session')] class DatabaseSessionHandlerTest extends DatabaseTestCase { - public function testBasicReadWriteFunctionality() + public function testBasicReadWriteFunctionality(): void { RequestContext::set(Request::create('/', 'GET', server: [ 'REMOTE_ADDR' => '127.0.0.1', @@ -56,7 +56,7 @@ public function testBasicReadWriteFunctionality() $this->assertEquals(2, $connection->table('sessions')->count()); // read expired: - Carbon::setTestNow(Carbon::now()->addMinutes(2)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addMinutes(2)); $this->assertEquals('', $handler->read('valid_session_id_2425')); // rewriting an expired session-id, makes it live: @@ -64,24 +64,24 @@ public function testBasicReadWriteFunctionality() $this->assertEquals(['come' => 'alive'], json_decode($handler->read('valid_session_id_2425'), true)); } - public function testGarbageCollector() + public function testGarbageCollector(): void { $resolver = $this->app->make('db'); $connection = $this->app['db']->connection(); $handler = new DatabaseSessionHandler($resolver, null, 'sessions', 1, $this->app); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $handler->write('simple_id_1', 'abcd'); $this->assertEquals(0, $handler->gc(1)); - Carbon::setTestNow(Carbon::now()->addSeconds(2)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(2)); $handler = new DatabaseSessionHandler($resolver, null, 'sessions', 1, $this->app); $handler->write('simple_id_2', 'abcd'); $this->assertEquals(1, $handler->gc(2)); $this->assertEquals(1, $connection->table('sessions')->count()); - Carbon::setTestNow(Carbon::now()->addSeconds(2)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(2)); $this->assertEquals(1, $handler->gc(1)); $this->assertEquals(0, $connection->table('sessions')->count()); diff --git a/tests/Session/ArraySessionHandlerTest.php b/tests/Session/ArraySessionHandlerTest.php index c8f9d61be..32ed0f0d7 100644 --- a/tests/Session/ArraySessionHandlerTest.php +++ b/tests/Session/ArraySessionHandlerTest.php @@ -5,13 +5,14 @@ namespace Hypervel\Tests\Session; use Hypervel\Session\ArraySessionHandler; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; use SessionHandlerInterface; class ArraySessionHandlerTest extends TestCase { - public function testIsSessionHandlerInterface() + public function testIsSessionHandlerInterface(): void { $this->assertInstanceOf( SessionHandlerInterface::class, @@ -19,21 +20,21 @@ public function testIsSessionHandlerInterface() ); } - public function testInitializeSession() + public function testInitializeSession(): void { $handler = new ArraySessionHandler(10); $this->assertTrue($handler->open('', '')); } - public function testCloseSession() + public function testCloseSession(): void { $handler = new ArraySessionHandler(10); $this->assertTrue($handler->close()); } - public function testReadDataFromSession() + public function testReadDataFromSession(): void { $handler = new ArraySessionHandler(10); @@ -42,38 +43,38 @@ public function testReadDataFromSession() $this->assertSame('bar', $handler->read('foo')); } - public function testReadDataFromAlmostExpiredSession() + public function testReadDataFromAlmostExpiredSession(): void { $handler = new ArraySessionHandler(10); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(Date::now()); $handler->write('foo', 'bar'); - Carbon::setTestNow(Carbon::now()->addMinutes(10)); + CarbonImmutable::setTestNow(Date::now()->addMinutes(10)); $this->assertSame('bar', $handler->read('foo')); } - public function testReadDataFromExpiredSession() + public function testReadDataFromExpiredSession(): void { $handler = new ArraySessionHandler(10); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(Date::now()); $handler->write('foo', 'bar'); - Carbon::setTestNow(Carbon::now()->addMinutes(10)->addSecond()); + CarbonImmutable::setTestNow(Date::now()->addMinutes(10)->addSecond()); $this->assertSame('', $handler->read('foo')); } - public function testReadDataFromNonExistingSession() + public function testReadDataFromNonExistingSession(): void { $handler = new ArraySessionHandler(10); $this->assertSame('', $handler->read('foo')); } - public function testWriteSessionData() + public function testWriteSessionData(): void { $handler = new ArraySessionHandler(10); @@ -84,7 +85,7 @@ public function testWriteSessionData() $this->assertSame('baz', $handler->read('foo')); } - public function testDestroySession() + public function testDestroySession(): void { $handler = new ArraySessionHandler(10); @@ -96,22 +97,22 @@ public function testDestroySession() $this->assertSame('', $handler->read('foo')); } - public function testCleanOldSession() + public function testCleanOldSession(): void { $handler = new ArraySessionHandler(10); $this->assertSame(0, $handler->gc(300)); - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(Date::now()); $handler->write('foo', 'bar'); $this->assertSame(0, $handler->gc(300)); $this->assertSame('bar', $handler->read('foo')); - Carbon::setTestNow(Carbon::now()->addSecond()); + CarbonImmutable::setTestNow(Date::now()->addSecond()); $handler->write('baz', 'qux'); - Carbon::setTestNow(Carbon::now()->addMinutes(5)); + CarbonImmutable::setTestNow(Date::now()->addMinutes(5)); $this->assertSame(1, $handler->gc(300)); $this->assertSame('', $handler->read('foo')); diff --git a/tests/Session/FileSessionHandlerTest.php b/tests/Session/FileSessionHandlerTest.php index 589644187..2ca503cec 100644 --- a/tests/Session/FileSessionHandlerTest.php +++ b/tests/Session/FileSessionHandlerTest.php @@ -7,7 +7,7 @@ use Hypervel\Contracts\Filesystem\FileNotFoundException; use Hypervel\Filesystem\Filesystem; use Hypervel\Session\FileSessionHandler; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -37,15 +37,15 @@ public function testClose() $this->assertTrue($this->sessionHandler->close()); } - public function testReadReturnsDataWhenFileExistsAndIsValid() + public function testReadReturnsDataWhenFileExistsAndIsValid(): void { $sessionId = 'session_id'; $path = '/path/to/sessions/' . $sessionId; - Carbon::setTestNow(Carbon::parse('2025-02-02 01:30:00')); + CarbonImmutable::setTestNow(CarbonImmutable::parse('2025-02-02 01:30:00')); $this->files->shouldReceive('isFile')->with($path)->andReturn(true); - $minutesAgo30 = Carbon::parse('2025-02-02 01:00:00')->getTimestamp(); + $minutesAgo30 = CarbonImmutable::parse('2025-02-02 01:00:00')->getTimestamp(); $this->files->shouldReceive('lastModified')->with($path)->andReturn($minutesAgo30); $this->files->shouldReceive('sharedGet')->with($path)->once()->andReturn('session_data'); @@ -54,15 +54,15 @@ public function testReadReturnsDataWhenFileExistsAndIsValid() $this->assertSame('session_data', $result); } - public function testReadReturnsEmptyWhenFileExistsButExpired() + public function testReadReturnsEmptyWhenFileExistsButExpired(): void { $sessionId = 'session_id'; $path = '/path/to/sessions/' . $sessionId; - Carbon::setTestNow(Carbon::parse('2025-02-02 01:30:01')); + CarbonImmutable::setTestNow(CarbonImmutable::parse('2025-02-02 01:30:01')); $this->files->shouldReceive('isFile')->with($path)->andReturn(true); - $minutesAgo30 = Carbon::parse('2025-02-02 01:00:00')->getTimestamp(); + $minutesAgo30 = CarbonImmutable::parse('2025-02-02 01:00:00')->getTimestamp(); $this->files->shouldReceive('lastModified')->with($path)->andReturn($minutesAgo30); $this->files->shouldReceive('sharedGet')->never(); @@ -87,10 +87,10 @@ public function testReadReturnsEmptyStringWhenTheSessionFileDisappearsBeforeRead { $sessionId = 'vanished_session_id'; $path = '/path/to/sessions/' . $sessionId; - Carbon::setTestNow(Carbon::parse('2025-02-02 01:30:00')); + CarbonImmutable::setTestNow(CarbonImmutable::parse('2025-02-02 01:30:00')); $this->files->shouldReceive('isFile')->with($path)->andReturnTrue(); - $this->files->shouldReceive('lastModified')->with($path)->andReturn(Carbon::now()->getTimestamp()); + $this->files->shouldReceive('lastModified')->with($path)->andReturn(CarbonImmutable::now()->getTimestamp()); $this->files->shouldReceive('sharedGet')->with($path)->once()->andThrow( new FileNotFoundException("Unable to read file at path {$path}.") ); From 17905143180fb34ed2d3440d6dd7492e61132668 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:17:40 +0000 Subject: [PATCH 20/34] fix(grpc): prewarm protobuf descriptors before workers Construct every registered request message and eligible concrete response message during the existing pre-fork router compilation phase. This moves process-global protobuf descriptor registration out of concurrent request coroutines, avoids first-use registration races, and removes descriptor initialization from the request path. Keep response warming limited to statically named message classes that can be constructed without required arguments. Also canonicalize gRPC call timestamps on Hypervel CarbonImmutable and add cold-process regressions for request and response descriptor warming alongside coroutine client isolation coverage. --- src/grpc/src/Server/GrpcRouter.php | 21 ++++++++++++++-- src/grpc/src/Server/Middleware/HandleCall.php | 2 +- src/grpc/src/Server/ServerCallContext.php | 2 +- tests/Grpc/ClientCoroutineIsolationTest.php | 3 +++ tests/Grpc/GrpcRouterTest.php | 24 +++++++++++++++++++ tests/Grpc/ServerCallContextTest.php | 2 +- 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/grpc/src/Server/GrpcRouter.php b/src/grpc/src/Server/GrpcRouter.php index 67667216b..c1c6047c3 100644 --- a/src/grpc/src/Server/GrpcRouter.php +++ b/src/grpc/src/Server/GrpcRouter.php @@ -90,9 +90,9 @@ public function compileAndWarm(): void } [$requestParameter, $requestClass] = $requestParameters[0]; - $reflection = new ReflectionClass($requestClass); + $requestReflection = new ReflectionClass($requestClass); - if ($requestClass === Message::class || ! $reflection->isInstantiable()) { + if ($requestClass === Message::class || ! $requestReflection->isInstantiable()) { throw new InvalidArgumentException( "The gRPC route action [{$action}] request parameter [\${$requestParameter->getName()}] must name a concrete protobuf message class." ); @@ -104,6 +104,23 @@ public function compileAndWarm(): void throw new InvalidArgumentException("The gRPC route action [{$action}] is missing its protocol marker."); } + // Generated descriptor registration is process-global and not coroutine-safe. + // Construct known messages before workers fork and requests can run concurrently. + $requestReflection->newInstance(); + + $responseType = $requestParameter->getDeclaringFunction()->getReturnType(); + + if ($responseType instanceof ReflectionNamedType + && ($responseClass = $this->parameterClassName($requestParameter, $responseType)) !== null + && is_subclass_of($responseClass, Message::class)) { + $responseReflection = new ReflectionClass($responseClass); + + if ($responseReflection->isInstantiable() + && ($responseReflection->getConstructor()?->getNumberOfRequiredParameters() ?? 0) === 0) { + $responseReflection->newInstance(); + } + } + $route->setAction([ ...$route->getAction(), '_grpc' => [ diff --git a/src/grpc/src/Server/Middleware/HandleCall.php b/src/grpc/src/Server/Middleware/HandleCall.php index f21bf277d..c9526ac5e 100644 --- a/src/grpc/src/Server/Middleware/HandleCall.php +++ b/src/grpc/src/Server/Middleware/HandleCall.php @@ -4,7 +4,6 @@ namespace Hypervel\Grpc\Server\Middleware; -use Carbon\CarbonImmutable; use Closure; use Google\Protobuf\Internal\Message; use Hypervel\Coordinator\Timer; @@ -24,6 +23,7 @@ use Hypervel\Grpc\StatusCode; use Hypervel\Http\Request; use Hypervel\Routing\Route; +use Hypervel\Support\CarbonImmutable; use InvalidArgumentException; use LogicException; use Swoole\Coroutine\CanceledException; diff --git a/src/grpc/src/Server/ServerCallContext.php b/src/grpc/src/Server/ServerCallContext.php index b59110b6f..77b51dfb5 100644 --- a/src/grpc/src/Server/ServerCallContext.php +++ b/src/grpc/src/Server/ServerCallContext.php @@ -4,9 +4,9 @@ namespace Hypervel\Grpc\Server; -use Carbon\CarbonImmutable; use Hypervel\Grpc\Metadata; use Hypervel\Grpc\Protocol\Deadline; +use Hypervel\Support\CarbonImmutable; readonly class ServerCallContext { diff --git a/tests/Grpc/ClientCoroutineIsolationTest.php b/tests/Grpc/ClientCoroutineIsolationTest.php index 1f1bcfb11..46ff63f74 100644 --- a/tests/Grpc/ClientCoroutineIsolationTest.php +++ b/tests/Grpc/ClientCoroutineIsolationTest.php @@ -23,6 +23,9 @@ class ClientCoroutineIsolationTest extends TestCase { public function testClientsCallsMetadataDeadlinesAndResponsesRemainIsolatedAcrossCoroutines(): void { + // Protobuf's generated descriptor registration is not coroutine-safe on first use. + new TestRequest; + $clients = []; $engines = []; $tasks = []; diff --git a/tests/Grpc/GrpcRouterTest.php b/tests/Grpc/GrpcRouterTest.php index 072ad1060..be823e586 100644 --- a/tests/Grpc/GrpcRouterTest.php +++ b/tests/Grpc/GrpcRouterTest.php @@ -7,6 +7,7 @@ use Google\Protobuf\Any; use Google\Protobuf\GPBEmpty; use Google\Protobuf\Internal\Message; +use GPBMetadata\Google\Protobuf\GPBEmpty as GPBEmptyMetadata; use Hypervel\Container\Container; use Hypervel\Events\Dispatcher; use Hypervel\Grpc\Exceptions\RpcException; @@ -24,9 +25,13 @@ use Hypervel\Routing\CompiledRouteCollection; use Hypervel\Routing\Route; use Hypervel\Routing\Router; +use Hypervel\Tests\Grpc\Fixtures\Metadata\TestService; +use Hypervel\Tests\Grpc\Fixtures\TestRequest; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; +use PHPUnit\Framework\Attributes\PreserveGlobalState; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use RuntimeException; class GrpcRouterTest extends TestCase @@ -75,6 +80,25 @@ public function testValidatesEverySupportedActionShapeBeforeCompilation(): void } } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testWarmsProtobufMessageDescriptorsBeforeWorkerFork(): void + { + $this->assertFalse(TestService::$is_initialized); + $this->assertFalse(GPBEmptyMetadata::$is_initialized); + [$router, $registrar] = $this->router(); + $route = $registrar->unary( + 'testing.Router/Warm', + static fn (TestRequest $request): GPBEmpty => new GPBEmpty, + ); + + $router->compileAndWarm(); + + $this->assertTrue(TestService::$is_initialized); + $this->assertTrue(GPBEmptyMetadata::$is_initialized); + $this->assertSame(TestRequest::class, $route->getAction('_grpc.request_class')); + } + public function testRejectsInvalidProtobufAndCallContextParameters(): void { foreach ([ diff --git a/tests/Grpc/ServerCallContextTest.php b/tests/Grpc/ServerCallContextTest.php index 4a331a4fe..8ab23b1b6 100644 --- a/tests/Grpc/ServerCallContextTest.php +++ b/tests/Grpc/ServerCallContextTest.php @@ -4,11 +4,11 @@ namespace Hypervel\Tests\Grpc; -use Carbon\CarbonImmutable; use Hypervel\Grpc\Metadata; use Hypervel\Grpc\Protocol\Deadline; use Hypervel\Grpc\Server\CallContextStore; use Hypervel\Grpc\Server\ServerCallContext; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use LogicException; From 5d86aa03cf48dcb5986ce6e597acc9b165684eb0 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:17:56 +0000 Subject: [PATCH 21/34] refactor(collections): use immutable lazy clocks Use Hypervel CarbonImmutable for the optional LazyCollection clock path and capture advancing timestamps explicitly during time-based iteration. Keep native microtime behavior and lazy evaluation unchanged while making Carbon-backed intervals safe under immutable semantics. Update lazy collection coverage for clock progression, throttling, and exact immutable values. --- src/collections/src/LazyCollection.php | 8 ++--- .../SupportLazyCollectionIsLazyTest.php | 12 +++---- tests/Support/SupportLazyCollectionTest.php | 32 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/collections/src/LazyCollection.php b/src/collections/src/LazyCollection.php index 865a36d9a..7cf80a0e5 100644 --- a/src/collections/src/LazyCollection.php +++ b/src/collections/src/LazyCollection.php @@ -1830,8 +1830,8 @@ protected function passthru(string $method, array $params): static */ protected function now(): int { - return class_exists(Carbon::class) - ? Carbon::now()->getTimestamp() + return class_exists(CarbonImmutable::class) + ? CarbonImmutable::now()->getTimestamp() : time(); } @@ -1840,8 +1840,8 @@ protected function now(): int */ protected function preciseNow(): float { - return class_exists(Carbon::class) - ? Carbon::now()->getPreciseTimestamp() + return class_exists(CarbonImmutable::class) + ? CarbonImmutable::now()->getPreciseTimestamp() : microtime(true) * 1_000_000; } diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index e3a60834c..0ff125213 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -5,7 +5,7 @@ namespace Hypervel\Tests\Support; use Exception; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\ItemNotFoundException; use Hypervel\Support\LazyCollection; use Hypervel\Support\MultipleItemsFoundException; @@ -1310,7 +1310,7 @@ public function testTakeUntilTimeoutIsLazy(): void { tap(m::mock(LazyCollection::class . '[now]')->times(100), function ($mock) { $this->assertDoesNotEnumerateCollection($mock, function ($mock) { - $timeout = Carbon::now(); + $timeout = CarbonImmutable::now(); $results = $mock ->tap(function ($collection) use ($mock, $timeout) { @@ -1330,7 +1330,7 @@ public function testTakeUntilTimeoutIsLazy(): void tap(m::mock(LazyCollection::class . '[now]')->times(100), function ($mock) { $this->assertEnumeratesCollection($mock, 1, function ($mock) { - $timeout = Carbon::now(); + $timeout = CarbonImmutable::now(); $results = $mock ->tap(function ($collection) use ($mock, $timeout) { @@ -1340,7 +1340,7 @@ public function testTakeUntilTimeoutIsLazy(): void ->shouldReceive('now') ->times(2) ->andReturn( - (clone $timeout)->sub(1, 'minute')->getTimestamp(), + $timeout->sub(1, 'minute')->getTimestamp(), $timeout->getTimestamp() ); }) @@ -1351,7 +1351,7 @@ public function testTakeUntilTimeoutIsLazy(): void tap(m::mock(LazyCollection::class . '[now]')->times(100), function ($mock) { $this->assertEnumeratesCollectionOnce($mock, function ($mock) { - $timeout = Carbon::now(); + $timeout = CarbonImmutable::now(); $results = $mock ->tap(function ($collection) use ($mock, $timeout) { @@ -1361,7 +1361,7 @@ public function testTakeUntilTimeoutIsLazy(): void ->shouldReceive('now') ->times(100) ->andReturn( - (clone $timeout)->sub(1, 'minute')->getTimestamp() + $timeout->sub(1, 'minute')->getTimestamp() ); }) ->takeUntilTimeout($timeout) diff --git a/tests/Support/SupportLazyCollectionTest.php b/tests/Support/SupportLazyCollectionTest.php index ca58be618..05e2bac72 100644 --- a/tests/Support/SupportLazyCollectionTest.php +++ b/tests/Support/SupportLazyCollectionTest.php @@ -8,7 +8,7 @@ use Carbon\CarbonInterval as Duration; use Generator; use Hypervel\Foundation\Testing\Wormhole; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; use Hypervel\Support\Sleep; @@ -226,7 +226,7 @@ public function testRememberWithDuplicateKeys(): void public function testTakeUntilTimeout(): void { - $timeout = Carbon::now(); + $timeout = CarbonImmutable::now(); $mock = m::mock(LazyCollection::class . '[now]'); @@ -241,8 +241,8 @@ public function testTakeUntilTimeout(): void ->shouldReceive('now') ->times(3) ->andReturn( - (clone $timeout)->sub(2, 'minute')->getTimestamp(), - (clone $timeout)->sub(1, 'minute')->getTimestamp(), + $timeout->sub(2, 'minute')->getTimestamp(), + $timeout->sub(1, 'minute')->getTimestamp(), $timeout->getTimestamp() ); }) @@ -299,7 +299,7 @@ public function testThrottle(): void public function testThrottleAccountsForTimePassed(): void { Sleep::fake(); - Carbon::setTestNow(now()); + CarbonImmutable::setTestNow(now()); $data = LazyCollection::times(3) ->throttle(3) @@ -326,7 +326,7 @@ public function testThrottleAccountsForTimePassed(): void $this->assertSame([1, 2, 3], $data); Sleep::fake(false); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testUniqueDoubleEnumeration(): void @@ -491,31 +491,31 @@ public function testDot(): void public function testWithHeartbeat(): void { - $start = Carbon::create(2000, 1, 1); - $after2Minutes = $start->copy()->addMinutes(2); - $after5Minutes = $start->copy()->addMinutes(5); - $after7Minutes = $start->copy()->addMinutes(7); - $after11Minutes = $start->copy()->addMinutes(11); + $start = CarbonImmutable::create(2000, 1, 1); + $after2Minutes = $start->addMinutes(2); + $after5Minutes = $start->addMinutes(5); + $after7Minutes = $start->addMinutes(7); + $after11Minutes = $start->addMinutes(11); - Carbon::setTestNow($start); + CarbonImmutable::setTestNow($start); $output = new Collection; $numbers = LazyCollection::range(1, 10) // Move the clock to possibly trigger the heartbeat... - ->tapEach(fn ($number) => Carbon::setTestNow( + ->tapEach(fn ($number) => CarbonImmutable::setTestNow( match ($number) { 3 => $after2Minutes, 4 => $after5Minutes, 6 => $after7Minutes, 9 => $after11Minutes, - default => Carbon::now(), + default => CarbonImmutable::now(), } )) // Push the current date to `output` when heartbeat is triggered... - ->withHeartbeat(Duration::minutes(5), fn () => $output[] = Carbon::now()) + ->withHeartbeat(Duration::minutes(5), fn () => $output[] = CarbonImmutable::now()) // Push every number onto `output` as it's enumerated... ->tapEach(fn ($number) => $output[] = $number)->all(); @@ -533,7 +533,7 @@ public function testWithHeartbeat(): void $output->all(), ); - Carbon::setTestNow(); + CarbonImmutable::setTestNow(); } public function testRandomPreservesKeys(): void From 832d25beb7f1f2b26c85e90f364e5b50f8b3fa70 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:18:05 +0000 Subject: [PATCH 22/34] refactor(http): use immutable expiry and signing dates Use Hypervel CarbonImmutable for response cache expiry and temporary signed URL deadlines while preserving the existing HTTP and URL signature formats. Capture date modifiers explicitly and keep user-supplied DateTimeInterface boundaries unchanged. Align HTTP client, Redis throttling, and URL signing fixtures with the framework immutable default. --- src/http/src/Middleware/SetCacheHeaders.php | 6 +++--- src/routing/src/UrlGenerator.php | 4 ++-- tests/Http/HttpClientTest.php | 8 ++++---- .../Http/ThrottleRequestsWithRedisTest.php | 10 +++++----- tests/Integration/Routing/UrlSigningTest.php | 18 +++++++++--------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/http/src/Middleware/SetCacheHeaders.php b/src/http/src/Middleware/SetCacheHeaders.php index 85f16c3e2..12e08b04b 100644 --- a/src/http/src/Middleware/SetCacheHeaders.php +++ b/src/http/src/Middleware/SetCacheHeaders.php @@ -6,7 +6,7 @@ use Closure; use Hypervel\Http\Request; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; use InvalidArgumentException; @@ -65,9 +65,9 @@ public function handle(Request $request, Closure $next, array|string $options = if (isset($options['last_modified'])) { if (is_numeric($options['last_modified'])) { - $options['last_modified'] = Carbon::createFromTimestamp($options['last_modified'], date_default_timezone_get()); + $options['last_modified'] = CarbonImmutable::createFromTimestamp($options['last_modified'], date_default_timezone_get()); } else { - $options['last_modified'] = Carbon::parse($options['last_modified']); + $options['last_modified'] = CarbonImmutable::parse($options['last_modified']); } } diff --git a/src/routing/src/UrlGenerator.php b/src/routing/src/UrlGenerator.php index 47af04da4..463d3b4e5 100755 --- a/src/routing/src/UrlGenerator.php +++ b/src/routing/src/UrlGenerator.php @@ -14,7 +14,7 @@ use Hypervel\Contracts\Routing\UrlRoutable; use Hypervel\Http\Request; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Str; @@ -417,7 +417,7 @@ public function signatureHasNotExpired(Request $request): bool { $expires = $request->query('expires'); - return ! ($expires && Carbon::now()->getTimestamp() > $expires); + return ! ($expires && CarbonImmutable::now()->getTimestamp() > $expires); } /** diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 55e3d1b6b..63612d86a 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -29,7 +29,7 @@ use Hypervel\Http\Client\ResponseSequence; use Hypervel\Http\Response as HttpResponse; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Fluent; use Hypervel\Support\Sleep; @@ -3754,15 +3754,15 @@ public function testTheTransferStatsAreCustomizableOnFake(): void $this->assertTrue($onStatsFunctionCalled); } - public function testItCanAddGlobalMiddleware() + public function testItCanAddGlobalMiddleware(): void { - Carbon::setTestNow(now()->startOfDay()); + CarbonImmutable::setTestNow(now()->startOfDay()); $requests = []; $responses = []; $this->factory->fake(function ($r) use (&$requests) { $requests[] = $r; - Carbon::setTestNow(now()->addSeconds(6 * count($requests))); + CarbonImmutable::setTestNow(now()->addSeconds(6 * count($requests))); return $this->factory::response('expected content'); }); diff --git a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php index 1332972fc..c24ac7a02 100644 --- a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php +++ b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php @@ -6,7 +6,7 @@ use Hypervel\Foundation\Testing\Concerns\InteractsWithRedis; use Hypervel\Routing\Middleware\ThrottleRequestsWithRedis; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Route; use Hypervel\Testbench\TestCase; use Throwable; @@ -15,11 +15,11 @@ class ThrottleRequestsWithRedisTest extends TestCase { use InteractsWithRedis; - public function testLockOpensImmediatelyAfterDecay() + public function testLockOpensImmediatelyAfterDecay(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); - Carbon::setTestNow($now); + CarbonImmutable::setTestNow($now); Route::get('/', function () { return 'yes'; @@ -35,7 +35,7 @@ public function testLockOpensImmediatelyAfterDecay() $this->assertEquals(2, $response->headers->get('X-RateLimit-Limit')); $this->assertEquals(0, $response->headers->get('X-RateLimit-Remaining')); - Carbon::setTestNow($finish = $now->addSeconds(58)); + CarbonImmutable::setTestNow($finish = $now->addSeconds(58)); try { $this->withoutExceptionHandling()->get('/'); diff --git a/tests/Integration/Routing/UrlSigningTest.php b/tests/Integration/Routing/UrlSigningTest.php index 19a5b7d8e..56a7bf726 100644 --- a/tests/Integration/Routing/UrlSigningTest.php +++ b/tests/Integration/Routing/UrlSigningTest.php @@ -8,7 +8,7 @@ use Hypervel\Http\Request; use Hypervel\Routing\Exceptions\InvalidSignatureException; use Hypervel\Routing\Middleware\ValidateSignature; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Route; use Hypervel\Support\Facades\URL; use Hypervel\Tests\Integration\Routing\RoutingTestCase; @@ -76,17 +76,17 @@ public function testSigningUrlWithCustomRouteSlug() }); } - public function testTemporarySignedUrls() + public function testTemporarySignedUrls(): void { Route::get('/foo/{id}', function (Request $request, $id) { return $request->hasValidSignature() ? 'valid' : 'invalid'; })->name('foo'); - Carbon::setTestNow(Carbon::create(2018, 1, 1)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2018, 1, 1)); $this->assertIsString($url = URL::temporarySignedRoute('foo', now()->addMinutes(5), ['id' => 1])); $this->assertSame('valid', $this->get($url)->original); - Carbon::setTestNow(Carbon::create(2018, 1, 1)->addMinutes(10)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2018, 1, 1)->addMinutes(10)); $this->assertSame('invalid', $this->get($url)->original); } @@ -233,26 +233,26 @@ public function testExceptedParameterCanBeAPrefixOrSuffixOfAnotherParameter() $this->assertSame('valid', $this->get($url . '&pre=fix&fix=suff')->original); } - public function testSignedMiddleware() + public function testSignedMiddleware(): void { Route::get('/foo/{id}', function (Request $request, $id) { return $request->hasValidSignature() ? 'valid' : 'invalid'; })->name('foo')->middleware(ValidateSignature::class); - Carbon::setTestNow(Carbon::create(2018, 1, 1)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2018, 1, 1)); $this->assertIsString($url = URL::temporarySignedRoute('foo', now()->addMinutes(5), ['id' => 1])); $this->assertSame('valid', $this->get($url)->original); } - public function testSignedMiddlewareWithInvalidUrl() + public function testSignedMiddlewareWithInvalidUrl(): void { Route::get('/foo/{id}', function (Request $request, $id) { return $request->hasValidSignature() ? 'valid' : 'invalid'; })->name('foo')->middleware(ValidateSignature::class); - Carbon::setTestNow(Carbon::create(2018, 1, 1)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2018, 1, 1)); $this->assertIsString($url = URL::temporarySignedRoute('foo', now()->addMinutes(5), ['id' => 1])); - Carbon::setTestNow(Carbon::create(2018, 1, 1)->addMinutes(10)); + CarbonImmutable::setTestNow(CarbonImmutable::create(2018, 1, 1)->addMinutes(10)); $response = $this->get($url); $response->assertStatus(403); From f3feffaae57112ff03212d0ced0a786902de1b2e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:18:13 +0000 Subject: [PATCH 23/34] refactor(telescope): use immutable entry dates Use Hypervel CarbonImmutable for Telescope pruning and exception time windows, and express entry timestamp inputs through the existing CarbonInterface contract. Preserve query watcher payloads, stored timestamps, and pruning behavior while removing mutable-only metadata. Update watcher fixtures to capture immutable clock changes explicitly. --- src/telescope/src/Console/PruneCommand.php | 4 ++-- src/telescope/src/EntryResult.php | 2 +- .../src/Http/Controllers/ExceptionController.php | 4 ++-- tests/Telescope/Watchers/QueryWatcherTest.php | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/telescope/src/Console/PruneCommand.php b/src/telescope/src/Console/PruneCommand.php index 97f6efc6b..1c591409d 100644 --- a/src/telescope/src/Console/PruneCommand.php +++ b/src/telescope/src/Console/PruneCommand.php @@ -5,7 +5,7 @@ namespace Hypervel\Telescope\Console; use Hypervel\Console\Command; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Telescope\Contracts\PrunableRepository; use Symfony\Component\Console\Attribute\AsCommand; @@ -27,6 +27,6 @@ class PruneCommand extends Command */ public function handle(PrunableRepository $repository) { - $this->info($repository->prune(Carbon::now()->subHours((int) $this->option('hours')), $this->option('keep-exceptions')) . ' entries pruned.'); + $this->info($repository->prune(CarbonImmutable::now()->subHours((int) $this->option('hours')), $this->option('keep-exceptions')) . ' entries pruned.'); } } diff --git a/src/telescope/src/EntryResult.php b/src/telescope/src/EntryResult.php index 6c1856a8e..0151b1e3e 100644 --- a/src/telescope/src/EntryResult.php +++ b/src/telescope/src/EntryResult.php @@ -24,7 +24,7 @@ class EntryResult implements JsonSerializable * @param string $type the entry's type * @param null|string $familyHash the entry's family hash * @param array $content the entry's content - * @param \Carbon\Carbon|\Carbon\CarbonInterface $createdAt the datetime that the entry was recorded + * @param CarbonInterface $createdAt the datetime that the entry was recorded * @param array $tags the tags assigned to the entry */ public function __construct( diff --git a/src/telescope/src/Http/Controllers/ExceptionController.php b/src/telescope/src/Http/Controllers/ExceptionController.php index 4dfcb62f6..0a5486d87 100644 --- a/src/telescope/src/Http/Controllers/ExceptionController.php +++ b/src/telescope/src/Http/Controllers/ExceptionController.php @@ -5,7 +5,7 @@ namespace Hypervel\Telescope\Http\Controllers; use Hypervel\Http\Request; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Telescope\Contracts\EntriesRepository; use Hypervel\Telescope\EntryType; use Hypervel\Telescope\EntryUpdate; @@ -39,7 +39,7 @@ public function update(EntriesRepository $storage, Request $request, string $id) if ($request->input('resolved_at') === 'now') { $update = new EntryUpdate($entry->id, $entry->type, [ - 'resolved_at' => Carbon::now()->toDateTimeString(), + 'resolved_at' => CarbonImmutable::now()->toDateTimeString(), ]); $storage->update(collect([$update])); diff --git a/tests/Telescope/Watchers/QueryWatcherTest.php b/tests/Telescope/Watchers/QueryWatcherTest.php index 277e0020c..ab584f56a 100644 --- a/tests/Telescope/Watchers/QueryWatcherTest.php +++ b/tests/Telescope/Watchers/QueryWatcherTest.php @@ -7,7 +7,7 @@ use Exception; use Hypervel\Database\Connection; use Hypervel\Database\Events\QueryExecuted; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Telescope\EntryType; use Hypervel\Telescope\Storage\EntryModel; @@ -62,13 +62,13 @@ public function testQueryWatcherCanTagSlowQueries() $this->assertTrue($entry->content['slow']); } - public function testQueryWatcherCanPrepareBindings() + public function testQueryWatcherCanPrepareBindings(): void { EntryModel::where('type', 'query') ->where('should_display_on_index', true) ->whereNull('family_hash') ->where('sequence', '>', 100) - ->where('created_at', '<', Carbon::parse('2019-01-01')) + ->where('created_at', '<', CarbonImmutable::parse('2019-01-01')) ->update([ 'content' => null, 'should_display_on_index' => false, @@ -87,7 +87,7 @@ public function testQueryWatcherCanPrepareBindings() $this->assertSame('testing', $entry->content['connection']); } - public function testQueryWatcherCanPrepareNamedBindings() + public function testQueryWatcherCanPrepareNamedBindings(): void { // using the "sequence"-condition twice is intentional // to test whether named parameters can be used multiple times. @@ -100,7 +100,7 @@ public function testQueryWatcherCanPrepareNamedBindings() 'sequence' => 100, 'index_old' => 1, 'type' => 'query', - 'created_at' => Carbon::parse('2019-01-01'), + 'created_at' => CarbonImmutable::parse('2019-01-01'), 'index_new' => 0, 'content' => null, ] From badb963cd188d965db1b78ab7192ac22e06a5a2f Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:18:22 +0000 Subject: [PATCH 24/34] fix(models): type cast dates through CarbonInterface Describe Passkey and Sanctum model date attributes as CarbonInterface rather than concrete mutable Carbon classes so their metadata matches configurable Eloquent cast output. Keep nullable attribute semantics and model behavior unchanged while allowing the immutable framework default and explicit mutable opt-out. Align Sanctum expiry-pruning coverage with immutable modifier semantics. --- src/passkeys/src/Passkey.php | 8 ++++---- src/sanctum/src/PersonalAccessToken.php | 5 +++-- tests/Sanctum/PruneExpiredTest.php | 14 +++++++------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/passkeys/src/Passkey.php b/src/passkeys/src/Passkey.php index aa4802f92..3fdae4292 100644 --- a/src/passkeys/src/Passkey.php +++ b/src/passkeys/src/Passkey.php @@ -4,13 +4,13 @@ namespace Hypervel\Passkeys; +use Carbon\CarbonInterface; use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Casts\Attribute; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\MorphTo; use Hypervel\Passkeys\Contracts\PasskeyUser; use Hypervel\Passkeys\Support\Aaguids; -use Hypervel\Support\Carbon; /** * @mixin Builder @@ -21,9 +21,9 @@ * @property string $name * @property string $credential_id * @property array $credential - * @property null|Carbon $last_used_at - * @property null|Carbon $created_at - * @property null|Carbon $updated_at + * @property ?CarbonInterface $last_used_at + * @property ?CarbonInterface $created_at + * @property ?CarbonInterface $updated_at * @property-read PasskeyUser $user * @property-read null|string $authenticator */ diff --git a/src/sanctum/src/PersonalAccessToken.php b/src/sanctum/src/PersonalAccessToken.php index 9f3e82b95..40ebc7503 100644 --- a/src/sanctum/src/PersonalAccessToken.php +++ b/src/sanctum/src/PersonalAccessToken.php @@ -4,6 +4,7 @@ namespace Hypervel\Sanctum; +use Carbon\CarbonInterface; use Hypervel\Container\Container; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Cache\Repository as CacheRepository; @@ -21,8 +22,8 @@ * @property string $token * @property string $name * @property \Hypervel\Database\Eloquent\Model $tokenable - * @property null|\Carbon\Carbon $last_used_at - * @property null|\Carbon\Carbon $expires_at + * @property ?CarbonInterface $last_used_at + * @property ?CarbonInterface $expires_at * @method static \Hypervel\Database\Eloquent\Builder where(string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and') * @method static static|null find(mixed $id, array $columns = ['*']) */ diff --git a/tests/Sanctum/PruneExpiredTest.php b/tests/Sanctum/PruneExpiredTest.php index 9288ed8c9..fc16fd220 100644 --- a/tests/Sanctum/PruneExpiredTest.php +++ b/tests/Sanctum/PruneExpiredTest.php @@ -10,7 +10,7 @@ use Hypervel\Sanctum\Console\Commands\PruneExpired; use Hypervel\Sanctum\PersonalAccessToken; use Hypervel\Sanctum\SanctumServiceProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; class PruneExpiredTest extends TestCase @@ -55,7 +55,7 @@ public function testCanDeleteExpiredTokensWithIntegerExpiration(): void 'tokenable_id' => 1, 'name' => 'Test_1', 'token' => hash('sha256', 'test_1'), - 'created_at' => Carbon::now()->subMinutes(181), + 'created_at' => CarbonImmutable::now()->subMinutes(181), ]); PersonalAccessToken::forceCreate([ @@ -63,7 +63,7 @@ public function testCanDeleteExpiredTokensWithIntegerExpiration(): void 'tokenable_id' => 1, 'name' => 'Test_2', 'token' => hash('sha256', 'test_2'), - 'created_at' => Carbon::now()->subMinutes(179), + 'created_at' => CarbonImmutable::now()->subMinutes(179), ]); PersonalAccessToken::forceCreate([ @@ -71,7 +71,7 @@ public function testCanDeleteExpiredTokensWithIntegerExpiration(): void 'tokenable_id' => 1, 'name' => 'Test_3', 'token' => hash('sha256', 'test_3'), - 'created_at' => Carbon::now()->subMinutes(121), + 'created_at' => CarbonImmutable::now()->subMinutes(121), ]); $this->artisan('sanctum:prune-expired --hours=2') @@ -92,7 +92,7 @@ public function testCantDeleteExpiredTokensWithNullExpiration(): void 'tokenable_id' => 1, 'name' => 'Test', 'token' => hash('sha256', 'test'), - 'created_at' => Carbon::now()->subMinutes(70), + 'created_at' => CarbonImmutable::now()->subMinutes(70), ]); $this->artisan('sanctum:prune-expired --hours=2') @@ -111,7 +111,7 @@ public function testCanDeleteExpiredTokensWithExpiresAtExpiration(): void 'tokenable_id' => 1, 'name' => 'Test_1', 'token' => hash('sha256', 'test_1'), - 'expires_at' => Carbon::now()->subMinutes(121), + 'expires_at' => CarbonImmutable::now()->subMinutes(121), ]); PersonalAccessToken::forceCreate([ @@ -119,7 +119,7 @@ public function testCanDeleteExpiredTokensWithExpiresAtExpiration(): void 'tokenable_id' => 1, 'name' => 'Test_2', 'token' => hash('sha256', 'test_2'), - 'expires_at' => Carbon::now()->subMinutes(119), + 'expires_at' => CarbonImmutable::now()->subMinutes(119), ]); PersonalAccessToken::forceCreate([ From f64bb97f9c172cb9525b28502ade5a73b08a17a9 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:18:38 +0000 Subject: [PATCH 25/34] refactor(testing): compare cookie expiry immutably Use Hypervel CarbonImmutable when TestResponse converts cookie timestamps and compares them with the current time. Keep assertion messages and session-cookie handling unchanged while ensuring test helpers follow the same date semantics as the framework they exercise. --- src/testing/src/TestResponse.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/testing/src/TestResponse.php b/src/testing/src/TestResponse.php index 42d89094e..2c411a180 100644 --- a/src/testing/src/TestResponse.php +++ b/src/testing/src/TestResponse.php @@ -16,7 +16,7 @@ use Hypervel\Http\Request; use Hypervel\Http\Response as HypervelResponse; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Traits\Conditionable; @@ -475,10 +475,10 @@ public function assertCookieExpired(string $cookieName): static "Cookie [{$cookieName}] not present on response." ); - $expiresAt = Carbon::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); + $expiresAt = CarbonImmutable::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); PHPUnit::withResponse($this)->assertTrue( - $cookie->getExpiresTime() !== 0 && $expiresAt->lessThan(Carbon::now()), + $cookie->getExpiresTime() !== 0 && $expiresAt->lessThan(CarbonImmutable::now()), "Cookie [{$cookieName}] is not expired, it expires at [{$expiresAt}]." ); @@ -495,10 +495,10 @@ public function assertCookieNotExpired(string $cookieName): static "Cookie [{$cookieName}] not present on response." ); - $expiresAt = Carbon::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); + $expiresAt = CarbonImmutable::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); PHPUnit::withResponse($this)->assertTrue( - $cookie->getExpiresTime() === 0 || $expiresAt->greaterThan(Carbon::now()), + $cookie->getExpiresTime() === 0 || $expiresAt->greaterThan(CarbonImmutable::now()), "Cookie [{$cookieName}] is expired, it expired at [{$expiresAt}]." ); From 91c7856a1ae4876ffcde6524175ad678b2f4949e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:18:48 +0000 Subject: [PATCH 26/34] feat(testbench): make immutable dates the test default Point the WithImmutableDates attribute and Testbench workbench casts at Hypervel CarbonImmutable, and treat the attribute as an explicit forcing mechanism for applications that otherwise opt into mutable dates. Update Testbench default configuration, timezone, migration, and attribute contracts to assert exact immutable output while preserving package-mode database behavior. Keep all runtime skeleton cleanup and environment isolation aligned with the shared Testbench base classes. --- .../src/Attributes/WithImmutableDates.php | 2 +- ...26_182750_create_testbench_users_table.php | 2 +- .../Attributes/WithImmutableDatesTest.php | 19 +++++++++++++------ .../MigrateWithHypervelMigrationsTest.php | 4 ++-- ...lMigrationsUsingDatabaseMigrationsTest.php | 4 ++-- ...rvelMigrationsUsingRefreshDatabaseTest.php | 4 ++-- .../Databases/MigrateWithHypervelTest.php | 6 +++--- tests/Testbench/DefaultConfigurationTest.php | 10 +++------- tests/Testbench/TimezoneTest.php | 4 ++-- 9 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/testbench/src/Attributes/WithImmutableDates.php b/src/testbench/src/Attributes/WithImmutableDates.php index a447f2ae6..657b7d2af 100644 --- a/src/testbench/src/Attributes/WithImmutableDates.php +++ b/src/testbench/src/Attributes/WithImmutableDates.php @@ -5,8 +5,8 @@ namespace Hypervel\Testbench\Attributes; use Attribute; -use Carbon\CarbonImmutable; use Hypervel\Contracts\Foundation\Application as ApplicationContract; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Testbench\Contracts\Attributes\AfterEach; use Hypervel\Testbench\Contracts\Attributes\BeforeEach; diff --git a/src/testbench/workbench/database/migrations/2013_07_26_182750_create_testbench_users_table.php b/src/testbench/workbench/database/migrations/2013_07_26_182750_create_testbench_users_table.php index e0781dfd6..df9d03921 100644 --- a/src/testbench/workbench/database/migrations/2013_07_26_182750_create_testbench_users_table.php +++ b/src/testbench/workbench/database/migrations/2013_07_26_182750_create_testbench_users_table.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use Carbon\CarbonImmutable; use Hypervel\Database\Migrations\Migration; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Hash; use Hypervel\Support\Facades\Schema; diff --git a/tests/Testbench/Attributes/WithImmutableDatesTest.php b/tests/Testbench/Attributes/WithImmutableDatesTest.php index 545a47fa6..628267ae4 100644 --- a/tests/Testbench/Attributes/WithImmutableDatesTest.php +++ b/tests/Testbench/Attributes/WithImmutableDatesTest.php @@ -4,9 +4,8 @@ namespace Hypervel\Tests\Testbench\Attributes; -use Carbon\CarbonInterface; -use DateTimeImmutable; -use DateTimeInterface; +use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Testbench\Attributes\WithImmutableDates; use Hypervel\Testbench\TestCase; @@ -20,8 +19,16 @@ public function itUsesImmutableDates(): void { $date = Date::parse('2023-01-01'); - $this->assertInstanceOf(CarbonInterface::class, $date); - $this->assertInstanceOf(DateTimeInterface::class, $date); - $this->assertInstanceOf(DateTimeImmutable::class, $date); + $this->assertSame(CarbonImmutable::class, $date::class); + } + + public function testItForcesImmutableDatesAfterAMutableApplicationOptOut(): void + { + Date::use(Carbon::class); + + $attribute = new WithImmutableDates; + $attribute->beforeEach($this->app); + + $this->assertSame(CarbonImmutable::class, Date::now()::class); } } diff --git a/tests/Testbench/Databases/MigrateWithHypervelMigrationsTest.php b/tests/Testbench/Databases/MigrateWithHypervelMigrationsTest.php index ed9f86c3d..d8e340b7b 100644 --- a/tests/Testbench/Databases/MigrateWithHypervelMigrationsTest.php +++ b/tests/Testbench/Databases/MigrateWithHypervelMigrationsTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Testbench\Databases; -use Carbon\Carbon; use Hypervel\Foundation\Testing\LazilyRefreshDatabase; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Hash; use Hypervel\Testbench\Attributes\WithConfig; @@ -23,7 +23,7 @@ class MigrateWithHypervelMigrationsTest extends TestCase #[Test] public function itLoadsTheMigrations(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); DB::table('users')->insert([ 'name' => 'Orchestra', diff --git a/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseMigrationsTest.php b/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseMigrationsTest.php index 86c6b259b..db9ccadc6 100644 --- a/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseMigrationsTest.php +++ b/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseMigrationsTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Testbench\Databases; -use Carbon\Carbon; use Hypervel\Foundation\Testing\DatabaseMigrations; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Hash; use Hypervel\Testbench\Attributes\WithConfig; @@ -22,7 +22,7 @@ class MigrateWithHypervelMigrationsUsingDatabaseMigrationsTest extends TestCase #[Test] public function itLoadsTheMigrations(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); DB::table('users')->insert([ 'name' => 'Orchestra', diff --git a/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingRefreshDatabaseTest.php b/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingRefreshDatabaseTest.php index 06004f8a1..15b3a5a30 100644 --- a/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingRefreshDatabaseTest.php +++ b/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingRefreshDatabaseTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Testbench\Databases; -use Carbon\Carbon; use Hypervel\Foundation\Testing\RefreshDatabase; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Hash; use Hypervel\Testbench\Attributes\WithConfig; @@ -22,7 +22,7 @@ class MigrateWithHypervelMigrationsUsingRefreshDatabaseTest extends TestCase #[Test] public function itLoadsTheMigrations(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); DB::table('users')->insert([ 'name' => 'Orchestra', diff --git a/tests/Testbench/Databases/MigrateWithHypervelTest.php b/tests/Testbench/Databases/MigrateWithHypervelTest.php index 38d3457f3..7c148a7a8 100644 --- a/tests/Testbench/Databases/MigrateWithHypervelTest.php +++ b/tests/Testbench/Databases/MigrateWithHypervelTest.php @@ -4,7 +4,7 @@ namespace Hypervel\Tests\Testbench\Databases; -use Carbon\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Support\Facades\Hash; use Hypervel\Testbench\Attributes\DefineDatabase; @@ -22,7 +22,7 @@ class MigrateWithHypervelTest extends TestCase #[DefineDatabase('loadApplicationMigrations')] public function itLoadsTheMigrations(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); DB::table('users')->insert([ 'name' => 'Orchestra', @@ -42,7 +42,7 @@ public function itLoadsTheMigrations(): void #[DefineDatabase('runApplicationMigrations')] public function itRunsTheMigrations(): void { - $now = Carbon::now(); + $now = CarbonImmutable::now(); DB::table('users')->insert([ 'name' => 'Orchestra', diff --git a/tests/Testbench/DefaultConfigurationTest.php b/tests/Testbench/DefaultConfigurationTest.php index ec8c340f7..18f678dd3 100644 --- a/tests/Testbench/DefaultConfigurationTest.php +++ b/tests/Testbench/DefaultConfigurationTest.php @@ -4,11 +4,9 @@ namespace Hypervel\Tests\Testbench; -use Carbon\CarbonInterface; -use DateTimeImmutable; -use DateTimeInterface; use Hypervel\Foundation\Auth\User; use Hypervel\Foundation\Bootstrap\LoadConfiguration; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Date; use Hypervel\Testbench\Attributes\WithConfig; use Hypervel\Testbench\Foundation\Env; @@ -84,13 +82,11 @@ public function itPopulatesExpectedRedisConnections(): void } #[Test] - public function itUsesMutableDatesByDefault(): void + public function itUsesImmutableDatesByDefault(): void { $date = Date::parse('2023-01-01'); - $this->assertInstanceOf(CarbonInterface::class, $date); - $this->assertInstanceOf(DateTimeInterface::class, $date); - $this->assertNotInstanceOf(DateTimeImmutable::class, $date); + $this->assertSame(CarbonImmutable::class, $date::class); } #[Test] diff --git a/tests/Testbench/TimezoneTest.php b/tests/Testbench/TimezoneTest.php index 88e32ba6d..823a99545 100644 --- a/tests/Testbench/TimezoneTest.php +++ b/tests/Testbench/TimezoneTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Testbench; -use Carbon\Carbon; use Hypervel\Contracts\Foundation\Application as ApplicationContract; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Override; use PHPUnit\Framework\Attributes\Test; @@ -21,6 +21,6 @@ protected function getApplicationTimezone(ApplicationContract $app): ?string #[Test] public function itCanOverrideTimezone(): void { - $this->assertSame('Asia/Kuala_Lumpur', Carbon::now()->timezoneName); + $this->assertSame('Asia/Kuala_Lumpur', CarbonImmutable::now()->timezoneName); } } From 609a6cd842beafd136ba2fd2348385e24a8a779c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:18:59 +0000 Subject: [PATCH 27/34] test(jwt): use the immutable framework clock Freeze JWT tests through Hypervel CarbonImmutable and obtain configured current timestamps through the Date facade. Keep claim encoding, blacklist TTL, provider, and temporal validation behavior unchanged while making every fixture match the framework default. Add missing native test return types in the touched validation coverage. --- tests/Jwt/BlacklistTest.php | 11 ++++++----- tests/Jwt/ClaimFactoryTest.php | 2 +- tests/Jwt/JwtManagerTest.php | 7 ++++--- tests/Jwt/Providers/LcobucciTest.php | 7 ++++--- tests/Jwt/Validations/ExpiredClaimTest.php | 17 +++++++++-------- tests/Jwt/Validations/IssuedAtClaimTest.php | 17 +++++++++-------- tests/Jwt/Validations/NotBeforeClaimTest.php | 13 +++++++------ 7 files changed, 40 insertions(+), 34 deletions(-) diff --git a/tests/Jwt/BlacklistTest.php b/tests/Jwt/BlacklistTest.php index e92bc526b..93ac3534a 100644 --- a/tests/Jwt/BlacklistTest.php +++ b/tests/Jwt/BlacklistTest.php @@ -4,10 +4,11 @@ namespace Hypervel\Tests\Jwt; -use Carbon\Carbon; use Hypervel\Jwt\Blacklist; use Hypervel\Jwt\Contracts\StorageContract; use Hypervel\Jwt\Exceptions\TokenInvalidException; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; use Mockery as m; use Mockery\MockInterface; @@ -26,9 +27,9 @@ class BlacklistTest extends TestCase protected function setUp(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); - $this->testNowTimestamp = Carbon::now()->timestamp; + $this->testNowTimestamp = Date::now()->timestamp; $this->storage = m::mock(StorageContract::class); $this->blacklist = new Blacklist($this->storage); } @@ -125,9 +126,9 @@ public function testReturnTrueEarlyWhenAddingAnItemAndItAlreadyExists(): void public function testBlacklistTtlRoundsFractionalMinutesUp(): void { - Carbon::setTestNow('2000-01-01T00:00:00.500000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.500000Z'); - $nowTimestamp = Carbon::now()->timestamp; + $nowTimestamp = Date::now()->timestamp; $payload = [ 'sub' => 1, 'iss' => 'http://example.com', diff --git a/tests/Jwt/ClaimFactoryTest.php b/tests/Jwt/ClaimFactoryTest.php index d6e1630a7..84dd9ab26 100644 --- a/tests/Jwt/ClaimFactoryTest.php +++ b/tests/Jwt/ClaimFactoryTest.php @@ -4,13 +4,13 @@ namespace Hypervel\Tests\Jwt; -use Carbon\CarbonImmutable; use Hypervel\Config\Repository; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Auth\UserProvider; use Hypervel\Jwt\ClaimFactory; use Hypervel\Jwt\Contracts\JwtSubject; use Hypervel\Jwt\Exceptions\JwtException; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use ReflectionProperty; use SensitiveParameter; diff --git a/tests/Jwt/JwtManagerTest.php b/tests/Jwt/JwtManagerTest.php index bef2424eb..2627b0cef 100644 --- a/tests/Jwt/JwtManagerTest.php +++ b/tests/Jwt/JwtManagerTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Jwt; -use Carbon\Carbon; use Hypervel\Config\Repository; use Hypervel\Contracts\Container\Container; use Hypervel\Jwt\ClaimFactory; @@ -17,6 +16,8 @@ use Hypervel\Jwt\Validations\ExpiredClaim; use Hypervel\Jwt\Validations\NotBeforeClaim; use Hypervel\Jwt\Validations\RequiredClaims; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Support\Str; use Hypervel\Tests\Jwt\Fixtures\ValidationStub; use Hypervel\Tests\TestCase; @@ -579,9 +580,9 @@ public function testThrowAnExceptionWhenEnableBlacklistIsSetToFalse(): void private function setTestNow(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); - $this->testNowTimestamp = Carbon::now()->timestamp; + $this->testNowTimestamp = Date::now()->timestamp; } private function mockContainer(): void diff --git a/tests/Jwt/Providers/LcobucciTest.php b/tests/Jwt/Providers/LcobucciTest.php index aef38d34c..b961caa0f 100644 --- a/tests/Jwt/Providers/LcobucciTest.php +++ b/tests/Jwt/Providers/LcobucciTest.php @@ -4,12 +4,13 @@ namespace Hypervel\Tests\Jwt\Providers; -use Carbon\Carbon; use Hypervel\Jwt\Exceptions\JwtException; use Hypervel\Jwt\Exceptions\SecretMissingException; use Hypervel\Jwt\Exceptions\TokenInvalidException; use Hypervel\Jwt\Providers\Lcobucci; use Hypervel\Jwt\Providers\Provider; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; class LcobucciTest extends TestCase @@ -20,9 +21,9 @@ protected function setUp(): void { parent::setUp(); - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); - $this->testNowTimestamp = Carbon::now()->timestamp; + $this->testNowTimestamp = Date::now()->timestamp; } public function testEncodeClaimsUsingASymmetricKey(): void diff --git a/tests/Jwt/Validations/ExpiredClaimTest.php b/tests/Jwt/Validations/ExpiredClaimTest.php index 032e1d491..66de8d524 100644 --- a/tests/Jwt/Validations/ExpiredClaimTest.php +++ b/tests/Jwt/Validations/ExpiredClaimTest.php @@ -4,35 +4,36 @@ namespace Hypervel\Tests\Jwt\Validations; -use Carbon\Carbon; use Hypervel\Jwt\Exceptions\TokenExpiredException; use Hypervel\Jwt\Validations\ExpiredClaim; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; class ExpiredClaimTest extends TestCase { - public function testValid() + public function testValid(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); $this->expectNotToPerformAssertions(); $validation = new ExpiredClaim(['leeway' => 3600]); $validation->validate([]); - $validation->validate(['exp' => Carbon::now()->timestamp + 3600]); - $validation->validate(['exp' => Carbon::now()->timestamp - 3600]); + $validation->validate(['exp' => Date::now()->timestamp + 3600]); + $validation->validate(['exp' => Date::now()->timestamp - 3600]); } - public function testInvalid() + public function testInvalid(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); $this->expectException(TokenExpiredException::class); $this->expectExceptionMessage('Token has expired'); $validation = new ExpiredClaim; - $validation->validate(['exp' => Carbon::now()->timestamp - 3600]); + $validation->validate(['exp' => Date::now()->timestamp - 3600]); } } diff --git a/tests/Jwt/Validations/IssuedAtClaimTest.php b/tests/Jwt/Validations/IssuedAtClaimTest.php index 8fa10117e..366b57fc4 100644 --- a/tests/Jwt/Validations/IssuedAtClaimTest.php +++ b/tests/Jwt/Validations/IssuedAtClaimTest.php @@ -4,35 +4,36 @@ namespace Hypervel\Tests\Jwt\Validations; -use Carbon\Carbon; use Hypervel\Jwt\Exceptions\TokenInvalidException; use Hypervel\Jwt\Validations\IssuedAtClaim; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; class IssuedAtClaimTest extends TestCase { - public function testValid() + public function testValid(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); $this->expectNotToPerformAssertions(); $validation = new IssuedAtClaim(['leeway' => 3600]); $validation->validate([]); - $validation->validate(['iat' => Carbon::now()->timestamp - 3600]); - $validation->validate(['iat' => Carbon::now()->timestamp + 3600]); + $validation->validate(['iat' => Date::now()->timestamp - 3600]); + $validation->validate(['iat' => Date::now()->timestamp + 3600]); } - public function testInvalid() + public function testInvalid(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); $this->expectException(TokenInvalidException::class); $this->expectExceptionMessage('Issued At (iat) timestamp cannot be in the future'); $validation = new IssuedAtClaim; - $validation->validate(['iat' => Carbon::now()->timestamp + 3600]); + $validation->validate(['iat' => Date::now()->timestamp + 3600]); } } diff --git a/tests/Jwt/Validations/NotBeforeClaimTest.php b/tests/Jwt/Validations/NotBeforeClaimTest.php index eeea068cc..cc3663755 100644 --- a/tests/Jwt/Validations/NotBeforeClaimTest.php +++ b/tests/Jwt/Validations/NotBeforeClaimTest.php @@ -4,35 +4,36 @@ namespace Hypervel\Tests\Jwt\Validations; -use Carbon\Carbon; use Hypervel\Jwt\Exceptions\TokenInvalidException; use Hypervel\Jwt\Validations\NotBeforeClaim; +use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Facades\Date; use Hypervel\Tests\TestCase; class NotBeforeClaimTest extends TestCase { public function testValid(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); $this->expectNotToPerformAssertions(); $validation = new NotBeforeClaim(['leeway' => 3600]); $validation->validate([]); - $validation->validate(['nbf' => Carbon::now()->timestamp - 3600]); - $validation->validate(['nbf' => Carbon::now()->timestamp + 3600]); + $validation->validate(['nbf' => Date::now()->timestamp - 3600]); + $validation->validate(['nbf' => Date::now()->timestamp + 3600]); } public function testInvalid(): void { - Carbon::setTestNow('2000-01-01T00:00:00.000000Z'); + CarbonImmutable::setTestNow('2000-01-01T00:00:00.000000Z'); $this->expectException(TokenInvalidException::class); $this->expectExceptionMessage('Not Before (nbf) timestamp cannot be in the future'); $validation = new NotBeforeClaim; - $validation->validate(['nbf' => Carbon::now()->timestamp + 3600]); + $validation->validate(['nbf' => Date::now()->timestamp + 3600]); } } From 45a008c43f5ab1b87aa673d90b3396b3a3e1d8b7 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:19:09 +0000 Subject: [PATCH 28/34] test(support): assert immutable date consumers Update array, Fluent, validated input, and InteractsWithData coverage to use and assert exact Hypervel CarbonImmutable results from default date parsing. Retain explicit coverage that InteractsWithData follows the mutable DateFactory opt-out, keeping configurable public boundaries honest. Capture stronger exact-class assertions without weakening existing value, timezone, enum, or formatting checks. --- tests/Support/SupportArrTest.php | 4 +-- tests/Support/SupportFluentTest.php | 11 +++++--- .../Support/Traits/InteractsWithDataTest.php | 26 ++++++++++++++----- tests/Support/ValidatedInputTest.php | 11 +++++--- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index 84dfd14f1..ed347e1ee 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -7,7 +7,7 @@ use ArrayObject; use DateTime; use Hypervel\Support\Arr; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\ItemNotFoundException; use Hypervel\Support\MultipleItemsFoundException; @@ -1155,7 +1155,7 @@ public function testPluckWithKeys(): void public function testPluckWithCarbonKeys(): void { $array = [ - ['start' => new Carbon('2017-07-25 00:00:00'), 'end' => new Carbon('2017-07-30 00:00:00')], + ['start' => new CarbonImmutable('2017-07-25 00:00:00'), 'end' => new CarbonImmutable('2017-07-30 00:00:00')], ]; $array = Arr::pluck($array, 'end', 'start'); $this->assertEquals(['2017-07-25 00:00:00' => '2017-07-30 00:00:00'], $array); diff --git a/tests/Support/SupportFluentTest.php b/tests/Support/SupportFluentTest.php index 9312040fd..3111c3fe7 100644 --- a/tests/Support/SupportFluentTest.php +++ b/tests/Support/SupportFluentTest.php @@ -5,7 +5,7 @@ namespace Hypervel\Tests\Support; use ArrayIterator; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Fluent; use Hypervel\Support\Stringable; @@ -314,7 +314,7 @@ public function testCollectMethod() $this->assertEquals(['users' => [1, 2, 3], 'roles' => [4, 5, 6], 'foo' => ['bar', 'baz'], 'email' => 'test@example.com'], $fluent->collect()->all()); } - public function testDateMethod() + public function testDateMethod(): void { $fluent = new Fluent([ 'as_null' => null, @@ -328,12 +328,15 @@ public function testDateMethod() 'as_time' => '16:30:25', ]); - $current = Carbon::create(2020, 1, 1, 16, 30, 25); + $current = CarbonImmutable::create(2020, 1, 1, 16, 30, 25); $this->assertNull($fluent->date('as_null')); $this->assertNull($fluent->date('doesnt_exists')); - $this->assertEquals($current, $fluent->date('as_datetime')); + $dateTime = $fluent->date('as_datetime'); + + $this->assertSame(CarbonImmutable::class, $dateTime::class); + $this->assertEquals($current, $dateTime); $this->assertEquals($current->format('Y-m-d H:i:s P'), $fluent->date('as_format', 'U')->format('Y-m-d H:i:s P')); $this->assertEquals($current, $fluent->date('as_timezone', null, 'America/Santiago')); diff --git a/tests/Support/Traits/InteractsWithDataTest.php b/tests/Support/Traits/InteractsWithDataTest.php index 7166e1aff..c4f7b2d79 100644 --- a/tests/Support/Traits/InteractsWithDataTest.php +++ b/tests/Support/Traits/InteractsWithDataTest.php @@ -9,7 +9,9 @@ use Hypervel\Container\Container; use Hypervel\Foundation\Application; use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; +use Hypervel\Support\Facades\Date; use Hypervel\Support\Traits\InteractsWithData; use Hypervel\Tests\TestCase; use TypeError; @@ -54,17 +56,27 @@ public function testDateParsesWithoutFormat(): void $result = $instance->date('date'); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('2024-01-15 10:30:00', $result->format('Y-m-d H:i:s')); } + public function testDateHonorsTheMutableDateFactoryOptOut(): void + { + Date::use(Carbon::class); + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + $result = $instance->date('date'); + + $this->assertSame(Carbon::class, $result::class); + } + public function testDateParsesWithFormat(): void { $instance = new TestInteractsWithDataClass(['date' => '15/01/2024']); $result = $instance->date('date', 'd/m/Y'); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('2024-01-15', $result->format('Y-m-d')); } @@ -74,7 +86,7 @@ public function testDateWithStringTimezone(): void $result = $instance->date('date', null, 'America/New_York'); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } @@ -84,7 +96,7 @@ public function testDateWithStringBackedEnumTimezone(): void $result = $instance->date('date', null, InteractsWithDataTestStringEnum::NewYork); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('America/New_York', $result->timezone->getName()); } @@ -95,7 +107,7 @@ public function testDateWithUnitEnumTimezone(): void // UnitEnum uses ->name, so 'UTC' will be the timezone $result = $instance->date('date', null, InteractsWithDataTestUnitEnum::UTC); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); $this->assertEquals('UTC', $result->timezone->getName()); } @@ -107,7 +119,7 @@ public function testDateWithIntBackedEnumTimezoneUsesEnumValue(): void // This tests that enum_value() is called and passes the value to Carbon $result = $instance->date('date', null, InteractsWithDataTestIntEnum::One); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); // Carbon interprets int as UTC offset, so timezone offset will be +01:00 $this->assertEquals('+01:00', $result->timezone->getName()); } @@ -118,7 +130,7 @@ public function testDateWithNullTimezone(): void $result = $instance->date('date', null, null); - $this->assertInstanceOf(Carbon::class, $result); + $this->assertSame(CarbonImmutable::class, $result::class); } public function testIntervalMethod(): void diff --git a/tests/Support/ValidatedInputTest.php b/tests/Support/ValidatedInputTest.php index 65b5298e6..4d1d4106f 100644 --- a/tests/Support/ValidatedInputTest.php +++ b/tests/Support/ValidatedInputTest.php @@ -5,7 +5,7 @@ namespace Hypervel\Tests\Support; use Hypervel\Http\UploadedFile; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Stringable; use Hypervel\Support\ValidatedInput; @@ -441,7 +441,7 @@ public function testFloatMethod() $this->assertSame(0.0, $input->float('null', 123.456)); } - public function testDateMethod() + public function testDateMethod(): void { $input = new ValidatedInput([ 'as_null' => null, @@ -455,12 +455,15 @@ public function testDateMethod() 'as_time' => '16:30:25', ]); - $current = Carbon::create(2024, 1, 1, 16, 30, 25); + $current = CarbonImmutable::create(2024, 1, 1, 16, 30, 25); $this->assertNull($input->date('as_null')); $this->assertNull($input->date('doesnt_exists')); - $this->assertEquals($current, $input->date('as_datetime')); + $dateTime = $input->date('as_datetime'); + + $this->assertSame(CarbonImmutable::class, $dateTime::class); + $this->assertEquals($current, $dateTime); $this->assertEquals($current->format('Y-m-d H:i:s P'), $input->date('as_format', 'U')->format('Y-m-d H:i:s P')); $this->assertEquals($current, $input->date('as_timezone', null, 'America/Santiago')); From 730846836916cd28f282c044ec8adf6bf80bbb4c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:19:24 +0000 Subject: [PATCH 29/34] test: align framework date fixtures with immutability Move remaining filesystem, exception throttling, nested set, pagination, permission, and translation fixtures to Hypervel CarbonImmutable. Type native integration boundaries as DateTimeInterface, capture immutable clock changes, and assert canonical Eloquent date classes where the framework owns the result. Preserve each package contract and existing behavioral assertions while removing mutable-default assumptions from the test suite. --- tests/Filesystem/FilesystemAdapterTest.php | 20 +++++++------- .../FoundationExceptionsHandlerTest.php | 14 +++++----- tests/NestedSet/NodeTest.php | 6 ++--- tests/Pagination/CursorTest.php | 26 +++++++++---------- .../HasPermissionsWithCustomModelsTest.php | 3 ++- .../Traits/HasRolesWithCustomModelsTest.php | 3 ++- .../Translation/TranslationTranslatorTest.php | 8 +++--- 7 files changed, 41 insertions(+), 39 deletions(-) diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 3f5ddc617..299fceb2e 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -4,7 +4,6 @@ namespace Hypervel\Tests\Filesystem; -use Carbon\Carbon; use DateTimeInterface; use GuzzleHttp\Psr7\Stream; use Hypervel\Container\Container; @@ -18,6 +17,7 @@ use Hypervel\Http\Request; use Hypervel\Http\Response; use Hypervel\Http\UploadedFile; +use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; use Hypervel\Testing\FakeWritableConnection; use Hypervel\Testing\ParallelTesting; @@ -743,12 +743,12 @@ public function testTemporaryUrlWithCustomCallback() { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $filesystemAdapter->buildTemporaryUrlsUsing(function ($path, Carbon $expiration, $options) { + $filesystemAdapter->buildTemporaryUrlsUsing(function ($path, DateTimeInterface $expiration, $options) { return $path . $expiration->toString() . implode('', $options); }); $path = 'foo'; - $expiration = Carbon::create(2021, 18, 12, 13); + $expiration = CarbonImmutable::create(2021, 18, 12, 13); $options = ['bar' => 'baz']; $this->assertSame( @@ -760,7 +760,7 @@ public function testTemporaryUrlWithCustomCallback() public function testTemporaryUrlCallbacksSupportStaticFirstClassAndBoundClosures(): void { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $expiration = Carbon::create(2021, 12, 18, 13); + $expiration = CarbonImmutable::create(2021, 12, 18, 13); $handler = new FilesystemTemporaryUrlCallbackHandler; $filesystemAdapter->buildTemporaryUrlsUsing(static fn (): string => 'static'); @@ -808,7 +808,7 @@ public function testLocalAdapterOverridesInvokeNormalizedStaticCallbacksDirectly { $filesystemAdapter = (new HypervelLocalFilesystemAdapter($this->filesystem, $this->adapter)) ->diskName('local'); - $expiration = Carbon::create(2021, 12, 18, 13); + $expiration = CarbonImmutable::create(2021, 12, 18, 13); $filesystemAdapter->buildTemporaryUrlsUsing(static fn (): string => 'local-static'); $filesystemAdapter->buildTemporaryUploadUrlsUsing( static fn (): array => ['kind' => 'local-static'], @@ -1062,7 +1062,7 @@ public function testGetAllFiles() public function testProvidesTemporaryUrls() { $localAdapter = new class($this->tempDir) extends LocalFilesystemAdapter { - public function getTemporaryUrl($path, Carbon $expiration, $options): string + public function getTemporaryUrl($path, DateTimeInterface $expiration, $options): string { return $path . $expiration->toString() . implode('', $options); } @@ -1076,7 +1076,7 @@ public function testProvidesTemporaryUrlsWithCustomCallback() { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $filesystemAdapter->buildTemporaryUrlsUsing(function ($path, Carbon $expiration, $options) { + $filesystemAdapter->buildTemporaryUrlsUsing(function ($path, DateTimeInterface $expiration, $options) { return $path . $expiration->toString() . implode('', $options); }); @@ -1151,7 +1151,7 @@ public function testTemporaryUploadUrlWithCustomCallback() { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $filesystemAdapter->buildTemporaryUploadUrlsUsing(function ($path, Carbon $expiration, $options) { + $filesystemAdapter->buildTemporaryUploadUrlsUsing(function ($path, DateTimeInterface $expiration, $options) { return [ 'url' => $path . $expiration->toString() . implode('', $options), 'headers' => ['X-Custom' => 'header'], @@ -1159,7 +1159,7 @@ public function testTemporaryUploadUrlWithCustomCallback() }); $path = 'foo'; - $expiration = Carbon::create(2021, 18, 12, 13); + $expiration = CarbonImmutable::create(2021, 18, 12, 13); $options = ['bar' => 'baz']; $result = $filesystemAdapter->temporaryUploadUrl($path, $expiration, $options); @@ -1188,7 +1188,7 @@ public function testProvidesTemporaryUploadUrlsWithCustomCallback() { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $filesystemAdapter->buildTemporaryUploadUrlsUsing(function ($path, Carbon $expiration, $options) { + $filesystemAdapter->buildTemporaryUploadUrlsUsing(function ($path, DateTimeInterface $expiration, $options) { return [ 'url' => $path . $expiration->toString() . implode('', $options), 'headers' => [], diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index afbe4efa7..856c89805 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -33,7 +33,7 @@ use Hypervel\Routing\Redirector; use Hypervel\Routing\ResponseFactory; use Hypervel\Session\Store; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Lottery; use Hypervel\Support\MessageBag; use Hypervel\Support\ViewErrorBag; @@ -1119,7 +1119,7 @@ public function attempt(string $key, int $maxAttempts, Closure $callback, DateIn return parent::attempt(...func_get_args()); } }); - Carbon::setTestNow(Carbon::now()->startOfDay()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->startOfDay()); for ($i = 0; $i < 100; ++$i) { $handler->report(new Exception('Something in the app went wrong.')); @@ -1129,7 +1129,7 @@ public function attempt(string $key, int $maxAttempts, Closure $callback, DateIn $this->assertCount(7, $reported); $this->assertSame('Something in the app went wrong.', $reported[0]->getMessage()); - Carbon::setTestNow(Carbon::now()->addMinute()); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addMinute()); for ($i = 0; $i < 100; ++$i) { $handler->report(new Exception('Something in the app went wrong.')); @@ -1165,18 +1165,18 @@ public function attempt(string $key, int $maxAttempts, Closure $callback, DateIn } }); - Carbon::setTestNow('2000-01-01 00:00:00.000'); + CarbonImmutable::setTestNow('2000-01-01 00:00:00.000'); $handler->report(new Exception('Something in the app went wrong 1.')); - Carbon::setTestNow('2000-01-01 00:00:59.999'); + CarbonImmutable::setTestNow('2000-01-01 00:00:59.999'); $handler->report(new Exception('Something in the app went wrong 1.')); $this->assertSame(2, $limiter->attempted); $this->assertCount(1, $reported); $this->assertSame('Something in the app went wrong 1.', $reported[0]->getMessage()); - Carbon::setTestNow('2000-01-01 00:01:00.000'); + CarbonImmutable::setTestNow('2000-01-01 00:01:00.000'); $handler->report(new Exception('Something in the app went wrong 2.')); - Carbon::setTestNow('2000-01-01 00:01:59.999'); + CarbonImmutable::setTestNow('2000-01-01 00:01:59.999'); $handler->report(new Exception('Something in the app went wrong 2.')); $this->assertSame(4, $limiter->attempted); diff --git a/tests/NestedSet/NodeTest.php b/tests/NestedSet/NodeTest.php index c04309356..223706d66 100644 --- a/tests/NestedSet/NodeTest.php +++ b/tests/NestedSet/NodeTest.php @@ -5,11 +5,11 @@ namespace Hypervel\Tests\NestedSet; use BadMethodCallException; -use Carbon\Carbon; use Hypervel\Database\Eloquent\ModelNotFoundException; use Hypervel\Database\QueryException; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\NestedSet\Eloquent\Collection; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Facades\DB; use Hypervel\Testbench\TestCase; @@ -377,7 +377,7 @@ public function testNodeIsDeletedWithDescendants(): void public function testNodeIsSoftDeleted(): void { - Carbon::setTestNow('2025-07-03 12:00:00'); + CarbonImmutable::setTestNow('2025-07-03 12:00:00'); $root = Category::root(); @@ -387,7 +387,7 @@ public function testNodeIsSoftDeleted(): void $this->assertTreeNotBroken(); $this->assertNull($this->findCategory('galaxy')); - Carbon::setTestNow('2025-07-03 12:00:01'); + CarbonImmutable::setTestNow('2025-07-03 12:00:01'); $node = $this->findCategory('mobile'); $node->delete(); diff --git a/tests/Pagination/CursorTest.php b/tests/Pagination/CursorTest.php index 0d01c5b27..4f42327a1 100644 --- a/tests/Pagination/CursorTest.php +++ b/tests/Pagination/CursorTest.php @@ -5,43 +5,43 @@ namespace Hypervel\Tests\Pagination; use Hypervel\Pagination\Cursor; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use UnexpectedValueException; class CursorTest extends TestCase { - public function testCanEncodeAndDecodeSuccessfully() + public function testCanEncodeAndDecodeSuccessfully(): void { $cursor = new Cursor([ 'id' => 422, - 'created_at' => Carbon::now()->toDateTimeString(), + 'created_at' => CarbonImmutable::now()->toDateTimeString(), ], true); $this->assertEquals($cursor, Cursor::fromEncoded($cursor->encode())); } - public function testCanGetParams() + public function testCanGetParams(): void { $cursor = new Cursor([ 'id' => 422, - 'created_at' => ($now = Carbon::now()->toDateTimeString()), + 'created_at' => ($now = CarbonImmutable::now()->toDateTimeString()), ], true); $this->assertEquals([$now, 422], $cursor->parameters(['created_at', 'id'])); } - public function testCanGetParam() + public function testCanGetParam(): void { $cursor = new Cursor([ 'id' => 422, - 'created_at' => ($now = Carbon::now()->toDateTimeString()), + 'created_at' => ($now = CarbonImmutable::now()->toDateTimeString()), ], true); $this->assertEquals($now, $cursor->parameter('created_at')); } - public function testPointsToNextItems() + public function testPointsToNextItems(): void { $cursor = new Cursor(['id' => 1], true); @@ -49,7 +49,7 @@ public function testPointsToNextItems() $this->assertFalse($cursor->pointsToPreviousItems()); } - public function testPointsToPreviousItems() + public function testPointsToPreviousItems(): void { $cursor = new Cursor(['id' => 1], false); @@ -57,7 +57,7 @@ public function testPointsToPreviousItems() $this->assertTrue($cursor->pointsToPreviousItems()); } - public function testToArray() + public function testToArray(): void { $cursor = new Cursor(['id' => 422, 'name' => 'test'], true); @@ -75,17 +75,17 @@ public function testToArray() ], $cursor->toArray()); } - public function testFromEncodedReturnsNullForNull() + public function testFromEncodedReturnsNullForNull(): void { $this->assertNull(Cursor::fromEncoded(null)); } - public function testFromEncodedReturnsNullForInvalidString() + public function testFromEncodedReturnsNullForInvalidString(): void { $this->assertNull(Cursor::fromEncoded('not-valid-json!@#')); } - public function testParameterThrowsForMissingKey() + public function testParameterThrowsForMissingKey(): void { $cursor = new Cursor(['id' => 1], true); diff --git a/tests/Permission/Traits/HasPermissionsWithCustomModelsTest.php b/tests/Permission/Traits/HasPermissionsWithCustomModelsTest.php index ef0b528d2..42347ad7f 100644 --- a/tests/Permission/Traits/HasPermissionsWithCustomModelsTest.php +++ b/tests/Permission/Traits/HasPermissionsWithCustomModelsTest.php @@ -4,8 +4,8 @@ namespace Hypervel\Tests\Permission\Traits; -use Carbon\CarbonImmutable; use Hypervel\Permission\PermissionRegistrar; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Tests\Permission\Fixtures\Models\Admin; use Hypervel\Tests\Permission\Fixtures\Models\Permission; @@ -160,6 +160,7 @@ public function testItTouchesWhenAssigningNewPermissions(): void $permission1 = Permission::create(['name' => 'edit-news', 'guard_name' => 'admin']); $permission2 = Permission::create(['name' => 'edit-blog', 'guard_name' => 'admin']); + $this->assertSame(CarbonImmutable::class, $permission1->updated_at::class); $this->assertSame('2021-07-19 10:13:14', $permission1->updated_at->format('Y-m-d H:i:s')); CarbonImmutable::setTestNow('2021-07-20 19:13:14'); diff --git a/tests/Permission/Traits/HasRolesWithCustomModelsTest.php b/tests/Permission/Traits/HasRolesWithCustomModelsTest.php index f4bed3265..d1ad95c1b 100644 --- a/tests/Permission/Traits/HasRolesWithCustomModelsTest.php +++ b/tests/Permission/Traits/HasRolesWithCustomModelsTest.php @@ -4,7 +4,7 @@ namespace Hypervel\Tests\Permission\Traits; -use Carbon\CarbonImmutable; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\DB; use Hypervel\Tests\Permission\Fixtures\Models\Admin; use Hypervel\Tests\Permission\Fixtures\Models\Role; @@ -117,6 +117,7 @@ public function testItTouchesWhenAssigningNewRoles(): void $role1 = Role::create(['name' => 'testRoleInWebGuard', 'guard_name' => 'admin']); $role2 = Role::create(['name' => 'testRoleInWebGuard1', 'guard_name' => 'admin']); + $this->assertSame(CarbonImmutable::class, $role1->updated_at::class); $this->assertSame('2021-07-19 10:13:14', $role1->updated_at->format('Y-m-d H:i:s')); CarbonImmutable::setTestNow('2021-07-20 19:13:14'); diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php index 5843b7135..f91fb6d23 100644 --- a/tests/Translation/TranslationTranslatorTest.php +++ b/tests/Translation/TranslationTranslatorTest.php @@ -6,7 +6,7 @@ use Hypervel\Contracts\Translation\Loader; use Hypervel\Coroutine\Coroutine; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Tests\TestCase; use Hypervel\Tests\Translation\Fixtures\Enums\Bar; @@ -276,7 +276,7 @@ public function testEmptyFallbacks() $this->assertSame('foo ', $translator->get('foo :message', ['message' => null])); } - public function testGetJsonReplacesWithStringable() + public function testGetJsonReplacesWithStringable(): void { $translator = new Translator($this->getLoader(), 'en'); $translator->getLoader() @@ -285,14 +285,14 @@ public function testGetJsonReplacesWithStringable() ->with('en', '*', '*') ->andReturn(['test' => 'the date is :date']); - $date = Carbon::createFromTimestamp(0); + $date = CarbonImmutable::createFromTimestamp(0); $this->assertSame( 'the date is 1970-01-01 00:00:00', $translator->get('test', ['date' => $date]) ); - $translator->stringable(function (Carbon $carbon) { + $translator->stringable(function (CarbonImmutable $carbon) { return $carbon->format('jS M Y'); }); $this->assertSame( From ba0ef2d6b236ef9639a5950c32f5736e6b82eb5f Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:19:45 +0000 Subject: [PATCH 30/34] docs: document immutable date behavior Describe the immutable default across helpers, Eloquent, requests, data objects, collections, strings, mail, notifications, queues, mocking, and Testbench. Show modifier-result assignment, CarbonInterface boundaries, exact concrete DataObject targets, explicit mutable opt-out, and the role of WithImmutableDates when an application changes its factory. Document gRPC descriptor prewarming for route-discoverable messages and the boot-time responsibility for client, iterable, union, or otherwise undiscoverable protobuf message classes. --- src/boost/docs/collections.md | 4 ++-- src/boost/docs/data-objects.md | 6 ++++-- src/boost/docs/eloquent-mutators.md | 4 ++-- src/boost/docs/eloquent.md | 2 +- src/boost/docs/grpc.md | 4 +++- src/boost/docs/helpers.md | 30 ++++++++++++++++++++++------- src/boost/docs/mail.md | 2 +- src/boost/docs/mocking.md | 8 ++++---- src/boost/docs/notifications.md | 10 +++++----- src/boost/docs/queues.md | 10 +++++----- src/boost/docs/requests.md | 2 +- src/boost/docs/strings.md | 10 ++++++---- src/boost/docs/testbench.md | 2 +- 13 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/boost/docs/collections.md b/src/boost/docs/collections.md index 32be82c6e..b9bd19add 100644 --- a/src/boost/docs/collections.md +++ b/src/boost/docs/collections.md @@ -4606,11 +4606,11 @@ To illustrate the usage of this method, imagine an application that submits invo ```php use App\Models\Invoice; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; Invoice::pending()->cursor() ->takeUntilTimeout( - Carbon::createFromTimestamp(HYPERVEL_START)->add(14, 'minutes') + CarbonImmutable::createFromTimestamp(HYPERVEL_START)->add(14, 'minutes') ) ->each(fn (Invoice $invoice) => $invoice->submit()); ``` diff --git a/src/boost/docs/data-objects.md b/src/boost/docs/data-objects.md index 041256a16..b65aa581f 100644 --- a/src/boost/docs/data-objects.md +++ b/src/boost/docs/data-objects.md @@ -193,7 +193,9 @@ $data = TypeConversionData::make([ ### Date and Time Values -When the second argument passed to `make` is `true`, data objects will resolve supported object dependencies. Date and time values typed as `DateTimeInterface`, `DateTime`, `Hypervel\Support\Carbon`, `Carbon\Carbon`, or `Carbon\CarbonInterface` are resolved to a Carbon-compatible instance: +When the second argument passed to `make` is `true`, data objects will resolve supported object dependencies. The built-in date resolver supports `DateTimeInterface`, `Carbon\CarbonInterface`, native `DateTime` and `DateTimeImmutable`, `Hypervel\Support\Carbon` and `Hypervel\Support\CarbonImmutable`, and Carbon's base mutable and immutable classes. + +Interface-typed properties use Hypervel's configured date factory and therefore receive an exact `Hypervel\Support\CarbonImmutable` instance by default. A concrete property type always receives that exact concrete class, regardless of the configured factory. This allows a data object to request mutable or immutable behavior explicitly while keeping interfaces application-configurable: ```php ## Updating Data Objects diff --git a/src/boost/docs/eloquent-mutators.md b/src/boost/docs/eloquent-mutators.md index 14e48b36c..94f3cf3d2 100644 --- a/src/boost/docs/eloquent-mutators.md +++ b/src/boost/docs/eloquent-mutators.md @@ -597,7 +597,7 @@ return $user->uuid; ### Date Casting -By default, Eloquent will cast the `created_at` and `updated_at` columns to instances of [Carbon](https://github.com/briannesbitt/Carbon), which extends the PHP `DateTime` class and provides an assortment of helpful methods. You may cast additional date attributes by defining additional date casts within your model's `casts` method. Typically, dates should be cast using the `datetime` or `immutable_datetime` cast types. +By default, Eloquent will cast the `created_at` and `updated_at` columns to `Hypervel\Support\CarbonImmutable` instances. Carbon provides an assortment of helpful date and time methods while immutable instances ensure modifiers return new values instead of changing the original. You may cast additional date attributes by defining `date` or `datetime` casts within your model's `casts` method. These casts use Hypervel's configured date factory and are immutable by default. The `immutable_date` and `immutable_datetime` casts always return `CarbonImmutable`, even when an application explicitly configures the date factory to create mutable dates. When defining a `date` or `datetime` cast, you may also specify the date's format. This format will be used when the [model is serialized to an array or JSON](/docs/{{version}}/eloquent-serialization): @@ -615,7 +615,7 @@ protected function casts(): array } ``` -When a column is cast as a date, you may set the corresponding model attribute value to a UNIX timestamp, date string (`Y-m-d`), date-time string, or a `DateTime` / `Carbon` instance. The date's value will be correctly converted and stored in your database. +When a column is cast as a date, you may set the corresponding model attribute value to a UNIX timestamp, date string (`Y-m-d`), date-time string, or any `DateTimeInterface` instance, including mutable or immutable Carbon instances. The date's value will be correctly converted and stored in your database. You may customize the default serialization format for all of your model's dates by defining a `serializeDate` method on your model. This method does not affect how your dates are formatted for storage in the database: diff --git a/src/boost/docs/eloquent.md b/src/boost/docs/eloquent.md index a8a56f4c5..682f39de8 100644 --- a/src/boost/docs/eloquent.md +++ b/src/boost/docs/eloquent.md @@ -1188,7 +1188,7 @@ class Flight extends Model ``` > [!NOTE] -> The `SoftDeletes` trait will automatically cast the `deleted_at` attribute to a `DateTime` / `Carbon` instance for you. +> The `SoftDeletes` trait will automatically cast the `deleted_at` attribute to a `Hypervel\Support\CarbonImmutable` instance for you by default. You should also add the `deleted_at` column to your database table. The Hypervel [schema builder](/docs/{{version}}/migrations) contains a helper method to create this column: diff --git a/src/boost/docs/grpc.md b/src/boost/docs/grpc.md index 949cb0dc5..1c4b98611 100644 --- a/src/boost/docs/grpc.md +++ b/src/boost/docs/grpc.md @@ -101,6 +101,8 @@ The temporary output directory is necessary because `protoc` includes the comple Generated message classes use the `google/protobuf` package. Installing the optional `ext-protobuf` extension improves serialization performance. +Hypervel initializes registered server request classes and concrete response return types that can be constructed without arguments before workers start. If another generated message may be used for the first time by concurrent coroutines—such as a server response behind an iterable or union return type, or a client request or response—construct one instance in a service provider's `boot` method. This registers Protocol Buffers' process-global descriptor metadata before concurrent work begins. + The official `grpc_php_plugin` may also generate client classes. These classes may be adapted to Hypervel as described in [Generated-Style Clients](#generated-style-clients). @@ -272,7 +274,7 @@ $expired = $call->deadlineExceeded(); $previousAttempts = $call->previousAttempts(); ``` -The `deadline` method returns a `CarbonImmutable` instance or `null` when the client did not set a deadline. The `timeRemaining` method returns the remaining number of seconds, while `deadlineExceeded` determines whether the deadline has passed. +The `deadline` method returns a `Hypervel\Support\CarbonImmutable` instance or `null` when the client did not set a deadline. The `timeRemaining` method returns the remaining number of seconds, while `deadlineExceeded` determines whether the deadline has passed. The `previousAttempts` method returns the number of completed retry attempts reported by the client. This can be useful for logging or idempotency handling. diff --git a/src/boost/docs/helpers.md b/src/boost/docs/helpers.md index d6bb89ab9..6dca6168c 100644 --- a/src/boost/docs/helpers.md +++ b/src/boost/docs/helpers.md @@ -2882,7 +2882,7 @@ The `method_field` function generates an HTML `hidden` input field containing th #### `now()` {.collection-method} -The `now` function creates a new `Hypervel\Support\Carbon` instance for the current time: +The `now` function creates a new `Hypervel\Support\CarbonImmutable` instance for the current time: ```php $now = now(); @@ -3267,7 +3267,7 @@ throw_unless( #### `today()` {.collection-method} -The `today` function creates a new `Hypervel\Support\Carbon` instance for the current date: +The `today` function creates a new `Hypervel\Support\CarbonImmutable` instance for the current date: ```php $today = today(); @@ -3431,21 +3431,21 @@ Sometimes, you may want to benchmark the execution of a callback while still obt ### Dates and Time -Hypervel includes [Carbon](https://carbon.nesbot.com/guide/getting-started/introduction.html), a powerful date and time manipulation library. To create a new `Carbon` instance, you may invoke the `now` function. This function is globally available within your Hypervel application: +Hypervel includes [Carbon](https://carbon.nesbot.com/guide/getting-started/introduction.html), a powerful date and time manipulation library. Hypervel uses immutable Carbon instances by default. To create a new `CarbonImmutable` instance, you may invoke the `now` function. This function is globally available within your Hypervel application: ```php $now = now(); ``` -Or, you may create a new `Carbon` instance using the `Hypervel\Support\Carbon` class: +Or, you may create a new instance using the `Hypervel\Support\CarbonImmutable` class: ```php -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; -$now = Carbon::now(); +$now = CarbonImmutable::now(); ``` -Hypervel also augments `Carbon` instances with `plus` and `minus` methods, allowing easy manipulation of the instance's date and time: +Hypervel also augments its mutable and immutable Carbon classes with `plus` and `minus` methods, allowing easy manipulation of the instance's date and time: ```php return now()->plus(minutes: 5); @@ -3457,6 +3457,22 @@ return now()->minus(hours: 8); return now()->minus(weeks: 4); ``` +Since the default date is immutable, assign the result of a modifier when you want to retain the changed value: + +```php +$expiresAt = now(); +$expiresAt = $expiresAt->addMinutes(5); +``` + +Applications that deliberately require mutable dates may configure the date factory during boot: + +```php +use Hypervel\Support\Carbon; +use Hypervel\Support\Facades\Date; + +Date::use(Carbon::class); +``` + For a thorough discussion of Carbon and its features, please consult the [official Carbon documentation](https://carbon.nesbot.com/guide/getting-started/introduction.html). diff --git a/src/boost/docs/mail.md b/src/boost/docs/mail.md index c621c0a66..77986639e 100644 --- a/src/boost/docs/mail.md +++ b/src/boost/docs/mail.md @@ -1068,7 +1068,7 @@ This method will automatically take care of pushing a job onto the queue so the #### Delayed Message Queueing -If you wish to delay the delivery of a queued email message, you may use the `later` method. As its first argument, the `later` method accepts a `DateTime` instance indicating when the message should be sent: +If you wish to delay the delivery of a queued email message, you may use the `later` method. As its first argument, the `later` method accepts a `DateTimeInterface` instance indicating when the message should be sent: ```php Mail::to($request->user()) diff --git a/src/boost/docs/mocking.md b/src/boost/docs/mocking.md index 421473471..5b43b9d67 100644 --- a/src/boost/docs/mocking.md +++ b/src/boost/docs/mocking.md @@ -194,7 +194,7 @@ public function test_values_are_stored_in_cache(): void ## Interacting With Time -When testing, you may occasionally need to modify the time returned by helpers such as `now` or `Hypervel\Support\Carbon::now()`. Thankfully, Hypervel's base feature test class includes helpers that allow you to manipulate the current time: +When testing, you may occasionally need to modify the time returned by helpers such as `now` or `Hypervel\Support\CarbonImmutable::now()`. Thankfully, Hypervel's base feature test class includes helpers that allow you to manipulate the current time: ```php tab=Pest test('time can be manipulated', function () { @@ -256,15 +256,15 @@ $this->travelTo(now()->minus(days: 10), function () { The `freezeTime` method may be used to freeze the current time. Similarly, the `freezeSecond` method will freeze the current time but at the start of the current second: ```php -use Hypervel\Support\Carbon; +use Carbon\CarbonInterface; // Freeze time and resume normal time after executing closure... -$this->freezeTime(function (Carbon $time) { +$this->freezeTime(function (CarbonInterface $time) { // ... }); // Freeze time at the current second and resume normal time after executing closure... -$this->freezeSecond(function (Carbon $time) { +$this->freezeSecond(function (CarbonInterface $time) { // ... }) ``` diff --git a/src/boost/docs/notifications.md b/src/boost/docs/notifications.md index 300c892cd..bb4b4cb9b 100644 --- a/src/boost/docs/notifications.md +++ b/src/boost/docs/notifications.md @@ -193,7 +193,7 @@ Alternatively, you may define a `withDelay` method on the notification class its /** * Determine the notification's delivery delay. * - * @return array + * @return array */ public function withDelay(object $notifiable): array { @@ -320,7 +320,7 @@ class InvoicePaid extends Notification implements ShouldQueue, ShouldBeEncrypted In addition to defining these attributes directly on your notification class, you may also define `backoff` and `retryUntil` methods to specify the backoff strategy and retry timeout for the queued notification job: ```php -use DateTime; +use DateTimeInterface; /** * Calculate the number of seconds to wait before retrying the notification. @@ -333,7 +333,7 @@ public function backoff(): int /** * Determine the time at which the notification should timeout. */ -public function retryUntil(): DateTime +public function retryUntil(): DateTimeInterface { return now()->plus(minutes: 5); } @@ -1023,7 +1023,7 @@ public function toArray(object $notifiable): array When a notification is stored in your application's database, the `type` column will be set to the notification's class name by default, and the `read_at` column will be `null`. However, you can customize this behavior by defining the `databaseType` and `initialDatabaseReadAtValue` methods in your notification class: ```php -use Hypervel\Support\Carbon; +use Carbon\CarbonInterface; /** * Get the notification's database type. @@ -1036,7 +1036,7 @@ public function databaseType(object $notifiable): string /** * Get the initial value for the "read_at" column. */ -public function initialDatabaseReadAtValue(): ?Carbon +public function initialDatabaseReadAtValue(): ?CarbonInterface { return null; } diff --git a/src/boost/docs/queues.md b/src/boost/docs/queues.md index d99daef8a..0f5e8654e 100644 --- a/src/boost/docs/queues.md +++ b/src/boost/docs/queues.md @@ -850,7 +850,7 @@ Hypervel includes a `Hypervel\Queue\Middleware\ThrottlesExceptions` middleware t For example, let's imagine a queued job that interacts with a third-party API that begins throwing exceptions. To throttle exceptions, you can return the `ThrottlesExceptions` middleware from your job's `middleware` method. Typically, this middleware should be paired with a job that implements [time based attempts](#time-based-attempts): ```php -use DateTime; +use DateTimeInterface; use Hypervel\Queue\Middleware\ThrottlesExceptions; /** @@ -866,7 +866,7 @@ public function middleware(): array /** * Determine the time at which the job should timeout. */ -public function retryUntil(): DateTime +public function retryUntil(): DateTimeInterface { return now()->plus(minutes: 30); } @@ -1628,15 +1628,15 @@ public function tries(): int #### Time Based Attempts -As an alternative to defining how many times a job may be attempted before it fails, you may define a time at which the job should no longer be attempted. This allows a job to be attempted any number of times within a given time frame. To define the time at which a job should no longer be attempted, add a `retryUntil` method to your job class. This method should return a `DateTime` instance: +As an alternative to defining how many times a job may be attempted before it fails, you may define a time at which the job should no longer be attempted. This allows a job to be attempted any number of times within a given time frame. To define the time at which the job should no longer be attempted, add a `retryUntil` method to your job class. This method should return a `DateTimeInterface` instance: ```php -use DateTime; +use DateTimeInterface; /** * Determine the time at which the job should timeout. */ -public function retryUntil(): DateTime +public function retryUntil(): DateTimeInterface { return now()->plus(minutes: 10); } diff --git a/src/boost/docs/requests.md b/src/boost/docs/requests.md index 5d382b987..06b342085 100644 --- a/src/boost/docs/requests.md +++ b/src/boost/docs/requests.md @@ -427,7 +427,7 @@ $versions = $request->array('versions'); #### Retrieving Date Input Values -For convenience, input values containing dates / times may be retrieved as Carbon instances using the `date` method. If the request does not contain an input value with the given name, `null` will be returned: +For convenience, input values containing dates / times may be retrieved as `CarbonInterface` instances using the `date` method. Request dates use Hypervel's configured date factory and are exact `Hypervel\Support\CarbonImmutable` instances by default. If the request does not contain an input value with the given name, `null` will be returned: ```php $birthday = $request->date('birthday'); diff --git a/src/boost/docs/strings.md b/src/boost/docs/strings.md index 03aaf8ca4..618ca9735 100644 --- a/src/boost/docs/strings.md +++ b/src/boost/docs/strings.md @@ -1969,15 +1969,17 @@ return (string) Str::ulid(); // 01gd6r360bp37zj17nxb55yv40 ``` -If you would like to retrieve a `Hypervel\Support\Carbon` date instance representing the date and time that a given ULID was created, you may use the `createFromId` method provided by Hypervel's Carbon integration: +If you would like to retrieve a `Hypervel\Support\CarbonImmutable` date instance representing the date and time that a given ULID was created, you may use the `createFromId` method provided by Hypervel's Carbon integration: ```php -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Str; -$date = Carbon::createFromId((string) Str::ulid()); +$date = CarbonImmutable::createFromId((string) Str::ulid()); ``` +The explicit mutable `Hypervel\Support\Carbon` class provides the same `createFromId` method when mutable behavior is required. + During testing, it may be useful to "fake" the value that is returned by the `Str::ulid` method. To accomplish this, you may use the `createUlidsUsing` method: ```php @@ -3913,7 +3915,7 @@ $boolean = Str::of('yes')->toBoolean(); #### `toDate` {.collection-method} -The `toDate` method returns the underlying string value as a date instance: +The `toDate` method returns the underlying string value as an instance of the configured `CarbonInterface`. By default, this is an exact `Hypervel\Support\CarbonImmutable` instance: ```php use Hypervel\Support\Str; diff --git a/src/boost/docs/testbench.md b/src/boost/docs/testbench.md index 324c893e0..671fd297b 100644 --- a/src/boost/docs/testbench.md +++ b/src/boost/docs/testbench.md @@ -517,7 +517,7 @@ class CourierLiveApiTest extends TestCase } ``` -The `#[WithImmutableDates]` attribute configures Hypervel's date factory to use immutable dates for the duration of a test: +Hypervel uses immutable dates by default. The `#[WithImmutableDates]` attribute explicitly restores that default for the duration of a test, which is useful when the tested application or package bootstrap deliberately configures mutable dates: ```php use Hypervel\Testbench\Attributes\WithImmutableDates; From 91e7c2f9d5b08571127d9a1120e3e0cce503bb2f Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:19:53 +0000 Subject: [PATCH 31/34] docs: add immutable date upgrade guidance Record Hypervel CarbonImmutable as a 0.4 framework default and explain the value-semantics and worker-safety motivation. Provide migration guidance for concrete mutable type declarations, retained modifier calls, custom CarbonInterface handlers, callable handlers, and the boot-time mutable opt-out. Keep release notes concise while giving upgrading applications the exact code changes required. --- src/boost/docs/releases.md | 5 +++++ src/boost/docs/upgrade.md | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/boost/docs/releases.md b/src/boost/docs/releases.md index 7c6b35b77..e2649a699 100644 --- a/src/boost/docs/releases.md +++ b/src/boost/docs/releases.md @@ -67,6 +67,11 @@ Many Hypervel 0.4 packages are fresh ports of Laravel packages. These ports aim Some differences are intentional. Hypervel uses `Hypervel\` namespaces, adapts internals for coroutine safety, adds Swoole-specific performance optimizations, and removes drivers or integrations that Hypervel does not support. + +### Immutable Dates + +Hypervel 0.4 uses `Hypervel\Support\CarbonImmutable` as the modern default for date helpers, factory-created dates, Eloquent date casts, request casts, and framework-owned timestamps. Immutable values provide predictable value semantics, with additional safety when dates are retained or shared in long-lived workers. Applications may still opt into the explicit mutable `Hypervel\Support\Carbon` class through the `Date` facade. + ### Coroutine Safety diff --git a/src/boost/docs/upgrade.md b/src/boost/docs/upgrade.md index 7baaf0ffc..2d8b78ff5 100644 --- a/src/boost/docs/upgrade.md +++ b/src/boost/docs/upgrade.md @@ -3,6 +3,7 @@ - [Upgrading To 0.4 From 0.3](#upgrade-04) - [Recommended Upgrade Path](#recommended-upgrade-path) - [What Changed](#what-changed) +- [Immutable Dates](#immutable-dates) - [Migration References](#migration-references) @@ -37,6 +38,28 @@ The biggest areas to review are: + +## Immutable Dates + +Hypervel's date factory, `now()` and `today()` helpers, ordinary Eloquent date casts, and request date casts now return `Hypervel\Support\CarbonImmutable` by default. Review concrete `Hypervel\Support\Carbon` type declarations that receive factory-created values and change configurable boundaries to `Carbon\CarbonInterface`, or use `CarbonImmutable` when the value must always be immutable. + +Immutable date modifiers return a new instance. Assign the result whenever the changed value must be retained: + +```php +$expiresAt = $expiresAt->addMinutes(5); +``` + +Applications that deliberately require mutable dates may opt out during application boot: + +```php +use Hypervel\Support\Carbon; +use Hypervel\Support\Facades\Date; + +Date::use(Carbon::class); +``` + +Custom date classes configured through `Date::use()` or `Date::useClass()` must implement `Carbon\CarbonInterface`. Callable date handlers may transform the generated value into another mutable or immutable Carbon implementation, but must also return a `CarbonInterface` because typed helpers and other factory-routed APIs enforce that contract. + ## Migration References From ee1106606b1125d49dbc39e66782067bb93afc66 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:20:08 +0000 Subject: [PATCH 32/34] docs: establish immutable date framework policy Add immutable dates to Hypervel code and porting conventions, including construction boundaries, interface typing, modifier assignment, and the explicit mutable Carbon exception. Record the Laravel difference, update historical plans whose examples describe current framework types, and remove the completed framework todo. Include the framework-wide audit and implementation record covering architecture, performance research, migration ledgers, edge cases, testing strategy, and deliberate non-changes. --- AGENTS.md | 2 + docs/ai/differences-vs-laravel.md | 4 + ...rray-cache-coroutine-local-worker-array.md | 28 +- .../plans/2026-07-01-fortify-passkeys-port.md | 2 +- .../2026-07-22-carbon-immutable-default.md | 1118 +++++++++++++++++ docs/todo.md | 3 +- 6 files changed, 1140 insertions(+), 17 deletions(-) create mode 100644 docs/plans/2026-07-22-carbon-immutable-default.md diff --git a/AGENTS.md b/AGENTS.md index ec2bf6762..a462e7db9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,6 +163,7 @@ Build complete, long-term solutions, not MVPs or local workarounds. A broad chan - **Prefer `timestamp` over `timestampTz` in migrations** — store times in UTC with plain `timestamp` columns, matching the normal convention in Laravel and Hypervel first-party migrations. Reserve `timestampTz` for columns that genuinely need database-level timezone semantics, such as integrating with an existing timezone-aware schema or columns written by clients in different session timezones. The schema API supports both — this is a column-choice convention, not an API restriction. - **Guard optional event dispatches with `hasListeners()`** — before constructing and dispatching framework events, guard them with `hasListeners()` so hot paths skip event overhead when nobody is listening. Do not guard dispatches where dispatching is the side effect, such as jobs, broadcasts, webhooks, or command bus calls. - **Use `Sleep::usleep()` / `Sleep::sleep()` for delays in source code** — `Sleep` is fakeable in tests. Use raw `sleep()` / `usleep()` only where real time must pass, such as test harnesses and external-process polling. +- **Use immutable dates by default** — Hypervel defaults to `Hypervel\Support\CarbonImmutable`, including where Laravel uses mutable Carbon. Create public or application-configurable dates through the `Date` facade or date helpers, and use exact `CarbonImmutable` for framework-owned internal or held values. Type configurable Carbon boundaries as `CarbonInterface` and native or third-party boundaries as `DateTimeInterface`. Capture the return value of every date modifier whose result must persist. Use `Hypervel\Support\Carbon` only for explicit mutable opt-out or conversion behavior. - **Use typed config getters, no call-site defaults** — prefer `$config->string()`, `$config->integer()`, `$config->float()`, `$config->boolean()`, `$config->array()` over `$config->get()` for any key that can't legitimately be null. Typed getters fail fast on misconfiguration — an `InvalidArgumentException` naming the key, instead of a wrong type propagating silently — and give phpstan the real type. Defaults live in config files: `LoadConfiguration` merges the framework base config, and package providers must merge their defaults through `mergeConfigFrom()`. A call-site default is then dead code that can drift from the real default. Declare every key in the package's config file and call the getter without a default — a missing key then fails loudly instead of being silently papered over. Exceptions: bootstrap code that runs before or without config merging (e.g. `LoadConfiguration` itself), and genuinely nullable keys, which keep `get()`. - **Env var naming** — ported config keeps its upstream env var names. New Hypervel-specific keys start with the config file's domain prefix (`SERVER_`, `APP_`) with the clearest name for the rest, e.g. `SERVER_WORKERS` for the `Constant::OPTION_WORKER_NUM` server option. If a config value mirrors another config key, reuse that key's env var instead of defining a duplicate — e.g. the Slack log channel username falls back to `APP_NAME`. - **Use `resolve...Using` for Hypervel-owned config resolvers** — prefer this naming for callbacks that resolve config-derived values, unless an established Laravel domain convention already exists, such as `redirectUsing()`. @@ -672,6 +673,7 @@ Run `./vendor/bin/phpstan` and `./vendor/bin/php-cs-fixer fix` without flags — When porting Laravel packages, whether first-party or third-party, keep them as close to 1:1 with upstream as possible so future changes are easy to merge. The exceptions are: - Modernizing PHP types (PHP 8.4+ features, strict types, strict comparisons) +- Converting mutable Laravel date construction to Hypervel's immutable date conventions, typing configurable factory output as `CarbonInterface`, and capturing date-modifier return values - Converting container array access (`$app['events']`) to `make()`, and untyped `$config->get()` calls to the typed getters where the key isn't nullable (see Container and the typed-getter rule under Development Conventions) - Adding Laravel-style title docblocks to methods (not classes — see Development Conventions) - For ported Laravel packages: making them coroutine-safe, adding Swoole performance enhancements (e.g., static property caching), making them pass PHPStan diff --git a/docs/ai/differences-vs-laravel.md b/docs/ai/differences-vs-laravel.md index beae1d2cf..abe6c65c9 100644 --- a/docs/ai/differences-vs-laravel.md +++ b/docs/ai/differences-vs-laravel.md @@ -66,6 +66,10 @@ Hypervel caches more aggressively than Laravel: any class resolved via `make()` - Supported drivers exclude Memcached, DynamoDB, MongoDB. +## Dates + +- Hypervel's `Date` facade, `now()` / `today()` helpers, and ordinary Eloquent date casts return `Hypervel\Support\CarbonImmutable` by default. Assign date-modifier results (`$date = $date->addDay()`) when retaining the changed value. Applications that deliberately need Laravel's mutable default may configure `Date::use(Hypervel\Support\Carbon::class)` during boot. + ## Event Dispatch - **`hasListeners()` guards skip event construction when no listeners exist.** Framework code checks `hasListeners()` before constructing event objects. If nothing is listening, the event is never created or dispatched. This is a Hypervel-specific performance optimization — Laravel always constructs and dispatches events regardless of listeners. diff --git a/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md b/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md index 13bb6068b..3a45f9d71 100644 --- a/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md +++ b/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md @@ -50,7 +50,7 @@ Current `ArrayStore` keeps values and locks on object properties: protected array $storage = []; /** - * @var array + * @var array */ public array $locks = []; ``` @@ -135,7 +135,7 @@ Remove `public array $locks` from the public store surface. `ArrayLock` should c $record = $this->store->getLockRecord($this->name); $this->store->putLockRecord($this->name, [ 'owner' => $this->owner, - 'expiresAt' => $this->seconds === 0 ? null : Carbon::now()->addSeconds($this->seconds), + 'expiresAt' => $this->seconds === 0 ? null : CarbonImmutable::now()->addSeconds($this->seconds), ]); ``` @@ -258,7 +258,7 @@ namespace Hypervel\Cache; use Hypervel\Contracts\Cache\CanFlushLocks; use Hypervel\Contracts\Cache\LockProvider; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use RuntimeException; @@ -419,14 +419,14 @@ abstract class AbstractArrayStore extends TaggableStore implements CanFlushLocks /** * Get the lock record for the given name. * - * @return array{owner: ?string, expiresAt: ?Carbon}|null + * @return array{owner: ?string, expiresAt: ?CarbonImmutable}|null */ abstract public function getLockRecord(string $name): ?array; /** * Store the lock record for the given name. * - * @param array{owner: ?string, expiresAt: ?Carbon} $record + * @param array{owner: ?string, expiresAt: ?CarbonImmutable} $record */ abstract public function putLockRecord(string $name, array $record): void; @@ -506,7 +506,7 @@ declare(strict_types=1); namespace Hypervel\Cache; use Hypervel\Context\CoroutineContext; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; class ArrayStore extends AbstractArrayStore { @@ -599,7 +599,7 @@ class ArrayStore extends AbstractArrayStore protected function getLockRecords(): array { - /** @var array $records */ + /** @var array $records */ $records = CoroutineContext::get($this->locksContextKey, []); return $records; @@ -622,7 +622,7 @@ declare(strict_types=1); namespace Hypervel\Cache; -use Hypervel\Support\Carbon; +use Hypervel\Support\CarbonImmutable; class WorkerArrayStore extends AbstractArrayStore { @@ -632,7 +632,7 @@ class WorkerArrayStore extends AbstractArrayStore protected array $storage = []; /** - * @var array + * @var array */ protected array $locks = []; @@ -712,7 +712,7 @@ Core changes: public function acquire(): bool { $record = $this->store->getLockRecord($this->name); - $expiration = $record['expiresAt'] ?? Carbon::now()->addSecond(); + $expiration = $record['expiresAt'] ?? CarbonImmutable::now()->addSecond(); if ($record !== null && $expiration->isFuture()) { return false; @@ -721,7 +721,7 @@ public function acquire(): bool // WorkerArrayStore shares this check/write path across coroutines; keep it non-yielding. $this->store->putLockRecord($this->name, [ 'owner' => $this->owner, - 'expiresAt' => $this->seconds === 0 ? null : Carbon::now()->addSeconds($this->seconds), + 'expiresAt' => $this->seconds === 0 ? null : CarbonImmutable::now()->addSeconds($this->seconds), ]); return true; @@ -766,7 +766,7 @@ public function refresh(?int $seconds = null): bool return false; } - $record['expiresAt'] = Carbon::now()->addSeconds($seconds); + $record['expiresAt'] = CarbonImmutable::now()->addSeconds($seconds); $this->store->putLockRecord($this->name, $record); return true; @@ -790,7 +790,7 @@ public function getRemainingLifetime(): ?float return null; } - return (float) Carbon::now()->diffInSeconds($expiresAt); + return (float) CarbonImmutable::now()->diffInSeconds($expiresAt); } ``` @@ -1010,7 +1010,7 @@ public function testSeparateArrayStoreInstancesDoNotShareContextData(): void ```php public function testAllOnlyReturnsCurrentStoreContextData(): void { - Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(CarbonImmutable::now()); $first = new ArrayStore; $second = new ArrayStore; diff --git a/docs/plans/2026-07-01-fortify-passkeys-port.md b/docs/plans/2026-07-01-fortify-passkeys-port.md index 2ebecc240..2ac8407ab 100644 --- a/docs/plans/2026-07-01-fortify-passkeys-port.md +++ b/docs/plans/2026-07-01-fortify-passkeys-port.md @@ -1130,7 +1130,7 @@ Use `CoroutineContext` only if a ported controller or middleware introduces requ Port the model with strict types and Hypervel namespaces. -Keep date annotations aligned with the actual casts. A `datetime` cast returns Hypervel's configured date class, which defaults to mutable `Hypervel\Support\Carbon`; do not document `CarbonImmutable` unless the model intentionally uses `immutable_datetime`. +Keep date annotations aligned with the actual casts. A `datetime` cast returns Hypervel's configured date class, which defaults to `Hypervel\Support\CarbonImmutable`; type that configurable boundary as `CarbonInterface`. An `immutable_datetime` cast always returns `CarbonImmutable`, even when the application explicitly opts into mutable dates. Use a polymorphic `user` owner relation instead of Laravel's single-model `belongsTo` relation. This avoids a global `Passkeys::userModel()` static and lets one application register passkeys for multiple authenticatable model classes in the same table. diff --git a/docs/plans/2026-07-22-carbon-immutable-default.md b/docs/plans/2026-07-22-carbon-immutable-default.md new file mode 100644 index 000000000..4b46ff251 --- /dev/null +++ b/docs/plans/2026-07-22-carbon-immutable-default.md @@ -0,0 +1,1118 @@ +# Make CarbonImmutable the Framework Default + +## Status + +Implementation plan for the framework-wide date audit. The source audit, runtime probes, performance investigation, and second-opinion design loop are complete. This document records the settled implementation, the complete migration ledger, and the verification needed to leave the repository as though immutable dates had been the original design. + +Backward compatibility with earlier Hypervel versions, minimizing churn, and preserving the current mutable default are not constraints. Laravel-compatible public APIs remain the reference where they do not conflict with this approved Hypervel modernization. The design deliberately avoids a new clock abstraction, raw timestamp rewrites, compatibility shims, duplicate date helpers, or permanent audit tooling. + +## Scope + +Make `Hypervel\Support\CarbonImmutable` the canonical framework date value and the default returned by the date factory, facade, helpers, Eloquent date casts, request casts, data objects, test-time helpers, and the PSR clock. Retain `Hypervel\Support\Carbon` as the explicit mutable opt-out: + +```php +use Hypervel\Support\Carbon; +use Hypervel\Support\Facades\Date; + +Date::use(Carbon::class); +``` + +Complete the change across the whole components repository: + +- add the immutable Hypervel class without duplicating Hypervel's Carbon additions; +- ensure mutable/immutable conversions stay inside the Hypervel class pair; +- tighten the date-factory handlers to the contracts the framework already advertises; +- canonicalize framework-owned immutable values that currently use Carbon's base class; +- convert remaining framework-owned mutable direct construction deliberately; +- fix every mutation or native type that is invalid under an immutable default; +- make DataObject hydration honor both the declared property type and the configured date factory; +- make the existing PSR clock return the canonical Hypervel class; +- update test-time APIs, static teardown, public metadata, application docs, contributor policy, upgrade guidance, and the Laravel-differences record; +- delete superseded fixtures, comments, documentation, todo text, and redundant cleanup. + +Private packages under `packages/hypervel` and `packages/hypervel-dev` are outside this framework worktree and require no migration in this work unit. Their live source has no Carbon usage requiring conversion; the one native `DateTimeImmutable` value found in `SecurityEvent` is already correct. Future private-package work follows the same convention added to `AGENTS.md`. + +## Desired final architecture + +| Boundary | Final date type and construction rule | +|---|---| +| `Date`, `now()`, `today()`, Eloquent `date` / `datetime`, request casts | `CarbonInterface` contract; exact `Hypervel\Support\CarbonImmutable` by default | +| Explicit application opt-out | `Date::use(Hypervel\Support\Carbon::class)`; factory-routed APIs then return the mutable Hypervel class | +| Framework-owned internal, cached, property-held, or shared timestamps | Direct `Hypervel\Support\CarbonImmutable`; unaffected by an application's mutable opt-out | +| Explicit Eloquent `immutable_date` / `immutable_datetime` casts | `Hypervel\Support\CarbonImmutable`, including while the general factory is configured mutable | +| Concrete mutable conversions and mutable-input serialization | `Hypervel\Support\Carbon` only where mutability is intentional and named | +| User-configurable public date values | `CarbonInterface`; creation routes through `Date` | +| Native and third-party boundaries | `DateTimeInterface`, or the exact native/base Carbon class when that declared target is the point of the API | +| PSR-20 clock | Existing `Carbon\FactoryImmutable`, configured to create `Hypervel\Support\CarbonImmutable` | +| Elapsed-time mechanisms already using monotonic/native time | Leave `hrtime()`, `microtime()`, and integer timestamps unchanged | + +Immutability is a value-semantics choice, not permission to replace Carbon with raw PHP primitives. Date modifiers continue to use Carbon; code captures the returned value whenever it must be retained. + +## Audit method and completeness baseline + +The pre-plan audit did not rely on the factory flip or PHPStan to discover scope. It built the union of: + +- every mutable, immutable, and interface Carbon import/reference across `src/` and `tests/`; +- every `Date` facade import and `now()` / `today()` helper call; +- every direct `Carbon::now()`, parse/create/instance call, and `new Carbon` construction; +- concrete `Carbon` / `DateTime` properties, parameters, return types, facade metadata, and model property docs; +- date modifier calls, including multiline/discarded-return forms; +- date values stored in properties, statics, coroutine context, arrays, caches, batches, and worker-lifetime services; +- active Boost documentation examples and contributor/AI/upgrade documentation. + +Every mutation-risk file was read in context. The complete direct-construction ledger is recorded below so implementation has a per-file checklist. The final verification repeats the broad searches over the entire trees and reviews the complete diff file by file; PHPStan and the test suites are safety nets, not substitutes for the manual audit. + +## Findings and fixed decisions + +| Finding | Evidence | Decision | +|---|---|---| +| Factory and direct construction are independent surfaces | `DateFactory::DEFAULT_CLASS_NAME` affects `Date` and helpers, but not `Hypervel\Support\Carbon::now()` | Flip the factory and separately migrate every direct site | +| Hypervel needs its own immutable class | The mutable subclass adds Conditionable, Dumpable, `createFromId`, `plus`, and `minus`; base immutable lacks them | Add `Hypervel\Support\CarbonImmutable` and share only those additions through one trait | +| Carbon conversions lose Hypervel behavior today | Carbon 3.13.1's `Mutability` trait casts to `Carbon\Carbon` / `Carbon\CarbonImmutable`; runtime probes confirm the subclass is dropped | Override both directions on the Hypervel pair | +| Carbon's immutable magic modifier metadata erases subclasses in static analysis | The base immutable class hardcodes `CarbonImmutable` for seven aliases used across exact Hypervel-class boundaries, although runtime probes confirm every operation preserves the subclass | Override those seven inherited magic annotations with `static` on the Hypervel immutable owner; leave unlisted parent metadata visible so future exact-boundary gaps fail visibly | +| Carbon's test state is already shared | Carbon 3.13.1 routes both classes through `FactoryImmutable::getDefaultInstance()`; runtime probes verified test time, macros, serializer, and to-string state | Delete the dual-set override and duplicate immutable reset; keep one authoritative cleanup path | +| Current class handlers violate the advertised date contract | `DateFactory::use(DateTime::class)` makes `now(): CarbonInterface` and the global helper throw native return `TypeError`s | Restrict class handlers to `class-string`; keep callable and Carbon `Factory` escape hatches | +| DateFactory's generic constructor fallback is dead | Installed `CarbonInterface` declares `instance(DateTimeInterface): static` | Delete the `new $dateClass(...)` fallback and its non-Carbon fixture | +| Facade magic-method type resolution borrowed constructor context | DateFactory has no constructor, so the documenter emitted its imported `Closure` name verbatim into another namespace; inherited constructors also resolve imports against the parent file | Resolve class-level `@method` types through a class-backed context and fail loudly on parser/resolution defects instead of emitting namespace-unsafe verbatim metadata | +| Two lifecycle mutations discard an immutable return | HTTP and Console kernels call `setTimezone()` without assignment | Reassign the coroutine-local/property value before handlers run | +| Scheduler mutates its mutex input under mutable opt-out | `repeatEvents()` calls `endOfMinute()` on stored `startedAt`, then passes it to `serverShouldRun()` | Port Laravel 13.x's separate copied end-of-minute boundary and inner expiry check | +| Four native return declarations reject immutable values | Two validation parsing methods and two queued `retryUntil()` wrappers declare `?DateTime` | Use `?DateTimeInterface` and add regressions | +| Full-suite verification exposed a protobuf first-registration race | Concurrent gRPC client fixtures corrupted Google Protobuf's process-global descriptor pool; the server also first constructed request messages inside request coroutines | Prewarm server request and eligible declared response messages before workers fork, prewarm the client isolation fixture before parallel work, and document remaining application-owned message classes | +| DataObject is neither factory-aware nor target-aware | Exact handler/serializer maps omit native/base/Hypervel immutable targets and always hydrate mutable Hypervel Carbon | Normalize through `Date`, then cast to the declared target; serialize all date targets through their common interface | +| Carbon `instance()` preserves same-mutability subclasses | A configured Hypervel subclass leaks through direct `Carbon::instance()` / `CarbonImmutable::instance()` calls, violating DataObject's exact concrete-target contract | Cross to the opposite mutability before all four concrete Carbon conversions so `instance()` constructs the exact target while retaining Carbon settings | +| Existing immutable subsystems use the base class | Bus, Horizon, gRPC, maintenance caching, batch fakes, and Testbench already choose immutability | Canonicalize them to the Hypervel immutable class rather than preserving two framework-owned immutable types | +| Ordinary Eloquent date reads are not an aliasing defect | Built-in date casts construct a fresh value on each read and do not use the class-cast cache | Do not justify the change with a false model-aliasing claim; focus on value semantics and held/shared values | + +## Backing research + +### Installed Carbon behavior + +The repository installs Carbon `3.13.1` (`2937ad3d1d2c506fd2bc97d571438a95641f44e2`). The load-bearing installed APIs are: + +```php +// Carbon\Traits\Cast +/** @template T + * @param class-string $className + * @return T + */ +public function cast(string $className): mixed; +``` + +```php +// Carbon\CarbonInterface +public static function instance(DateTimeInterface $date): static; +``` + +```php +// Carbon\Factory +public function __construct(array $settings = [], ?string $className = null); +``` + +Runtime probes confirmed: + +- current `Hypervel\Support\Carbon::toMutable()` returns base `Carbon\Carbon`; +- current `Hypervel\Support\Carbon::toImmutable()` returns base `Carbon\CarbonImmutable`; +- `Carbon\CarbonImmutable::toImmutable()` returns the same object; +- mutable `setTestNow()` controls base and subclass immutable clocks; +- macros, serialization callbacks, and the static to-string format registered through mutable Carbon are visible to immutable Carbon and cleared by the mutable base reset methods. + +These facts permit the small class pair and single cleanup owner below. Do not add macro synchronization, a second test clock, or per-class teardown registries. + +### Current Laravel reference + +The local reference is the monorepo checkout at `examples/laravel/framework` (`../../../examples/laravel/framework` from this worktree), branch `13.x`, commit `23e9e71f382b91510c70b5b6f9ae0776f1b88e12` (2026-07-21). Relevant current source: + +- `src/Illuminate/Support/DateFactory.php` remains mutable by default and retains Laravel's broad compatibility paths; +- `src/Illuminate/Console/Scheduling/ScheduleRunCommand.php` computes `$endOfMinute = $this->startedAt->copy()->endOfMinute()` once and stops inside the event loop when `Date::now()->gt($endOfMinute)`; +- the Foundation HTTP and Console kernels still discard `setTimezone()` because Laravel's direct Carbon remains mutable; +- Validation's two parsing methods and the Mail/Notifications retry wrappers remain untyped with mutable `DateTime` docblocks. + +Hypervel ports the scheduler's structural correction, then applies its approved immutable-default and modern native typing adaptations. It does not copy Laravel's mutable default or retain a broken handler solely for 1:1 shape. + +### Performance conclusion + +Pre-plan profiles measured the mechanism that changes: immutable modifier allocation. Construction/factory dispatch was neutral; Eloquent cast overhead was approximately `0.14µs`; a single modifier added approximately `1.16µs`; three modifiers approximately `1.65µs`; same-worker HTTP profiles were about `0.8–1%`, with an artificial ten-modifier request about `3.3%`; memory impact was negligible. Bus, Horizon, and gRPC already pay immutable modifier costs. + +The implementation adds no unmeasured hot-path mechanism: the Hypervel subclass has no per-instance wrapper, the DateFactory dispatch removes a branch, and DataObject target dispatch is not a per-request framework loop. Functional, static, structural, and full-suite verification are the completion bar. Do not add a branch benchmark, threshold, CI job, or raw timestamp micro-optimization. + +### Protobuf descriptor race exposed by full-suite verification + +The final ParaTest run exposed Google Protobuf's unsynchronized generated `initOnce()` path when several gRPC client coroutines constructed the same message class for the first time. The isolated test then passed because its descriptor pool was already warm, but the stack trace proved a real process-global registration race. The supported server path had the same defect: `GrpcRouter::compileAndWarm()` reflected request classes before fork without constructing them, while `MessageSerializer::deserialize()` performed the first construction inside concurrent request coroutines. + +Warm every validated server request class during the router's existing boot-only, pre-fork compilation pass. Also warm a declared response type when it is a named, concrete protobuf `Message` subclass whose constructor has no required parameters: + +```php +// Generated descriptor registration is process-global and not coroutine-safe. +// Construct known messages before workers fork and requests can run concurrently. +$requestReflection->newInstance(); + +$responseType = $requestParameter->getDeclaringFunction()->getReturnType(); + +if ($responseType instanceof ReflectionNamedType + && ($responseClass = $this->parameterClassName($requestParameter, $responseType)) !== null + && is_subclass_of($responseClass, Message::class)) { + $responseReflection = new ReflectionClass($responseClass); + + if ($responseReflection->isInstantiable() + && ($responseReflection->getConstructor()?->getNumberOfRequiredParameters() ?? 0) === 0) { + $responseReflection->newInstance(); + } +} +``` + +The zero-required-parameters guard matters only for responses: Hypervel already constructs every request without arguments, but it serializes the response instance supplied by the handler and must not reject a response class whose handler supplies constructor arguments. A separate-process router regression starts with two independent cold metadata owners and proves request plus eligible response initialization. The existing client isolation test constructs one request before entering `parallel()` because its contract is call isolation, not dependency bootstrap. User documentation tells applications to construct any other message class that may first initialize concurrently, including client messages and untyped, union, or iterable server responses. Do not add a descriptor registry, lock, generic preloader, generated fixture, or todo entry. + +## Implementation order + +Implement lower owners before consumers: + +1. add the Hypervel immutable class and shared date helpers; +2. make DateFactory and the Date facade honestly immutable by default; +3. configure the existing PSR clock and simplify Carbon test-state cleanup; +4. make DataObject hydration and serialization target-aware; +5. fix the kernels, scheduler, validation, and queued retry native contracts; +6. migrate direct mutable construction and existing base-immutable framework values using the ledgers; +7. update test-time APIs, public/model types, facade metadata, tests, and fixtures; +8. update active documentation, contributor policy, differences, release/upgrade guidance, and remove the todo; +9. perform the final structural sweep and complete validation. + +## 1. Add the canonical Hypervel class pair + +### Files + +- new `src/support/src/Traits/DateHelpers.php` +- `src/support/src/Carbon.php` +- new `src/support/src/CarbonImmutable.php` +- `tests/Support/SupportCarbonTest.php` +- new `tests/Support/SupportCarbonImmutableTest.php` + +### Shared behavior + +Move only the Hypervel additions shared by both date classes into the existing Support `Traits/` convention: + +```php +namespace Hypervel\Support\Traits; + +use InvalidArgumentException; +use Symfony\Component\Uid\TimeBasedUidInterface; +use Symfony\Component\Uid\Ulid; +use Symfony\Component\Uid\Uuid; + +trait DateHelpers +{ + use Conditionable; + use Dumpable; + + public static function createFromId(Uuid|Ulid|string $id): static + { + if (is_string($id)) { + $id = Ulid::isValid($id) ? Ulid::fromString($id) : Uuid::fromString($id); + } + + if (! $id instanceof TimeBasedUidInterface) { + throw new InvalidArgumentException( + 'The given UUID is not time-based and cannot be converted to a date.' + ); + } + + return static::createFromInterface($id->getDateTime()); + } + + public function plus( + int $years = 0, + int $months = 0, + int $weeks = 0, + int $days = 0, + int $hours = 0, + int $minutes = 0, + int $seconds = 0, + int $microseconds = 0 + ): static + { + return $this->add(" + {$years} years {$months} months {$weeks} weeks {$days} days + {$hours} hours {$minutes} minutes {$seconds} seconds {$microseconds} microseconds + "); + } + + public function minus( + int $years = 0, + int $months = 0, + int $weeks = 0, + int $days = 0, + int $hours = 0, + int $minutes = 0, + int $seconds = 0, + int $microseconds = 0 + ): static + { + return $this->sub(" + {$years} years {$months} months {$weeks} weeks {$days} days + {$hours} hours {$minutes} minutes {$seconds} seconds {$microseconds} microseconds + "); + } +} +``` + +Preserve these signatures and interval strings exactly when moving them. + +Do not put conversion methods or test-clock workarounds in the trait. Conversion direction differs by class, and Carbon 3.13.1 already shares test state. + +### Conversion-preserving classes + +```php +namespace Hypervel\Support; + +use Carbon\Carbon as BaseCarbon; +use Hypervel\Support\Traits\DateHelpers; + +class Carbon extends BaseCarbon +{ + use DateHelpers; + + public function toMutable(): static + { + return $this->cast(static::class); + } + + public function toImmutable(): CarbonImmutable + { + return $this->cast(CarbonImmutable::class); + } +} +``` + +```php +namespace Hypervel\Support; + +use Carbon\CarbonImmutable as BaseCarbonImmutable; +use Hypervel\Support\Traits\DateHelpers; + +/** + * Carbon's immutable magic modifier metadata names its base class even though + * these methods preserve subclasses at runtime. + * + * @method static addMicroseconds(int|float $value = 1) + * @method static addMinute() + * @method static addSecond() + * @method static addSeconds(int|float $value = 1) + * @method static ceilSeconds(float $precision = 1) + * @method static subMinutes(int|float $value = 1) + * @method static subSeconds(int|float $value = 1) + */ +class CarbonImmutable extends BaseCarbonImmutable +{ + use DateHelpers; + + public function toMutable(): Carbon + { + return $this->cast(Carbon::class); + } + + public function toImmutable(): static + { + return $this; + } +} +``` + +The mutable-to-mutable conversion preserves Carbon's copy behavior while keeping late-static subclasses. Immutable-to-immutable preserves Carbon's identity behavior. Do not build a hierarchy between the mutable and immutable classes, a strategy object, or duplicated helper implementations. + +### Tests + +Test exact classes rather than only base-class `instanceof` checks: + +- direct construction, parse, serialization/unserialization, conditionable, and dumpable behavior on each class; +- `createFromId` with time-based UUID, ULID, string input, and invalid non-time-based UUID; +- all named `plus` / `minus` units and proof that immutable operations leave the original unchanged; +- mutable -> mutable, mutable -> immutable, immutable -> mutable, and immutable -> immutable conversions; +- subclasses of each Hypervel class retain late-static behavior where the API promises `static`; +- conversions preserve instant, microseconds, timezone, locale/settings, and the Hypervel helper surface. + +## 2. Make DateFactory honest and immutable by default + +### Files + +- `src/support/src/DateFactory.php` +- `src/support/src/Facades/Date.php` +- `src/facade-documenter/facade.php` +- `src/support/src/functions.php` +- `src/support/src/Stringable.php` +- `src/foundation/src/helpers.php` +- `tests/FacadeDocumenter/ClassDocblockResolutionTest.php` +- `tests/Support/DateFacadeTest.php` +- `tests/Support/SupportStringableTest.php` +- delete `tests/Support/Fixtures/CustomDateClass.php` +- update factory/cast expectations in `tests/Database/Eloquent/Concerns/DateFactoryTest.php` + +### State and handler types + +Use native types plus class-string PHPDoc where PHP cannot express it: + +```php +use Carbon\CarbonInterface; +use Carbon\Factory; +use Closure; +use ReflectionClass; + +/** @var class-string */ +public const string DEFAULT_CLASS_NAME = CarbonImmutable::class; + +/** @var class-string|null */ +protected static ?string $dateClass = null; + +protected static ?Closure $callable = null; + +protected static ?Factory $factory = null; +``` + +Normalize callables once and validate class handlers at configuration time: + +```php +public static function use(mixed $handler): void +{ + if ($handler instanceof Factory) { + static::useFactory($handler); + return; + } + + if (is_string($handler) && is_a($handler, CarbonInterface::class, true)) { + static::useClass($handler); + return; + } + + if (is_callable($handler)) { + static::useCallable($handler); + return; + } + + throw new InvalidArgumentException( + 'Invalid date creation handler. Please provide a Carbon class, callable, or Carbon factory.' + ); +} + +public static function useCallable(callable $callable): void +{ + static::$callable = Closure::fromCallable($callable); + static::$dateClass = null; + static::$factory = null; +} + +/** @param class-string $dateClass */ +public static function useClass(string $dateClass): void +{ + if ( + ! is_a($dateClass, CarbonInterface::class, true) + || ! (new ReflectionClass($dateClass))->isInstantiable() + ) { + throw new InvalidArgumentException( + 'The date class must be an instantiable CarbonInterface implementation.' + ); + } + + static::$dateClass = $dateClass; + static::$factory = null; + static::$callable = null; +} + +public static function useFactory(Factory $factory): void +{ + static::$factory = $factory; + static::$dateClass = null; + static::$callable = null; +} +``` + +This supports closure, invokable-object, callable-string, mutable Hypervel class, custom concrete Carbon subclass, `Factory`, and `FactoryImmutable` handlers. It deliberately rejects `DateTime::class`, `CarbonInterface::class`, abstract Carbon classes, non-Carbon wrappers with an `instance()` method, and arbitrary class strings. Reflection runs only when configuring the boot-time handler, not during date creation. + +### Dispatch + +Keep the three real handler modes and remove the impossible generic constructor fallback: + +```php +public function __call(string $method, array $parameters): mixed +{ + $defaultClassName = static::DEFAULT_CLASS_NAME; + + if (static::$callable !== null) { + return (static::$callable)($defaultClassName::$method(...$parameters)); + } + + if (static::$factory !== null) { + return static::$factory->{$method}(...$parameters); + } + + $dateClass = static::$dateClass ?? $defaultClassName; + + if ( + method_exists($dateClass, $method) + || $dateClass::hasMacro($method) + ) { + return $dateClass::$method(...$parameters); + } + + return $dateClass::instance($defaultClassName::$method(...$parameters)); +} +``` + +Do not add a per-call `instanceof CarbonInterface` guard around callable output. The callable is the deliberate Laravel-style escape hatch; its supported return contract remains `CarbonInterface`, but validating every call adds hot-path machinery without strengthening the class/factory contract. It may transform the generated value into a different mutable or immutable Carbon implementation, not into an unrelated date type. + +Remove the stale `RuntimeException` import/throws documentation if no code path explicitly uses it after this simplification. + +### Public metadata + +Change only date-producing `@method` returns in `DateFactory` from concrete mutable Carbon to `CarbonInterface` (including nullable forms): `create`, `createFromDate`, `createFromFormat`, `createFromIsoFormat`, `createFromLocaleFormat`, `createFromLocaleIsoFormat`, `createFromTime`, `createFromTimeString`, `createFromTimestamp`, `createFromTimestampMs`, `createFromTimestampMsUTC`, `createFromTimestampUTC`, `createMidnightDate`, `createSafe`, `createStrict`, `fromSerialized`, `getTestNow`, `instance`, `make`, `now`, `parse`, `parseFromLocale`, `rawCreateFromFormat`, `rawParse`, `today`, `tomorrow`, and `yesterday`. Do not mechanically change unrelated bool, scalar, array, interval, translator, or `mixed` methods. Update the real public method metadata to show `useFactory(Factory $factory)` and `useClass(class-string)` in PHPDoc where native syntax cannot; the generated facade will retain native `string` for `useClass()` because the documenter reflects native parameter types. + +Regenerate the Date facade from the corrected owner, then lint it: + +```bash +php -f src/facade-documenter/facade.php -- Hypervel\\Support\\Facades\\Date +php -f src/facade-documenter/facade.php -- --lint Hypervel\\Support\\Facades\\Date +``` + +Class-level `@method` tags must be resolved against the class that owns the docblock. The facade documenter uses a minimal class-backed context for that path instead of reflecting a synthetic `__construct`: constructor-less owners otherwise emit namespace-unsafe metadata, while inherited constructors resolve imports from the parent file. Delete the old text fallback; parser or resolution defects must stop this boot-time development tool with a useful stack trace instead of silently committing verbatim types. Cover both constructor-less imported types and inherited-constructor import conflicts in `ClassDocblockResolutionTest`. DateFactory's `getTranslationMessageWith()` metadata uses its real imported `Closure` name, which the class-backed context resolves to the global class. When a facade imports a global class, render that class through the facade's short or aliased import in `@method` lines so generated output shares PHP-CS-Fixer's `global_namespace_import` fixed point; keep namespaced and unimported classes fully qualified. Require both left and right class-name boundaries so an imported global basename cannot corrupt a namespaced FQCN. Cover imported global, unimported global, imported namespaced, and colliding namespaced-basename cases in `ClassDocblockResolutionTest`. + +Change the stale `@return \Hypervel\Support\Carbon` on `src/support/src/functions.php::now()` to `CarbonInterface`. Keep the already-correct native `CarbonInterface` returns on Foundation's global `now()` / `today()` helpers, and ensure their prose does not claim a concrete mutable result. Change `Stringable::toDate()` from `mixed` to `?CarbonInterface`; its parse branch is non-null while the formatted-creation branch can return null: + +```php +public function toDate( + ?string $format = null, + ?string $tz = null +): ?CarbonInterface { + return $format === null + ? Date::parse($this->value, $tz) + : Date::createFromFormat($format, $this->value, $tz); +} +``` + +Generated/default examples may describe the exact immutable class, but public contracts accepting configured handlers use the interface. Accept the facade documenter's removal of legacy methods that are no longer present on `DateFactory`; do not manually re-add stale `maxValue()` / `minValue()` entries after regeneration. Delete the Carbon 2-era `setWeekEndsAt()` / `setWeekStartsAt()` metadata, which neither Carbon 3 class nor its factory supports. Type `withTimeZone()` as returning `Carbon\Factory`; it is a factory-only API and the default Carbon class correctly rejects it. + +### Tests + +- default `Date::now()`, `Date::parse()`, `Date::today()`, global `now()`, and global `today()` are exact `Hypervel\Support\CarbonImmutable`; +- mutable opt-out returns exact `Hypervel\Support\Carbon` across those same routes; +- `Stringable::toDate()` returns exact immutable by default, exact mutable under opt-out, and null/throws according to the existing formatted/parse failure contracts; +- explicit class handlers for Hypervel and base Carbon mutable/immutable concrete classes are accepted and return the configured exact class; +- a named test-local class extending a Hypervel Carbon class is accepted and returned exactly; delete the old wrapper fixture instead of replacing it with another fixture file; +- closure, invokable object, callable string, mutable/immutable `Factory`, locale, macro, and flush-state behavior remain covered; +- `DateFactory::use()` and `useClass()` reject `DateTime::class`, `CarbonInterface::class`, an abstract Carbon subclass, the deleted wrapper shape, a non-Carbon class, and invalid scalar handlers with `InvalidArgumentException`; +- the callable escape hatch may deliberately transform the generated value into another `CarbonInterface`; do not retain the current test's unsupported native `DateTime` result; +- all tests use full native return types while touched. + +## 3. Keep one clock and one Carbon static-state cleanup owner + +### Files + +- `src/foundation/src/Providers/FoundationServiceProvider.php` +- `tests/Foundation/Providers/FoundationServiceProviderTest.php` +- `src/testing/src/PHPUnit/AfterEachTestSubscriber.php` +- `tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php` +- `src/support/src/Carbon.php` (removal already covered) + +### Existing PSR clock + +Configure the already-bound PSR clock to construct the canonical subclass: + +```php +use Carbon\FactoryImmutable; +use Hypervel\Support\CarbonImmutable; +use Psr\Clock\ClockInterface; + +$this->app->singleton( + ClockInterface::class, + fn () => new FactoryImmutable(className: CarbonImmutable::class) +); +``` + +Tests assert exact `Hypervel\Support\CarbonImmutable`, the same frozen instant as `Date::now()` / `now()`, and continued immutability when `Date::use(Carbon::class)` opts the application factory into mutable dates. + +Do not add a second Symfony container binding, call `Symfony\Component\Clock\Clock::set()`, or create a Hypervel clock wrapper. Carbon's `FactoryImmutable` already implements Symfony's clock interface, which extends PSR-20, while Foundation intentionally exposes the PSR key. + +### Static teardown + +Delete `Hypervel\Support\Carbon::setTestNow()`; the inherited Carbon 3.13.1 method already updates the shared default factory. Reduce the test subscriber to the single authoritative base reset: + +```php +\Carbon\Carbon::resetMacros(); +\Carbon\Carbon::resetToStringFormat(); +\Carbon\Carbon::serializeUsing(null); +\Carbon\Carbon::useStrictMode(); +\Carbon\Carbon::setTestNow(); +``` + +Delete the redundant `\Carbon\CarbonImmutable::setTestNow()` call. Add one focused subscriber regression that registers/fixes state through the new Hypervel immutable class, invokes `flushFrameworkState()`, and proves mutable and immutable classes both return to real/default macro, serializer, string-format, strict-mode, and test-clock state. Do not duplicate a separate test for each reset line or add another cleanup registry. + +## 4. Make DataObject date resolution target-aware + +### Files + +- `src/support/src/DataObject.php` +- `tests/Support/DataObjectTest.php` +- `src/boost/docs/data-objects.md` + +### Supported declared targets + +Hydrate all eight meaningful targets: + +1. `DateTimeInterface` — configured DateFactory result; +2. `CarbonInterface` — configured DateFactory result; +3. native `DateTime` — exact native mutable value; +4. native `DateTimeImmutable` — exact native immutable value; +5. `Hypervel\Support\Carbon` — exact Hypervel mutable value; +6. `Hypervel\Support\CarbonImmutable` — exact Hypervel immutable value; +7. base `Carbon\Carbon` — exact base mutable value; +8. base `Carbon\CarbonImmutable` — exact base immutable value. + +Interface targets follow `Date::use(...)`; concrete targets remain assignable and exact regardless of the current default. This is the declared-property contract, not a second global configuration system. + +### Handler shape + +Build one target-specific resolver per declared map key and stop treating falsy timestamps as null: + +```php +protected static function getCustomizedDependencies(): array +{ + $dependencies = []; + $dateTargets = [ + DateTimeInterface::class, + CarbonInterface::class, + DateTime::class, + DateTimeImmutable::class, + Carbon::class, + CarbonImmutable::class, + BaseCarbon::class, + BaseCarbonImmutable::class, + ]; + + foreach ($dateTargets as $target) { + $dependencies[$target] = static fn (mixed $value): ?DateTimeInterface => + $value === [] ? null : static::asDateTime($value, $target); + } + + return $dependencies; +} +``` + +`replaceDependenciesData()` already skips null for nullable properties. For a non-nullable dependency it converts explicit null to the empty-array sentinel before invoking the handler so nested DataObjects can still hydrate from defaults. The date resolver must map that exact sentinel back to null, allowing the final constructor to raise its natural parameter `TypeError`; do not pass `[]` into Carbon. Do not retain the broader `$value ? ... : null`, which incorrectly maps timestamp `0` and string `'0'` to null. Keep this handling scoped to the date resolver rather than changing nested-DataObject null hydration. + +Normalize every non-null input once through the configured Date factory, then cast by target: + +```php +/** @param DateTimeInterface::class|CarbonInterface::class|DateTime::class|DateTimeImmutable::class|Carbon::class|CarbonImmutable::class|BaseCarbon::class|BaseCarbonImmutable::class $target */ +protected static function asDateTime(mixed $value, string $target): DateTimeInterface +{ + if ($value instanceof DateTimeInterface) { + $date = Date::instance($value); + } elseif (is_numeric($value)) { + $date = Date::createFromTimestamp( + $value, + date_default_timezone_get() + ); + } elseif (static::isStandardDateFormat($value)) { + $date = Date::parse($value)->startOfDay(); + } else { + try { + $date = Date::createFromFormat(static::$dateFormat, $value); + // @phpstan-ignore catch.neverThrown (the Date facade's magic dispatch hides Carbon's @throws from analysis) + } catch (InvalidFormatException) { + $date = null; + } + + $date ??= Date::parse($value); + } + + return match ($target) { + DateTimeInterface::class, CarbonInterface::class => $date, + DateTime::class => DateTime::createFromInterface($date), + DateTimeImmutable::class => DateTimeImmutable::createFromInterface($date), + // instance() clones same-mutability subclasses, so cross the mutability + // boundary first to honor the exact target while retaining Carbon settings. + Carbon::class => Carbon::instance($date->toImmutable()), + CarbonImmutable::class => CarbonImmutable::instance($date->toMutable()), + BaseCarbon::class => BaseCarbon::instance($date->toImmutable()), + BaseCarbonImmutable::class => BaseCarbonImmutable::instance($date->toMutable()), + }; +} +``` + +Keep the exact eight-class PHPDoc and explicit match: eight public target semantics are clearer than reflective method probing. Building the handlers from one local target list lets PHPStan 2.2.5 infer the captured literal union; local Closure PHPDocs do not narrow closure parameters in that version. Do not add a class-level registry, runtime default guard, converter objects, or per-target classes. + +### Serialization + +All eight values implement `DateTimeInterface` and share the same ISO serialization. Replace the exact-class date map with one interface serializer while preserving exact custom serializers for non-date objects: + +```php +protected static function getSerializers(): array +{ + return [ + DateTimeInterface::class => + fn (DateTimeInterface $value): string => $value->format('c'), + ]; +} +``` + +In `toArray()`, resolve nested DataObjects first, then the `DateTimeInterface` serializer, then existing exact custom-object and `toArray()` paths. This covers custom date subclasses without listing every runtime class and does not change non-date extension behavior. + +The date-specific branch is explicit while the existing exact-class extension point remains after it: + +```php +if ($value instanceof self) { + $value = $value->toArray(); +} elseif ( + $value instanceof DateTimeInterface + && $serializer = $serializers[DateTimeInterface::class] ?? null +) { + $value = $serializer($value); +} elseif ( + is_object($value) + && $serializer = $serializers[$value::class] ?? null +) { + $value = $serializer($value); +} elseif (is_object($value) && method_exists($value, 'toArray')) { + $value = $value->toArray(); +} +``` + +### Matrix tests + +Use one compact fixture with eight typed date properties and a data provider, not eight near-identical fixture classes. For each declared target, cover inputs of: + +- database-format string; +- standard date string; +- UNIX timestamp including `0`; +- native mutable and native immutable DateTime; +- base mutable and base immutable Carbon; +- Hypervel mutable and Hypervel immutable Carbon. + +Assert exact target class, instant, microseconds, and timezone. Then separately cover: + +- interface targets under immutable default and mutable opt-out; +- concrete immutable targets while the factory is mutable, and concrete mutable targets while it is immutable; +- configured mutable and immutable Hypervel subclasses while every concrete Carbon target remains exact and retains Carbon-local settings; +- nullable/union resolution, nested data objects, missing/default values, auto-resolution disabled, and cache flush; +- explicit null for a non-nullable date target reaches the constructor as null and produces its natural parameter `TypeError`, rather than a Carbon error about an array; +- `toArray()` / JSON round trips for every target; +- user-overridden non-date dependency resolvers and serializers remain intact. + +## 5. Fix immutable-sensitive behavior and native contracts + +### Foundation lifecycle timestamps + +Files: + +- `src/foundation/src/Http/Kernel.php` +- `src/foundation/src/Console/Kernel.php` +- `tests/Foundation/Http/KernelTest.php` +- `tests/Foundation/Console/KernelTerminateTest.php` +- `tests/Integration/Console/CommandDurationThresholdTest.php` + +Both timestamps are framework-owned internal values, so create and type them as exact `Hypervel\Support\CarbonImmutable`. Capture timezone conversion: + +```php +$requestStartedAt = $requestStartedAt->setTimezone( + $this->app->make('config')->string('app.timezone') +); +CoroutineContext::set( + self::REQUEST_STARTED_AT_CONTEXT_KEY, + $requestStartedAt +); +``` + +```php +$this->commandStartedAt = $this->commandStartedAt->setTimezone( + $this->app->make('config')->string('app.timezone') +); +``` + +Return `?CarbonImmutable` from `requestStartedAt()` and `commandStartedAt()`. The HTTP value must be written back to `CoroutineContext` after timezone conversion because `requestStartedAt()` reads the context while lifecycle handlers are still running; the Console assignment already updates the owning property. Use immutable values for the end timestamp too. Test handler arguments, configured timezone, exact start value, threshold behavior, exception cleanup, and the null state after termination. From inside an HTTP duration handler, assert that `requestStartedAt()` returns the same converted instance and configured timezone as the handler argument. Retype the existing HTTP callback fixture from mutable Carbon. + +### Scheduler minute boundary and mutex time + +Files: + +- `src/console/src/Commands/ScheduleRunCommand.php` +- `tests/Console/Scheduling/ScheduleRunCommandTest.php` + +Preserve factory configurability of `$startedAt` as `CarbonInterface`, but never mutate it to compute the loop boundary: + +```php +$endOfMinute = $this->startedAt->copy()->endOfMinute(); + +while (Date::now()->lte($endOfMinute)) { + // pause/stop/repeatability checks... + + if (Date::now()->gt($endOfMinute)) { + return; + } + + // filters and execution... +} +``` + +Keep the copy even though the default is immutable because the public mutable opt-out is supported here. In `ScheduleRunCommandTest`, add a focused test that runs the repeat path in both default and mutable modes and proves the original minute—not end-of-minute—is passed from the command into the single-server mutex path. Retain Hypervel's existing `shouldStop`, pause, maintenance, Waiter, and dispatcher adaptations around the Laravel-shaped correction. + +### Native DateTime return defects + +Files: + +- `src/validation/src/Concerns/ValidatesAttributes.php` +- `tests/Validation/ValidationValidatorTest.php` +- `src/mail/src/SendQueuedMailable.php` +- `tests/Mail/MailableQueuedTest.php` +- `src/notifications/src/SendQueuedNotifications.php` +- `tests/Notifications/NotificationSendQueuedNotificationTest.php` + +Change the exhaustive concrete return set: + +```php +protected function getDateTimeWithOptionalFormat( + string $format, + string $value +): ?DateTimeInterface; + +protected function getDateTime( + DateTimeInterface|string $value +): ?DateTimeInterface; + +public function retryUntil(): ?DateTimeInterface; +``` + +There are four methods: two validation methods plus Mail and Notifications retry wrappers. No concrete-`DateTime` property case was found. Add default-immutable regressions for fallback parsing through `after`, `before`, and sibling `date_format`, plus queued mailable/notification objects whose property or method returns Hypervel immutable. Keep native `DateTime::createFromFormat()` values valid through the interface. + +## 6. Migrate direct mutable construction deliberately + +### Framework-owned direct immutable values + +Replace mutable Hypervel or base Carbon imports/construction with `Hypervel\Support\CarbonImmutable` in the following audited files. Preserve existing Carbon APIs and captured modifier chains; do not translate them to integer arithmetic: + +| Package | Files | +|---|---| +| Auth | `Notifications/VerifyEmail.php`, `Passwords/CacheTokenRepository.php`, `Passwords/DatabaseTokenRepository.php` | +| Cache | `Repository.php`, `SessionStore.php`, `StackStore.php`, `SwooleStore.php` | +| Collections | `LazyCollection.php` optional Carbon clock path | +| Console | `Commands/ScheduleListCommand.php`, direct log timestamps in `Commands/ScheduleRunCommand.php`, `Scheduling/ManagesFrequencies.php` | +| Database | `Concerns/BuildsWhereDateClauses.php`, `Eloquent/Factories/Factory.php` | +| Foundation | `Console/DownCommand.php`, `Console/Kernel.php`, `Http/Kernel.php`, `Http/MaintenanceModeBypassCookie.php` | +| HTTP | `Middleware/SetCacheHeaders.php` | +| Queue | `Console/PruneBatchesCommand.php`, `Console/PruneFailedJobsCommand.php`, `Console/WorkCommand.php`, `DatabaseQueue.php`, `Queue.php`, `Worker.php` | +| Routing | `UrlGenerator.php` | +| Session | `DatabaseSessionHandler.php`, `FileSessionHandler.php`, `Middleware/StartSession.php` | +| Telescope | `Console/PruneCommand.php`, `Http/Controllers/ExceptionController.php` | +| Testing | `TestResponse.php` | + +Use exact immutable return types where a method owns the exact value, including: + +```php +protected function now(): CarbonImmutable +{ + $queueTimezone = $this->config->get('queue.output_timezone'); + + if ( + $queueTimezone + && $queueTimezone !== $this->config->get('app.timezone') + ) { + return CarbonImmutable::now()->setTimezone($queueTimezone); + } + + return CarbonImmutable::now(); +} +``` + +and `ScheduleListCommand::getNextDueDateForEvent(): CarbonImmutable`. Remove now-redundant `copy()` calls only when the receiver is statically exact immutable and mutable opt-out cannot reach the path. Do not remove the scheduler boundary copy described above. + +### Worker-array lock records and type-only imports + +Canonicalize stored record types and creation together: + +- `src/cache/src/AbstractArrayStore.php` +- `src/cache/src/ArrayLock.php` +- `src/cache/src/ArrayStore.php` +- `src/cache/src/WorkerArrayStore.php` + +The final shapes use `?CarbonImmutable` for `expiresAt`. This is a framework-owned stored value and must remain immutable even if an application opts its public Date factory into mutable dates. + +### Factory-routed construction sites + +Keep public/configurable paths on `Date` and remove mutable intermediate creation: + +- `src/database/src/Eloquent/Concerns/HasAttributes.php` +- `src/foundation/src/Http/Traits/HasCasts.php` +- `src/support/src/InteractsWithTime.php` +- `src/support/src/Sleep.php` +- `src/foundation/src/Testing/Concerns/InteractsWithTime.php` +- `src/foundation/src/Testing/Wormhole.php` +- `src/support/src/DataObject.php` (covered above) + +For standard-date paths, create through the facade directly: + +```php +return Date::parse($value)->startOfDay(); +``` + +Do not generate mutable Carbon and immediately recast it through `Date::instance()`. + +Use the same creator-owned formatted-parse path in Eloquent `HasAttributes::asDateTime()`, HTTP request `HasCasts::asDateTime()`, and DataObject. Keep the declared `CarbonInterface` return non-null without reverting to mutable Carbon: + +```php +try { + $date = Date::createFromFormat($format, $value); + // @phpstan-ignore catch.neverThrown (the Date facade's magic dispatch hides Carbon's @throws from analysis) +} catch (InvalidFormatException) { + $date = null; +} + +return $date ?? Date::parse($value); +``` + +Carbon's `hasFormat()` rejects valid PHP format modifiers such as `!`, `|`, and `*`; `hasFormatWithModifiers()` supports those but Carbon 3.13.1 still rejects valid trailing-data `+` input that its own `createFromFormat()` accepts. Pre-validating with either method creates a second, incomplete grammar. The creator therefore owns the contract in all three paths. The dominant matching path performs one parse without a preflight regex; only genuine format mismatches pay the exception needed to reach generic parsing. Catch the exact `Carbon\Exceptions\InvalidFormatException`, let unrelated exceptions fail fast, and keep the identifier-scoped PHPStan suppression because facade magic cannot expose Carbon's runtime `@throws` metadata to analysis. Keep `Date::parse()` outside the `try` so a non-strict handler returning null cannot cause a failing fallback parse to be caught and invoked twice. The existing native `InvalidArgumentException` import in `HasAttributes` remains because the trait uses it elsewhere. + +Retain Eloquent's existing `!` / escaped-literal / `|` / `|*` regressions and cover `+` plus a modifier/literal case in each separate owner. Do not add a shared parser helper or record Carbon's internal regex inconsistency as unfinished Hypervel work; no final Hypervel path depends on that approximation. + +`HasAttributes::serializeDate()` may retain explicit Hypervel mutable conversion for mutable input and Hypervel immutable conversion for immutable input; this is a serialization boundary where preserving input mutability selection is intentional. Its existing `immutable_date` / `immutable_datetime` branches must now end at the Hypervel immutable subclass through the conversion override. + +### Test-time APIs + +Generate public test-time values through the Date factory while using Carbon's one shared static test clock: + +```php +$now = Date::now(); +Carbon::setTestNow($now->addDays($this->value)); +``` + +Apply this pattern to freeze time/second, Wormhole units/back, Support `InteractsWithTime`, and Sleep's numeric-until/sync behavior. Update callback PHPDocs from concrete mutable Carbon to `CarbonInterface` where the framework supplies the value. Test exact immutable default and exact mutable opt-out, callback return values, and `finally` restoration after exceptions. + +Retain `WithImmutableDates`, switch it to `Hypervel\Support\CarbonImmutable`, and document that it forces immutability for a test whose application/bootstrap may globally opt into mutable dates. Do not add `WithMutableDates` without a demonstrated consumer. + +## 7. Canonicalize existing base CarbonImmutable values + +All listed packages already depend on `hypervel/support`; changing the class does not add split-package dependencies. Replace base `Carbon\CarbonImmutable` with `Hypervel\Support\CarbonImmutable` in this exhaustive source set: + +| Package | Files | +|---|---| +| Bus | `Batch.php`, `BatchFactory.php`, `Batchable.php`, `DatabaseBatchRepository.php`, `DebounceLock.php` | +| Database | the immutable import/conversion in `Eloquent/Concerns/HasAttributes.php` | +| Foundation | `WorkerCachedMaintenanceMode.php` | +| gRPC | `Server/Middleware/HandleCall.php`, `Server/ServerCallContext.php` | +| Horizon | `Jobs/RetryFailedJob.php`; `Listeners/MonitorWaitTimes.php`, `TrimFailedJobs.php`, `TrimMonitoredJobs.php`, `TrimRecentJobs.php`; `MasterSupervisor.php`; `ProcessPool.php`; `Repositories/RedisJobRepository.php`, `RedisMasterSupervisorRepository.php`, `RedisMetricsRepository.php`, `RedisProcessRepository.php`, `RedisSupervisorRepository.php`; `Supervisor.php`; `WorkerProcess.php` | +| Support fakes | `Testing/Fakes/BatchFake.php`, `Testing/Fakes/BatchRepositoryFake.php` | +| Testbench | `Attributes/WithImmutableDates.php`, `src/testbench/workbench/database/migrations/2013_07_26_182750_create_testbench_users_table.php` | + +Update their exact source tests to assert the Hypervel class, especially property types and values crossing repository hydration. Base Carbon remains appropriate only in: + +- the two Hypervel classes' inheritance imports; +- DataObject's explicitly supported base-class declared targets; +- tests that intentionally exercise third-party/base Carbon input or conversion; +- installed Carbon `Factory` / `FactoryImmutable` infrastructure. + +Do not create aliases or accept two framework-owned immutable property types merely to reduce edits. + +## 8. Correct public/model metadata and affected tests + +### Model and framework PHPDocs + +- change `HasAttributes::asDate()` / `asDateTime()` docs to `CarbonInterface` and remove mutable-only prose; +- change `src/passkeys/src/Passkey.php` `last_used_at`, `created_at`, and `updated_at` properties to `?CarbonInterface`; +- change `src/sanctum/src/PersonalAccessToken.php` `last_used_at` and `expires_at` properties from base mutable Carbon to `?CarbonInterface`; +- replace `src/telescope/src/EntryResult.php`'s stale mutable/base Carbon parameter union with its existing native `CarbonInterface` contract; +- change cache record shapes to exact Hypervel immutable as above; +- change Notification documentation callback/delay returns to `CarbonInterface`; +- update all DateFactory/Date facade/helper metadata from concrete mutable output to `CarbonInterface`. + +### Existing test migration + +Update expectations by semantics, never by weakening every assertion to the interface: + +- default factory/cast output: assert exact `Hypervel\Support\CarbonImmutable`; +- explicit mutable opt-out/conversion: assert exact `Hypervel\Support\Carbon`; +- configurable contract-only boundaries: assert `CarbonInterface` plus the expected exact class for each configuration; +- base-class input/output boundary tests: retain base assertions intentionally. + +The known assertion files are: + +- `tests/Database/DatabaseEloquentModelTest.php` +- `tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php` +- `tests/Database/DatabaseSoftDeletingTest.php` +- `tests/Database/DatabaseSoftDeletingTraitTest.php` +- `tests/Database/Eloquent/Concerns/DateFactoryTest.php` +- `tests/Foundation/FoundationHelpersTest.php` +- `tests/Integration/Database/EloquentModelDateCastingTest.php` +- `tests/Support/DateFacadeTest.php` +- `tests/Support/Traits/InteractsWithDataTest.php` +- `tests/Testbench/DefaultConfigurationTest.php` — rename the former mutable-default test and assert the exact immutable default; this interface-only test has no concrete mutable Carbon import and therefore requires the full type sweep below + +`tests/Support/SupportCarbonTest.php` continues to test the explicit mutable class and is not blanket-converted. Strengthen immutable-cast tests that currently pass merely because the Hypervel subclass is an `instanceof` the base class. + +Correct non-assertion native test types found by the audit: + +- the two static `?Carbon $ranAt` properties in `tests/Integration/Queue/JobChainingTest.php` become `?CarbonInterface` or exact immutable according to the value source (`now()` is configurable, so use the interface); +- the Foundation HTTP duration callback parameter becomes exact `CarbonImmutable`, matching the public kernel contract; +- `tests/Console/Scheduling/CacheSchedulingMutexTest.php::$time` becomes exact `CarbonImmutable` because that fixture is not testing the opt-out; +- `tests/Console/Scheduling/ScheduleRunCommandTest.php::invokeRunEvents()` accepts `?CarbonInterface` and defaults through `Date::now()` because that test owner covers mutable opt-out; +- base immutable imports in Bus, Horizon, gRPC, Foundation clock, WorkerCachedMaintenanceMode, Queue, and database tests switch to Hypervel immutable unless the test is intentionally a base-input boundary. + +Run a full test-tree concrete-type sweep after edits; the known list is a starting ledger, not permission to ignore new matches. + +The current base-immutable test imports are a second explicit ledger: `tests/Bus/BusBatchTest.php`, `BusBatchableTest.php`; `tests/Database/Eloquent/Concerns/DateFactoryTest.php`; `tests/Foundation/Providers/FoundationServiceProviderTest.php`, `Testing/WormholeTest.php`, `WorkerCachedMaintenanceModeTest.php`; `tests/Grpc/ServerCallContextTest.php`; `tests/Integration/Database/EloquentModelDateCastingTest.php`, `EloquentModelImmutableDateCastingTest.php`; the Horizon feature tests `JobRetrievalTest.php`, `MetricsTest.php`, `MonitorWaitTimesTest.php`, `ProcessRepositoryTest.php`, `QueueProcessingTest.php`, `SupervisorTest.php`, `TrimMonitoredJobsTest.php`, `TrimRecentJobsTest.php`, `WorkerProcessTest.php`; `tests/Integration/Queue/DebouncedJobTest.php`, `SkipIfBatchCancelledTest.php`; `tests/Jwt/ClaimFactoryTest.php`; `tests/Permission/Traits/HasPermissionsWithCustomModelsTest.php`, `HasRolesWithCustomModelsTest.php`; `tests/Queue/RetryBatchCommandTest.php`; and `tests/Support/DateFacadeTest.php`, `SupportCarbonTest.php` (aliased as `BaseCarbonImmutable`). Switch framework-owned expected values to Hypervel immutable; retain a base immutable only where the test explicitly supplies a third-party/base input and assert the canonical output separately. `SupportCarbonTest` intentionally retains the aliased base class where it verifies shared mutable/immutable test-clock state. + +### Canonical test-setup sweep + +The audit also found mutable Hypervel Carbon imported as test setup in the following current files. Read each before editing. Move ordinary clocks, timestamps, expected framework values, and immutable-source fixtures to `CarbonImmutable` or `Date` according to the production boundary; retain mutable Carbon only where the test explicitly proves mutable opt-out, mutable conversion, mutable serialization, or a custom mutable cast: + +| Area | Files | +|---|---| +| Auth | `tests/Auth/AuthDatabaseTokenRepositoryTest.php`, `CacheTokenRepositoryTest.php`, `RequirePasswordMiddlewareTest.php` | +| Cache | `tests/Cache/CacheArrayStoreTest.php`, `CacheFileStoreTest.php`, `CacheMemoizedStoreTest.php`, `CacheRepositoryTest.php`, `CacheSessionStoreTest.php`, `CacheWorkerArrayStoreTest.php` | +| Console scheduling | `tests/Console/Scheduling/CacheSchedulingMutexTest.php`, `EventTest.php`, `FrequencyTest.php`, `ScheduleRunCommandTest.php`, `ScheduleRunContextPropagationTest.php`, `ScheduleTest.php` | +| Database | `tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php`, `DatabaseEloquentBelongsToManySyncTouchesParentTest.php`, `DatabaseEloquentBuilderCreateOrFirstTest.php`, `DatabaseEloquentBuilderTest.php`, `DatabaseEloquentHasManyCreateOrFirstTest.php`, `DatabaseEloquentHasManyThroughCreateOrFirstTest.php`, `DatabaseEloquentIntegrationTest.php`, `DatabaseEloquentIrregularPluralTest.php`, `DatabaseEloquentModelTest.php`, `DatabaseEloquentRelationTest.php`, `DatabaseEloquentSoftDeletesIntegrationTest.php`, `DatabaseEloquentTimestampsTest.php`, `DatabaseSoftDeletingTest.php`, `DatabaseSoftDeletingTraitTest.php`, `QueryDurationThresholdTest.php` | +| Foundation | `tests/Foundation/Console/KernelTerminateTest.php`, `FoundationExceptionsHandlerTest.php`, `FoundationInteractsWithTimeTest.php`, `Http/MaintenanceModeBypassCookieTest.php`, `Providers/FoundationServiceProviderTest.php`, `Testing/WormholeTest.php` | +| Other unit packages | `tests/Http/HttpClientTest.php`, `tests/Pagination/CursorTest.php`, `tests/Sanctum/PruneExpiredTest.php`, `tests/Session/ArraySessionHandlerTest.php`, `tests/Session/FileSessionHandlerTest.php`, `tests/Telescope/Watchers/QueryWatcherTest.php`, `tests/Translation/TranslationTranslatorTest.php`, `tests/Validation/ValidationDateRuleTest.php`, `tests/Validation/ValidationValidatorTest.php` | +| Integration: Cache/Console/Foundation | `tests/Integration/Cache/RepositoryTest.php`, `tests/Integration/Console/CommandDurationThresholdTest.php`, `CommandSchedulingTest.php`, `Scheduling/ScheduleGroupTest.php`, `Scheduling/ScheduleListCommandTest.php`, `Scheduling/ScheduleTestCommandTest.php`, `Scheduling/SubMinuteSchedulingTest.php`, `tests/Integration/Foundation/Configuration/WithScheduleTest.php`, `MaintenanceModeTest.php` | +| Integration: Database | `tests/Integration/Database/DatabaseCacheStoreTest.php`, `DatabaseEloquentModelAttributeCastingTest.php`, `DatabaseEloquentModelCustomCastingTest.php`, `DatabaseLockTest.php`, `EloquentBelongsToManyTest.php`, `EloquentEagerLoadingLimitTest.php`, `EloquentModelTest.php`, `EloquentMorphManyTest.php`, `MariaDb/EloquentCastTest.php`, `MySql/EloquentCastTest.php`, `QueryBuilderTest.php` | +| Integration: other | `tests/Integration/Http/ThrottleRequestsWithRedisTest.php`, `tests/Integration/Mail/SendingMailWithLocaleTest.php`, `tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php`, `tests/Integration/Queue/JobChainingTest.php`, `RateLimitedTest.php`, `ThrottlesExceptionsTest.php`, `ThrottlesExceptionsWithRedisTest.php`, `WorkCommandTest.php`, `tests/Integration/Routing/UrlSigningTest.php`, `tests/Integration/Session/DatabaseSessionHandlerTest.php` | +| Queue | `tests/Queue/DatabaseFailedJobProviderTest.php`, `DatabaseUuidFailedJobProviderTest.php`, `FileFailedJobProviderTest.php`, `QueueBackgroundQueueTest.php`, `QueueBeanstalkdQueueTest.php`, `QueueDatabaseQueueIntegrationTest.php`, `QueueDatabaseQueueUnitTest.php`, `QueueDeferredQueueTest.php`, `QueuePauseResumeTest.php`, `QueueRedisQueueTest.php`, `QueueSqsQueueTest.php`, `QueueWorkerTest.php` | +| Support | `tests/Support/DateFacadeTest.php`, `SleepTest.php`, `SupportArrTest.php`, `SupportCarbonTest.php`, `SupportFluentTest.php`, `SupportLazyCollectionIsLazyTest.php`, `SupportLazyCollectionTest.php`, `SupportStringableTest.php`, `Traits/InteractsWithDataTest.php`, `ValidatedInputTest.php` | + +`SupportCarbonTest`, explicit mutable DateFactory branches, and the custom mutable Eloquent caster fixtures are intentional allowlist candidates; ordinary test clocks are not. Correct stale test PHPDocs encountered in this sweep, including `QueryDurationThresholdTest::$now`, and capture modifier returns where switching a fixture to immutable exposes an assumed side effect. + +## 9. Documentation, policy, and release guidance + +### Contributor and AI policy + +Files: + +- `AGENTS.md` +- `docs/ai/differences-vs-laravel.md` +- `docs/todo.md` + +Add the approved modernization to Porting Packages: mutable Laravel date construction is converted to Hypervel immutable construction, concrete factory outputs are typed `CarbonInterface`, and discarded mutation returns are captured. Add a Code conventions rule with these points: + +- Hypervel defaults to `Hypervel\Support\CarbonImmutable` where Laravel defaults to mutable Carbon; +- use `Date` / helpers for public or application-configurable creation; +- use exact Hypervel immutable for framework-owned internal/held values; +- use `CarbonInterface` at configurable Carbon boundaries and `DateTimeInterface` at native/third-party boundaries; +- capture the return of every modifier that must persist; +- use mutable Hypervel Carbon only for explicit opt-out/conversion behavior. + +Add a concise `Dates` difference explaining immutable default, modifier reassignment, and the boot-time mutable opt-out. Remove the completed CarbonImmutable todo entry; do not leave a duplicate historical instruction. + +### Active user documentation + +Audit and update these known active surfaces: + +- `src/boost/docs/helpers.md` — exact immutable default, helpers, `plus` / `minus`, direct class example, and mutable opt-out; +- `src/boost/docs/eloquent-mutators.md` and `eloquent.md` — `date` / `datetime` default to Hypervel immutable, explicit immutable casts, interface language, and assignment examples; +- `src/boost/docs/data-objects.md` — all eight target types and interface-vs-concrete behavior; +- `src/boost/docs/mocking.md` — immutable default and `CarbonInterface` callback examples; +- `src/boost/docs/testbench.md` — `WithImmutableDates` is a forcing attribute for suites/apps that otherwise opt into mutable; +- `src/boost/docs/collections.md` — immutable class in date examples; +- `src/boost/docs/notifications.md` — `CarbonInterface` return types; +- `src/boost/docs/requests.md` — request dates use the configured Carbon interface and immutable default; +- `src/boost/docs/strings.md` — `CarbonImmutable::createFromId()` as the default example while noting the mutable class retains parity; +- `src/boost/docs/grpc.md` — name the Hypervel immutable class, not an ambiguous/base class; +- any remaining active `Hypervel\Support\Carbon` default-output claim found by the final docs sweep. + +Examples must not teach ignored mutation returns. Use either chaining into immediate consumption or assignment: + +```php +$expiresAt = now()->addMinutes(5); +$expiresAt = $expiresAt->addDay(); +``` + +### Upgrade and release notes + +Update `src/boost/docs/upgrade.md` with a migration section: + +- concrete mutable type hints receiving helper/factory/cast values become `CarbonInterface` or immutable; +- retained modifier calls require assignment; +- applications that temporarily require the old behavior may configure `Date::use(Carbon::class)` during boot; +- custom Date class handlers must implement `CarbonInterface`; callable handlers may deliberately transform the generated date but must still return a `CarbonInterface`, because typed helpers and other factory-routed APIs enforce that contract. + +Add a concise immutable-date item to the 0.4 release notes in `src/boost/docs/releases.md`. Do not frame this as Swoole-only: immutable date values are the modern default, while worker/coroutine safety is an additional benefit for held/shared values. + +## 10. Verification and completion criteria + +### Focused implementation loop + +After each workstream, run its tests immediately. At minimum: + +```bash +vendor/bin/phpunit tests/Support/SupportCarbonTest.php tests/Support/SupportCarbonImmutableTest.php tests/Support/DateFacadeTest.php tests/Support/DataObjectTest.php tests/Support/SupportStringableTest.php +vendor/bin/phpunit tests/Foundation/Providers/FoundationServiceProviderTest.php tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php +vendor/bin/phpunit tests/Foundation/Http/KernelTest.php tests/Foundation/Console/KernelTerminateTest.php tests/Integration/Console/CommandDurationThresholdTest.php +vendor/bin/phpunit tests/Console/Scheduling/ScheduleRunCommandTest.php tests/Console/Scheduling/CacheSchedulingMutexTest.php +vendor/bin/phpunit tests/Validation/ValidationValidatorTest.php +``` + +Run the affected Mail, Notifications, Database/Eloquent, Bus, Cache, gRPC, Horizon, Queue, Session, Testbench, and Testing test files/directories as their owners are edited. Use the copied worktree `.env` for integration services. + +### Static and generated metadata + +```bash +php -f src/facade-documenter/facade.php -- --lint Hypervel\\Support\\Facades\\Date +composer analyse +composer lint:fix +``` + +Fix source types and PHPDocs at the owner. Do not add global PHPStan suppressions, runtime `assert()` narrowing, or defensive branches that only compensate for stale metadata. + +### Full repository validation + +```bash +composer test:parallel +composer test:testbench +composer test:dogfood +composer fix +php -f src/facade-documenter/facade.php -- --lint Hypervel\\Support\\Facades\\Date +``` + +`composer fix` is the final combined formatter, analysis, parallel-suite, Testbench, and dogfood confirmation. Run the Date facade lint again afterward because the formatter and generator must converge on the same committed metadata representation. Record any environment-dependent failure with the exact command and evidence; do not silently omit a suite. + +### Structural/stale-code sweep + +Repeat broad searches over all of `src/`, `tests/`, active docs, stubs, dogfood, and types. Completion requires: + +- no default-output metadata claiming `Hypervel\Support\Carbon`; +- no factory-produced value assigned to a concrete mutable-only property, parameter, or return; +- no concrete `?DateTime` return in the four corrected methods; +- no direct mutable Carbon construction except the explicit mutable class itself, mutable opt-out tests, DataObject's declared mutable target, and mutable-input serialization; +- no base `Carbon\CarbonImmutable` framework property/construction outside the documented inheritance/input boundary allowlist; +- no date modifier return discarded where persistence is intended; +- no mutable `expiresAt` cache record shape; +- no `DateFactory::use(DateTime::class)` success test or non-Carbon `CustomDateClass` fixture; +- no redundant immutable `setTestNow()` reset or dual-set Hypervel override; +- no stale imports, `copy()` calls on exact immutable-only values, comments describing mutable-only side effects, or docs teaching mutable defaults; +- no CarbonImmutable todo entry after the work is complete. + +Use the following searches as a reproducible minimum, then inspect the surrounding code rather than treating zero raw matches as the audit: + +```bash +rg -n '^use Hypervel\\Support\\Carbon;' src tests +rg -n '^use Carbon\\Carbon(Immutable)?;' src tests +rg -n 'Carbon::(now|today|parse|create|instance)|new Carbon' src tests +rg -n 'CarbonImmutable|CarbonInterface|DateTimeInterface|\?DateTime|\?Carbon' src tests +rg -n 'Date::use|DateFactory::use|useClass\(|useFactory\(|useCallable\(' src tests +rg -n -- '->(add|sub|startOf|endOf|setTimezone|timezone|modify|setDate|setTime|setTimestamp)' src tests +rg -n -F -e 'Hypervel\Support\Carbon' -e 'CarbonImmutable' -e 'CarbonInterface' -e 'Date::use' src/boost/docs docs AGENTS.md +``` + +The full type search is required in addition to concrete Carbon-import ledgers: it covers stale default assertions that mention only interfaces or native date types, such as Testbench's default-configuration contract. + +For mutation results, re-read every match and its value flow; a chained immediate comparison/format is correct, while a standalone modifier intended to persist is not. Review PHPStan's parsed full-source result as the AST/type safety net. Do not commit a custom scanner or lint rule for this one migration. + +Review every changed file in the final diff, not just matching lines, and compare every Laravel-derived structural edit against the current local 13.x reference plus the approved Hypervel adaptations. Run `git status --short` and inspect generated/fixture deletions so the final worktree contains only intentional source, tests, documentation, and plan changes. + +### Explicit non-changes + +- no new clock abstraction or service beyond the existing PSR binding; +- no Symfony global clock installation; +- no replacement of Carbon expiry/deadline code with raw integers or `hrtime()`; +- no benchmark gate or permanent performance harness; +- no macro synchronization layer or per-date-class cleanup registry; +- no runtime guard on every callable-produced date; +- no generic DataObject converter registry; +- no `WithMutableDates` attribute without a real consumer; +- no compatibility wrapper for `DateTime::class` or arbitrary non-Carbon DateFactory class handlers; +- no changes to intentional existing monotonic timers and native timestamp protocols; +- no private-package edits in this framework worktree. + +The finished codebase has one canonical immutable Hypervel value class, one explicit mutable counterpart, one configurable Date factory, one PSR clock binding, and one Carbon static-state cleanup path. diff --git a/docs/todo.md b/docs/todo.md index 89997fbb7..b73ca474f 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -21,8 +21,7 @@ ## Framework-wide -- Make `CarbonImmutable` the framework's default date class. Change `DateFactory::DEFAULT_CLASS_NAME` (`src/support/src/DateFactory.php`) from `Carbon::class` to `CarbonImmutable::class` so `now()`, `today()`, and factory-produced date casts return immutable instances; `Date::use(Carbon::class)` remains the opt-out. Mutable dates held on singletons or statics are shared mutable state across coroutines — the bug class the rest of the framework is designed to prevent — and immutable-by-default matches modern PHP practice (Symfony's Clock, Doctrine's immutable types) that Laravel can't adopt only because of its backwards-compatibility burden. Scope: audit the framework files importing mutable `Carbon` and retype those receiving factory-produced values to `CarbonImmutable` or `CarbonInterface`; review ported code relying on in-place mutation; add mutable-date conversion to the Porting Packages modernization list in `AGENTS.md`; update tests. When done, add a Code conventions rule to `AGENTS.md` stating Hypervel defaults to `CarbonImmutable` where Laravel defaults to mutable `Carbon`, and record the divergence in `docs/ai/differences-vs-laravel.md`. - +- Convert the remaining tests that extend `PHPUnit\Framework\TestCase` to `Hypervel\Tests\TestCase` as required by `AGENTS.md`, verifying each file individually under coroutine execution and opting out only when the test explicitly exercises coroutine transitions. - Convert container array access to `make()` across `src/`. About 40 files use `$app['...']` (e.g. `LogManager`, `ViewServiceProvider`, `TranslationServiceProvider`), carried over from upstream Laravel. `offsetGet()` always returns `mixed`, while `make()` has class-string generics phpstan can follow, so the conversion makes static analysis strictly more useful. Approved modernization per the Porting Packages policy in `AGENTS.md`; new code already follows the rule. - Convert untyped `$config->get()` calls across `src/` to the typed getters (`string()`, `integer()`, `float()`, `boolean()`, `array()`) without call-site defaults, for every key that isn't genuinely nullable. Defaults live in the merged config files — declare any key currently defaulted only at a call site in its package's config file as part of the conversion. Typed getters throw `InvalidArgumentException` naming the key on misconfiguration instead of letting a wrong type propagate silently, and give phpstan real return types. Bootstrap code that runs before config merging keeps its call-site defaults. Approved modernization per the Porting Packages policy in `AGENTS.md`; new code already follows the rule. From 9f86184899d8b2869453e9bb85e425150b4406d3 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:25:19 +0000 Subject: [PATCH 33/34] docs: preserve historical date plan context Restore the original mutable Carbon examples and guidance in the array-cache and passkeys plans so those documents remain accurate records of their implementation context. Add concise notes near the top of each plan that identify the later immutable-date change, state the current behavior, and link to the framework-wide CarbonImmutable plan. This keeps past debugging context intact without allowing historical snippets to masquerade as current framework guidance. --- ...rray-cache-coroutine-local-worker-array.md | 32 +++++++++++-------- .../plans/2026-07-01-fortify-passkeys-port.md | 4 ++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md b/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md index 3a45f9d71..77ab7e0c8 100644 --- a/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md +++ b/docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md @@ -1,5 +1,9 @@ # Array Cache Request-Local Store and Worker-Array Store +## Historical status + +This plan predates Hypervel's immutable date default. Its Carbon examples are preserved as implementation history; current cache expiry records use `Hypervel\Support\CarbonImmutable`. See [Make CarbonImmutable the Framework Default](2026-07-22-carbon-immutable-default.md) for the current date architecture. + ## Goal Make Hypervel's `array` cache store behave like Laravel developers expect in a long-lived Swoole worker: data written to the `array` store must be isolated to the current request, job, scheduled task, or other unit of work. @@ -50,7 +54,7 @@ Current `ArrayStore` keeps values and locks on object properties: protected array $storage = []; /** - * @var array + * @var array */ public array $locks = []; ``` @@ -135,7 +139,7 @@ Remove `public array $locks` from the public store surface. `ArrayLock` should c $record = $this->store->getLockRecord($this->name); $this->store->putLockRecord($this->name, [ 'owner' => $this->owner, - 'expiresAt' => $this->seconds === 0 ? null : CarbonImmutable::now()->addSeconds($this->seconds), + 'expiresAt' => $this->seconds === 0 ? null : Carbon::now()->addSeconds($this->seconds), ]); ``` @@ -258,7 +262,7 @@ namespace Hypervel\Cache; use Hypervel\Contracts\Cache\CanFlushLocks; use Hypervel\Contracts\Cache\LockProvider; -use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Carbon; use Hypervel\Support\InteractsWithTime; use RuntimeException; @@ -419,14 +423,14 @@ abstract class AbstractArrayStore extends TaggableStore implements CanFlushLocks /** * Get the lock record for the given name. * - * @return array{owner: ?string, expiresAt: ?CarbonImmutable}|null + * @return array{owner: ?string, expiresAt: ?Carbon}|null */ abstract public function getLockRecord(string $name): ?array; /** * Store the lock record for the given name. * - * @param array{owner: ?string, expiresAt: ?CarbonImmutable} $record + * @param array{owner: ?string, expiresAt: ?Carbon} $record */ abstract public function putLockRecord(string $name, array $record): void; @@ -506,7 +510,7 @@ declare(strict_types=1); namespace Hypervel\Cache; use Hypervel\Context\CoroutineContext; -use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Carbon; class ArrayStore extends AbstractArrayStore { @@ -599,7 +603,7 @@ class ArrayStore extends AbstractArrayStore protected function getLockRecords(): array { - /** @var array $records */ + /** @var array $records */ $records = CoroutineContext::get($this->locksContextKey, []); return $records; @@ -622,7 +626,7 @@ declare(strict_types=1); namespace Hypervel\Cache; -use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Carbon; class WorkerArrayStore extends AbstractArrayStore { @@ -632,7 +636,7 @@ class WorkerArrayStore extends AbstractArrayStore protected array $storage = []; /** - * @var array + * @var array */ protected array $locks = []; @@ -712,7 +716,7 @@ Core changes: public function acquire(): bool { $record = $this->store->getLockRecord($this->name); - $expiration = $record['expiresAt'] ?? CarbonImmutable::now()->addSecond(); + $expiration = $record['expiresAt'] ?? Carbon::now()->addSecond(); if ($record !== null && $expiration->isFuture()) { return false; @@ -721,7 +725,7 @@ public function acquire(): bool // WorkerArrayStore shares this check/write path across coroutines; keep it non-yielding. $this->store->putLockRecord($this->name, [ 'owner' => $this->owner, - 'expiresAt' => $this->seconds === 0 ? null : CarbonImmutable::now()->addSeconds($this->seconds), + 'expiresAt' => $this->seconds === 0 ? null : Carbon::now()->addSeconds($this->seconds), ]); return true; @@ -766,7 +770,7 @@ public function refresh(?int $seconds = null): bool return false; } - $record['expiresAt'] = CarbonImmutable::now()->addSeconds($seconds); + $record['expiresAt'] = Carbon::now()->addSeconds($seconds); $this->store->putLockRecord($this->name, $record); return true; @@ -790,7 +794,7 @@ public function getRemainingLifetime(): ?float return null; } - return (float) CarbonImmutable::now()->diffInSeconds($expiresAt); + return (float) Carbon::now()->diffInSeconds($expiresAt); } ``` @@ -1010,7 +1014,7 @@ public function testSeparateArrayStoreInstancesDoNotShareContextData(): void ```php public function testAllOnlyReturnsCurrentStoreContextData(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + Carbon::setTestNow(Carbon::now()); $first = new ArrayStore; $second = new ArrayStore; diff --git a/docs/plans/2026-07-01-fortify-passkeys-port.md b/docs/plans/2026-07-01-fortify-passkeys-port.md index 2ac8407ab..f7ef0068e 100644 --- a/docs/plans/2026-07-01-fortify-passkeys-port.md +++ b/docs/plans/2026-07-01-fortify-passkeys-port.md @@ -2,6 +2,8 @@ > Superseded note: the Fortify password broker inference described in this historical plan was later removed by `2026-07-06-auth-guard-declared-password-brokers.md`. Guards now declare password brokers with `auth.guards.{guard}.passwords`; `Fortify::passwordBrokerName()` no longer exists. +> Date note: this plan predates Hypervel's immutable date default. Its mutable Carbon guidance is preserved as implementation history; configurable Eloquent date casts now return `Hypervel\Support\CarbonImmutable` by default and should be typed as `CarbonInterface`. See [Make CarbonImmutable the Framework Default](2026-07-22-carbon-immutable-default.md) for the current date architecture. + ## Scope Port Laravel Fortify and Laravel Passkeys Server into the Hypervel components monorepo, with Swoole coroutine safety, worker-lifetime performance, clean Hypervel-native APIs, full tests, and updated Boost documentation. @@ -1130,7 +1132,7 @@ Use `CoroutineContext` only if a ported controller or middleware introduces requ Port the model with strict types and Hypervel namespaces. -Keep date annotations aligned with the actual casts. A `datetime` cast returns Hypervel's configured date class, which defaults to `Hypervel\Support\CarbonImmutable`; type that configurable boundary as `CarbonInterface`. An `immutable_datetime` cast always returns `CarbonImmutable`, even when the application explicitly opts into mutable dates. +Keep date annotations aligned with the actual casts. A `datetime` cast returns Hypervel's configured date class, which defaults to mutable `Hypervel\Support\Carbon`; do not document `CarbonImmutable` unless the model intentionally uses `immutable_datetime`. Use a polymorphic `user` owner relation instead of Laravel's single-model `belongsTo` relation. This avoids a global `Passkeys::userModel()` static and lets one application register passkeys for multiple authenticatable model classes in the same table. From 76e3ff59da166ebacbe41dfb9323eabaf26e31eb Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 23 Jul 2026 04:20:31 +0000 Subject: [PATCH 34/34] build: raise the Carbon minimum to 3.13.1 Require the audited Carbon release from the monorepo and every split package that declares a direct Carbon dependency. The immutable-date implementation is built and tested against Carbon 3.13.1. The previous 3.8.4 floor was not covered by a lowest-dependency suite, so retaining it would claim support for behavior this change did not verify. Record the dependency baseline and its completion check in the implementation plan. --- composer.json | 2 +- docs/plans/2026-07-22-carbon-immutable-default.md | 7 +++++-- src/auth/composer.json | 2 +- src/bus/composer.json | 4 ++-- src/database/composer.json | 2 +- src/foundation/composer.json | 2 +- src/grpc/composer.json | 2 +- src/horizon/composer.json | 2 +- src/http/composer.json | 2 +- src/jwt/composer.json | 2 +- src/nested-set/composer.json | 4 ++-- src/queue/composer.json | 2 +- src/sanctum/composer.json | 4 ++-- src/support/composer.json | 2 +- 14 files changed, 21 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 98d7bb2b7..b7ebda6ed 100644 --- a/composer.json +++ b/composer.json @@ -173,7 +173,7 @@ "league/flysystem-read-only": "^3.25.1", "league/uri": "^7.5.1", "monolog/monolog": "^3.1", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "nikic/php-parser": "^5.7", "nunomaduro/termwind": "^2.0", "nyholm/psr7": "^1.0", diff --git a/docs/plans/2026-07-22-carbon-immutable-default.md b/docs/plans/2026-07-22-carbon-immutable-default.md index 4b46ff251..860f0dd90 100644 --- a/docs/plans/2026-07-22-carbon-immutable-default.md +++ b/docs/plans/2026-07-22-carbon-immutable-default.md @@ -19,6 +19,7 @@ Date::use(Carbon::class); Complete the change across the whole components repository: +- require Carbon `^3.13.1` in the monorepo and every split package that depends on it; - add the immutable Hypervel class without duplicating Hypervel's Carbon additions; - ensure mutable/immutable conversions stay inside the Hypervel class pair; - tighten the date-factory handlers to the contracts the framework already advertises; @@ -67,6 +68,7 @@ Every mutation-risk file was read in context. The complete direct-construction l | Finding | Evidence | Decision | |---|---|---| | Factory and direct construction are independent surfaces | `DateFactory::DEFAULT_CLASS_NAME` affects `Date` and helpers, but not `Hypervel\Support\Carbon::now()` | Flip the factory and separately migrate every direct site | +| The declared Carbon floor must match the audited dependency | The local development install uses 3.13.1, while the root and split-package manifests still allowed 3.8.4 and the repository has no lowest-dependency test suite | Require `^3.13.1` everywhere rather than claiming support for an untested older patch | | Hypervel needs its own immutable class | The mutable subclass adds Conditionable, Dumpable, `createFromId`, `plus`, and `minus`; base immutable lacks them | Add `Hypervel\Support\CarbonImmutable` and share only those additions through one trait | | Carbon conversions lose Hypervel behavior today | Carbon 3.13.1's `Mutability` trait casts to `Carbon\Carbon` / `Carbon\CarbonImmutable`; runtime probes confirm the subclass is dropped | Override both directions on the Hypervel pair | | Carbon's immutable magic modifier metadata erases subclasses in static analysis | The base immutable class hardcodes `CarbonImmutable` for seven aliases used across exact Hypervel-class boundaries, although runtime probes confirm every operation preserves the subclass | Override those seven inherited magic annotations with `static` on the Hypervel immutable owner; leave unlisted parent metadata visible so future exact-boundary gaps fail visibly | @@ -85,9 +87,9 @@ Every mutation-risk file was read in context. The complete direct-construction l ## Backing research -### Installed Carbon behavior +### Required Carbon behavior -The repository installs Carbon `3.13.1` (`2937ad3d1d2c506fd2bc97d571438a95641f44e2`). The load-bearing installed APIs are: +The repository requires Carbon `^3.13.1`. The audited development install is `3.13.1` (`2937ad3d1d2c506fd2bc97d571438a95641f44e2`). The load-bearing APIs are: ```php // Carbon\Traits\Cast @@ -1071,6 +1073,7 @@ php -f src/facade-documenter/facade.php -- --lint Hypervel\\Support\\Facades\\Da Repeat broad searches over all of `src/`, `tests/`, active docs, stubs, dogfood, and types. Completion requires: +- every root and split-package Carbon constraint requires `^3.13.1`; - no default-output metadata claiming `Hypervel\Support\Carbon`; - no factory-produced value assigned to a concrete mutable-only property, parameter, or return; - no concrete `?DateTime` return in the four corrected methods; diff --git a/src/auth/composer.json b/src/auth/composer.json index 066ac3732..ad5033eb7 100644 --- a/src/auth/composer.json +++ b/src/auth/composer.json @@ -24,7 +24,7 @@ ], "require": { "php": "^8.4", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "hypervel/collections": "^0.4", "hypervel/config": "^0.4", "hypervel/container": "^0.4", diff --git a/src/bus/composer.json b/src/bus/composer.json index 9879369fd..3216d16ac 100644 --- a/src/bus/composer.json +++ b/src/bus/composer.json @@ -26,7 +26,7 @@ "require": { "php": "^8.4", "laravel/serializable-closure": "^2.0.10", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", @@ -58,4 +58,4 @@ "sort-packages": true }, "minimum-stability": "dev" -} \ No newline at end of file +} diff --git a/src/database/composer.json b/src/database/composer.json index 57839af97..0490a121e 100644 --- a/src/database/composer.json +++ b/src/database/composer.json @@ -32,7 +32,7 @@ "require": { "php": "^8.4", "brick/math": "^0.16", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "symfony/console": "^8.1", "symfony/polyfill-php86": "^1.36", "symfony/process": "^8.1", diff --git a/src/foundation/composer.json b/src/foundation/composer.json index d79726a6f..821402e2f 100644 --- a/src/foundation/composer.json +++ b/src/foundation/composer.json @@ -26,7 +26,7 @@ "php": "^8.4", "composer-runtime-api": "^2.2", "laravel/serializable-closure": "^2.0.10", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "psr/clock": "^1.0", "symfony/console": "^8.1", "symfony/error-handler": "^8.1", diff --git a/src/grpc/composer.json b/src/grpc/composer.json index fb4555d6a..71a503c15 100644 --- a/src/grpc/composer.json +++ b/src/grpc/composer.json @@ -48,7 +48,7 @@ "hypervel/routing": "^0.4", "hypervel/server": "^0.4", "hypervel/support": "^0.4", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "symfony/console": "^8.1", "symfony/http-foundation": "^8.1", "symfony/http-kernel": "^8.1" diff --git a/src/horizon/composer.json b/src/horizon/composer.json index 131b7d7d0..a370613a7 100644 --- a/src/horizon/composer.json +++ b/src/horizon/composer.json @@ -24,7 +24,7 @@ ], "require": { "php": "^8.4", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "hypervel/broadcasting": "^0.4", "hypervel/bus": "^0.4", "hypervel/cache": "^0.4", diff --git a/src/http/composer.json b/src/http/composer.json index 1bebe0f78..633ec33fd 100644 --- a/src/http/composer.json +++ b/src/http/composer.json @@ -33,7 +33,7 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.15.1", "guzzlehttp/uri-template": "^1.0", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "symfony/http-foundation": "^8.1", "symfony/http-kernel": "^8.1", "symfony/mime": "^8.1", diff --git a/src/jwt/composer.json b/src/jwt/composer.json index 59dbc7332..6d32edd9e 100644 --- a/src/jwt/composer.json +++ b/src/jwt/composer.json @@ -25,7 +25,7 @@ "require": { "php": "^8.4", "lcobucci/jwt": "^5.0", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "psr/simple-cache": "^3.0", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", diff --git a/src/nested-set/composer.json b/src/nested-set/composer.json index 146a265b4..67d042f99 100644 --- a/src/nested-set/composer.json +++ b/src/nested-set/composer.json @@ -30,7 +30,7 @@ }, "require": { "php": "^8.4", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/database": "^0.4", @@ -44,4 +44,4 @@ "dev-main": "0.4-dev" } } -} \ No newline at end of file +} diff --git a/src/queue/composer.json b/src/queue/composer.json index b29ac0ad2..a6365ab75 100644 --- a/src/queue/composer.json +++ b/src/queue/composer.json @@ -25,7 +25,7 @@ "require": { "php": "^8.4", "laravel/serializable-closure": "^2.0.10", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "symfony/console": "^8.1", "symfony/process": "^8.1", "hypervel/bus": "^0.4", diff --git a/src/sanctum/composer.json b/src/sanctum/composer.json index 3fa4c0e7a..13f91af59 100644 --- a/src/sanctum/composer.json +++ b/src/sanctum/composer.json @@ -24,7 +24,7 @@ ], "require": { "php": "^8.4", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", @@ -57,4 +57,4 @@ "sort-packages": true }, "minimum-stability": "dev" -} \ No newline at end of file +} diff --git a/src/support/composer.json b/src/support/composer.json index fdff28a96..3b78b3c82 100644 --- a/src/support/composer.json +++ b/src/support/composer.json @@ -27,7 +27,7 @@ "doctrine/inflector": "^2.0.5", "laravel/serializable-closure": "^2.0.10", "league/uri": "^7.5.1", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^3.13.1", "phpoption/phpoption": "^1.9", "symfony/uid": "^8.1", "vlucas/phpdotenv": "^5.6.1",