Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Blasp Laravel Package

blaspsoft/blasp

Blasp is an advanced profanity filter for Laravel with a driver-based architecture, multi-language support, severity scoring, masking strategies, Eloquent integration, middleware, validation rules, and testing utilities. Clean fluent API for detecting and filtering profanity.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish everything in one pass — you want the language files locally, because they are the filter:

composer require blaspsoft/blasp
php artisan vendor:publish --tag="blasp"   # config + language files

Three places to look first, in this order:

  1. config/blasp.php — default driver, language, mask, severity, cache, middleware, model, drivers, allow/block, false_positives. Everything you'll tune lives here.
  2. Blaspsoft\Blasp\Core\Result — the object every check() returns. Learn its shape (isOffensive(), clean(), score(), uniqueWords(), severity(), words()) before writing any integration; almost every pattern below is just "what do I do with a Result".
  3. lang/vendor/blasp/* — your actual word lists. If a word isn't flagged, it's usually a word-list problem, not a code problem.

First use case — gate a comment form at the edge:

use Blaspsoft\Blasp\Rules\Profanity;
use Blaspsoft\Blasp\Enums\Severity;

$request->validate([
    'body' => ['required', 'max:2000', Profanity::severity(Severity::Moderate)],
]);

That's a working feature in one line. Everything else is refinement.

Implementation Patterns

Pick the enforcement layer deliberately. The package offers four overlapping entry points; don't stack them blindly.

Layer Use when
Validation rule User-facing forms, you want field-level error messages
Middleware Blanket policy on a route group, especially APIs
Blaspable trait Data can arrive from many paths (jobs, imports, tinker) and you want a last line of defence
@clean / Str macros Display-time only — never mutate stored data

The trap is doing both middleware and validation on the same endpoint: the middleware reject action returns a 422 JSON payload, so an Inertia/Blade form POST will fail with an unhelpful response before your validation errors ever render. Choose sanitize middleware + validation, or reject via validation alone.

Tiered moderation via score. The 0–100 score is the most underused API. Route by score instead of binary rejecting:

$result = Blasp::check($request->input('body'));

if ($result->score() >= 70) {
    return back()->withErrors(['body' => 'That language is not allowed here.']);
}

$comment = Comment::create([
    'body'   => $result->score() >= 30 ? $result->clean() : $result->original(),
    'status' => $result->score() >= 30 ? 'needs_review' : 'published',
]);

Store the original alongside the sanitized version — Result::original() is only available during the request, and in sanitize mode the model's attribute is already mutated by the time you see it.

Trait as the safety net, with per-model posture. Keep the global default lenient and make specific models strict:

class Comment extends Model
{
    use Blaspable;

    protected array $blaspable = ['body', 'title'];
    protected string $blaspMode = 'sanitize';
}

class DisplayName extends Model
{
    use Blaspable;

    protected array $blaspable = ['name'];
    protected string $blaspMode = 'reject';   // fail loudly, user must rename
}

Catch ProfanityRejectedException at the controller boundary and map $e->attribute / $e->result back into your error bag. This gives you a free, precise validation message without duplicating the rule.

Audit everything with one listener. Both trait modes fire ModelProfanityDetected, which is the cheapest way to build a moderation log or a "repeat offenders" signal:

Event::listen(ModelProfanityDetected::class, function ($e) {
    ModerationLog::create([
        'model'     => $e->model::class,
        'model_id'  => $e->model->getKey(),
        'attribute' => $e->attribute,
        'words'     => $e->result->uniqueWords(),
        'score'     => $e->result->score(),
    ]);
});

Note this is separate from the ProfanityDetected event, which is gated behind 'events' => false in config.

Escape hatches for bulk work. Use withoutBlaspChecking() around seeders, fixtures, and imports, and checkMany() for batch scoring a backlog:

Comment::withoutBlaspChecking(fn () => Comment::factory()->count(50)->create());

$results = Blasp::checkMany($rows->pluck('body')->all());

Testability. Mock the facade in unit tests rather than asserting on real word lists — otherwise every list tweak breaks your suite:

Blasp::fake(['isOffensive' => true]);

Blasp::assertChecked('This is a ******* sentence');

Keep a small set of integration tests against real detection (obfuscation, phonetic, pipeline) — that's the behaviour you can't fake.

Gotchas and Tips

The regex driver's 3-separator cap is a real bypass. f--u--c--k is caught; f---u---c---k and beyond are not. This is a deliberate PHP-FPM performance trade-off. If you face adversarial users, layer pipeline('regex','phonetic') and accept that no filter is complete — pair it with human review for high-score content.

The phonetic driver is effectively English-only. It's built on metaphone() (English-oriented) and its config restricts supported_languages to ['english']. Do not add it to a pipeline for Spanish/German/French content — you'll get noise. Also budget time for the false_positives list: "fork", "duck", "beach" are pre-seeded, but your domain's vocabulary (product names, surnames, place names) won't be. The Scunthorpe problem is yours to solve via allow() and false_positives.

Severity filtering changes the output, not just the verdict. withSeverity(Severity::High) means mild words are neither counted nor masked. If a mild word appears unredacted in clean(), this is why — check config('blasp.severity') and any withSeverity() in the chain before assuming a bug.

Verify cache interaction with per-call options. cache.results keys on content hash, but allow(), block(), driver(), and withSeverity() are per-call modifiers. Confirm on your version that cached results don't leak across differing fluent chains — if in doubt, disable cache.results in environments where you use ad-hoc allow lists, or key your own cache on the full option set. Config caching (php artisan config:cache) is fine, but remember to re-run it after editing word lists.

Middleware fields => ['*'] is broad. It will walk the whole request payload, and the default except list only covers password, email, _token. Exclude file uploads, base64 blobs, JSON columns, and anything large — sanitizing megabytes of text per request is an easy way to blow a timeout. Narrow fields per route group where you can.

Sanitize mode silently rewrites user input. That's the point, but it means $request->validated() after the middleware no longer matches what the user typed, and Result::original() is gone. If rewriting is legally or product-wise sensitive, prefer reject at the validation layer and let the user fix their own text.

Multibyte and mask length. Masking substitutes per matched character, so accented or substituted forms (fÛck!ng) mask to the original length, not the dictionary word's. Callback masks receive ($word, $len) — use $len to emit [CENSORED] only once, or you'll get a wall of brackets. For display, @clean and Str::cleanProfanity() both go through e(), so don't double-escape.

Macro registration is global. Str::isProfane, Str::cleanProfanity, and the Stringable equivalents are registered by the service provider. If another package claims those names you'll see silent overrides; check Str::hasMacro('isProfane') if behaviour looks wrong.

Extending is the intended path. Custom DriverInterface implementations registered with the manager are the right answer for domain-specific rules (e.g. competitor names, leaked codenames) — cheaper and more maintainable than growing block lists into the hundreds. For a handful of words, block() at the call site is fine; for a curated taxonomy with severities, write a driver.

Result is Countable and Stringable. count($result) is the match count, and (string) $result is the clean text. Handy in Blade and in log contexts, but it also means a Result accidentally passed to a string-typed function will succeed with masked text — a silent bug worth being aware of when refactoring.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings