8000 [7.x] Custom Cast Types + Object / Value Object Casts by taylorotwell · Pull Request #31035 · laravel/framework · GitHub
[go: up one dir, main page]

Skip to content

[7.x] Custom Cast Types + Object / Value Object Casts #31035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 5, 2020
Next Next commit
add custom cast support using classes - first pass
  • Loading branch information
taylorotwell committed Jan 4, 2020
commit 7befbb6020d027d84f0bbd6b8b5bf39c71d5b70d
149 changes: 137 additions & 12 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@ trait HasAttributes
*/
protected $casts = [];

/**
* The attributes that have been cast using custom classes.
*
* @var array
*/
protected $classCastCache = [];

/**
* The built-in, primitive cast types supported by Eloquent.
*
* @var array
*/
protected static $primitiveCastTypes = [
'array',
'bool',
'boolean',
'collection',
'custom_datetime',
'date',
'datetime',
'decimal',
'double',
'float',
'int',
'integer',
'json',
'object',
'real',
'string',
'timestamp',
];

/**
* The attributes that should be mutated to dates.
*
Expand Down Expand Up @@ -173,7 +205,9 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated
protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
{
foreach ($this->getCasts() as $key => $value) {
if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
if (! array_key_exists($key, $attributes) ||
in_array($key, $mutatedAttributes) ||
$this->isClassCastable($key)) {
continue;
}

Expand Down Expand Up @@ -211,7 +245,7 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
*/
protected function getArrayableAttributes()
{
return $this->getArrayableItems($this->attributes);
return $this->getArrayableItems($this->getAttributes());
}

/**
Expand Down Expand Up @@ -318,8 +352,9 @@ public function getAttribute($key)
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if (array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key)) {
if (array_key_exists($key, $this->getAttributes()) ||
$this->hasGetMutator($key) ||
$this->isClassCastable($key)) {
return $this->getAttributeValue($key);
}

Expand Down Expand Up @@ -352,7 +387,7 @@ public function getAttributeValue($key)
*/
protected function getAttributeFromArray($key)
{
return $this->attributes[$key] ?? null;
return $this->getAttributes()[$key] ?? null;
}

