pindab0ter/constrained-morph-to-for-laravel
composer require pindab0ter/constrained-morph-to-for-laravel
HasConstrainedMorphTo trait in your model and define a constrained morph-to relationship:
use pindab0ter\ConstrainedMorphtoForLaravel\HasConstrainedMorphTo;
class Comment extends Model
{
use HasConstrainedMorphTo;
public function commentable()
{
return $this->constrainedMorphTo(Post::class, 'commentable_type', 'commentable_id');
}
}
$post = Post::create(['title' => 'Test Post']);
$comment = Comment::create([
'commentable_id' => $post->id,
'commentable_type' => Post::class,
]);
// Returns the Post instance
$comment->commentable;
// Returns null (invalid type)
$user = User::create(['name' => 'Test User']);
$invalidComment = Comment::create([
'commentable_id' => $user->id,
'commentable_type' => User::class,
]);
$invalidComment->commentable;
Single-Type Constraints:
public function post()
{
return $this->constrainedMorphTo(Post::class, 'post_type', 'post_id');
}
Comment → Post).Multi-Type Constraints:
public function commentable()
{
return $this
->constrainedMorphTo([Post::class, Video::class], 'commentable_type', 'commentable_id');
}
Comment → Post or Video).Custom Column Names:
public function owner()
{
return $this->constrainedMorphTo(
User::class,
'owner_model_type', // Custom type column
'owner_model_id', // Custom ID column
'owner' // Custom relationship name
);
}
*_type, *_id).Integration with morphMap:
class Comment extends Model
{
protected $morphClass = Comment::class;
public function commentable()
{
return $this->constrainedMorphTo(
['App\Models\Post', 'App\Models\Video'],
'commentable_type',
'commentable_id'
);
}
}
morphMap entries exist for all allowed types if using custom model names./** @return ConstrainedMorphTo<Post, $this> */
public function post() { ... }
// Check for invalid morph types in a migration
DB::table('comments')
->whereNotIn('commentable_type', [Post::class, Video::class])
->update(['commentable_type' => null]);
null IDs, non-existent models).with() or load() eager loading.Dynamic Constraints:
public function dynamicOwner()
{
$allowedTypes = $this->getAllowedOwnerTypes(); // Fetch from config/API
return $this->constrainedMorphTo($allowedTypes, 'owner_type', 'owner_id');
}
Composite Constraints:
public function constrainedMorphToWithScope()
{
return $this->constrainedMorphTo(
Post::class,
'type',
'id'
)->where('published', true);
}
Event Listeners:
// Ensure constraints are validated on creation
protected static function booted()
{
static::creating(function ($model) {
if (!$model->isValidMorphType()) {
throw new \InvalidArgumentException('Invalid morph type');
}
});
}
Silent Failures:
null for invalid types. Tip: Add a custom accessor to throw exceptions:
public function getCommentableOrFail()
{
return $this->commentable ?: throw new \RuntimeException('Invalid commentable type');
}
Case-Sensitive Class Names:
commentable_type in the database exactly matches the FQCN (e.g., App\Models\Post vs. app\Models\Post). Tip: Normalize class names in migrations:
DB::table('comments')->update([
'commentable_type' => str_replace('\\', '\\\\', Post::class),
]);
Caching Issues:
null results. Tip: Clear caches after updating constraints:
php artisan cache:clear
Morph Map Conflicts:
morphMap, ensure all constrained types are registered. Tip: Add a check in a model observer:
Morph::register([Post::class => 'posts']);
Serialization Quirks:
null for invalid morphs. Tip: Use ->makeHidden() or custom JSON serialization:
protected $hidden = ['commentable']; // Hide invalid relationships
public function commentable()
{
$result = $this->constrainedMorphTo([Post::class, Video::class], ...);
if ($result === null) {
\Log::warning("Invalid morph type for commentable: {$this->commentable_type}");
}
return $result;
}
SELECT commentable_type, commentable_id FROM comments WHERE commentable_type NOT IN ('App\Models\Post', 'App\Models\Video');
get_declared_classes() to ensure all constrained models are autoloaded:
dd(in_array(Post::class, get_declared_classes()));
Custom Constraint Logic: Override the constraint check in a service provider:
ConstrainedMorphTo::macro('customConstraint', function ($callback) {
return $this->constrainedMorphTo(...)->when(
fn ($query) => $callback($query)
);
});
Add Validation Rules: Extend Laravel’s validation to reject invalid morph types:
use Illuminate\Validation\Rule;
$validator = Validator::make($data, [
'commentable_type' => [
'required',
Rule::in([Post::class, Video::class]),
],
]);
Integrate with Policies: Use constraints in authorization:
public function authorize()
{
return $this->commentable instanceof Post;
}
config/relationships.php file for consistency:
return [
'commentable' => [
'type_column' => 'commentable_type',
'id_column' => 'commentable_id',
'allowed_types' => [Post::class, Video::class],
],
];
morphMap compatibility:
composer require pindab0ter/constrained-morph-to-for-laravel:^1.2.0
How can I help you explore Laravel packages today?