Skip to content
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

Allow for Eloquent model attributes to be type juggled (casted). #4948

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 66 additions & 19 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ abstract class Model implements ArrayAccess, ArrayableInterface, JsonableInterfa
*/
protected $dates = array();

/**
* Attributes that are to be casted to a different type.
*
* @var array
*/
protected $cast = array();

/**
* The relationships that should be touched on save.
*
Expand Down Expand Up @@ -2164,14 +2171,14 @@ public function attributesToArray()
{
$attributes = $this->getArrayableAttributes();

// If an attribute is a date, we will cast it to a string after converting it
// to a DateTime / Carbon instance. This is so we will get some consistent
// formatting while accessing attributes vs. arraying / JSONing a model.
foreach ($this->getDates() as $key)
// PLACEHOLDER FOR TAYLOR COMMENT
// Iterate the castable fields, and if the field is present
// cast the attribute and replace within the array.
foreach ($this->getCasts() as $key => $type)
{
if ( ! isset($attributes[$key])) continue;

$attributes[$key] = (string) $this->asDateTime($attributes[$key]);
$attributes[$key] = $this->castAttribute($type, $attributes[$key]);
}

// We want to spin through all the mutated attributes for this model and call
Expand Down Expand Up @@ -2337,14 +2344,36 @@ protected function getAttributeValue($key)
return $this->mutateAttribute($key, $value);
}

// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
elseif (in_array($key, $this->getDates()))
// PLACEHOLDER FOR TAYLOR COMMENT
// If the field is present in the castable array, then
// retrieve the intended type, and return the casted
// value.
elseif (array_key_exists($key, $casts = $this->getCasts()))
{
if ($value) return $this->castAttribute($casts[$key], $value);
}

return $value;
}

/**
* Cast an attribute to a new type.
*
* @param string $type
* @param mixed $value
* @return mixed
*/
protected function castAttribute($type, $value)
{
$type = strtolower($type);

if ($type === 'date')
{
if ($value) return $this->asDateTime($value);
}

if ($value !== null) settype($value, $type);

return $value;
}

Expand Down Expand Up @@ -2440,14 +2469,22 @@ public function setAttribute($key, $value)
return $this->{$method}($value);
}

// If an attribute is listed as a "date", we'll convert it from a DateTime
// instance into a form proper for storage on the database tables using
// the connection grammar's date format. We will auto set the values.
elseif (in_array($key, $this->getDates()))
// PLACEHOLDER FOR TAYLOR COMMENT
// If the attribute being set is held within the castable array
// then cast before setting the attribute. Date types are handled
// seperately due to being complex objects.
elseif (array_key_exists($key, $casts = $this->getCasts()))
{
if ($value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't change 0 to false.

{
$value = $this->fromDateTime($value);
if ($casts[$key] === 'date')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already handled in castAttribute, so no need to do it here again.

{
$value = $this->fromDateTime($value);
}
else
{
$value = $this->castAttribute($casts[$key], $value);
}
}
}

Expand All @@ -2466,15 +2503,25 @@ public function hasSetMutator($key)
}

/**
* Get the attributes that should be converted to dates.
* Get the attributes that should be cast upon retrieval.
*
* @return array
*/
public function getDates()
public function getCasts()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be done once in the boot method? Just augment the casts property and be done with it. Do we really have to spin through all this for every single attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was replicating what was there for the getDates() but I'll take a look at the possibility of using boot. :)

{
$defaults = array(static::CREATED_AT, static::UPDATED_AT);
// PLACEHOLDER FOR TAYLOR COMMENT
// This block allows for backwards compatibility with $dates by
// merging values present in the original date array with our
// defaults.
$originals = array();
foreach ($this->dates as $key)
{
$originals[$key] = 'date';
}

$defaults = array(static::CREATED_AT => 'date', static::UPDATED_AT => 'date');

return array_merge($this->dates, $defaults);
return array_merge($this->cast, $originals, $defaults);
}

/**
Expand Down Expand Up @@ -2667,7 +2714,7 @@ public function getDirty()
$dirty[$key] = $value;
}
elseif ($value !== $this->original[$key] &&
! $this->originalIsNumericallyEquivalent($key))
! $this->originalIsNumericallyEquivalent($key))
{
$dirty[$key] = $value;
}
Expand Down
25 changes: 22 additions & 3 deletions tests/Database/DatabaseEloquentModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,17 @@ public function testTimestampsAreReturnedAsObjectsOnCreate()
}


public function testAttributesInCastingArrayAreReturnedAsCorrectType()
{
$model = new EloquentModelWithCastsStub;
$model->first = 1337;
$model->second = 1;
$model->third = '13.77';
$this->assertTrue(is_string($model->first));
$this->assertTrue(is_bool($model->second));
$this->assertTrue(is_float($model->third));
}

public function testDateTimeAttributesReturnNullIfSetToNull()
{
$timestamps = array(
Expand Down Expand Up @@ -849,7 +860,7 @@ public function incorrectRelationStub()
{
return 'foo';
}
public function getDates()
public function getCasts()
{
return array();
}
Expand All @@ -864,9 +875,9 @@ class EloquentModelCamelStub extends EloquentModelStub {
}

class EloquentDateModelStub extends EloquentModelStub {
public function getDates()
public function getCasts()
{
return array('created_at', 'updated_at');
return array('created_at' => 'date', 'updated_at' => 'date');
}
}

Expand Down Expand Up @@ -949,3 +960,11 @@ public static function isBooted()
return array_key_exists(get_called_class(), static::$booted);
}
}

class EloquentModelWithCastsStub extends Illuminate\Database\Eloquent\Model {
protected $cast = array(
'first' => 'string',
'second' => 'bool',
'third' => 'float',
);
}