diff --git a/src/Illuminate/Contracts/Database/Eloquent/Castable.php b/src/Illuminate/Contracts/Database/Eloquent/Castable.php new file mode 100644 index 000000000000..e63cc547802b --- /dev/null +++ b/src/Illuminate/Contracts/Database/Eloquent/Castable.php @@ -0,0 +1,24 @@ +isCustomCastable($key)) { + return $this->fromCustomCastable($key, $value); + } + if (is_null($value)) { return $value; } @@ -523,15 +541,17 @@ protected function castAttribute($key, $value) */ protected function getCastType($key) { - if ($this->isCustomDateTimeCast($this->getCasts()[$key])) { + $cast = $this->getCast($key); + + if ($this->isCustomDateTimeCast($cast)) { return 'custom_datetime'; } - if ($this->isDecimalCast($this->getCasts()[$key])) { + if ($this->isDecimalCast($cast)) { return 'decimal'; } - return trim(strtolower($this->getCasts()[$key])); + return trim(strtolower($cast)); } /** @@ -557,12 +577,28 @@ protected function isDecimalCast($cast) return strncmp($cast, 'decimal:', 8) === 0; } + /** + * Is the checked value a custom Cast. + * + * @param string $key + * @return bool + */ + protected function isCustomCastable($key) + { + if ($cast = $this->getCast($key)) { + return is_subclass_of($cast, Castable::class); + } + + return false; + } + /** * Set a given attribute on the model. * * @param string $key * @param mixed $value * @return mixed + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function setAttribute($key, $value) { @@ -580,6 +616,12 @@ public function setAttribute($key, $value) $value = $this->fromDateTime($value); } + // If the attribute is specified as Cast, we will convert it according to + // the method specified in it. + if ($this->isCustomCastable($key)) { + $value = $this->toCustomCastable($key, $value); + } + if ($this->isJsonCastable($key) && ! is_null($value)) { $value = $this->castAttributeAsJson($key, $value); } @@ -929,6 +971,17 @@ public function getCasts() return $this->casts; } + /** + * Get the cast from casts array. + * + * @param string $key + * @return string|null + */ + public function getCast($key) + { + return $this->getCasts()[$key] ?? null; + } + /** * Determine whether a value is Date / DateTime castable for inbound manipulation. * @@ -951,6 +1004,53 @@ protected function isJsonCastable($key) return $this->hasCast($key, ['array', 'json', 'object', 'collection']); } + /** + * Getting the execution result from a user Cast object. + * + * @param string $key + * @param mixed $value + * @return mixed + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function fromCustomCastable($key, $value = null) + { + return $this + ->normalizeCastToCallable($key) + ->fromDatabase($key, $value); + } + + /** + * Converting a value by custom Cast. + * + * @param string $key + * @param mixed $value + * @return mixed + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function toCustomCastable($key, $value = null) + { + return $this + ->normalizeCastToCallable($key) + ->toDatabase($key, $value); + } + + /** + * Getting a custom cast instance. + * + * @param string $key + * @return \Illuminate\Contracts\Database\Eloquent\Castable + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function normalizeCastToCallable($key) + { + if (! isset(static::$castsCache[$key])) { + static::$castsCache[$key] = Container::getInstance() + ->make($this->getCast($key)); + } + + return static::$castsCache[$key]; + } + /** * Get all of the current attributes on the model. * @@ -996,6 +1096,7 @@ public function getOriginal($key = null, $default = null) * * @param array|mixed $attributes * @return array + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function only($attributes) { @@ -1052,6 +1153,7 @@ public function syncOriginalAttributes($attributes) * Sync the changed attributes. * * @return $this + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function syncChanges() { @@ -1065,6 +1167,7 @@ public function syncChanges() * * @param array|string|null $attributes * @return bool + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function isDirty($attributes = null) { @@ -1078,6 +1181,7 @@ public function isDirty($attributes = null) * * @param array|string|null $attributes * @return bool + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function isClean($attributes = null) { @@ -1129,6 +1233,7 @@ protected function hasChanges($changes, $attributes = null) * Get the attributes that have been changed since last sync. * * @return array + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function getDirty() { @@ -1159,6 +1264,7 @@ public function getChanges() * @param string $key * @param mixed $current * @return bool + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function originalIsEquivalent($key, $current) { diff --git a/src/Illuminate/Foundation/Console/CastMakeCommand.php b/src/Illuminate/Foundation/Console/CastMakeCommand.php new file mode 100644 index 000000000000..6728ecf0c71c --- /dev/null +++ b/src/Illuminate/Foundation/Console/CastMakeCommand.php @@ -0,0 +1,51 @@ + 'command.mail.make', 'MiddlewareMake' => 'command.middleware.make', 'ModelMake' => 'command.model.make', + 'CastMake' => 'command.cast.make', 'NotificationMake' => 'command.notification.make', 'NotificationTable' => 'command.notification.table', 'ObserverMake' => 'command.observer.make', @@ -486,6 +488,18 @@ protected function registerModelMakeCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerCastMakeCommand() + { + $this->app->singleton('command.cast.make', function ($app) { + return new CastMakeCommand($app['files']); + }); + } + /** * Register the command. * diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php new file mode 100644 index 000000000000..b78549a88553 --- /dev/null +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -0,0 +1,141 @@ + ['f', 'o', 'o', 'b', 'a', 'r'], + 'field_2' => 20, + 'field_3' => '08:19:12', + 'field_4' => null, + 'field_5' => null, + ]); + + $this->assertSame(['f', 'o', 'o', 'b', 'a', 'r'], $item->toArray()['field_1']); + + $this->assertSame(0.2, $item->toArray()['field_2']); + + $this->assertInstanceOf(DateTimeInterface::class, $item->toArray()['field_3']); + + $this->assertSame('08:19:12', $item->toArray()['field_3']->format('H:i:s')); + + $this->assertSame(null, $item->toArray()['field_4']); + + $this->assertSame('foo', $item->toArray()['field_5']); + } + + protected function setUp(): void + { + parent::setUp(); + + Schema::create('test_model1', function (Blueprint $table) { + $table->increments('id'); + $table->string('field_1')->nullable(); + $table->integer('field_2')->nullable(); + $table->time('field_3')->nullable(); + $table->string('field_4')->nullable(); + $table->string('field_5')->nullable(); + }); + } +} + +class TestModel extends Model +{ + public $table = 'test_model1'; + + public $timestamps = false; + + public $casts = [ + 'field_1' => StringCast::class, + 'field_2' => NumberCast::class, + 'field_3' => TimeCast::class, + 'field_4' => NullCast::class, + 'field_5' => NullChangedCast::class, + ]; + + protected $guarded = ['id']; +} + +class TimeCast implements Castable +{ + /** + * @param mixed $value + * @return DateTime + * @throws \Exception + */ + public function fromDatabase($key, $value = null) + { + return new DateTime($value); + } + + public function toDatabase($key, $value = null) + { + return is_numeric($value) + ? DateTime::createFromFormat('H:i:s', $value)->format('H:i:s') + : $value; + } +} + +class StringCast implements Castable +{ + public function fromDatabase($key, $value = null) + { + return str_split($value); + } + + public function toDatabase($key, $value = null) + { + return is_array($value) + ? implode('', $value) + : $value; + } +} + +class NumberCast implements Castable +{ + public function fromDatabase($key, $value = null) + { + return $value / 100; + } + + public function toDatabase($key, $value = null) + { + return $value; + } +} + +class NullCast implements Castable +{ + public function fromDatabase($key, $value = null) + { + return $value; + } + + public function toDatabase($key, $value = null) + { + return $value; + } +} + +class NullChangedCast implements Castable +{ + public function fromDatabase($key, $value = null) + { + return 'foo'; + } + + public function toDatabase($key, $value = null) + { + return $value; + } +}