Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here
Premium Members Only
Join to unlock this tutorial and all of our courses.
Tutorial Premium Tutorial

Polymorphic Relations in Laravel: 8 Open-Source Practical Examples

September 27, 2024
14 min read

Polymorphic relationships are one of the most complex relationships in Laravel. In this post, let's examine eight examples from Laravel open-source projects and how they use them.

By the end of this article, I hope you will see a general pattern of when to use polymorphic relationships.

The article is very long, so here's the Table of Contents:

  1. Polymorphic with Traits: Tags
  2. Polymorphic with Traits: Likes
  3. Same Relationship in Two Different Models
  4. Issues with Comments
  5. Get Model Type using Scopes
  6. One Model Has Many Polymorphic Relationships
  7. Polymorphic with UUID as Primary Key
  8. Reusable with Traits & Only One Type of Address

Let's dive in!


Example 1: Polymorphic with Traits: Tags

The first example is from an open-source project called laravelio/laravel.io. This project has more than one polymorphic relationship, but we will look at the tags example.

The relationship is described in a trait.

app/Concerns/HasTags.php:

use App\Models\Tag;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
trait HasTags
{
public function tags(): Collection
{
return $this->tagsRelation;
}
 
public function syncTags(array $tags)
{
$this->save();
$this->tagsRelation()->sync($tags);
 
$this->unsetRelation('tagsRelation');
}
 
public function removeTags()
{
$this->tagsRelation()->detach();
 
$this->unsetRelation('tagsRelation');
}
 
public function tagsRelation(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable')->withTimestamps();
}
}

What's different here is that when you regularly define a relationship, this is defined in a method with a name of relationship and a suffix of Relation. Then, this method is called when in the actual relationship method.

This trait is used in two models, one of them is Article.

app/Models/Article.php:

use App\Concerns\HasTags;
use Illuminate\Database\Eloquent\Model;
use Spatie\Feed\Feedable;
 
final class Article extends Model implements Feedable
{
use HasAuthor;
use HasFactory;
use HasLikes;
use HasSlug;
use HasTags;
 
// ...
}

Then, when creating an article, the syncTags() method from the trait is used.

app/Jobs/CreateArticle.php:

use App\Events\ArticleWasSubmittedForApproval;
use App\Models\Article;
 
final class CreateArticle
{
// ...
 
public function handle(): void
{
$article = new Article([
'uuid' => $this->uuid->toString(),
'title' => $this->title,
'body' => $this->body,
'original_url' => $this->originalUrl,
'slug' => $this->title,
'submitted_at' => $this->shouldBeSubmitted ? now() : null,
]);
$article->authoredBy($this->author);
$article->syncTags($this->tags);
 
if ($article->isAwaitingApproval()) {
event(new ArticleWasSubmittedForApproval($article));
}
}
}

Example 2: Polymorphic with Traits: Likes

This example comes from the guillaumebriday/laravel-blog open-source project. Here, we have an example for the likes.

First, in this project, the likeable morphs database columns are nullable, and created using the nullableMorphs() Laravel helper.

database/migrations/# 2017_11_15_003340_create_likes_table.php:

Schema::create('likes', function (Blueprint $table) {
$table->increments('id');
$table->integer('author_id')->unsigned();
$table->foreign('author_id')->references('id')->on('users');
 
$table->nullableMorphs('likeable');
 
$table->timestamps();
});

Similar to the previous example, the relationship is described in a trait.

app/Concern/Likeable.php:

use App\Models\Like;
use Illuminate\Database\Eloquent\Relations\morphMany;
 
trait Likeable
{
protected static function bootLikeable(): void
{
static::deleting(fn ($resource) => $resource->likes->each->delete());
}
 
public function likes(): morphMany
{
return $this->morphMany(Like::class, 'likeable');
}
 
public function like()
{
if ($this->likes()->where('author_id', auth()->id())->doesntExist()) {
return $this->likes()->create(['author_id' => auth()->id()]);
}
}
 
public function isLiked(): bool
{
return $this->likes->where('author_id', auth()->id())->isNotEmpty();
}
 
public function dislike()
{
return $this->likes()->where('author_id', auth()->id())->get()->each->delete();
}
}

The trait is used in two models. Let's take a look at the Post Model.

app/Models/Post.php:

use App\Concern\Likeable;
use Illuminate\Database\Eloquent\Model;
 
class Post extends Model
{
use HasFactory, Likeable;
 
// ...
}

