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