Demagicfying Laravel: Model properties; getters and setters

I mostly enjoy working with Laravel. It provides a range of tools that makes it really fast to create a proof of concept of an idea, allowing you to test out your ideas without spending too much time on ideas before knowing if they are actually worth spending time on.

When you're starting out with Laravel a lot of what's going on behind the scene can feel like magic. This is really nice in the sense that you don't often have to worry about what is actually going on, but on the other hand, it can make things pretty hard to debug, it is not clear what is causing a bug when you're not sure what is going.

In this post I'll look into Eloquent models' object properties, and how Laravel's Eloquent ORM handles getting and setting property values.

Table of content

Setting up

To have an example to work with, we'll start by setting up a database table for a simple Todolist.

php artisan make:migration create_todolists_table --create=todolists

The Todolist is pretty simple, it has a unique auto-incrementing ID, a string name, a text description, and Eloquent's default timestamps. The most interesting part of the migration file is the up() method, where we define the table.

// Class definitions
public function up()
{
    Schema::create('todolists', function(Blueprint $table) {
        $table->increments('id');

        $table->string('name');
        $table->text('description');

        $table->timestamps();
    });
}
// The rest of the class

Besides the migration, we need to create our Todolist model.

php artisan make:model Todolist

This gives us a basic model class, that we can use to create Todolist objects. The full class looks like this:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Todolist extends Model
{
}

That's pretty much as bare bones as it gets.

Setting object properties

$list = new App\Todolist();
$list->name = 'My new list';
$list->description 'A list of important tasks';

This saves the model object with its fancy new name and description. But how does this happen? How does Laravel know which properties to save, when the properties isn't even defined on the object? Let's look at our object:

Todolist {#144
  #connection: null
  #table: null
  #primaryKey: "id"
  #keyType: "int"
  #perPage: 15
  +incrementing: true
  +timestamps: true
  #attributes: array:2 [
    "name" => "My new list"
    "description" => "A list of important tasks"
  ]
  #original: []
  #relations: []
  #hidden: []
  #visible: []
  #appends: []
  #fillable: []
  #guarded: array:1 [
    0 => "*"
  ]
  #dates: []
  #dateFormat: null
  #casts: []
  #touches: []
  #observables: []
  #with: []
  #morphClass: null
  +exists: false
  +wasRecentlyCreated: false
}

We see that our data is set in an array named $properties, and not as standard object properties. Let's look into this.

We start by looking at our Todolist model. This is just an empty class, so nothing happens here. The Todolist class extends the Eloquent Model, so let's look at that one. In a standard Laravel application, you'll find it in

vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

Obviously our model specific properties aren't defined here either, since Laravel can't predict what we need, so something else is going on.

We can see that Laravel uses magic methods, in this case the __set() magic method.

In PHP the __set() magic method is used as a catch all, that is called when trying to set an inaccessible object property, which means a property that either isn't defined, or that is inaccessible due to being defined with a protected or private scope.

The method in the Eloquent Model class is defined as:

/**
 * Dynamically set attributes on the model.
 *
 * @param  string  $key
 * @param  mixed  $value
 * @return void
 */
public function __set($key, $value)
{
    $this->setAttribute($key, $value);
}

__set() is passed 2 arguments, $key is the name of the property to be accessed, and $value is the value we're trying to set on the property.

When calling the code:

$list->name = 'My new list';

$key will have the value 'name', and $value will have the value 'My new list'.

In this case, __set() is only used as a wrapper for the setAttribute()-method, so let's have a look at that one.

/**
 * Set a given attribute on the model.
 *
 * @param  string  $key
 * @param  mixed  $value
 * @return $this
 */
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)) {
        $method = 'set'.Str::studly($key).'Attribute';

        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 ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
        $value = $this->fromDateTime($value);
    }

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

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

    return $this;
}

This is an important part of the Laravel setter magic, so lets go through it step by step.

if ($this->hasSetMutator($key)) {
    $method = 'set'.Str::studly($key).'Attribute';

    return $this->{$method}($value);
}

The first thing that happens is a check whether a set mutator method exists for the given property.

In an Eloquent context a set mutator is an object method on the form set[Property]Attribute, so for our name attribute, that would be setNameAttribute(). If a method with that name is defined, Laravel will call it with our value. This makes it possible to define our own setter methods, overriding the standard Laravel behavior.

elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
    $value = $this->fromDateTime($value);
}

If no setter method is defined, Laravel checks whether the property name is listed in the class' $dates array, or if it should be cast to a Date or DateTime object, according to the class' $casts property. If Laravel determines that the value is a datetime type, it will convert the value into a time string, to make sure it is safe to save the value to the database.

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

The last check determines whether the value should be encoded as a JSON string in which case it is converted to a json string, to make sure it's ready to be saved to the database.

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

As the last thing, the method saves the value, which may or may not have been cast to a different type, into the object's $attributes properties. This is an array where the object's current state is saved.

Getting object properties

Now that we have an idea what's happening when setting properties, let's look into getting the data back out.

var_dump($list->name);

Produces the output:

string(11) "My new list"

Just like __set($name, $value) is PHP's fallback when trying to set a property that doesn't exist, PHP has a magic get method, called __get($name). This method is called when trying to read the value of a property that hasn't been defined.

Again, our Todolist class doesn't have a $name property. Trying to read it will call PHP's __set() method, which again is defined on the base Eloquent Model class.

/**
 * Dynamically retrieve attributes on the model.
 *
 * @param  string  $key
 * @return mixed
 */
