blessing/rejection
A Laravel package for managing rejection flows: record, track, and handle rejected records through a simple, expressive API that integrates with your Eloquent models and existing application logic.
Install the package:
composer require blessing/rejection
Publish any config and migrations (confirm the exact tag in the repo's README or src/Providers):
php artisan vendor:publish --provider="Blessing\Rejection\RejectionServiceProvider"
php artisan migrate
Look first at:
src/ — the service provider, the trait (likely Rejectable), and any Rejection model.database/migrations/ — the schema you're inheriting (columns like rejected_at, rejected_by, reason, or a polymorphic rejections table).config/rejection.php if present — table names, model bindings, and default reasons usually live here.First use case: add the trait to a model that needs an approval/rejection lifecycle, e.g. a Post in a moderation flow:
use Blessing\Rejection\Concerns\Rejectable;
class Post extends Model
{
use Rejectable;
}
Then reject from a controller/action and render the reason in your admin UI:
$post->reject($request->user(), 'Contains prohibited content');
Because the package has effectively no public adoption, treat the source as the documentation: skim the trait and migration before wiring anything into a real domain model.
Moderation lifecycle. The natural fit is a state machine around a rejected_at/rejected_by pair (or a separate rejections table). Combine the trait with your existing status enum so "rejected" is a derived state, not a duplicate column:
public function scopePending($q) { return $q->whereNull('rejected_at'); }
Reason capture via Form Requests. Validate rejection input in a dedicated request so reasons are never empty and are length-capped before they hit the DB:
'reason' => ['required', 'string', 'max:1000'],
Audit trail pattern. If the package writes a row per rejection (polymorphic rejections table), lean into it: keep history instead of overwriting. Query the relation for a timeline in your admin panel, and eager load it to avoid N+1s:
Post::with('rejections.user')->rejected()->paginate();
Event-driven follow-up. Listen for whatever the package dispatches (check src/Events) — or wrap the call in your own action class — to send notifications, write to an activity log, or transition a workflow:
class RejectPost
{
public function __construct(private Post $post, private User $actor, private string $reason) {}
public function handle(): void
{
$this->post->reject($this->actor, $this->reason);
Rejected::dispatch($this->post, $this->actor, $this->reason);
}
}
Policy integration. Gate the action rather than guarding it in controllers:
public function reject(User $user, Post $post): bool
{
return $user->can('moderate') && $post->rejected_at === null;
}
Reusable across models. Give several models the trait (Comment, Listing, Application) and query them uniformly through the polymorphic relation if one exists — that's the main reason to prefer this over a hand-rolled boolean column.
boot{TraitName} convention means bootRejectable() runs on model boot. If your model already defines a reject() method, a rejected() relationship, or a rejections relation, you'll get a fatal trait/method conflict. Rename via insteadof/as or wrap the trait in your own concern.rejected_by field is usually guarded; set it from the authenticated user inside the action rather than passing request data to create()/update().$model->rejected() will silently include them.datetime) and that comparisons use now() consistently; a raw string in rejected_at will break whereNull/isRejected() checks.Relation::enforceMorphMap) before shipping, or renaming a model class will orphan rejection history.reject() silently does nothing, check: trait is actually used, migration ran, the model isn't guarded against the columns, and any package event listener isn't throwing. DB::listen() or Model::preventSilentlyDiscardingAttributes() in AppServiceProvider::boot() will surface dropped attributes fast.Rejection model (and rebinding via config/container) over editing vendor code. If no listener hooks exist, wrap the package call in your own service class so you have one seam to swap later.{{ }} is fine; watch for anything that renders reasons as Markdown or raw HTML in admin views.How can I help you explore Laravel packages today?