Installation:
composer require moneo/laravel-morphmap
No additional configuration or service provider registration is required.
Apply the Trait:
Add HasCustomMorphMap to your model:
use Moneo\LaravelMorphMap\HasCustomMorphMap;
class Post extends Model
{
use HasCustomMorphMap;
// ...
}
Define Custom Morph Maps:
Override the $customMorphMap property in your model’s constructor:
protected $customMorphMap = [
'author' => [
'App\Models\User' => 'user',
'App\Models\GuestAuthor' => 'guest',
],
'commenter' => [
'App\Models\User' => 'user',
'App\Models\AICommenter' => 'ai',
],
];
First Use Case:
Define a polymorphic relationship (e.g., belongsTo) and use the custom morph map:
public function author()
{
return $this->belongsTo(User::class, 'author_id')->setMorphClass('author');
}
Now, when querying Post::find(1)->author, the morph map for the author relation will be used.
Per-Relationship Morph Maps: Define distinct morph maps for each polymorphic relationship on a model. For example:
$customMorphMap = [
'owner' => ['App\Models\Admin' => 'admin', 'App\Models\User' => 'user'],
'moderator' => ['App\Models\Moderator' => 'mod', 'App\Models\SuperModerator' => 'supermod'],
];
Dynamic Morph Maps:
Override the $customMorphMap property dynamically based on conditions (e.g., tenant ID):
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->customMorphMap = $this->resolveMorphMapByTenant();
}
Integration with Eloquent Relationships: Use the trait with standard polymorphic relationships:
public function comments()
{
return $this->morphMany(Comment::class, 'commentable')->setMorphClass('commenter');
}
Fallback to Global Morph Map:
If a relationship lacks a custom morph map, Laravel’s global morphMap will be used automatically.
Migrating Existing Polymorphic Relations:
setMorphClass($relationName).$customMorphMap.API Versioning:
Use different morph maps for different API versions by conditionally setting $customMorphMap in the constructor.
Testing:
Mock the $customMorphMap property in tests to isolate behavior:
$post = new Post();
$post->customMorphMap = ['author' => ['App\Models\User' => 'test_user']];
Leverage with API Resources: Customize JSON:API or REST responses by mapping morph types to consistent API formats:
public function toArray($request)
{
return [
'author_type' => $this->author->getMorphClass(),
'author_data' => $this->author->toArray(),
];
}
Combine with Policies: Use morph maps to enforce access control:
public function authorize(Post $post, User $user)
{
return $post->author->getMorphClass() === 'admin' || $user->isAdmin();
}
Seeding Data: Ensure seeded polymorphic relations use the correct morph types:
Post::create([
'title' => 'Test Post',
'author_id' => 1,
'author_type' => 'user', // Explicitly set if needed
]);
Case Sensitivity:
Morph map keys (e.g., 'user') are case-sensitive. Ensure consistency:
// Correct:
$customMorphMap = ['author' => ['App\Models\User' => 'user']];
// Incorrect (will fail):
$customMorphMap = ['author' => ['App\Models\User' => 'User']];
Missing setMorphClass:
Forgetting to call setMorphClass($relationName) on a polymorphic relationship will cause the global morph map to be used instead of the custom one:
// Wrong: Uses global morphMap
return $this->belongsTo(User::class, 'author_id');
// Correct: Uses custom morphMap for 'author'
return $this->belongsTo(User::class, 'author_id')->setMorphClass('author');
Circular Dependencies:
Avoid circular references in morph maps (e.g., ModelA referencing ModelB which references ModelA with conflicting maps). Test thoroughly.
Database Consistency:
Ensure the *_type column in your database matches the keys defined in $customMorphMap. Mismatches will cause ModelNotFoundException.
Check Morph Resolution: Log the resolved morph class to debug:
dd($this->author->getMorphClass()); // Debug the actual resolved type
Verify Custom Morph Map:
Temporarily add a dump() to confirm the map is loaded:
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
dump($this->customMorphMap); // Debug in Tinker or tests
}
Query Builder Issues: If queries fail, check for typos in relationship names or morph map keys. Use Laravel’s query logging:
DB::enableQueryLog();
$post = Post::find(1);
dd(DB::getQueryLog());
Use Enums for Morph Types: Define morph types as enums for type safety and autocompletion:
enum AuthorType: string
{
case USER = 'user';
case GUEST = 'guest';
}
Then reference them in $customMorphMap:
$customMorphMap = ['author' => [User::class => AuthorType::USER->value]];
Leverage Model Events:
Dynamically update morph maps during events (e.g., retrieved):
protected static function boot()
{
static::retrieved(function ($model) {
if ($model->isDemoMode()) {
$model->customMorphMap['author'] = ['App\Models\User' => 'demo_user'];
}
});
}
Document Morph Maps: Add PHPDoc comments to clarify morph map usage:
/**
* Custom morph maps for polymorphic relations.
*
* @var array{
* author: array<class-string, string>, // Maps author relations
* commenter: array<class-string, string> // Maps commenter relations
* }
*/
protected $customMorphMap;
Performance: Cache the resolved morph maps if your application has high query volume:
protected function getCustomMorphMap(string $relation): array
{
return cache()->remember("morphmap_{$this->id}_{$relation}", now()->addHours(1), function () {
return $this->customMorphMap[$relation] ?? [];
});
}
(Note: Requires extending the trait or using a macro.)
Testing Edge Cases: Test with:
How can I help you explore Laravel packages today?