From 3c923c1a4d2638adfa5ef2c3a979bfa22a33e7d9 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:58:37 +0000 Subject: [PATCH] Add Date factory support for model date handling Override Hyperf's date handling methods to use the Date facade instead of hardcoding Carbon directly. This allows applications to configure a custom date class (e.g., CarbonImmutable) via Date::use() and have it respected throughout the framework. Changes: - Add asDate() and asDateTime() methods to HasAttributes trait - Create HasTimestamps trait with freshTimestamp() using Date::now() - Update Model, Pivot, and MorphPivot to use the new HasTimestamps trait - Add comprehensive test coverage for DateFactory integration --- .../Eloquent/Concerns/HasAttributes.php | 64 ++++ .../Eloquent/Concerns/HasTimestamps.php | 28 ++ src/core/src/Database/Eloquent/Model.php | 2 + .../Eloquent/Relations/MorphPivot.php | 2 + .../src/Database/Eloquent/Relations/Pivot.php | 2 + .../Eloquent/Concerns/DateFactoryTest.php | 351 ++++++++++++++++++ 6 files changed, 449 insertions(+) create mode 100644 src/core/src/Database/Eloquent/Concerns/HasTimestamps.php create mode 100644 tests/Core/Database/Eloquent/Concerns/DateFactoryTest.php diff --git a/src/core/src/Database/Eloquent/Concerns/HasAttributes.php b/src/core/src/Database/Eloquent/Concerns/HasAttributes.php index ebe428fc..b6c04518 100644 --- a/src/core/src/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/core/src/Database/Eloquent/Concerns/HasAttributes.php @@ -4,9 +4,13 @@ namespace Hypervel\Database\Eloquent\Concerns; +use Carbon\Carbon; +use Carbon\CarbonInterface; +use DateTimeInterface; use Hyperf\Contract\Castable; use Hyperf\Contract\CastsAttributes; use Hyperf\Contract\CastsInboundAttributes; +use Hypervel\Support\Facades\Date; trait HasAttributes { @@ -76,4 +80,64 @@ protected function casts(): array { return []; } + + /** + * Return a timestamp as DateTime object with time set to 00:00:00. + * + * Uses the Date facade to respect any custom date class configured + * via Date::use() (e.g., CarbonImmutable). + */ + protected function asDate(mixed $value): CarbonInterface + { + return $this->asDateTime($value)->startOfDay(); + } + + /** + * Return a timestamp as DateTime object. + * + * Uses the Date facade to respect any custom date class configured + * via Date::use() (e.g., CarbonImmutable). + */ + protected function asDateTime(mixed $value): CarbonInterface + { + // 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 Date::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 Date::parse( + $value->format('Y-m-d H:i:s.u'), + $value->getTimezone() + ); + } + + // 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 Date::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 ($this->isStandardDateFormat($value)) { + return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay()); + } + + $format = $this->getDateFormat(); + + // 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. + $date = Date::createFromFormat($format, $value); + + return $date ?: Date::parse($value); + } } diff --git a/src/core/src/Database/Eloquent/Concerns/HasTimestamps.php b/src/core/src/Database/Eloquent/Concerns/HasTimestamps.php new file mode 100644 index 00000000..d8e16682 --- /dev/null +++ b/src/core/src/Database/Eloquent/Concerns/HasTimestamps.php @@ -0,0 +1,28 @@ +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 + { + Date::use(CarbonImmutable::class); + + $model = new DateFactoryTestModel(); + $model->setRawAttributes(['published_at' => new DateTimeImmutable('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 testAsDateTimeReturnsCarbonImmutableFromTimestamp(): void + { + 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); + } + + public function testAsDateTimeReturnsCarbonImmutableFromStandardDateFormat(): 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); + + $model = new DateFactoryDateCastModel(); + $model->setRawAttributes(['event_date' => '2024-01-15']); + + $date = $model->event_date; + + $this->assertInstanceOf(CarbonImmutable::class, $date); + // Date cast should return start of day + $this->assertSame('00:00:00', $date->format('H:i:s')); + } + + public function testDatetimeCastReturnsCarbonImmutableWhenConfigured(): void + { + Date::use(CarbonImmutable::class); + + $model = new DateFactoryDateCastModel(); + $model->setRawAttributes(['event_datetime' => '2024-01-15 10:30:00']); + + $date = $model->event_datetime; + + $this->assertInstanceOf(CarbonImmutable::class, $date); + $this->assertSame('2024-01-15 10:30:00', $date->format('Y-m-d H:i:s')); + } + + // ========================================== + // Pivot Model Tests + // ========================================== + + public function testPivotFreshTimestampReturnsCarbonImmutableWhenConfigured(): void + { + Date::use(CarbonImmutable::class); + + $pivot = new DateFactoryTestPivot(); + $timestamp = $pivot->freshTimestamp(); + + $this->assertInstanceOf(CarbonImmutable::class, $timestamp); + } + + public function testMorphPivotFreshTimestampReturnsCarbonImmutableWhenConfigured(): void + { + Date::use(CarbonImmutable::class); + + $pivot = new DateFactoryTestMorphPivot(); + $timestamp = $pivot->freshTimestamp(); + + $this->assertInstanceOf(CarbonImmutable::class, $timestamp); + } + + // ========================================== + // 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'; + + protected array $dates = ['published_at']; +} + +class DateFactoryDateCastModel extends Model +{ + protected ?string $table = 'test_models'; + + protected array $casts = [ + 'event_date' => 'date', + 'event_datetime' => 'datetime', + ]; +} + +class DateFactoryMultipleDatesModel extends Model +{ + protected ?string $table = 'test_models'; + + protected array $dates = ['published_at']; +} + +class DateFactoryTestPivot extends Pivot +{ + protected ?string $table = 'test_pivots'; +} + +class DateFactoryTestMorphPivot extends MorphPivot +{ + protected ?string $table = 'test_morph_pivots'; +}