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:
- Polymorphic with Traits: Tags
- Polymorphic with Traits: Likes
- Same Relationship in Two Different Models
- Issues with Comments
- Get Model Type using Scopes
- One Model Has Many Polymorphic Relationships
- Polymorphic with UUID as Primary Key
- 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.
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.
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.
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.
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.
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.
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.
Already a member? Login here
Premium membership includes:
Comments & Discussion
Great article ! Thanks. Please how do you find laravel open source project on github ?
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.
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.
There's a few things here:
- It could be left over from the old days where it was popular to have it like that
- It could be just to have model auto-completion, as these functions are on the model
- 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
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
IDdatabase column,UUIDis used in this project. It's only that the primary column's name is stillID. 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) [...]