Polymorphic Relations in Laravel: 8 Open-Source Practical Examples

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

The full tutorial [14 mins, 2675 words] is only for Premium Members

Login Or Become a Premium Member for $129/year or $29/month
What else you will get:
  • 64 courses (1141 lessons, total 42 h 01 min)
  • 88 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent New Courses