And finally, the methods for like or dislike from the trait are used in the Controller.

app/Http/Controllers/PostLikeController.php:

use App\Models\Post;
use Illuminate\Support\Str;
use Tonysm\TurboLaravel\Http\MultiplePendingTurboStreamResponse;
 
use function Tonysm\TurboLaravel\dom_id;
 
class PostLikeController extends Controller
{
public function store(Post $post): MultiplePendingTurboStreamResponse
{
$post->like();
 
return response()->turboStream([
response()->turboStream()->replace(dom_id($post, 'like'))->view('likes._like', ['post' => $post]),
response()->turboStream()->update(dom_id($post, 'likes_count'), Str::of($post->likes()->count()))
]);
}
 
public function destroy(Post $post): MultiplePendingTurboStreamResponse
{
$post->dislike();
 
return response()->turboStream([
response()->turboStream()->replace(dom_id($post, 'like'))->view('likes._like', ['post' => $post]),
response()->turboStream()->update(dom_id($post, 'likes_count'), Str::of($post->likes()->count()))
]);
}
}

Example 3: Same Relationship in Two Different Models

This example comes from the serversideup/financial-freedom open-source project. In this project, the same accountable polymorphic relationship exists but in two different models, and the accountable_type is different.

app/Models/Rule.php:

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Rule extends Model
{
// ...
 
public function accountable()
{
return $this->morphTo();
}
 
// ...
}

Modules/Transaction/app/Models/Transaction.php:

class Transaction extends Model
{
// ...
 
public function accountable()
{
return $this->morphTo();
}
 
// ...
}

When a rule or transaction is created, the account model is passed in, and the class is taken using the get_class() method. Example from storing a rule:

app/Services/Rules/StoreRule.php:

use App\Data\Rules\StoreRuleData;
use App\Models\CashAccount;
use App\Models\CreditCard;
use App\Models\Loan;
use App\Models\Rule;
 
class StoreRule
{
public function execute( StoreRuleData $data )
{
$account = $this->findAccount( $data->account );
 
Rule::create([
'accountable_id' => $account->id,
'accountable_type' => get_class( $account ),
'search_string' => $data->searchString,
'replace_string' => $data->replaceString,
'category_id' => $data->category['id'],
]);
}
 
private function findAccount( $account )
{
switch( $account['type'] ){
case 'cash-account':
return CashAccount::find( $account['id'] );
break;
case 'credit-card':
return CreditCard::find( $account['id'] );
break;
case 'loan':
return Loan::find( $account['id'] );
break;
}
}
}

Example 4: Issues with Comments

This example comes from...

Premium Members Only

This advanced tutorial is available exclusively to Laravel Daily Premium members.

Premium membership includes:

Access to all premium tutorials
Video and Text Courses
Private Discord Channel

Comments & Discussion

P
pleveris ✓ Link copied!

Great article, many interesting use-cases!

Just noticed a typo in one of the sample links in the example 7: [...] The difference from other examples is that instead of an ID database column, UUID is used in this project. It's only that the primary column's name is still ID. But, when using UUID as a primary key Laravel have a [uuidMorphs()](https://laraThis example comes from the guillaumebriday/laravel-blog open-source project.vel.com/docs/migrations#column-method-uuidMorphs) [...]

A
angel ✓ Link copied!

Great article ! Thanks. Please how do you find laravel open source project on github ?

N
Nerijus ✓ Link copied!

It's a bit hard to answer such question. Some repositories are from the X (twitter) where people share them, some are sent directly to Povilas by email, some are from reddit or similar. There's also https://madewithlaravel.com.

JC
Jon Cody ✓ Link copied!

can you explain why in the first project laravelio/laravel.io, on their articles model, what the advantage or purpose is fo defining a function for every attribute?

to end up with {{ $article->title() }} in blade verses {{ $article->title }}

based on this
public function title(): string { return $this->title; }

I can potentially see the use of this, if those values have to be transformed somehow, but to simply return the data, i don't see the point of the extra code.

M
Modestas ✓ Link copied!

There's a few things here:

  1. It could be left over from the old days where it was popular to have it like that
  2. It could be just to have model auto-completion, as these functions are on the model
  3. Fully open code - you go into model and see everthing there :)

But to be fair, I'm not a fan of this approach. Just feels weird, and if I would do that - I would use getters logic (like getTitle()) instead of this :)

ps. For mutating - there's an easier and cleaner way using attributes

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.