/**
Expand Down Expand Up @@ -439,7 +474,9 @@ protected function mutateAttribute($key, $value)
*/
protected function mutateAttributeForArray($key, $value)
{
$value = $this->mutateAttribute($key, $value);
$value = $this->isClassCastable($key)
? $this->castUsingClass($key)
: $this->mutateAttribute($key, $value);

return $value instanceof Arrayable ? $value->toArray() : $value;
}
Expand All @@ -453,11 +490,13 @@ protected function mutateAttributeForArray($key, $value)
*/
protected function castAttribute($key, $value)
{
if (is_null($value)) {
$castType = $this->getCastType($key);

if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
return $value;
}

switch ($this->getCastType($key)) {
switch ($castType) {
case 'int':
case 'integer':
return (int) $value;
Expand Down Expand Up @@ -486,11 +525,32 @@ protected function castAttribute($key, $value)
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
default:
return $value;
}

if ($this->isClassCastable($key)) {
return $this->castUsingClass($key);
}

return $value;
}

/**
* Cast the given attribute using a custom cast class.
*
* @param string $key
* @return mixed
*/
protected function castUsingClass($key)
{
if (isset($this->classCastCache[$key])) {
return $this->classCastCache[$key];
} else {
return $this->classCastCache[$key] = forward_static_call(
[$this->getCasts()[$key], 'fromModelAttributes'], $this, $key, $this->attributes
);
}
}

/**
* Get the type of cast for a model attribute.
*
Expand Down Expand Up @@ -556,6 +616,12 @@ public function setAttribute($key, $value)
$value = $this->fromDateTime($value);
}

if ($this->isClassCastable($key)) {
$this->setClassCastableAttribute($key, $value);

return $this;
}

if ($this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}
Expand Down Expand Up @@ -625,6 +691,26 @@ public function fillJsonAttribute($key, $value)
return $this;
}

/**
* Set the value of a class castable attribute.
*
* @param string $key
* @param mixed $value
* @return void
*/
protected function setClassCastableAttribute($key, $value)
{
if (is_null($value)) {
$this->attributes = array_merge($this->attributes, array_map(
function () { return null; }, $this->castToClass($key)->toModelAttributes($this)
));

unset($this->classCastCache[$key]);
} else {
$this->classCastCache[$key] = $value;
}
}

/**
* Get an array attribute with the given key and value set.
*
Expand Down Expand Up @@ -926,13 +1012,50 @@ protected function isJsonCastable($key)
return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
}

/**
* Determine if the given key is cast using a custom class.
*
* @param string $key
* @return bool
*/
protected function isClassCastable($key)
{
if (! array_key_exists($key, $this->getCasts())) {
return false;
}

$class = $this->getCasts()[$key];

return class_exists($class) && ! in_array($class, static::$primitiveCastTypes);
}

/**
* Merge the cast class attributes back into the model.
*
* @return void
*/
protected function mergeAttributesFromClassCasts()
{
foreach ($this->classCastCache as $key => $value) {
$this->attributes = array_merge(
$this->attributes,
forward_static_call(
[$this->getCasts()[$key], 'toModelAttributes'],
$this, $key, $value, $this->attributes
)
);
}
}

/**
* Get all of the current attributes on the model.
*
* @return array
*/
public function getAttributes()
{
$this->mergeAttributesFromClassCasts();

return $this->attributes;
}

Expand Down Expand Up @@ -998,7 +1121,7 @@ public function only($attributes)
*/
public function syncOriginal()
{
$this->original = $this->attributes;
$this->original = $this->getAttributes();

return $this;
}
Expand All @@ -1024,8 +1147,10 @@ public function syncOriginalAttributes($attributes)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();

$modelAttributes = $this->getAttributes();

foreach ($attributes as $attribute) {
$this->original[$attribute] = $this->attributes[$attribute];
$this->original[$attribute] = $modelAttributes[$attribute];
}

return $this;
Expand Down
18 changes: 17 additions & 1 deletion src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ public function push()
*/
public function save(array $options = [])
{
$this->mergeAttributesFromClassCasts();

$query = $this->newModelQuery();

// If the "saving" event returns false we'll bail out of the save and return
Expand Down Expand Up @@ -905,6 +907,8 @@ public static function destroy($ids)
*/
public function delete()
{
$this->mergeAttributesFromClassCasts();

if (is_null($this->getKeyName())) {
throw new Exception('No primary key defined on model.');
}
Expand Down Expand Up @@ -1194,7 +1198,7 @@ public function replicate(array $except = null)
];

$attributes = Arr::except(
$this->attributes, $except ? array_unique(array_merge($except, $defaults)) : $defaults
$this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults
);

return tap(new static, function ($instance) use ($attributes) {
Expand Down Expand Up @@ -1676,6 +1680,18 @@ public function __toString()
return $this->toJson();
}

/**
* Prepare the object for serialization.
*
* @return array
*/
public function __sleep()
{
$this->mergeAttributesFromClassCasts();

return array_keys(get_object_vars($this));
}

/**
* When a model is being unserialized, check if it needs to be booted.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

/**
* @group integration
*/
class DatabaseEloquentModelCustomCastingTest extends DatabaseTestCase
{
public function testBasicCustomCasting()
{
$model = new TestEloquentModelWithCustomCast;
$model->encrypted = 'taylor';

$this->assertEquals('taylor', $model->encrypted);
$this->assertEquals('rolyat', $model->getAttributes()['encrypted']);
$this->assertEquals('rolyat', $model->toArray()['encrypted']);

$model->setRawAttributes([
'address_line_one' => '110 Kingsbrook St.',
'address_line_two' => 'My House',
]);

$this->assertEquals('110 Kingsbrook St.', $model->address->lineOne);
$this->assertEquals('My House', $model->address->lineTwo);

$this->assertEquals('110 Kingsbrook St.', $model->toArray()['address_line_one']);
$this->assertEquals('My House', $model->toArray()['address_line_two']);

$model->address->lineOne = '117 Spencer St.';

$this->assertFalse(isset($model->toArray()['address']));
$this->assertEquals('117 Spencer St.', $model->toArray()['address_line_one']);
$this->assertEquals('My House', $model->toArray()['address_line_two']);

$this->assertEquals('117 Spencer St.', json_decode($model->toJson(), true)['address_line_one']);
$this->assertEquals('My House', json_decode($model->toJson(), true)['address_line_two']);
}
}

class TestEloquentModelWithCustomCast extends Model
{
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'address' => AddressCaster::class,
'encrypted' => EncryptCaster::class,
];
}

class EncryptCaster
{
public static function fromModelAttributes($model, $key, $attributes)
{
return strrev($attributes[$key]);
}

public static function toModelAttributes($model, $key, $value, $attributes)
{
return [$key => strrev($value)];
}
}

class AddressCaster
{
public static function fromModelAttributes($model, $key, $attributes)
{
return new Address($attributes['address_line_one'], $attributes['address_line_two']);
}

public static function toModelAttributes($model, $key, $value, $attributes)
{
return ['address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo];
}
}

class Address
{
public $lineOne;
public $lineTwo;

public function __construct($lineOne, $lineTwo)
{
$this->lineOne = $lineOne;
$this->lineTwo = $lineTwo;
}
}
0