Polymorphic Many to Many

Polymorphic Many to Many and Custom Morph Types

Tags attach to products and categories through taggables (tag_id plus morphs('taggable'), all three the primary key). Product::tags() and Category::tags() return $this->morphToMany(Tag::class, 'taggable'); the tag declares one morphedByMany per owner, such as products() returning $this->morphedByMany(Product::class, 'taggable').

Rename a model and every row storing its class name stops matching. A morph map stores aliases instead: AppServiceProvider::boot() calls Relation::enforceMorphMap(['product' => Product::class, 'category' => Category::class, 'customer' => Customer::class]), and the enforce variant throws ClassMorphViolationException for any model you forgot to map.

Tags in both directions, and migrating rows written before the mapSQL
use App\Models\{Product, Tag};
use Illuminate\Database\Eloquent\Relations\Relation;
$pick = Tag::firstWhere('name', 'staff-pick');
echo $pick->products->pluck('title')->join(', '), ' | ',
    $pick->categories->pluck('name'), "\n";
echo Product::find(4)->tags->pluck('name')->join(', '), "\n";
echo (new Product)->getMorphClass(), ', images before: ', Product::find(1)->images()->count();
foreach (Relation::morphMap() as $alias => $class) {
    DB::table('images')->where('imageable_type', $class)->update(['imageable_type' => $alias]);
}
echo ', after: ', Product::find(1)->images()->count(), "\n";
Output
Laravel From the Ground Up | ["Databases"]
bestseller, staff-pick
product, images before: 0, after: 2

The images rows still held class names, so with the map active the first count found nothing. Ship such a conversion as a migration with the map, or register the map before the first polymorphic row exists.