NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Authorization is off by default, so existing callers are unaffected. Opt in with comments.authorization (COMMENTS_AUTHORIZATION=true) and register a policy for the Comment model. The package ships a permissive starting point — every ability returns true — that you extend:

use Illuminate\Support\Facades\Gate;
use RoundlyConsulting\Comments\Models\Comment;
use RoundlyConsulting\Comments\Policies\CommentPolicy;

Gate::policy(Comment::class, CommentPolicy::class);

Abilities

AbilityChecked byArgument
createEvery write — builder, write(), writeComment()The subject — for a reply, the root subject it joins
updateComments::update()The Comment
deleteComments::delete(), deleteAll()The Comment
restoreComments::restore()The Comment
moderateComments::approve() / hide(), approveAll() / hideAll()The Comment
lock / unlockComments::lock() / unlock(), lockThread() / unlockThread(), lockReplies() / unlockReplies()The subject, or the Comment for a thread lock

Checks run against the currently authenticated user ($user is null for a guest), and a denial throws UnauthorizedCommentActionException. A policy registered for Comment also covers a subclass configured in comments.model; with nothing registered, Laravel’s auto-discovery resolves the shipped, permissive CommentPolicy.

Your own policy

create receives the subject being commented on — for a reply, the root subject it joins — so you can decide per subject; lock and unlock receive what is being locked. A custom policy that lacks one of these methods denies that action, so extend the shipped CommentPolicy:

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Comments\Policies\CommentPolicy;

class ProjectCommentPolicy extends CommentPolicy
{
    public function create(?Model $user, ?Model $commentable = null): bool
    {
        return $user !== null && $commentable instanceof Project && $commentable->hasMember($user);
    }

    public function lock(?Model $user, Model $lockable): bool
    {
        return $user?->isModerator() ?? false;
    }
}
namespace App\Policies;

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Comments\Models\Comment;
use RoundlyConsulting\Comments\Policies\CommentPolicy as BaseCommentPolicy;

class CommentPolicy extends BaseCommentPolicy
{
    public function update(?Model $user, Comment $comment): bool
    {
        return $user !== null && $comment->actor?->is($user) === true;
    }

    public function delete(?Model $user, Comment $comment): bool
    {
        return $this->update($user, $comment);
    }
}

// AppServiceProvider::boot()
Gate::policy(\RoundlyConsulting\Comments\Models\Comment::class, \App\Policies\CommentPolicy::class);

Answering 403

The package ships no routes, so the exception is not an HTTP exception. Map it onto Laravel’s AuthorizationException to answer 403 from your own controllers:

use Illuminate\Auth\Access\AuthorizationException;
use RoundlyConsulting\Comments\Exceptions\UnauthorizedCommentActionException;

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->map(fn (UnauthorizedCommentActionException $e) => new AuthorizationException($e->getMessage()));
})

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.