Laratox

Laratox is content moderation for Laravel, powered by ToxicFilter. Every check returns one of three decisions (allow, review, block), the categories that fired (spam, scams, harassment, personal data, fifteen in all) and the reason in words.
Installation
Requires PHP 8.2+ and Laravel 12 or 13.
composer require edulazaro/laratox
Create a key in your ToxicFilter account (the free plan is enough) and add it to .env:
TOXICFILTER_KEY=tf_test_...
A tf_test_ key does the same work and is never charged. Check it with:
php artisan laratox:ping
Checking content
Say what it is, then check():
use ToxicFilter; $verdict = ToxicFilter::text($comment->body)->check(); if ($verdict->blocked()) { return back()->withErrors(['body' => $verdict->reason()]); } $comment->hidden = $verdict->needsReview();
Options chain before check():
ToxicFilter::text($listing->description) ->policy('marketplace') // one of your policies, instead of the default ->project('shop') // one of your projects ->locale('es') // the language it should be in ->surface('listing') // where it appears ->reference("listing_{$listing->id}") ->check();
Every kind of content has its own method: text(), name(), email(), url(), image(), imageData(), prompt(), conversation() and signup(). The verdict is the PHP SDK's: allowed(), needsReview(), blocked(), reason(), reasons(), scores(), flagged(), project().
In a form
use EduLazaro\Laratox\Rules\Moderated; $request->validate([ 'body' => ['bail', 'required', 'string', Moderated::text()->policy('comments')], 'website' => ['bail', 'nullable', 'url', Moderated::url()], ]);
The rule chains the same options as the facade. A block fails the field; a review passes, and ->orReview() fails it too. Put it last, after bail, since it is a network call. If the API cannot answer, laratox.rule.on_error decides: allow lets the field through and logs a warning, refuse fails it.
Projects
If one ToxicFilter organization moderates several sites, give each app its project once in .env:
TOXICFILTER_PROJECT=forum
Every check and every ToxicFilter::batch() is filed there, with its own activity, review queue and webhooks. ->project() overrides it for one call.
Testing
$fake = ToxicFilter::fake(); // everything allowed $fake->shouldBlock('spam', 'Contains a referral link'); // block everything $fake->shouldReview('toxicity', when: fn ($request) => // or only some requests str_contains($request['body']['content'] ?? '', 'idiot')); $fake->assertSent(fn ($request) => $request['path'] === '/api/v1/text'); $fake->assertSentCount(1);
The fake replaces only the network: the SDK's client and verdicts are the real ones. Like the API, it refuses more than ten locales, so a test fails where production would.
built and maintained by Edu Lazaro · MIT license