Under the hood: How model attributes work in Laravel

Laravel model attributes are basically representing the database fields for the given model. When data is retrieved from the database, it can be accessed through the model as they were actual properties of the model instance: $model->database_column_name.

However the attributes are not real properties of the instance, but they are stored in the protected $attributes property.

The attributes are accessed using the __get magic method. This makes it possible to do some cool things with them like mutations and casting.

As this in an under the hood post, I won’t explain what you can do with attributes, you can find it in the documentation. What I would like to show is how these things work, and also some tricks and caveats.

The HasAttributes trait

This trait takes care of handling the model attributes, so let’s see what happens when accessing a model attribute:

  • The magic __get method uses the getAttribute method to retrieve the value
  • If the requested key exists in $attributes it checks if it:
    • has an accessor
    • is defined as a cast
    • is defined as a date

The above mentioned things happen in the same order as described above, so you should be aware of that if any of them runs successfully the others doesn’t run.

Generally speaking if you define an accessor for the model, you should take care of casting the value to the desired type in the accessor method itself, because it won’t happen automatically.

Obviously you can use the castAttribute and asDateTime methods in you accessor to do so. I put here a relevant part of the code here:

public function getAttributeValue($key)
{
    $value = $this->getAttributeFromArray($key);

    // If the attribute has a get mutator, we will call that then return what
    // it returns as the value, which is useful for transforming values on
    // retrieval from the model to a form that is more useful for usage.
    if ($this->hasGetMutator($key)) {
        return $this->mutateAttribute($key, $value);
    }

    // If the attribute exists within the cast array, we will convert it to
    // an appropriate native PHP type dependant upon the associated value
    // given with the key in the pair. Dayle made this comment line up.
    if ($this->hasCast($key)) {
        return $this->castAttribute($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.
    if (in_array($key, $this->getDates()) &&
        ! is_null($value)) {
        return $this->asDateTime($value);
    }

    return $value;
}

The setters work in similar way as I’ve described above for the getters. Setting an attribute calls the setAttribute method through the __set magic method.

The order of execution is similar as we’ve seen at the getters:

  • call a mutator if exists. The same rule applies here, if you have a mutator you should take care of the type casts yourself, as it won’t happen automatically
  • cast to a date if defined for the given attribute
  • cast to json if it is necessary for the attribute
  • if the attribute key contains ‘->’ it will automatically set the proper value in the attribute’s underlying array

You can see the relevant code here:

public function setAttribute($key, $value)
{
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key)) {
return $this->setMutatedAttributeValue($key, $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 ($value && $this->isDateAttribute($key)) {
$value = $this->fromDateTime($value);
}

if ($this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}

// If this attribute contains a JSON ->, we'll set the proper value in the
// attribute's underlying array. This takes care of properly nesting an
// attribute in the array's value in the case of deeply nested items.
if (Str::contains($key, '->')) {
return $this->fillJsonAttribute($key, $value);
}

$this->attributes[$key] = $value;

return $this;
}

The reason of writing this article was that I already had a situation where it caused some debugging time to figure out what caused the issue in the application, because I was not ware that the casts don’t run when you use accessors.

Leave a Reply

Your email address will not be published. Required fields are marked *