public function __get($key)
{
    return $this->getAttribute($key);
}

Illuminate\Eloquent\Model::__set() itself is just a wrapper for the class' getAttribute() method.

/**
 * Get an attribute from the model.
 *
 * @param  string  $key
 * @return mixed
 */
public function getAttribute($key)
{
    if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
        return $this->getAttributeValue($key);
    }

    return $this->getRelationValue($key);
}
return $this->getRelationValue($key);

The last part of the method relates to Eloquent relationships. I might go into this in another post, but I will skip it for this post.

if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
    return $this->getAttributeValue($key);
}

The interesting part of the method is a check for whether the property name exists in the object's $attributes property. This is where you'll find it if it's been set with the default Eloquent property setter, or if it has been loaded from a database.

If the property doesn't exist in the $attributes array, a check is made to see if a get mutator exists. Like Laravel setters are in the form set[Property]Attribute(), getters are in the form get[Property]Attribute(). Ie. when trying to read our $name property, Laravel will check for the existense of a method called getNameAttribute().

/**
 * Get a plain attribute (not a relationship).
 *
 * @param  string  $key
 * @return mixed
 */
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 getAttributeValue() works like a reverse version of the setAttributeValue() discussed earlier. It's main purpose being to get a stored value, cast it to something useful, and return it. Let's go through it step by step.

$value = $this->getAttributeFromArray($key);

The first thing that happens is that the property's value is fetched, if the property exists in the $attributes array.

if ($this->hasGetMutator($key)) {
    return $this->mutateAttribute($key, $value);
}

If the class has a property mutator, the property value is run through it, and returned.

if ($this->hasCast($key)) {
    return $this->castAttribute($key, $value);
}

If the property name is specified in the $casts array, the property's value is cast to the specified type and returned.

if (in_array($key, $this->getDates()) && ! is_null($value)) {
    return $this->asDateTime($value);
}

If the property is listed as a date property in the $dates array, the value is converted to a DateTime object, and returned.

return $value;

And lastly, if the property shouldn't be changed in any way, the raw value is returned.

Summary

Eloquent uses PHP's magic __get($name) and __set($name, $value) methods to save and get data on model objects. During this process it provides a couple of ways to manipulate the data.

So far we've identified 3 ways to manipulate property values set on and gotten from Eloquent model objects.

  • Accessor and mutator methods
  • The $casts array
  • The $dates array

Accessor and mutator methods

The most flexible way to manipulate Eloquent data on getting and setting is using accessor and mutator methods. These and named on the form get[Property]Attribute() and set[Property]Attribute().

Examples

public function getNameAttribute($value)
{
    return ucfirst($value);
}
public function setNameAttribute($value)
{
    $this->attributes['name'] = strtoupper($value);
}

The $casts array

The casts array is an array property where casts are specified for object properties. If no accessor or mutator is defined for a property, and it's specified in the $casts array Eloquent will handle casting the value.

Example

protected $casts = [
    'deadline' => 'DateTime',
];

The $dates array

Since it's very common to work with dates and times, Eloquent provides a very easy way to specify which properties should be cast as date objects.

Example

protected $dates = [
    'deadline',
];

By default, the properties will be cast to Carbon objects when getting the property.

echo $list->deadline->format('Y-m-d H:i:s');

Common pitfalls

I've seen a couple of common errors developers make when working with Eloquent getters and setters, that cause issues.
  • Defining the object properties
  • Forgetting to set $attributes

Defining the object properties

Many OO programmers prefer to define their object properties in their class files, both to make it instantly visible which properties are available on class objects, and to allow PHP to make various optimizations. But since Laravel's Eloquent ORM relies on the magic PHP getter and setter methods, defining the class properties will make Eloquent unable to mutate the data, as well as preventing the data from being set in the $attributes array, preventing it from being saved to the database.
use Illuminate\Database\Eloquent\Model;

class Todolist extends Model
{
    public $name;
}

Defining the object property like this prevents Eloquent from saving the name attribute to the database.

$list = new Todolist;
$list->name = 'New list';
$list->save();

In this example the 'name' key doesn't exist in the $attributes array, hence it doesn't exist in the database.

Todolist {#144
  +name: "My new list"
  #connection: null
  #table: null
  #primaryKey: "id"
  #keyType: "int"
  #perPage: 15
  +incrementing: true
  +timestamps: true
  #attributes: array:4 [
    "description" => "A new list of important tasks"
    "updated_at" => "2016-08-15 08:30:11"
    "created_at" => "2016-08-15 08:30:11"
    "id" => 3
  ]
  #original: array:4 [
    "description" => "A new list of important tasks"
    "updated_at" => "2016-08-15 08:30:11"
    "created_at" => "2016-08-15 08:30:11"
    "id" => 3
  ]
  #relations: []
  #hidden: []
  #visible: []
  #appends: []
  #fillable: []
  #guarded: array:1 [
    0 => "*"
  ]
  #dates: []
  #dateFormat: null
  #casts: []
  #touches: []
  #observables: []
  #with: []
  #morphClass: null
  +exists: true
  +wasRecentlyCreated: true
}

Forgetting so set $attributes

Another common pitfall is to override a mutator method to manipulate the property value, but forgetting to add the data to the $attributes array.
public function setNameAttribute($value)
{
    return strtoupper($value);
}

In this example the value will never be saved to the database, and cannot be read using an accessor.

$list->name = 'My new list';
echo $list->name;

The example will echo an empty string.