Content Index
- What are morphic relationships in Laravel and why do they exist?
- Difference between normal relationships and polymorphic relationships
- How morphic relationships work in Eloquent
- What columns morphs() actually creates
- Types of morphic relationships in Laravel
- One-to-one morphic relationship (1:1)
- Many-to-many morphic relationship (N:M)
- Example: shopping cart with polymorphic products
- The problem: different types of products with different columns
- Why a Tutorial does not behave the same as a Book
- How to limit columns in a morphic relationship using constrain()
- Defining different selects for each model
- Customizing columns in selection within Morphic Relationships
- Complete example with ShoppingCart, Tutorial, and Book
- Why this solution avoids errors and improves performance
- Common errors when working with morphic relationships in Laravel
- Best practices when using morphic relationships in real projects
- Frequently asked questions about morphic relationships in Laravel
- Conclusion: when morphic relationships really make a difference
If you've ever thought while working with Laravel, “this should be solvable with a single relationship,” you were probably about to discover morphic relationships. In Eloquent, these types of relationships allow a model to be associated with different models without duplicating tables or logic, something especially useful when the business domain starts to grow.
In this guide, I'm going to explain what morphic relationships are in Laravel, how they work, the types that exist, and above all, how to solve real problems that appear when models do not share exactly the same columns.
What are morphic relationships in Laravel and why do they exist?
Morphic relationships (or polymorphic relationships) allow a model to belong to more than one type of model using a single relationship.
Instead of having multiple tables or multiple foreign keys, Laravel saves two key columns:
- *_id → the ID of the related model
- *_type → the class name of the related model
This makes it possible for, for example, a comments table to belong to both Post and Video without duplicating structure.
The great advantage is flexibility: you can extend your system without modifying the base schema every time a new related model appears.
Difference between normal relationships and polymorphic relationships
In a traditional relationship, the link is fixed:
a comment belongs to a post, an invoice belongs to a customer, etc.
In a morphic relationship, that link changes dynamically per record.
A comment can belong to a post or a video.
An image can belong to a user or a product.
This reduces maintenance, avoids repeated tables, and keeps the data model clean.
How morphic relationships work in Eloquent
Laravel abstracts all the complexity through very clear methods.
morphTo, morphOne, and morphMany explained simply
- morphTo() → used in the “child” model
- morphOne() → one-to-one relationship
- morphMany() → one-to-many relationship
For example, the model that "receives" the relationship always uses morphTo().
What columns morphs() actually creates
When you use:
$table->morphs('commentable');Laravel automatically creates:
- commentable_id
- commentable_type
And it is Eloquent, not the database, that interprets which model each record belongs to.
What Laravel saves in *_id and *_type
- *_id: the actual ID of the related model
- *_type: the full class name (App\Models\Post, for example)
This is important to understand because later we will see why Laravel might try to read columns that do not exist if we do not control the relationship well.
Types of morphic relationships in Laravel
One-to-one morphic relationship (1:1)
A model has a single related record, but that record can belong to different models.
Typical example: profile images for users and posts.
One-to-many morphic relationship (1:N)
A model can have many related records, such as comments on posts or videos.
It is the most common case and where polymorphism is most utilized.
Many-to-many morphic relationship (N:M)
Here come scenarios like tags that can be associated with multiple different models using a polymorphic pivot table.
Example: shopping cart with polymorphic products
This is where things get interesting.
The cart could contain different types of products:
- Tutorials
- Books
Up to that point, everything is perfect for a morphic relationship.
The problem appeared when tutorials had additional options (like source code per lesson or exclusive lessons) that affected the price, while books had no extra modality.
The problem: different types of products with different columns
Tutorials had columns like:
- exclusive_extra
- price_code_extra
But the Book model does not have those columns, nor should it.
If I left the relationship defined generically, Laravel would try to read non-existent columns and the relationship would fail.
Why a Tutorial does not behave the same as a Book
Although both are “products” from the cart's point of view, they do not share exactly the same structure.
And this is where many theoretical examples fall short:
In real projects, polymorphic models are not always identical.
What happens if Laravel tries to read columns that do not exist
If you do an implicit select * in a morphic relationship, Eloquent does not know which columns exist or not in each model.
Result:
SQL errors or unnecessary queries that affect performance.
How to limit columns in a morphic relationship using constrain()
The solution was to explicitly define which columns Laravel should select according to the model.
And for that, constrain() exists.
Defining different selects for each model
In the cart model, the relationship looked like this:
class ShoppingCart extends Model { public function itemable() { return $this->morphTo()->constrain([ Tutorial::class => function ($query) { $query->select( 'id', 'title', 'url_clean', 'price', 'price_offers', 'exclusive_extra', 'price_code_extra' ); }, Book::class => function ($query) { $query->select( 'id', 'title', 'url_clean', 'price', 'price_offers' ); }, ]); } }Customizing columns in selection within Morphic Relationships
With a morphic relationship, I'm going to show you how you can get different columns. It's that simple. Although it sounds more complex than it is, it's exactly what you can see here.
I am building a shopping cart. Understand that, for my online store, products can be tutorials or books. What is the dilemma?
For example, for courses, I have some that include the option to decide if you want:
- Source code per lesson.
- Exclusive lessons.
This corresponds to the panel we have here, and since it influences the price, I definitely need to pass that information to the shopping cart.
This pair of columns is not defined in the book model.
In the case of the book:
It is only purchased in full, without additional modalities.
Otherwise, the other columns are exactly the same as those I have referenced below.
The syntax is simple:
class ShoppingCart extends Model
{
***
public function itemable()
{
return $this->morphTo()->constrain([
Tutorial::class => function ($query) {
$query->select('id', 'title', 'url_clean', 'price', 'price_offers', 'exclusive_extra', 'price_code_extra');
},
Book::class => function ($query) {
$query->select('id', 'title', 'url_clean', 'price', 'price_offers'); // , NULL as price_exclusive_extra
},
]);
}- In the method that indicates it is a morphic relationship, we create a constraint.
- We indicate each of the relationships we have.
- We define the columns that should exist in each case.
In the book, the definition is simpler, since it does not need additional columns.
In the course, those extra columns do need to be defined.
This prevents the relationship from failing by searching for non-existent columns, as would happen if it were left generically defined.
Complete example with ShoppingCart, Tutorial, and Book
Each model returns only the columns that actually exist, without forcing artificial structures.
In the case of the book, the relationship is simpler because it does not need additional data.
In the case of the tutorial, extra columns that directly influence the final price are included.
Why this solution avoids errors and improves performance
- Laravel does not look for non-existent columns
- Queries are lighter
- The code better reflects the real business domain
Since I implemented this, the cart stopped being fragile and became completely predictable.
Common errors when working with morphic relationships in Laravel
- Non-existent columns in polymorphic models
- Assuming all models share the same structure.
- Overloading queries unnecessarily
- Not limiting columns when you know exactly what you need.
- Confusing primary keys with polymorphic keys
- Morphic relationships do not use traditional foreign keys at the database level.
Best practices when using morphic relationships in real projects
When to use polymorphism
- Reusable entities
- Models extensible over time
- Domains where the type can vary
When NOT to use morphic relationships
- When relationships are fixed
- When you need strict referential integrity at the SQL level
Frequently asked questions about morphic relationships in Laravel
- Can I use different selects in morphTo?
- Yes, using constrain(), as in the cart example.
- Do morphic relationships affect performance?
- Not necessarily. If misused, yes. Well-controlled, they are very efficient.
- Can they be validated at the database level?
- Not directly. Validation is primarily handled from Eloquent.
Conclusion: when morphic relationships really make a difference
Morphic relationships in Laravel are not just a curiosity of the framework.
Properly used, they allow for flexible, maintainable, and scalable systems, even when models are not identical to each other.
In real scenarios (like a cart with different products) understanding how to control columns, queries, and behavior makes the difference between a fragile system and a solid one.