blaspsoft / blasp
Blasp is a powerful and customisable profanity filter package for Laravel applications
Requires
- php: ^8.2
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.5.12
Suggests
- ext-intl: Folds accented letters when the homoglyphs option is enabled
- symfony/polyfill-intl-normalizer: Folds accented letters when the homoglyphs option is enabled and ext-intl is not installed
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v4.5.0
- v4.4.0
- v4.3.2
- v4.3.1
- v4.3.0
- v4.2.0
- v4.1.0
- v4.0.1
- v4.0.0
- v3.1.9
- v3.1.8
- v3.1.7
- v3.1.6
- v3.1.5
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.0
- v2.1.0
- v2.0.0
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.0
- v1.0.0
- dev-feat/v4.4
- dev-fix/v4.3.2-hardening
- dev-fix/normalizer-offsets
- dev-fix/zwj-emoji-sequences
- dev-coderabbitai/docstrings/46b33c6
This package is auto-updated.
Last update: 2026-11-03 15:21:08 UTC
README
Blasp - Advanced Profanity Filter for Laravel
Blasp is a powerful, extensible profanity filter for Laravel. Version 4 is a ground-up rewrite with a driver-based architecture, severity scoring, masking strategies, Eloquent model integration, and a clean fluent API.
Features
- Driver Architecture —
regex(detects obfuscation, substitutions, separators),pattern(fast exact matching),phonetic(catches sound-alike evasions), orpipeline(chains multiple drivers together). Extend with custom drivers. - Multi-Language — English, Spanish, German, French, Dutch, Hindi and Nepali with language-specific normalizers. Hindi and Nepali cover both Devanagari and romanized text. Check one, many, or all at once.
- Severity Scoring — Words categorised as mild/moderate/high/extreme. Filter by minimum severity and get a 0-100 score.
- Masking Strategies — Character mask (
*,#), grawlix (!@#$%), or a custom callback. - Eloquent Integration —
Blaspabletrait auto-sanitizes or rejects profanity on model save. - Middleware — Reject or sanitize profane request fields with configurable severity.
- Validation Rules — Fluent validation rule with language, severity, and score threshold support.
- Testing Utilities —
Blasp::fake()for test doubles with assertions. - Events —
ProfanityDetected,ContentBlocked, andModelProfanityDetected.
Requirements
- PHP 8.2+
- Laravel 10, 11, 12 or 13
Installation
composer require blaspsoft/blasp
Publish configuration:
# Everything (config + language files) php artisan vendor:publish --tag="blasp" # Config only php artisan vendor:publish --tag="blasp-config" # Language files only php artisan vendor:publish --tag="blasp-languages"
Quick Start
use Blaspsoft\Blasp\Facades\Blasp; $result = Blasp::check('This is a fucking sentence'); $result->isOffensive(); // true $result->clean(); // "This is a ******* sentence" $result->original(); // "This is a fucking sentence" $result->score(); // 30 $result->count(); // 1 $result->uniqueWords(); // ['fucking'] $result->severity(); // Severity::High
Fluent API
All builder methods return a PendingCheck and can be chained:
// Language selection Blasp::in('spanish')->check($text); Blasp::in('english', 'french')->check($text); Blasp::inAllLanguages()->check($text); // Language shortcuts Blasp::english()->check($text); Blasp::spanish()->check($text); Blasp::german()->check($text); Blasp::french()->check($text); Blasp::dutch()->check($text); Blasp::hindi()->check($text); Blasp::nepali()->check($text); // Driver selection Blasp::driver('regex')->check($text); // Full obfuscation detection (default) Blasp::driver('pattern')->check($text); // Fast exact matching Blasp::driver('phonetic')->check($text); // Sound-alike detection (e.g. "phuck", "sheit") Blasp::driver('pipeline')->check($text); // Chain multiple drivers (config-based) // Ad-hoc pipeline — chain any drivers without config Blasp::pipeline('regex', 'phonetic')->check($text); Blasp::pipeline('pattern', 'phonetic')->in('english')->mask('#')->check($text); // Shorthand modes Blasp::strict()->check($text); // Forces regex driver Blasp::lenient()->check($text); // Forces pattern driver Blasp::matchEmbedded()->check($text); // Also flag profanity embedded in longer words (regex driver) // Masking Blasp::mask('*')->check($text); // Character mask (default) Blasp::mask('#')->check($text); // Custom character Blasp::mask('grawlix')->check($text); // !@#$% cycling Blasp::mask(fn($word, $len) => '[CENSORED]')->check($text); // Callback // Severity filtering use Blaspsoft\Blasp\Enums\Severity; Blasp::withSeverity(Severity::High)->check($text); // Ignores mild/moderate // Allow/block lists (merged with config) Blasp::allow('damn', 'hell')->check($text); Blasp::block('customword')->check($text); // Also matched inside longer words ("xcustomwordx") // Chain everything Blasp::spanish() ->mask('#') ->withSeverity(Severity::Moderate) ->check($text); // Batch checking $results = Blasp::checkMany(['text one', 'text two']);
Result Object
The Result object is returned by every check() call:
| Method | Returns | Description |
|---|---|---|
isOffensive() |
bool |
Text contains profanity |
isClean() |
bool |
Text is clean |
clean() |
string |
Text with profanities masked |
original() |
string |
Original unmodified text |
score() |
int |
Severity score (0-100) |
count() |
int |
Total profanity matches |
uniqueWords() |
array |
Unique base words detected |
severity() |
?Severity |
Highest severity in matches |
words() |
Collection |
MatchedWord objects with position, length, severity |
toArray() |
array |
Full result as array |
toJson() |
string |
Full result as JSON |
Result implements JsonSerializable, Stringable (returns clean text), and Countable.
Detection Types
The regex driver detects obfuscated profanity:
| Type | Example | Detected As |
|---|---|---|
| Straight match | fucking |
fucking |
| Substitution | fÛck!ng, f4ck |
fucking, fuck |
| Separators | f-u-c-k-i-n-g, f@ck |
fucking, fuck |
| Doubled | ffuucckkiinngg |
fucking |
| Combination | f-uuck!ng |
fucking |
Separator limit: The regex driver allows up to 3 separator characters between each letter (e.g.,
f--u--c--k). This covers all realistic obfuscation patterns while keeping regex complexity low enough for PHP-FPM environments.
The pattern driver only detects straight word-boundary matches.
The phonetic driver uses metaphone() + Levenshtein distance to catch words that sound like profanity but are spelled differently:
| Type | Example | Detected As |
|---|---|---|
| Phonetic spelling | phuck |
fuck |
| Shortened form | fuk |
fuck |
| Sound-alike | sheit |
shit |
Configure sensitivity in config/blasp.php under drivers.phonetic. A curated false-positive list prevents common words like "fork", "duck", and "beach" from being flagged.
Hindi & Nepali
Both languages ship with Devanagari and romanized (Latin-script) word lists, so tu chutiya hai and तू चूतिया है are both caught. On top of the standard obfuscation handling, the following spelling variations are matched automatically:
| Variation | Example | Matches |
|---|---|---|
| Nukta present / absent / precomposed | भोसड़ी, भोसडी |
भोसडी |
| Chandrabindu vs anusvara | गाँड, गांड |
गांड |
| Short vs long vowel signs | चुतिया, चूतिया |
चूतिया |
| Separated characters | मा-दर-चोद |
मादरचोद |
| Romanized vowels | chootiya, chuteeya, mooji |
chutiya, muji |
| Romanized consonants | bhadva/bhadwa, haramjada, phuddu |
bhadwa, haramzada, fuddu |
Some words are intentionally left out because they are everyday vocabulary or collide with other languages under allLanguages(): bare animal nouns (kutta, kukur, gadha, ullu…; only insults built on them such as kutte ki aulad are listed), news vocabulary (balatkar), identity terms (hijra, caste and ethnic community names), chakka (also a cricket six), and romanized words such as lund, gand, lute and paji. Add them per-project with Blasp::block(...) or the block config key.
Pipeline Driver
The pipeline driver chains multiple drivers together so a single check() call runs all of them. It uses union merge semantics — text is flagged if any driver finds a match.
// Config-based: set 'default' => 'pipeline' or use driver('pipeline') Blasp::driver('pipeline')->check('phuck this sh1t'); // Ad-hoc: pick drivers on the fly (no config needed) Blasp::pipeline('regex', 'phonetic')->check('phuck this sh1t'); Blasp::pipeline('regex', 'pattern', 'phonetic')->check($text);
When multiple drivers detect the same word at the same position, duplicates are removed — only the longest match is kept. Masks are applied from the merged result, and the score is recalculated across all matches.
Configure the default sub-drivers in config/blasp.php:
'drivers' => [ 'pipeline' => [ 'drivers' => ['regex', 'phonetic'], // Drivers to chain ], ],
Eloquent Integration
The Blaspable trait automatically checks model attributes during save:
use Blaspsoft\Blasp\Blaspable; class Comment extends Model { use Blaspable; protected array $blaspable = ['body', 'title']; }
// Sanitize mode (default) — profanity is masked, model saves $comment = Comment::create(['body' => 'This is fucking great']); $comment->body; // "This is ******* great" // Check what happened $comment->hadProfanity(); // true $comment->blaspResults(); // ['body' => Result, 'title' => Result] $comment->blaspResult('body'); // Result instance
Per-Model Overrides
class Comment extends Model { use Blaspable; protected array $blaspable = ['body', 'title']; protected string $blaspMode = 'reject'; // 'sanitize' (default) | 'reject' protected string $blaspLanguage = 'spanish'; // null = config default protected string $blaspMask = '#'; // null = config default }
Reject Mode
In reject mode, saving a model with profanity throws ProfanityRejectedException and the model is not persisted:
use Blaspsoft\Blasp\Exceptions\ProfanityRejectedException; try { $comment = Comment::create(['body' => 'profane text']); } catch (ProfanityRejectedException $e) { $e->attribute; // 'body' $e->result; // Result instance $e->model; // The unsaved model }
Disabling Checking
Comment::withoutBlaspChecking(function () { Comment::create(['body' => 'unchecked content']); });
Events
A ModelProfanityDetected event fires whenever profanity is detected on a model attribute (both sanitize and reject modes):
use Blaspsoft\Blasp\Events\ModelProfanityDetected; Event::listen(ModelProfanityDetected::class, function ($event) { $event->model; // The model instance $event->attribute; // Which attribute had profanity $event->result; // Result instance });
Middleware
Use CheckProfanity to filter incoming request fields. A blasp middleware alias is registered automatically:
// Using the short alias (recommended) Route::post('/comment', CommentController::class) ->middleware('blasp'); // With parameters: action, severity Route::post('/comment', CommentController::class) ->middleware('blasp:sanitize,mild'); // Or using the class directly use Blaspsoft\Blasp\Middleware\CheckProfanity; Route::post('/comment', CommentController::class) ->middleware(CheckProfanity::class);
| Action | Behaviour |
|---|---|
reject (default) |
JSON requests get a 422 with an error per field; form posts are redirected back with validation errors |
sanitize |
Replaces profane fields in the request and continues |
Nested and array input is checked by dotted path (comment.body, tags.0), and every offending field is reported. Password fields (any field whose name contains password) are never checked.
Configure which fields to check in config/blasp.php:
'middleware' => [ 'action' => 'reject', 'fields' => ['*'], // '*' = all fields; or e.g. ['user.bio', 'comments.*.body'] 'except' => ['password', 'email', '_token'], // Skipped; names without a dot also match nested keys 'severity' => 'mild', ],
Validation Rules
String Rule
$request->validate([ 'comment' => ['required', 'blasp_check'], 'bio' => ['required', 'blasp_check:spanish'], ]);
Fluent Rule Object
use Blaspsoft\Blasp\Rules\Profanity; use Blaspsoft\Blasp\Enums\Severity; $request->validate([ 'comment' => ['required', Profanity::in('english')], 'bio' => ['required', Profanity::severity(Severity::High)], 'tagline' => ['required', Profanity::maxScore(50)], ]);
Options also chain on an instance: Profanity::make()->in('spanish')->severity(Severity::High).
Tip: checking time grows with the length of the text, so cap user input before it reaches Blasp. Set
max_lengthinconfig/blasp.php(orBLASP_MAX_LENGTH) and the middleware and rule reject longer text instead of checking it, or useProfanity::maxLength(5000)per rule. Amax:rule withbailworks too:['bail', 'string', 'max:5000', Profanity::in('english')].
Blade Directive
The @clean directive sanitizes and escapes text for safe display in views:
<p>@clean($comment->body)</p> {{-- Equivalent to: {{ app('blasp')->check($comment->body)->clean() }} --}}
Output is HTML-escaped via e() for XSS safety.
Str / Stringable Macros
Blasp registers macros on Laravel's Str and Stringable classes:
use Illuminate\Support\Str; // Static methods Str::isProfane('fuck this'); // true Str::isProfane('hello'); // false Str::cleanProfanity('fuck this'); // '**** this' Str::cleanProfanity('hello'); // 'hello' // Fluent Stringable methods Str::of('fuck this')->isProfane(); // true Str::of('fuck this')->cleanProfanity(); // Stringable('**** this') Str::of('hello')->cleanProfanity()->upper(); // 'HELLO' (chaining works)
Configuration
Full config/blasp.php reference:
return [ 'default' => env('BLASP_DRIVER', 'regex'), // 'regex' | 'pattern' | 'phonetic' | 'pipeline' 'language' => env('BLASP_LANGUAGE', 'english'), // Default language 'mask' => '*', // Default mask character 'severity' => 'mild', // Minimum severity 'events' => false, // Fire ProfanityDetected events 'cache' => [ 'enabled' => true, 'driver' => env('BLASP_CACHE_DRIVER'), 'ttl' => 86400, 'results' => true, // Cache check() results by content hash ], 'middleware' => [ 'action' => 'reject', 'fields' => ['*'], 'except' => ['password', 'email', '_token'], 'severity' => 'mild', ], 'model' => [ 'mode' => env('BLASP_MODEL_MODE', 'sanitize'), // 'sanitize' | 'reject' ], 'drivers' => [ 'pipeline' => [ 'drivers' => ['regex', 'phonetic'], // Sub-drivers to chain ], 'phonetic' => [ 'phonemes' => 4, // metaphone code length (2-8) 'min_word_length' => 3, // skip short words 'max_distance_ratio' => 0.6, // levenshtein threshold (0.3-0.8) 'supported_languages' => ['english'], // metaphone is English-oriented 'false_positives' => ['fork', '...'], // never flag these words ], ], 'allow' => [], // Global allow-list 'block' => [], // Global block-list 'separators' => [...], // Characters treated as separators 'substitutions' => [...], // Character leet-speak mappings 'substitutions_append' => [], // Extra characters merged into 'substitutions' per letter 'false_positives' => [...], // Words that should never be flagged // Opt-in matching options (all off by default, as they change results for existing text) 'homoglyphs' => false, // Fold Cyrillic/Greek lookalikes, fullwidth and accented letters 'extended_substitutions' => false, // Add 1 → l, 7 and + → t, 2 → z 'flexible_phrases' => false, // "sieg heil" also matches "sieg-heil" and "siegheil" ];
Adding substitutions
substitutions_append adds characters to the default substitution table from your published config/blasp.php, without copying the whole table. Characters are merged per letter:
'substitutions_append' => ['/u/' => ['v']], // "fvck" now matches "fuck"
Opt-in matching options
| Option | Catches | Trade-off |
|---|---|---|
homoglyphs |
fuсk (Cyrillic с), fuck, dāmn |
Cyrillic and Greek text is folded too, so apps with Russian or Greek users may see new matches in those scripts. Folding runs after the language normalizer, so German ä → ae still applies. Accented letters (dāmn) are only folded when ext-intl or symfony/polyfill-intl-normalizer is installed. |
extended_substitutions |
s1ut, sh17, j122 |
Digits next to letters (version numbers, codes) can now complete a word. |
flexible_phrases |
sieg-heil, siegheil for the entry sieg heil |
Multi-word entries also match when run together. |
The original text is always what gets masked; only matching sees the folded form.
Custom Drivers
Implement DriverInterface and register with the manager:
use Blaspsoft\Blasp\Core\Contracts\DriverInterface; use Blaspsoft\Blasp\Core\Result; use Blaspsoft\Blasp\Core\Dictionary; use Blaspsoft\Blasp\Core\Contracts\MaskStrategyInterface; class MyDriver implements DriverInterface { public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result { // Your detection logic } } // Register in a service provider Blasp::extend('my-driver', fn($app) => new MyDriver()); // Use it Blasp::driver('my-driver')->check($text);
Caching
Blasp caches check() results by default. When the same text is checked with the same configuration (language, driver, severity, allow/block lists), the cached result is returned instantly.
// First call — runs full analysis, caches result $result = Blasp::check('some text'); // Second call — returns cached result $result = Blasp::check('some text');
Configure caching in config/blasp.php:
'cache' => [ 'enabled' => true, // Master switch for all caching 'driver' => env('BLASP_CACHE_DRIVER'), // null = default cache driver 'ttl' => 86400, // Cache lifetime in seconds 'results' => true, // Cache check() results (disable independently) ],
Result caching is automatically bypassed when using a CallbackMask (closures can't be serialized). Clear both dictionary and result caches with:
php artisan blasp:clear
Or programmatically:
Dictionary::clearCache();
Artisan Commands
# Clear the profanity cache php artisan blasp:clear # Test text from the command line php artisan blasp:test "some text to check" --lang=english --detail # List available languages with word counts php artisan blasp:languages
Testing
Faking
use Blaspsoft\Blasp\Facades\Blasp; use Blaspsoft\Blasp\Core\Result; // Replace with a fake — all checks return clean by default Blasp::fake(); // Pre-configure specific responses Blasp::fake([ 'bad text' => Result::withMatches(['fuck']), 'clean text' => Result::none('clean text'), ]); $result = Blasp::check('bad text'); $result->isOffensive(); // true // Assertions Blasp::assertChecked(); Blasp::assertCheckedTimes(1); Blasp::assertCheckedWith('bad text');
Disabling Filtering
Blasp::withoutFiltering(function () { // All checks return clean results });
Events
Enable global events with 'events' => true in config:
| Event | Fired When | Properties |
|---|---|---|
ProfanityDetected |
check() finds profanity |
result, originalText |
ContentBlocked |
Middleware detects profanity | result, request, field, action |
ModelProfanityDetected |
Blaspable trait detects profanity | model, attribute, result |
ModelProfanityDetected always fires (not gated by the events config).
Upgrading to 4.4
4.4 changes what gets flagged and how misconfiguration is reported. Most apps need no changes; check these:
- Unknown languages throw.
Blasp::in('englsh'), a misspeltBLASP_LANGUAGEor$blaspLanguagenow throwUnknownLanguageExceptioninstead of silently flagging nothing. Language names are case-insensitive. severityandfalse_positivesinconfig/blasp.phpare now applied. They were documented but ignored.severitysets the default minimum (the default,mild, changes nothing); an invalid value throws.- Middleware checks nested and array input (
comment.body,tags.0) and reports every offending field. Form posts are redirected back with validation errors; JSON requests get the same 422 response as before. Password fields are never checked. An invalid action or severity throws. - Fewer false positives. Contractions ("Let's hit", "he'll", "can't"), common word pairs that spell a list word when joined ("a hole", "money shot"; add your own multi-word entries to
false_positives), runs of*(ratings,**markdown**, masked passwords), and stretched words ("Hellllo") are no longer flagged. Split obfuscations such as "fuc ker" or "cun't" are still caught. The phonetic driver no longer flags everyday words ("was", "great", "dog"). The French list drops about 1,250 ordinary words. - Base words are lowercase.
uniqueWords()andMatchedWord::$basereport list words in lowercase. - Laravel 10 or later is required (the validation rule never worked on 8 or 9).
- New: an optional
max_length(BLASP_MAX_LENGTH) for the middleware and validation rule, andProfanity::maxLength().
Migrating from v3
Namespace Changes
| v3 | v4 |
|---|---|
Blaspsoft\Blasp\Facades\Blasp |
Blaspsoft\Blasp\Facades\Blasp (unchanged) |
Blaspsoft\Blasp\ServiceProvider |
Blaspsoft\Blasp\BlaspServiceProvider |
The Laravel auto-discovery handles provider/alias registration automatically. The facade namespace is the same as v3, so no import changes are needed for the facade.
Config Changes
| v3 Key | v4 Key | Notes |
|---|---|---|
default_language |
language |
default_language still works as alias |
mask_character |
mask |
mask_character still works as alias |
cache_driver |
cache.driver |
cache_driver still works as alias |
| — | default |
New: driver selection (regex/pattern) |
| — | severity |
New: minimum severity level |
| — | events |
New: enable global events |
| — | allow / block |
New: global allow/block lists |
| — | middleware |
New: middleware configuration section |
| — | model |
New: Blaspable trait configuration |
Result API Changes
| v3 Method | v4 Method |
|---|---|
hasProfanity() |
isOffensive() |
getCleanString() |
clean() |
getSourceString() |
original() |
getProfanitiesCount() |
count() |
getUniqueProfanitiesFound() |
uniqueWords() |
All v3 methods still work as deprecated aliases.
Builder API Changes
| v3 Method | v4 Method |
|---|---|
maskWith($char) |
mask($char) |
allLanguages() |
inAllLanguages() |
language($lang) |
in($lang) |
configure($profanities, $falsePositives) |
block(...$words) / allow(...$words) |
All v3 methods still work as deprecated aliases.
New in v4
- Driver architecture —
regexandpatterndrivers, custom driver support - Severity system — Mild/Moderate/High/Extreme levels with scoring
- Masking strategies — Grawlix and callback masking
- Blaspable trait — Automatic Eloquent model profanity checking
- Middleware — Request-level profanity filtering
- Fluent validation rule —
Profanity::in('spanish')->severity(Severity::High) - Testing utilities —
Blasp::fake(), assertions,withoutFiltering() - Events —
ProfanityDetected,ContentBlocked,ModelProfanityDetected - Artisan commands —
blasp:clear,blasp:test,blasp:languages - Batch checking —
Blasp::checkMany([...]) - Multi-language in one call —
Blasp::in('english', 'spanish')->check($text)
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Changelog
See CHANGELOG.md for detailed version history.
License
Blasp is open-sourced software licensed under the MIT license.