diff --git a/.gitignore b/.gitignore index 8b7ef35..a2dfdd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /vendor +/.idea composer.lock +.phpunit.result.cache diff --git a/README.md b/README.md index 711fc50..057ddf9 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ $model = new TestModel(); $model->carbon_interval = now()->subHours(3)->diffAsCarbonInterval(); -$model->save(); // Saved as `P3H` +$model->save(); // Saved as `PT3H` $model->fresh(); $model->carbon_interval; // Instance of `CarbonInterval` diff --git a/composer.json b/composer.json index 8a1238c..9797007 100644 --- a/composer.json +++ b/composer.json @@ -11,13 +11,14 @@ ], "homepage": "https://github.com/atymic/laravel-dateinterval-cast", "require": { - "php": "^7.3", - "laravel/framework": "^7.0 || ^8.0" + "php": "^7.3|^8.0.2|^8.1|^8.2|^8.3|^8.4", + "laravel/framework": "^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0" }, "require-dev": { "phpunit/phpunit": "^9.0", - "orchestra/testbench": "^5.0 || ^6.0", - "phpstan/phpstan": "^0.12.9" + "orchestra/testbench": "^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpstan/phpstan": "^0.12.9", + "livewire/livewire": "^3" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Cast/CarbonIntervalCast.php b/src/Cast/CarbonIntervalCast.php index 0755436..4ce97d9 100644 --- a/src/Cast/CarbonIntervalCast.php +++ b/src/Cast/CarbonIntervalCast.php @@ -20,6 +20,9 @@ class CarbonIntervalCast extends DateIntervalCast */ public function get($model, string $key, $value, array $attributes) { + if (is_null($value)) { + return; + } try { return CarbonInterval::create($value); } catch (\Exception $e) { diff --git a/src/Cast/DateIntervalCast.php b/src/Cast/DateIntervalCast.php index d6a2e3c..d91e9c2 100644 --- a/src/Cast/DateIntervalCast.php +++ b/src/Cast/DateIntervalCast.php @@ -21,6 +21,9 @@ class DateIntervalCast implements CastsAttributes */ public function get($model, string $key, $value, array $attributes) { + if (is_null($value)) { + return; + } try { return new \DateInterval($value); } catch (\Exception $e) { @@ -39,9 +42,11 @@ public function get($model, string $key, $value, array $attributes) */ public function set($model, string $key, $value, array $attributes) { + if (is_null($value)) { + return; + } try { $value = is_string($value) ? CarbonInterval::create($value) : $value; - return [$key => CarbonInterval::getDateIntervalSpec($value)]; } catch (\Exception $e) { throw InvalidIsoDuration::make($value, $e); diff --git a/src/Synth/CarbonIntervalSynth.php b/src/Synth/CarbonIntervalSynth.php new file mode 100644 index 0000000..812a181 --- /dev/null +++ b/src/Synth/CarbonIntervalSynth.php @@ -0,0 +1,66 @@ + \DateInterval::class, + 'carbon' => CarbonInterval::class, + ]; + + public static $key = 'cbnint'; + + static function match($target) { + foreach (static::$types as $type => $class) { + if ($target instanceof $class) return true; + } + + return false; + } + + function dehydrate($target) { + return [ + $target instanceof CarbinInterval ? $target->spec() : $this->getDateIntervalSpec($target), + ['type' => array_search(get_class($target), static::$types)], + ]; + } + + function hydrate($value, $meta) { + return new static::$types[$meta['type']]($value); + } + + protected function getDateIntervalSpec(\DateInterval $interval): string + { + $spec = 'P'; + + if ($interval->y) { + $spec .= $interval->y . 'Y'; + } + if ($interval->m) { + $spec .= $interval->m . 'M'; + } + if ($interval->d) { + $spec .= $interval->d . 'D'; + } + + if ($interval->h || $interval->i || $interval->s) { + $spec .= 'T'; + if ($interval->h) { + $spec .= $interval->h . 'H'; + } + if ($interval->i) { + $spec .= $interval->i . 'M'; + } + if ($interval->s) { + $spec .= $interval->s . 'S'; + } + } + + return $spec; + } +} diff --git a/tests/CastsIntervalsTest.php b/tests/CastsIntervalsTest.php index 3473eef..5ccda31 100644 --- a/tests/CastsIntervalsTest.php +++ b/tests/CastsIntervalsTest.php @@ -28,6 +28,17 @@ public function testDateIntervalCast() $this->assertInstanceOf(\DateInterval::class, $model->date_interval); $this->assertDatabaseHas('test', ['id' => $model->id, 'date_interval' => 'P1D']); } + public function testForNullableColumnsDateIntervalCastIsSkippedIfColumnValueIsNull() + { + $model = new TestEloquentModelWithCustomCasts(); + $model->date_interval = null; + $this->assertNotInstanceOf(\DateInterval::class, $model->date_interval); + $this->assertNull($model->getAttributes()['date_interval']); + $model->save(); + $model->fresh(); + $this->assertNotInstanceOf(\DateInterval::class, $model->date_interval); + $this->assertDatabaseHas('test', ['id' => $model->id, 'date_interval' => null]); + } public function testCarbonIntervalCast() { @@ -43,6 +54,17 @@ public function testCarbonIntervalCast() $this->assertInstanceOf(CarbonInterval::class, $model->carbon_interval); $this->assertDatabaseHas('test', ['id' => $model->id, 'carbon_interval' => 'P4D']); } + public function testForNullableColumnsCarbonIntervalCastIsSkippedIfColumnValueIsNull() + { + $model = new TestEloquentModelWithCustomCasts(); + $model->carbon_interval = null; + $this->assertNotInstanceOf(CarbonInterval::class, $model->carbon_interval); + $this->assertSame(null, $model->getAttributes()['carbon_interval']); + $model->save(); + $model->fresh(); + $this->assertNotInstanceOf(CarbonInterval::class, $model->carbon_interval); + $this->assertDatabaseHas('test', ['id' => $model->id, 'carbon_interval' => null]); + } public function testThrowsExceptionOnInvalidDateInterval() { diff --git a/tests/Synth/CarbonIntervalSynthTest.php b/tests/Synth/CarbonIntervalSynthTest.php new file mode 100644 index 0000000..f532208 --- /dev/null +++ b/tests/Synth/CarbonIntervalSynthTest.php @@ -0,0 +1,69 @@ +createPartialMock(CarbonIntervalSynth::class, ['__construct']); + + $carbonInterval = CarbonInterval::days(2); + $dateInterval = new \DateInterval('P2D'); + + $this->assertTrue($synth::match($carbonInterval), "Should match CarbonInterval"); + $this->assertTrue($synth::match($dateInterval), "Should match DateInterval"); + $this->assertFalse($synth::match('Not an interval'), "Should not match non-interval values"); + } + + public function testDehydrate() + { + $synth = $this->createPartialMock(CarbonIntervalSynth::class, ['__construct']); + + $carbonInterval = CarbonInterval::days(2); + $expectedCarbonDehydration = [ + 'P2D', + ['type' => 'carbon'], + ]; + + $this->assertEquals( + $expectedCarbonDehydration, + $synth->dehydrate($carbonInterval), + "Dehydration of CarbonInterval should produce expected output" + ); + + $dateInterval = new \DateInterval('P2D'); + $expectedDateDehydration = [ + 'P2D', + ['type' => 'native'], + ]; + + $this->assertEquals( + $expectedDateDehydration, + $synth->dehydrate($dateInterval), + "Dehydration of DateInterval should produce expected output" + ); + } + + public function testHydrate() + { + $synth = $this->createPartialMock(CarbonIntervalSynth::class, ['__construct']); + + $carbonMeta = ['type' => 'carbon']; + $carbonValue = 'P2D'; + $hydratedCarbon = $synth->hydrate($carbonValue, $carbonMeta); + + $this->assertInstanceOf(CarbonInterval::class, $hydratedCarbon, "Hydrated object should be an instance of CarbonInterval"); + $this->assertEquals(CarbonInterval::days(2), $hydratedCarbon, "Hydrated CarbonInterval should match the expected interval"); + + $dateMeta = ['type' => 'native']; + $dateValue = 'P2D'; + $hydratedDate = $synth->hydrate($dateValue, $dateMeta); + + $this->assertInstanceOf(\DateInterval::class, $hydratedDate, "Hydrated object should be an instance of DateInterval"); + $this->assertEquals(new \DateInterval('P2D'), $hydratedDate, "Hydrated DateInterval should match the expected interval"); + } +}