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 formget[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.