Understanding Model Relationships in Laravel Eloquent

Models and their relationships are the heart of Laravel Eloquent. If they give you a hard time or you’re not able to find a simple, friendly, and complete guide, start here!
Sitting on the other side of their programming article, it’s easy for the writer to feign or blow up the aura of expertise/prestige the platform provides. But I’ll be honest — I had an extremely hard time learning Laravel, if only because it was my first full-stack framework. One reason was that I was not using it at work and was exploring it out of curiosity; so, I’d make an attempt, get to a point, get confused, give up, and eventually forget everything. I must have done this 5-6 times before it started making sense to me (of course, the documentation doesn’t help).
But what still didn’t make sense was Eloquent. Or at least, the relationships between models (because Eloquent is too large to learn completely). Examples modeling authors and blog posts are a joke because real projects are far more complex; sadly, the official docs use the very same (or similar) examples. Or even if I did come across some useful article/resource, the explanation was so bad or so badly missing that it was just no use.
(By the way, I’ve been attacked for attacking the official documentation before, so if you’re having similar ideas, here’s my standard answer: go check out the Django documentation and then talk to me.)
Eventually, bit by bit, it did come together and made sense. I was finally able to model projects properly and use the models comfortably. Then one day I came across some neat Collections tricks that make this work more pleasant. In this article, I intend to cover all of it, starting from the very basics and then covering all possible use cases that you will encounter in real projects.
Why are Eloquent model relationships hard?
Sadly, I come across far too many Laravel developers who don’t understand models properly.
But why?
Even today, when there’s an explosion of courses, articles, and videos on Laravel, the overall understanding is poor. I think it’s an important point and is worth some reflection.
If you ask me, I’ll say that Eloquent model relationships aren’t hard at all. At least when seen from the perspective of the definition of “hard”. Live schema migrations are hard; writing a new templating engine is hard; contributing code to the core of Laravel is hard. Compared to these, learning and using an ORM . . . well, that can’t be hard! 🤭🤭
What actually happens is that PHP developers learning Laravel find Eloquent hard. That’s the real underlying issue, and in my opinion, there are several factors contributing to this (harsh, unpopular opinion alert!):
- Prior to Laravel, the exposure to a framework for most PHP developers has been CodeIgniter (it’s still alive, by the way, even if it’s become more Laravel/CakePHP-like). In the older CodeIgniter community (if there was one), the “best practice” was to directly stick SQL queries where needed. And though we have a new CodeIgniter today, the habits have carried over. As a result, when learning Laravel, the idea of an ORM is 100% new to PHP developers.
- Discarding the very small percentage of PHP exposed to frameworks such as Yii, CakePHP, etc., the remaining are used to working in core PHP or in an environment such as WordPress. And here again, an OOP-based mindset doesn’t exist, so a framework, a service container, a design pattern, an ORM . . . these are alien concepts.
- There is little to no concept of continuous learning in the PHP world. The average developer is happy working with single-server setups using relational databases and issuing queries written as strings. Asynchronous programming, web sockets, HTTP 2/3, Linux (forget Docker), unit testing, Domain-Driven Design — these are all alien ideas to an overwhelming proportion of PHP developers. As a result, reading up on something new and challenging, to the point that one finds it comfortable, doesn’t happen when Eloquent is encountered.
- The overall understanding of databases and modeling is poor as well. Since database design is directly, inseparably linked to Eloquent models, it raises the difficulty bar higher.
I don’t mean to be harsh and generalize globally — there are excellent PHP developers as well, and many of them, but their overall percentage is very low.
If you’re reading this, it means you’ve crossed all these barriers, come across Laravel, and messed with Eloquent.
Congratulations! 👏
You’re almost there. All the building blocks are in place and we just need to go through them in the proper order and detail. In other words, let’s start at the database level.
Database models: Relationships and Cardinality
To keep things simple, let’s assume we’re working with relational databases only throughout this article. One reason is that ORMs were originally developed for relational databases; the other reason is that RDBMS are still overwhelmingly popular.
Data Model
First, let’s understand data models better. The idea of a model (or a data model, to be more precise), comes from the database. No database, no data, and so, no data model. And what is a data model? Quite simply, it’s the way you decide to store/structure your data. For example, in an e-commerce store, you might store everything in one giant table (HORRIBLE practice, but sadly, not uncommon in the PHP world); that’d be your data model. You might also split the data into 20 main and 16 connecting tables; that’s a data model as well.
Also, note that the way data is structured in the database need not match 100% how it’s arranged in the framework’s ORM. However, the effort is always to keep things as close as possible so that we don’t have one more thing to be mindful of when developing.
Cardinality
Let’s also get this term out the way fast: cardinality. It just refers to “count”, loosely speaking. So, 1, 2, 3 . . . can all be the cardinality of something. End of story. Let’s keep moving!
Relationships
Now, whenever we store data in any type of system, there are ways data points can be related to each other. I know this sounds abstract and boring, but bear with me a little. The ways different data items are connected are known as relationships. Let’s see some non-database examples first so that we’re convinced we fully understand the idea.
- If we store everything in an array, one possible relationship is: the next data item is at an index greater than the previous index by
1
. - If we store data in a binary tree, one possible relationship is that the child tree to the left always has smaller values than the parent node’s (if we choose to maintain the tree that way).
- If we store data as an array of arrays of equal length, we can mimic a matrix, and then its properties become the relationships for our data.
So we see that the word “relationship”, in the context of data, doesn’t have a fixed meaning. In fact, if two people were looking at the same data, they might identify two very different data relationships (hello, statistics!) and both of them could be valid.
Relational databases
Based on all the terms we’ve discussed till now, we can finally talk about something that has a direct link to models in a web framework (Laravel) — relational databases. For most of us, the primary database used is MySQL, MariaDB, PostgreSQL, MSSQL, SQL Server, SQLite, or something along those lines. We also might vaguely know that these are called RDBMS but most of us have forgotten what it actually means and why does it matter.
The “R” in RDBMS stands for Relational, of course. This is not an arbitrarily chosen term; by this, we highlight the fact that these database systems are designed to work efficiently with relationships between data stored. In fact, “relation” here has a strict mathematical meaning, and though no developer needs to bother about it, it helps to know that there’s a rigorous mathematical foundation underneath these types of databases.
Explore these resources to learn SQL and NoSQL.
Okay, so we know by experience that data in RDBMS are stored as tables. Where, then, are the relationships?
Types of relationships in RDBMS
This is perhaps the most important part of the entire topic of Laravel and model relationships. If you don’t understand this, Eloquent will never make sense, so please pay attention for the next few minutes (it’s not even that difficult).
An RDBMS allows us to have relationships between data — at a database level. This means that these relationships are not impractical/imaginary/subjective and can be created or inferred by different people with the same result.
At the same time, there are certain capabilities/tools within an RDBMS that allow us to create and enforce these relationships, such as:
- Primary Key
- Foreign Key
- Constraints
I don’t want this article to become a course in databases, so I’ll assume that you know what these concepts are. If not, or in case you feel shaky in your confidence, I recommend this friendly video (feel free to explore the entire series):
As it happens, these RDBMS-style relationships are also the most common ones that occur in real-world applications (not always, since a social network is best modeled as a graph and not as a collection of tables). So, let’s take a look at them one by one and also try to understand where they might be useful.
One-to-one relationship
In almost every web application, there are user accounts. Also, the following are true (generally speaking) about the users and accounts:
- A user can have only one account.
- An account can only be owned by one user.
Yes, we can argue that a person can sign up with another email and thus create two accounts, but from the perspective of the web application, those are two different people with two different accounts. The application will not, for example, show one account’s data in another.
What all this hair-splitting means is — if you have a situation like this in your application and you’re using a relational database, you’d need to design it as a one-to-one relationship. Note that nobody is forcing you artificially — there’s a clear situation in the business domain and you happen to be using a relational database . . . only when both these conditions are satisfied, do you reach for a one-to-one relationship.
For this example (users and accounts), this is how we can implement this relationship when creating the schema:
CREATE TABLE users(
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL,
password VARCHAR(100) NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE accounts(
id INT NOT NULL AUTO_INCREMENT,
role VARCHAR(50) NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(id) REFERENCES users(id)
);
Notice the trick here? It’s quite uncommon when building apps generally, but in the accounts
table, we have the field id
set as both primary key and foreign key! The foreign key property links it to the users
table (of course 🙄) whereas the primary key property makes the id
column unique — a true one-to-one relationship!
Granted, the fidelity of this relationship is not guaranteed. For instance, there’s nothing stopping me from adding 200 new users without adding a single entry to the accounts
table. If I do that, I end up with a one-to-zero relationship! 🤭🤭 But within the bounds of pure structure, that’s the best we can do. If we want to prevent adding users without accounts, we need to take help from some sort of programming logic, either in the form of database triggers or validations enforced by Laravel.
If you’re beginning to stress out, I have some very good advice:
- Take it slow. As slow as you need to. Instead of trying to finish this article and the 15 others that you have bookmarked for today, stick to this one. Let it take 3, 4, 5 days if that’s what it takes — your goal should be to knock Eloquent model relationships off your list forever. You’ve jumped from article to article before, wasting several hundred hours and yet it didn’t help. So, do something different this time. 😇
- While this article is about Laravel Eloquent, all that comes much later. The foundation of it all is database schema, so our focus should be on getting that right first. If you can’t work purely on a database level (assuming there are no frameworks in the world), then models and relationships will never make full sense. So, forget about Laravel for now. Completely. We’re only talking about and doing database design for now. Yes, I’ll make Laravel references now and then, but your job is to ignore them completely if they’re complicating the picture for you.
- Later on, read a little more on databases and what they offer. Indexes, performance, triggers, underlying data structures and their behavior, caching, relationships in MongoDB . . . whatever tangential topics you can cover will help you as an engineer. Remember, framework models are just ghost shells; the real functionality of a platform comes from its underlying databases.
One-to-many relationship
I’m not sure if you realized this, but this is the type of relationship we all intuitively create in our everyday work. When we create an orders
table (a hypothetical example), for example, to store a foreign key to the users
table, we create a one-to-many relationship between users and orders. Why is that? Well, look at it again from the perspective of who can have how many: one user is allowed to have more than one order, which is pretty much how all e-commerce works. And seen from the opposite side, the relationship says that an order can only belong to one user, which also makes a lot of sense.
In data modeling, RDBMS books, and system documentation, this situation is represented diagrammatically like this:
Notice the three lines making a trident of sorts? This is the symbol for “many”, and so this diagram says that one user can have many orders.
By the way, these “many” and “one” counts that we are encountering repeatedly are what’s called the Cardinality of a relationship (remember this word from a previous section?). Again, for this article, the term has no use, but it helps to know the concept in case it comes up during interviews or further reading.
Simple, right? And in terms of actual SQL, creating this relationship is also simple. In fact, it’s much simpler than the case of a one-to-one relationship!
CREATE TABLE users(
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL,
password VARCHAR(100) NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE orders(
id INT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
description VARCHAR(50) NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(user_id) REFERENCES users(id)
);
The orders
table stores user IDs for each order. Since there’s no constraint (restriction) that the user IDs in the orders
table have to be unique, it means we can repeat a single ID many times. This is what creates the one-to-many relationship, and not some arcane magic that’s hidden underneath. The user IDs are stored sort of in a dumb way in the orders
table, and SQL doesn’t have any concept of one-to-many, one-to-one, etc. But once we’re storing data this way, we can think of there being a one-to-many relationship.
Hopefully, it’s making sense now. Or at least, more sense than before. 😅 Remember that just like anything else, this is a mere matter of practice, and once you’ve done this 4-5 times in real-world situations, you will not even think about it.
Many-to-many relationships
The next type of relationship that arises in practice is the so-called many-to-many relationship. Once again, before worrying about frameworks or even diving into databases, let’s think of a real-world analog: books and authors. Think of your favorite author; they’ve written more than one book, right? At the same time, it’s pretty common to see several authors collaborating on a book (at least in the nonfiction genre). So, one author can write many books, and many authors can write one book. Between the two entities (book and author), this forms a many-to-many relationship.
Now, granted that you’re highly unlikely to create a real-world app involving libraries or books and authors, so let’s think of some more examples. In a B2B setting, a manufacturer orders items from a supplier and in turn receives an invoice. The invoice will contain several line items, each of them listing the quantity and item supplied; for example, 5-inch pipe pieces x 200, etc. In this situation, items and invoices have a many-to-many relationship (think about it and convince yourself). In a fleet management system, vehicles and drivers will have a similar relationship. In an e-commerce site, users and products can have a many-to-many relationship if we consider functionality such as favorites or wish lists.
Fair enough, now how to create this many-to-many relationship in SQL? Based on our knowledge of how the one-to-many relationship works, it might be tempting to think we should store foreign keys to the other table in both the tables. However, we run into major problems if we try to do this. Have a look at this example where books are authors are supposed to have a many-to-many relationship:
At first glance everything looks all right — books are mapped to authors exactly in a many-to-many fashion. But look closely at the authors
table data: book ids 12 and 13 are both written by Peter M. (author id 2), because of which we have no choice but to repeat the entries. Not only does the authors
table now have data integrity problems (proper normalization and all that), the values in the id
column are now repeating. This means that in the design we’ve chosen, there can be no primary key column (because primary keys can’t have duplicate values), and everything falls apart.
Clearly, we need a new way to do this, and thankfully, this problem has already been solved. Since storing foreign keys directly into both the tables screws things up, The right way of creating many-to-many relationships in RDBMS is by creating a so-called “joining table”. The idea is basically to let the two original tables stand undisturbed and create a third table to demonstrate the many-to-many mapping.
Let’s redo the failed example to contain a joining table:
Notice that there have been drastic changes:
- The number of columns in the
authors
table is reduced. - The number of columns in the
books
table is reduced. - The number of rows in the
authors
table is reduced as there’s no need for repetition anymore. - A new table called
authors_books
has appeared, containing info about which author id is connected to which book id. We could’ve named the joining table anything, but by convention is the result of simply joining the two tables it represents, using an underscore.
The joining table has no primary key and in most cases contains only two columns — IDs from the two tables. It’s almost as if we removed the foreign key columns from our earlier example and pasted them into this new table. Since there’s no primary key, there can be as much repetition as is needed to record all the relationships.
Now, we can see with our eyes how the joining table displays the relationships clearly, but how do we access them in our applications? The secret is linked to the name — joining table. This isn’t a course on SQL queries so I won’t dive into it but the idea is that if you want all the books by a particular author in one, efficient query, you SQL-join the tables in the same order –> authors
, authors_books
, and books
. The authors
and authors_books
tables are joined over the id
and author_id
columns, respectively, while the authors_books
and books
tables are joined on the book_id
and id
columns, respectively.
Exhausting, yes. But look at the bright side — we’ve finished all the necessary theory/groundwork we needed to do before tackling Eloquent models. And let me remind you that all this stuff is not optional! Not knowing database design will leave you in Eloquent confusion land forever. Moreover, whatever Eloquent does or tries to do, mirrors these database-level details perfectly, so it’s easy to see why trying to learn Eloquent while running away from RDBMS is an exercise in futility.
Creating model relationships in Laravel Eloquent
Finally, after a detour that lasted some 70,000 miles, we’ve reached the point where we can talk about Eloquent, its models, and how to create/use them. Now, we learned in the previous part of the article that everything begins with the database and how you model your data. This made me realize that I should use a single, complete example where I start a fresh project. At the same time, I want this example to be real-world, and not about blogs and authors or books and shelves (which are real-world, too, but have been done to death).
Let’s imagine a store that sells soft toys. Let’s also assume that we’ve been provided the requirements document, from which we can identify these four entities in the system: users, orders, invoices, items, categories, subcategories, and transactions. Yes, there’s likely to be more complication involved, but let’s just put that aside and focus on how we go from a document to an app.
Once the main entities in the system have been identified, we need to think of how they relate to each other, in terms of the database relationships we’ve discussed so far. Here are the ones that I can think of:
- Users and Orders: One to many.
- Orders and invoices: One to one. I realize this one isn’t cut and dried, and depending on your business domain, there might be a one to many, a many to one, or a many to many relationship. But when it comes to your average, small e-commerce store, one order will only result in one invoice and vice versa.
- Orders and Items: Many to many.
- Items and Categories: Many to one. Again, this is not so in large e-commerce sites, but we have a small operation.
- Categories and Subcategories: one to many. Again, you’ll find most real-world examples that contradict this, but hey, Eloquent is hard enough as it is, so let’s not make the data modeling harder!
- Orders and Transactions: One to many. I’d also like to add these two points as a justification for my choice: 1) We could have added a relationship between Transactions and Invoices as well. It’s just a data modeling decision. 2) Why one to many here? Well, it’s common that an order payment fails for some reason and succeeds the next time. In this case, we have two transactions created for that order. Whether we wish to show those failed transactions or not is a business decision, but it’s always a good idea to capture valuable data.
Are there any other relationships? Well, many more relationships are possible, but they are not practical. For example, we can say that a user has many transactions, so there should be a relationship between them. The thing to realize here is that there’s already an indirect relationship: users -> orders -> transactions, and generally speaking, it’s good enough as RDBMS are beasts in joining tables. Secondly, creating this relationship would mean adding a user_id
column to the transactions
table. If we did this for every possible direct relationship, then we’d be adding a lot more load on the database (in the form of more storage, especially if UUIDs are being used, and maintaining indexes), chaining down the overall system. Sure, if the business says they need transactions data and need it within 1.5 seconds, we might decide to add that relationship and speed things up (tradeoffs, tradeoffs . . .).
And now, ladies and gentlemen, the time has come to write the actual code!
Laravel model relationships — real example with code
The next phase of this article is about getting our hands dirty — but in a useful way. We’ll pick up the same database entities as in the earlier e-commerce example, and we’ll see how models in Laravel are created and connected, right from installing Laravel!
Naturally, I assume that you have your development environment set up and you know how to install and use Composer for managing dependencies.
$ composer global require laravel/installer -W
$ laravel new model-relationships-study
These two console commands install the Laravel installer (the -W
part is used for upgrading since I already had an older version installed). And in case you’re curious, as of writing, the Laravel version that got installed is 8.5.9. Should you panic and upgrade as well? I’d advise against it, since I don’t expect any major changes between Laravel 5 and Laravel 8 in the context of our application. Some things have changed and will impact this article (such as Model Factories), but I think you’ll be able to port the code.
Since we’ve already thought through the data model and their relationships, the part of creating the models will be trivial. And you’ll also see (I’m sounding like a broken record now!) how it mirrors the database schema as it’s 100% dependent on it!
In other words, we need to first create the migrations (and model files) for all the models, which will be applied to the database. Later, we can work on the models and tack on the relationships.
So, which model do we begin with? The simplest and the least connected one, of course. In our case, this means the User
model. Since Laravel ships with this model (and cannot work without it 🤣), let’s modify the migration file and also clean up the model to suit our simple needs.
Here’s the migration class:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
});
}
}
Since we’re not actually building a project, we don’t need to get into passwords, is_active, and all that. Our users
table will have only two columns: the id and the name of the user.
Let’s create the migration for Category
next. Since Laravel allows us the convenience to generate the model too in a single command, we’ll take advantage of that, though we won’t touch the model file for now.
$ php artisan make:model Category -m
Model created successfully.
Created Migration: 2021_01_26_093326_create_categories_table
And here’s the migration class:
class CreateCategoriesTable extends Migration
{
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
});
}
}
If you’re surprised at the absence of the down()
function, don’t be; in practice, you rarely end up using it as dropping a column or table or changing a column type results in data loss that can’t be recovered. In development, you’ll find yourself dropping the entire database and then re-running the migrations. But we digress, so let’s get back and tackle the next entity. Since subcategories are directly related to categories, I think it’s a good idea to do that next.
$ php artisan make:model SubCategory -m
Model created successfully.
Created Migration: 2021_01_26_140845_create_sub_categories_table
All right, now let’s fill up the migration file:
class CreateSubCategoriesTable extends Migration
{
public function up()
{
Schema::create('sub_categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->unsignedBigInteger('category_id');
$table->foreign('category_id')
->references('id')
->on('categories')
->onDelete('cascade');
});
}
}
As you can see, we add a separate column here, called category_id
, which will store IDs from the categories
table. No prizes for guessing, this creates a one to many relationships at the database level.
Now it’s the turn for items:
$ php artisan make:model Item -m
Model created successfully.
Created Migration: 2021_01_26_141421_create_items_table
And the migration:
class CreateItemsTable extends Migration
{
public function up()
{
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->string('type');
$table->unsignedInteger('price');
$table->unsignedInteger('quantity_in_stock');
$table->unsignedBigInteger('sub_category_id');
$table->foreign('sub_category_id')
->references('id')
->on('sub_categories')
->onDelete('cascade');
});
}
}
If you feel like things should be done differently, that’s fine. Two people will rarely come up with the exact same schema and architecture. Do note one thing, which is a best practice of sorts: I’ve stored the priced as an integer.
Why?
Well, folks realized that handling float divisions and all was ugly and error-prone on the database side, so they started stored the price in terms of the smallest currency unit. For example, if we were operating in USD, the price field here would represent cents. Throughout the system the values and calculations will be in cents; only when it’s time to display to the user or send a PDF by email, will we divide by 100 and round off. Clever, eh?
Anyway, notice that an item is linked to a subcategory in a many-to-one relationship. It’s also linked to a category . . . indirectly via its subcategory. We will see solid demonstrations of all these gymnastics, but for now, we need to appreciate the concepts and make sure we’re 100% clear.
Next up is the Order
model and its migration:
$ php artisan make:model Order -m
Model created successfully.
Created Migration: 2021_01_26_144157_create_orders_table
For the sake of brevity, I’ll include only some of the important fields in the migration. By that I mean, an order’s details can contain a great many things, but we’ll restrict them to a few so that we can focus on the concept of model relationships.
class CreateOrdersTable extends Migration
{
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('status');
$table->unsignedInteger('total_value');
$table->unsignedInteger('taxes');
$table->unsignedInteger('shipping_charges');
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('cascade');
});
}
}
Looks fine, but, wait a minute! Where are the items in this order? As we established earlier, there’s a many-to-many relationship between orders and items, so a simple foreign key doesn’t work. The solution is a so-called joining table or intermediate table. In other words, we need a joining table to store the many-to-many mapping between orders and items. Now, in the Laravel world, there’s a built-in convention that we follow to save time: If I create a new table by using the singular form of the two table names, place them in dictionary order, and join them using an underscore, Laravel will automatically recognize it as the joining table.
In our case, the joining table will be called item_order
(the word “item” comes before “order” in a dictionary). Also, as explained before, this joining table will normally contain only two columns, foreign keys to each table.
We could create a model + migration here, but the model will never be used as it’s more of a meta thing. Thus, we create a new migration in Laravel and tell it what’s what.
$ php artisan make:migration create_item_order_table --create="item_order"
Created Migration: 2021_01_27_093127_create_item_order_table
This results in a new migration, which we will change as follows:
class CreateItemOrderTable extends Migration
{
public function up()
{
Schema::create('item_order', function (Blueprint $table) {
$table->unsignedBigInteger('order_id');
$table->foreign('order_id')
->references('id')
->on('orders')
->onDelete('cascade');
$table->unsignedBigInteger('item_id');
$table->foreign('item_id')
->references('id')
->on('items')
->onDelete('cascade');
});
}
}
How to actually access these relationships through Eloquent method calls is a topic for later, but notice that we need to first painstakingly, by hand, create these foreign keys. Without these, there’s no Eloquent and there’s no “smart” in Laravel. 🙂
Are we there yet? Well, almost . . .
We only have a couple more models to worry about. The first one is the invoices table, and you’ll remember that we decided to make it a one-to-one relationship with orders.
$ php artisan make:model Invoice -m
Model created successfully.
Created Migration: 2021_01_27_101116_create_invoices_table
In the very early sections of this article, we saw that one way to enforce a one-to-one relationship is to make the primary key on the child table the foreign key as well. In practice, hardly anyone takes this overly cautious approach, and people generally design the schema as they would for a one-to-many relationship. My take is that a middle approach is better; just make the foreign key unique and you’ve made sure that the parent model’s IDs cannot be repeated:
class CreateInvoicesTable extends Migration
{
public function up()
{
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->timestamp('raised_at')->nullable();
$table->string('status');
$table->unsignedInteger('totalAmount');
$table->unsignedBigInteger('order_id')->unique();
$table->foreign('order_id')
->references('id')
->on('orders')
->onDelete('cascade')
->unique();
});
}
}
And yes, for the umpteenth time, I’m aware that this invoices table has a lot missing; however, our focus here is to see how model relationships work and not to design an entire database.
Okay, so, we’ve reached the point where we need to create the final migration of our system (I hope!). The focus is now on the Transaction
model, which we decided earlier is linked to the Order
model. By the way, here’s an exercise for you: Should the Transaction
model be instead linked to the Invoice
model? Why and why not? 🙂
$ php artisan make:model Transaction -m
Model created successfully.
Created Migration: 2021_01_31_145806_create_transactions_table
And the migration:
class CreateTransactionsTable extends Migration
{
public function up()
{
Schema::create('transactions', function (Blueprint $table) {
$table->id();
$table->timestamp('executed_at');
$table->string('status');
$table->string('payment_mode');
$table->string('transaction_reference')->nullable();
$table->unsignedBigInteger('order_id');
$table->foreign('order_id')
->references('id')
->on('orders')
->onDelete('cascade');
});
}
}
Phew! That was some hard work . . . let’s run the migrations and see how we’re doing in the eyes of the database.
$ php artisan migrate:fresh
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (3.45ms)
Migrating: 2021_01_26_093326_create_categories_table
Migrated: 2021_01_26_093326_create_categories_table (2.67ms)
Migrating: 2021_01_26_140845_create_sub_categories_table
Migrated: 2021_01_26_140845_create_sub_categories_table (3.83ms)
Migrating: 2021_01_26_141421_create_items_table
Migrated: 2021_01_26_141421_create_items_table (6.09ms)
Migrating: 2021_01_26_144157_create_orders_table
Migrated: 2021_01_26_144157_create_orders_table (4.60ms)
Migrating: 2021_01_27_093127_create_item_order_table
Migrated: 2021_01_27_093127_create_item_order_table (3.05ms)
Migrating: 2021_01_27_101116_create_invoices_table
Migrated: 2021_01_27_101116_create_invoices_table (3.95ms)
Migrating: 2021_01_31_145806_create_transactions_table
Migrated: 2021_01_31_145806_create_transactions_table (3.54ms)
Praise be to the Lord! 🙏🏻🙏🏻 Looks like we’ve survived the moment of trial.
And with that, we’re ready to move on to defining model relationships! For that, we need to go back to the list we created earlier, outlining the type of direct relationships between models (tables).
To begin with, we’ve established that there’s a one-to-many relationship between users and orders. We can confirm this by going to the orders’ migration file and seeing the presence of the field user_id
there. This field is what creates the relationship, because any relationship we’re interested in establishing needs to be honored by the database first; the rest (Eloquent syntax and where to write which function) is just pure formality.
In other words, the relationship is already there. We just need to tell Eloquent to make it available at run time. Let’s start with the Order
model, where we declare that it belongs to the User
model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
use HasFactory;
public function user() {
return $this->belongsTo(User::class);
}
}
The syntax must be familiar to you; we declare a function named user()
, which serves to access the user that owns this order (the function name can be anything; it’s what it returns that matters). Think back again for a moment — if there was no database and no foreign keys, a statement like $this->belongsTo
would be meaningless. It’s only because there’s a foreign key on the orders
table that Laravel is able to use that user_id
to look up the user with same id
and return it. By itself, without the cooperation of the database, Laravel cannot create relationships out of thin air.
Now, it’d also be nice to be able to write $user->orders
to access a user’s orders. This means we need to go to the User
model and write out a function for the “many” part of this one-to-many relationship:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
use HasFactory;
public $timestamps = false;
public function orders() {
return $this->hasMany(Order::class);
}
}
Yes, I heavily modified the default User
model because we don’t need all the other functionality for this tutorial. Anyway, the User
class now has a method called orders()
, which says that one user can be associated with multiple orders. In the ORM world, we say that the orders()
relationship here is the inverse of the user()
relationship we had on the Order
model.
But, wait a minute! How is this relationship working? I mean, there’s nothing at the database level that has multiple connections going out from the users
table to the orders
table.
Actually, there is an existing connection, and turns out, it’s enough on its own — the foreign key reference stored in the orders
table! This is, when we say something like $user->orders
, Laravel hits the orders()
function and knows by looking at it that there’s a foreign key on the orders
table. Then, it kinda does a SELECT * FROM orders WHERE user_id = 23
and returns the query results as a collection. Of course, the whole point of having an ORM is to forget about SQL, but we shouldn’t completely forget that the underlying base is the RDBMS that runs SQL queries.
Next, let’s breeze through the orders and invoices models, where we have a one-to-one relationship:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
use HasFactory;
public $timestamps = false;
public function user() {
return $this->belongsTo(User::class);
}
public function invoice() {
return $this->hasOne(Invoice::class);
}
}
And the invoice model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
use HasFactory;
public $timestamps = false;
public function order() {
return $this->belongsTo(Order::class);
}
}
Notice that on the database level, as well as almost on the Eloquent level, it’s a typical one-to-many relationship; we’ve just added some checks to make sure it stays one-to-one.
We now come to another type of relationship — the many-to-many between orders and items. Recall that we already have created an intermediate table called item_order
that stores the mapping between the primary keys. If this much has been done correctly, defining the relationship and working with it is trivial. As per the Laravel docs, to define a many-to-many relationship, your methods must return a belongsToMany()
instance.
So, in the Item
model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
use HasFactory;
public $timestamps = false;
public function orders() {
return $this->belongsToMany(Order::class);
}
}
Surprisingly, the inverse relationship is almost identical:
class Order extends Model
{
/* ... other code */
public function items() {
return $this->belongsToMany(Item::class);
}
}
And that’s it! As long as we’ve followed the naming conventions correctly, Laravel is able to deduce the mappings as well as their location.
Since all the three fundamental types of relationships have been covered (one-to-one, one-to-many, many-to-many), I’ll stop writing out the methods for other models, as they’ll be along the same lines. Instead, let’s create the factories for these models, create some dummy data, and see these relationships in action!
How do we do that? Well, let’s take the quick-and-dirty path and throw everything into the default seeder file. Then, when we run the migrations, we will run the seeder as well. So, here’s what my DatabaseSeeder.php
file looks like:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Category;
use App\Models\SubCategory;
use App\Models\Item;
use App\Models\Order;
use App\Models\Invoice;
use App\Models\User;
use Faker;
class DatabaseSeeder extends Seeder
{
public function run()
{
$faker = Faker\Factory::create();
// Let's make two users
$user1 = User::create(['name' => $faker->name]);
$user2 = User::create(['name' => $faker->name]);
// Create two categories, each having two subcategories
$category1 = Category::create(['name' => $faker->word]);
$category2 = Category::create(['name' => $faker->word]);
$subCategory1 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);
$subCategory2 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);
$subCategory3 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);
$subCategory4 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);
// After categories, well, we have items
// Let's create two items each for sub-category 2 and 4
$item1 = Item::create([
'sub_category_id' => 2,
'name' => $faker->name,
'description' => $faker->text,
'type' => $faker->word,
'price' => $faker->randomNumber(2),
'quantity_in_stock' => $faker->randomNumber(2),
]);
$item2 = Item::create([
'sub_category_id' => 2,
'name' => $faker->name,
'description' => $faker->text,
'type' => $faker->word,
'price' => $faker->randomNumber(3),
'quantity_in_stock' => $faker->randomNumber(2),
]);
$item3 = Item::create([
'sub_category_id' => 4,
'name' => $faker->name,
'description' => $faker->text,
'type' => $faker->word,
'price' => $faker->randomNumber(4),
'quantity_in_stock' => $faker->randomNumber(2),
]);
$item4 = Item::create([
'sub_category_id' => 4,
'name' => $faker->name,
'description' => $faker->text,
'type' => $faker->word,
'price' => $faker->randomNumber(1),
'quantity_in_stock' => $faker->randomNumber(3),
]);
// Now that we have users and items, let's make user1 place a couple of orders
$order1 = Order::create([
'status' => 'confirmed',
'total_value' => $faker->randomNumber(3),
'taxes' => $faker->randomNumber(1),
'shipping_charges' => $faker->randomNumber(2),
'user_id' => $user1->id
]);
$order2 = Order::create([
'status' => 'waiting',
'total_value' => $faker->randomNumber(3),
'taxes' => $faker->randomNumber(1),
'shipping_charges' => $faker->randomNumber(2),
'user_id' => $user1->id
]);
// now, assigning items to orders
$order1->items()->attach($item1);
$order1->items()->attach($item2);
$order1->items()->attach($item3);
$order2->items()->attach($item1);
$order2->items()->attach($item4);
// and finally, create invoices
$invoice1 = Invoice::create([
'raised_at' => $faker->dateTimeThisMonth(),
'status' => 'settled',
'totalAmount' => $faker->randomNumber(3),
'order_id' => $order1->id,
]);
}
}
And now we set up the database again and seed it:
$ php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (43.81ms)
Migrating: 2021_01_26_093326_create_categories_table
Migrated: 2021_01_26_093326_create_categories_table (2.20ms)
Migrating: 2021_01_26_140845_create_sub_categories_table
Migrated: 2021_01_26_140845_create_sub_categories_table (4.56ms)
Migrating: 2021_01_26_141421_create_items_table
Migrated: 2021_01_26_141421_create_items_table (5.79ms)
Migrating: 2021_01_26_144157_create_orders_table
Migrated: 2021_01_26_144157_create_orders_table (6.40ms)
Migrating: 2021_01_27_093127_create_item_order_table
Migrated: 2021_01_27_093127_create_item_order_table (4.66ms)
Migrating: 2021_01_27_101116_create_invoices_table
Migrated: 2021_01_27_101116_create_invoices_table (6.70ms)
Migrating: 2021_01_31_145806_create_transactions_table
Migrated: 2021_01_31_145806_create_transactions_table (6.09ms)
Database seeding completed successfully.
All, right! Now is the final part of this article, where we simply access these relationships and confirm all that we’ve learned so far. You’ll be delighted to know (I hope) that this will be a lightweight and fun section.
And now, let’s fire up the most fun Laravel component — the Tinker interactive console!
$ php artisan tinker
Psy Shell v0.10.6 (PHP 8.0.0 — cli) by Justin Hileman
>>>
Accessing one-to-one model relationships in Laravel Eloquent
Okay, so, first, let’s access the one-to-one relationship we have in our models of order and invoice:
>>> $order = Order::find(1);
[!] Aliasing 'Order' to 'App\Models\Order' for this Tinker session.
=> App\Models\Order {#4108
id: 1,
status: "confirmed",
total_value: 320,
taxes: 5,
shipping_charges: 12,
user_id: 1,
}
>>> $order->invoice
=> App\Models\Invoice {#4004
id: 1,
raised_at: "2021-01-21 19:20:31",
status: "settled",
totalAmount: 314,
order_id: 1,
}
Notice something? Remember that the way it’s been done at the database-level, this relationship is a one-to-many, if not for the extra constraints. So, Laravel could’ve returned a collection of objects (or only one object) as the result, and that would be technically accurate. BUT . . . we’ve told Laravel that it’s a one-to-one relationship, so, the result is a single Eloquent instance. Notice how the same thing happens when accessing the inverse relationship:
$invoice = Invoice::find(1);
[!] Aliasing 'Invoice' to 'App\Models\Invoice' for this Tinker session.
=> App\Models\Invoice {#3319
id: 1,
raised_at: "2021-01-21 19:20:31",
status: "settled",
totalAmount: 314,
order_id: 1,
}
>>> $invoice->order
=> App\Models\Order {#4042
id: 1,
status: "confirmed",
total_value: 320,
taxes: 5,
shipping_charges: 12,
user_id: 1,
}
Accessing one-to-many model relationships in Laravel Eloquent
We have a one-to-many relationship between users and orders. Let’s “tinker” with it now and see the output:
>>> User::find(1)->orders;
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
=> Illuminate\Database\Eloquent\Collection {#4291
all: [
App\Models\Order {#4284
id: 1,
status: "confirmed",
total_value: 320,
taxes: 5,
shipping_charges: 12,
user_id: 1,
},
App\Models\Order {#4280
id: 2,
status: "waiting",
total_value: 713,
taxes: 4,
shipping_charges: 80,
user_id: 1,
},
],
}
>>> Order::find(1)->user
=> App\Models\User {#4281
id: 1,
name: "Dallas Kshlerin",
}
Exactly as expected, accessing a user’s orders results in a collection of records, while the inverse produces just one single User
object. In other words, one-to-many.
Accessing many-to-many model relationships in Laravel Eloquent
Now, let’s explore a relationship that’s many-to-many. We have one such relationship between items and orders:
>>> $item1 = Item::find(1);
[!] Aliasing 'Item' to 'App\Models\Item' for this Tinker session.
=> App\Models\Item {#4253
id: 1,
name: "Russ Kutch",
description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",
type: "adipisci",
price: 26,
quantity_in_stock: 65,
sub_category_id: 2,
}
>>> $order1 = Order::find(1);
=> App\Models\Order {#4198
id: 1,
status: "confirmed",
total_value: 320,
taxes: 5,
shipping_charges: 12,
user_id: 1,
}
>>> $order1->items
=> Illuminate\Database\Eloquent\Collection {#4255
all: [
App\Models\Item {#3636
id: 1,
name: "Russ Kutch",
description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",
type: "adipisci",
price: 26,
quantity_in_stock: 65,
sub_category_id: 2,
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4264
order_id: 1,
item_id: 1,
},
},
App\Models\Item {#3313
id: 2,
name: "Mr. Green Cole",
description: "Maxime beatae porro commodi fugit hic. Et excepturi natus distinctio qui sit qui. Est non non aut necessitatibus aspernatur et aspernatur et. Voluptatem possimus consequatur exercitationem et.",
type: "pariatur",
price: 381,
quantity_in_stock: 82,
sub_category_id: 2,
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4260
order_id: 1,
item_id: 2,
},
},
App\Models\Item {#4265
id: 3,
name: "Brianne Weissnat IV",
description: "Delectus ducimus quia voluptas fuga sed eos esse. Rerum repudiandae incidunt laboriosam. Ea eius omnis autem. Cum pariatur aut voluptas sint aliquam.",
type: "non",
price: 3843,
quantity_in_stock: 26,
sub_category_id: 4,
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4261
order_id: 1,
item_id: 3,
},
},
],
}
>>> $item1->orders
=> Illuminate\Database\Eloquent\Collection {#4197
all: [
App\Models\Order {#4272
id: 1,
status: "confirmed",
total_value: 320,
taxes: 5,
shipping_charges: 12,
user_id: 1,
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4043
item_id: 1,
order_id: 1,
},
},
App\Models\Order {#4274
id: 2,
status: "waiting",
total_value: 713,
taxes: 4,
shipping_charges: 80,
user_id: 1,
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4257
item_id: 1,
order_id: 2,
},
},
],
}
This output can be a bit dizzying to read through but notice that item1 is part of order1’s items, and vice versa, which is how we set things up. Let’s also peek into the intermediate table that stores the mappings:
>>> use DB;
>>> DB::table('item_order')->select('*')->get();
=> Illuminate\Support\Collection {#4290
all: [
{#4270
+"order_id": 1,
+"item_id": 1,
},
{#4276
+"order_id": 1,
+"item_id": 2,
},
{#4268
+"order_id": 1,
+"item_id": 3,
},
{#4254
+"order_id": 2,
+"item_id": 1,
},
{#4267
+"order_id": 2,
+"item_id": 4,
},
],
}
Conclusion
Yes, this is it, really! It’s been a really long article, but I hope it has been useful. Is that all one needs to know about Laravel models?
Sadly, no. The rabbit hole is really, really deep, and there are many more challenging concepts such as Polymorphic Relationships and performance tuning, and whatnot, which you’ll encounter as you grow as a Laravel developer. For now, what this article covers is enough for 70% of the developers 70% of the time, roughly speaking. It will be really long before you’ll feel the need to upgrade your knowledge.
With that caveat out of the way, I want you to take away this most important insight: nothing is dark magic or out of reach in programming. It’s only that we don’t understand the foundations and how things are built, which makes us struggle and feel frustrated.
So . . . ?
Invest in yourself! Courses, books, articles, other programming communities (Python is my #1 recommendation) — use whatever resources you can find and consume them regularly if slowly. Pretty soon, the number of instances where you’re likely to botch the whole thing will diminish drastically.
Okay, enough preaching. Have a nice day! 🙂