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:
Let's dive in!
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)); } }}
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())) ]); }}
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; } }}
This example comes from the nafiesl/free-pmo open-source project. In this project, the relationship is for the comments, which is a very good use case for polymorphic relationships. In this project, comments have a project, an issue, a project, and a job. The relationship is defined as a morphMany
.
app/Entities/Projects/Issue.php:
class Issue extends Model{ // ... public function comments() { return $this->morphMany(Comment::class, 'commentable'); }}
When creating a comment for the issue you do it as with any other relationship.
app/Http/Controllers/Issues/CommentController.php:
use App\Entities\Projects\Comment;use App\Entities\Projects\Issue;use App\Http\Controllers\Controller;use Illuminate\Http\Request; class CommentController extends Controller{ public function store(Request $request, Issue $issue) { $this->authorize('comment-on', $issue); $newComment = $request->validate([ 'body' => 'required|string|max:255', ]); $issue->comments()->create([ 'body' => $newComment['body'], 'creator_id' => auth()->id(), ]); $issue->touch(); flash(__('comment.created'), 'success'); return back(); } // ...}
Getting records or eager loading is the same as with any other relationship.
app/Http/Controllers/Projects/IssueController.php:
use App\Entities\Projects\Comment;use App\Entities\Projects\Issue;use App\Entities\Projects\IssueStatus;use App\Entities\Projects\Priority;use App\Entities\Projects\Project;use App\Entities\Users\User;use App\Http\Controllers\Controller; class IssueController extends Controller{ // ... public function show(Project $project, Issue $issue) { $editableComment = null; $priorities = Priority::toArray(); $statuses = IssueStatus::toArray(); $users = User::pluck('name', 'id'); $comments = $issue->comments()->with('creator')->get(); if (request('action') == 'comment-edit' && request('comment_id') != null) { $editableComment = Comment::find(request('comment_id')); } return view('projects.issues.show', compact( 'project', 'issue', 'users', 'statuses', 'priorities', 'comments', 'editableComment' )); } // ...}
This example comes from the tighten/onramp open-source project. In this project, there is a completable
polymorphic relationship. A user can complete a module, resource, or skill.
A Model, for example, Module
, has a morphMany
relationship.
use App\Completable;use Illuminate\Database\Eloquent\Model; class Module extends Model implements Completable{ // ... public function completions() { return $this->morphMany(Completion::class, 'completable'); } // ...}
A user has multiple completions. However, from the completions
relation, scopes are used to get completions by type.
use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable{ // ... public function completions() { return $this->hasMany(Completion::class); } public function complete(Completable $completable) { return $this->completions()->create([ 'completable_id' => $completable->getKey(), 'completable_type' => $completable->getMorphClass(), ]); } public function undoComplete($completable) { return $this->completions()->where([ 'completable_id' => $completable->getKey(), 'completable_type' => $completable->getMorphClass(), ])->delete(); } public function moduleCompletions() { return $this->completions()->modules(); } public function resourceCompletions() { return $this->completions()->resources(); } public function skillCompletions() { return $this->completions()->skills(); } // ...}
The scopes are in the Completion
Model, which checks the completable_type
of the corresponding models class.
use Illuminate\Database\Eloquent\Model; class Completion extends Model{ // ... public function completable() { return $this->morphTo(); } public function scopeModules($query) { return $query->where('completable_type', (new Module)->getMorphClass()); } public function scopeResources($query) { return $query->where('completable_type', (new Resource)->getMorphClass()); } public function scopeSkills($query) { return $query->where('completable_type', (new Skill)->getMorphClass()); }}
Using methods from the User
Model, completions are made in the Controllers.
app/Http/Controllers/ModuleController.php:
use App\Facades\Preferences;use App\Models\Module;use Illuminate\View\View; class ModuleController extends Controller{ public function index(): View { return view('modules.index', [ 'pageTitle' => __('Modules'), 'standardModules' => auth()->check() && auth()->user()->hasTrack() ? Module::with('resourcesForCurrentSession')->standard()->get() : Module::standard()->get(), 'bonusModules' => auth()->check() && auth()->user()->hasTrack() ? Module::with('resourcesForCurrentSession')->bonus()->get() : Module::bonus()->get(), 'userModules' => auth()->check() && auth()->user()->hasTrack() ? auth()->user()->track->modules()->get()->pluck('id') : collect([]), 'completedModules' => auth()->check() ? auth()->user()->moduleCompletions()->pluck('completable_id') : collect([]), 'userResourceCompletions' => auth()->check() && auth()->user()->hasTrack() ? auth()->user()->resourceCompletions()->get()->pluck('completable_id') : collect([]), ]); } public function show($locale, Module $module, $resourceType): View { return view('modules.show', [ 'pageTitle' => $module->name, 'module' => $module, 'resources' => $module->resourcesForCurrentSession, 'skills' => $module->skills, 'completedModules' => auth()->check() ? auth()->user()->moduleCompletions()->pluck('completable_id') : collect([]), 'completedResources' => auth()->check() ? auth()->user()->resourceCompletions()->pluck('completable_id') : collect([]), 'completedSkills' => auth()->check() ? auth()->user()->skillCompletions()->pluck('completable_id') : collect([]), 'currentResourceLanguagePreference' => Preferences::get('resource-language'), 'resourceType' => $resourceType, 'previousModule' => $module->getPrevious(), 'nextModule' => $module->getNext(), ]); }}
This example comes from the academico-sis/academico open-source project. In this project, we have a phoneable
polymorphic relationship. A student and a contact can have many phones.
use Illuminate\Database\Eloquent\Model;use Spatie\MediaLibrary\HasMedia; class Student extends Model implements HasMedia{ // ... public function phone() { return $this->morphMany(PhoneNumber::class, 'phoneable'); } // ...}
use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;use Illuminate\Database\Eloquent\Relations\MorphMany; class Contact extends Model{ // ... public function phone(): MorphMany { return $this->morphMany(PhoneNumber::class, 'phoneable'); } // ...}
Example of how many phone numbers are saved for the student.
app/Http/Controllers/Admin/StudentCrudController.php:
use App\Models\PhoneNumber;use App\Models\Student; class StudentCrudController extends CrudController{ // ... public function store() { // ... $student = Student::create([ 'id' => $user->id, 'idnumber' => $request->idnumber, 'address' => $request->address, 'city' => $request->city, 'state' => $request->state, 'country' => $request->country, 'birthdate' => $request->birthdate, 'gender_id' => $request->gender_id, ]); // save phone number if ($request->phone) { foreach ($request->phone as $phone_number) { $phone_number = PhoneNumber::firstOrCreate(['phone_number' => $phone_number['phone_number'], 'phoneable_id' => $student->id, 'phoneable_type' => Student::class]); $student->phone()->save($phone_number); } } // ... } // ...}
This example comes from the idanieldrew/redact open-source project. After searching in this project for polymorphic relationships, there are at least four of them: mediaable, commentable, typeable, and statusable.
Defining these relationships in the Model isn't any different from other relationships.
Modules/Users/Models/User.php:
use Illuminate\Contracts\Auth\MustVerifyEmail;use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable implements MustVerifyEmail{ // ... public function types() { return $this->morphMany(Token::class, 'typeable'); } public function statuses() { return $this->morphMany(Status::class, 'statusable'); } // ...}
The same is true for creating a record with the polymorphic relationship.
Modules/User/Observers/v1/UserObserver.php:
use Illuminate\Support\Facades\App;use Module\Token\Services\v1\EmailVerify;use Module\Token\Services\v1\Verify;use Module\User\Models\User; class UserObserver{ // ... public function created(User $user) { $user->statuses()->create([ 'name' => 'pending', 'reason' => 'needs verification', ]); if (App::environment('local')) { $verify = new Verify; $verify->verify(new EmailVerify($user->withoutRelations())); } }}
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()
helper to create database columns for polymorphic relationship.
Modules/Status/Database/Migrations/2022_11_07_163620_create_statuses_table.php:
Schema::create('statuses', function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('name'); $table->string('reason'); $table->uuidMorphs('statusable'); $table->timestamps();});
This example comes from the mostafizurhimself/ecommerce open-source project. In this example, we have an addressable
polymorphic relationship where orders can have different addresses, such as different billing and shipping addresses.
The relationship is defined in a trait for re-usability. The address can have delivery, order, and customer.
backend/app/Traits/InteractsWithAddress.php:
use App\Models\Address; trait InteractsWithAddress{ public function address() { return $this->morphMany(Address::class, 'addressable'); } public function setAddress($address, $type) { $this->address()->updateOrCreate( [ 'type' => $type, ], [ 'street' => $address['street'], 'city' => $address['city'], 'state' => $address['state'] ?? null, 'zipcode' => $address['zipcode'] ?? null, 'country' => $address['country'], ] ); }}
However, while the model can have many addresses, it can only have one type. That's why the setAddress()
method uses the updateOrCreate()
, and the check is done only on the type
column.
Below is an example of address creation for the customer.
backend/app/Traits/CreateCustomer.php:
use App\Models\Customer;use App\Enums\AddressType; trait CreateCustomer{ public function createCustomer($request) { $customer = Customer::create(array_merge( $request->only('firstName', 'lastName', 'email', 'phone'), ['password' => bcrypt($request->password)] )); if (!empty($request->billingAddress)) { $customer->setAddress($request->billingAddress, AddressType::BILLING_ADDRESS()); } if (!empty($request->shippingAddress)) { $customer->setAddress($request->shippingAddress, AddressType::SHIPPING_ADDRESS()); } return $customer->id; }}
From the examples above, I hope you get a complete picture of when to use polymorphics.
This is a great way to have one common database table for repeating relationships, instead of many tables for each Model.