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.

Understanding Model Relationships in Laravel Eloquent Development PHP

(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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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.

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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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:

Understanding Model Relationships in Laravel Eloquent Development PHP

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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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 list.

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:

Understanding Model Relationships in Laravel Eloquent Development PHP

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 11 and 12 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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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:

Understanding Model Relationships in Laravel Eloquent Development PHP

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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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!

Understanding Model Relationships in Laravel Eloquent Development PHP

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?

Understanding Model Relationships in Laravel Eloquent Development PHP

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. 🙂

Understanding Model Relationships in Laravel Eloquent Development PHP

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:

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:

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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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:

belongsTo(User::class);
    }

    public function invoice() {
        return $this->hasOne(Invoice::class);
    }
}

And the invoice model:

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:

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:

 $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.

Understanding Model Relationships in Laravel Eloquent Development PHP

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 'AppModelsOrder' for this Tinker session.
=> AppModelsOrder {#4108
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }
>>> $order->invoice
=> AppModelsInvoice {#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 'AppModelsInvoice' for this Tinker session.
=> AppModelsInvoice {#3319
     id: 1,
     raised_at: "2021-01-21 19:20:31",
     status: "settled",
     totalAmount: 314,
     order_id: 1,
   }
>>> $invoice->order
=> AppModelsOrder {#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 'AppModelsUser' for this Tinker session.
=> IlluminateDatabaseEloquentCollection {#4291
     all: [
       AppModelsOrder {#4284
         id: 1,
         status: "confirmed",
         total_value: 320,
         taxes: 5,
         shipping_charges: 12,
         user_id: 1,
       },
       AppModelsOrder {#4280
         id: 2,
         status: "waiting",
         total_value: 713,
         taxes: 4,
         shipping_charges: 80,
         user_id: 1,
       },
     ],
   }
>>> Order::find(1)->user
=> AppModelsUser {#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 'AppModelsItem' for this Tinker session.
=> AppModelsItem {#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);
=> AppModelsOrder {#4198
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }
>>> $order1->items
=> IlluminateDatabaseEloquentCollection {#4255
     all: [
       AppModelsItem {#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: IlluminateDatabaseEloquentRelationsPivot {#4264
           order_id: 1,
           item_id: 1,
         },
       },
       AppModelsItem {#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: IlluminateDatabaseEloquentRelationsPivot {#4260
           order_id: 1,
           item_id: 2,
         },
       },
       AppModelsItem {#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: IlluminateDatabaseEloquentRelationsPivot {#4261
           order_id: 1,
           item_id: 3,
         },
       },
     ],
   }
>>> $item1->orders
=> IlluminateDatabaseEloquentCollection {#4197
     all: [
       AppModelsOrder {#4272
         id: 1,
         status: "confirmed",
         total_value: 320,
         taxes: 5,
         shipping_charges: 12,
         user_id: 1,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4043
           item_id: 1,
           order_id: 1,
         },
       },
       AppModelsOrder {#4274
         id: 2,
         status: "waiting",
         total_value: 713,
         taxes: 4,
         shipping_charges: 80,
         user_id: 1,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#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();
=> IlluminateSupportCollection {#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! 🙂