Understanding how Relationships work in Laravel like Magic

Image for post
Laravel is a trademark of Taylor Otwell. Copyright © Taylor Otwell

Laravel’s orientation towards syntactic sugar makes a lot of things that you constantly use “just work” — like magic. I am not talking about PHP magic methods, but more about features that work like magic — i.e. oneliners that do a lot of work in the background. This article assumes that the reader is familiar with PHP 7 and the Laravel framework. The goal is to make eloquent relationships more understandable and help you be more comfortable and confident using the framework.

This is a User model from the Eloquent Relationships section of the Laravel Documentation.

The first argument passed to the hasOne method is the name of the related model. Once the relationship is defined, we may retrieve the related record using Eloquent's dynamic properties. Dynamic properties allow you to access relationship methods as if they were properties defined on the model:

We need to take a look at the code to see how the Laravel framework manages to make this work very simply and seamlessly. Because in the end result you have a Phone object related to the User that you can access with the phone property. First of all, we need to take a look at the Illuminate\Database\Eloquent\Model class.

By looking at the Illuminate\Database\Eloquent\Model class we see that there is no hasOne() method like the one we called on the User model, so that means it’s implemented through another class, specifically Concerns\HasRelationships.

In this class, the first method we see is hasOne().

Let’s assume we are calling the hasOne() method with one parameter: the related class. Let’s take a look at what’s being executed line-by-line.

The model is created with the aid of the tap helper function, inside the newRelatedInstance() method.

Basically, what tap does is you pass a value and an anonymous function which will execute with the value as the parameter, and then the value is returned — useful for when you want to execute a method on an object but you want the object and not the method’s result returned. If you’re interested you can take a look at Taylor Otwell’s description & reasoning behind tap.

The next line is fairly simple — if a foreign key is not explicitly specified, Laravel will find the appropriate foreign key for the join.

The getForeignKey() method is executed on the Model.

The Str::snake method converts the class name from TitleCase to snake_case and appends the default key name, which is id unless otherwise specified. This means that the User model’s foreign key for other models is user_id. This means that the Phone object (and the underlying database table) must have the user_id attribute in order for this to work.

Again, unless otherwise specified, the local key for the User model is id. So now we can create a new hasOne relationship with the following line.

Things get a bit trickier from here so it’s a good idea to see what parameters newHasOne requires.

As we see newHasOne() requires 4 parameters. Let’s see the values that we are passing to the method.

Using these values we’re then creating an returning a new HasOne object. The constructor for the HasOne class is inherited from the HasOneOrMany class.

This constructor calls the parent constructor (from the Relation class) which executes $this->addConstraints();. This line is important because it adds the where methods to the query.

We see that 2 where clauses are added to the query. The first one takes the foreign key (user_id) and matches it with the parent objects primary key which in our case is 1. A second where is added which makes sure that the user_id attribute is not null. So finally we have the WHERE clause:

So all this happens under the hood to prepare a new hasOne relationship. The only question left right now is what happens when you access the phone property on the User model.

As per the Laravel documentation, ->phone returns the related Phone object while the ->phone() method returns a query builder. We’ll take a look at the first scenario.

When accessing a property, PHP dynamically calls the __get method and passes the name of the property as a string.

The __get magic method on the Model class returns

In this case ! $key resolves to false because the key is phone. The phone key isn’t an active attribute and it hasn’t been cached so the 2nd condition resolves to false as well.

Important note: The 2nd part of the 2nd condition, $this->hasGetMutator($key) will check if the Model has a getPhoneAttribute mutator as documented here. Our simple example doesn’t have such a mutator.

The final check before returning the relation’s value is making sure that the property isn’t a method that belongs to the default Model class. And finally in our case $this->getRelationValue($key) will be executed.

The first thing Laravel does is check whether this relationship is already loaded in order to avoid executing the same query twice. Then it makes sure that the magic attribute phone exists as a method on the model — which it does.

Finally $this->getRelationshipFromMethod($key); is returned. This method makes sure that the attribute is an instance of the Relation class — like HasOne for example (HasOne extends HasOneOrMany which extends Relation.

Then, using tap again the results of the relationship are returned and the results are also stored in the relations array using $this->setRelation. This way, the next time $this->getRelationValue() is called, the relationship will be cached and returned directly.

Before going full-circle and reaching the end of this article let’s take a look at how getResults() fetches the result. As we see it just executes the familiar first() method on the query builder object. The query executed was the following, built from the where conditions added after constructing the Relation object.

The equivalent getResults method for hasMany would return $this->query->get().

Takeaways:

  • You should now have a better understanding of how relationships work which can be useful for cleaner development, easier debugging when you’re facing issues and more confidence in working with relationships.
  • You’re more familiar with common issues and exceptions that can occur with relationships (e.g. calling the property with () instead of accessing the magic method)
  • The tap helper function is at your disposal! It’s used a lot inside the Laravel framework and you can make use of it too, but make sure everything you make is understandable and well-documented.

Founder & CTO @ GuestFlip

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store