zareismail/shaghool
Shaghool is a small Laravel package by zareismail that adds job-related functionality to Laravel applications, following framework conventions and Eloquent patterns for quick setup. It has minimal community adoption and limited documentation, so evaluate it carefully before production use.
This package adds a polymorphic "reporting" layer to Eloquent models: a Report model plus a trait you attach to anything that can be reported (posts, comments, users). Release 1.4.1 is a correctness release — it wires up the model boot, adds SoftDeletes to the report model, and makes report rows follow the reported model through delete and restore. If you are starting fresh, you get that behaviour for free; if you are upgrading, see the Gotchas section before you deploy.
Minimal path:
composer require <vendor>/<package>
php artisan vendor:publish --provider="<Vendor>\<Package>\<Package>ServiceProvider"
php artisan migrate
Then confirm two things before writing any code:
reports table should now include a nullable deleted_at. If your published migration predates 1.4.1, re-publish or add the column manually.src/ and read the trait and the Report model. Method names (report(), reports(), isReportedBy(), scopes) are the API surface you will live in day to day; the README is usually accurate but the trait is the source of truth.First use case — the canonical one:
use Vendor\Package\Concerns\Reportable;
class Post extends Model
{
use Reportable;
}
// Somewhere in a controller
$post->report(auth()->user(), 'Spam link in body');
// Moderation queue
Report::with('reportable', 'reporter')->latest()->paginate(50);
Get that loop working end-to-end (report → list → resolve) before layering on notifications or auto-moderation. It exercises the trait, the morph relation, and the config in one pass.
Attach the trait narrowly, not globally. Put Reportable on the models that genuinely receive abuse reports. Every model that uses it gets a morph entry, extra relations, and now delete/restore event hooks — applying it to a base model class multiplies that across the app for no benefit.
Separate "who reported" from "what was reported". The trait gives you the reported side; the reporter is a BelongsTo to your user model. Keep the reporter relation explicit rather than relying on auth() inside the package, so you can seed, backfill, and test reports without a session.
Moderation state belongs in your app, not the package. The package records that a report exists; it generally does not implement resolution workflows. Add your own status / resolved_at / resolved_by columns via a follow-up migration and scope on them:
Report::whereNull('resolved_at')->with('reportable')->get();
Use the cascade semantics as your audit trail. As of 1.4.1, deleting a reported post soft-deletes its reports and restoring the post restores them. That means your moderation history stays consistent with content lifecycle without custom code — but only if you soft-delete the parent. Hard deletes will typically cascade-delete reports outright (or orphan them, depending on your FK setup); decide which you want and configure foreign keys accordingly.
Aggregate before paginating. Report counts per model are the usual dashboard need and the usual N+1 source:
Post::withCount('reports')->orderByDesc('reports_count')->take(20)->get();
If you need "distinct reporters" rather than raw report rows, add a withCount(['reports as reporter_count' => fn ($q) => $q->select(DB::raw('count(distinct reporter_id)'))]) style query and cache the result — moderation dashboards get hit constantly.
Hook into the events now that boot is fixed. With model boot in place, deleting/deleted/restoring/restored fire as expected. Use an observer on your reportable model to trigger side effects — notifying moderators when a reported post is deleted, or clearing a cached report count:
class PostObserver
{
public function restored(Post $post): void
{
Cache::forget("post.{$post->id}.report_count");
}
}
Wrap write paths in transactions when reports drive other state. Reporting + incrementing a strike counter + dispatching a job should be atomic; the package will not do that for you.
SoftDeletes to the report model — this likely needs a migration. Verify deleted_at exists on reports before deploying. A missing column turns every report query into a SQLSTATE[42S22] at runtime, and because the trait is attached broadly the failure shows up in unrelated features first.deleted_at = NULL. Nullable is the default, so no backfill is needed — but if you wrote raw SQL with DELETE FROM reports, those rows are unrecoverable and the new restore path will not resurrect them.->touch() calls, observers, or a custom booted() hook, remove those workarounds or you will double-fire side effects (duplicate notifications, double-incremented counters) after upgrading.withTrashed() on the report relation when you deliberately want historical counts, and be explicit about which number the UI is showing.restore() only helps if deleted_at is set. Restoring a parent that was never soft-deleted is a no-op; make sure your deleted_at column and casts are actually present on the reportable model, not just on Report.Relation::enforceMorphMap(), register every reportable class. Storing FQCNs means renaming or moving a model silently breaks reportable resolution for existing rows — a morphMap makes that a one-line config change instead of a data migration.php artisan config:clear in local. A stale cached config is the most common cause of "the package ignores my settings" bug reports.Report::with('reportable', 'reporter') — the morph relation is not lazy-loadable in bulk without a query per row, and moderation tables are exactly where this bites.RefreshDatabase plus the package factory for reports. Assert the cascade explicitly so a future regression is caught:$post->report($user, 'spam');
$post->delete();
$this->assertSoftDeleted('reports', ['id' => $report->id]);
$post->restore();
$this->assertNotSoftDeleted('reports', ['id' => $report->id]);
Report subclass and swap it in via the package's config model binding rather than editing vendor code.How can I help you explore Laravel packages today?