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.
Architecture fit
Blasp is a leaf dependency with an intentionally small blast radius: no database migrations, no queue workers, no external network calls, no service container overrides beyond its own bindings. It auto-registers a service provider, a Blasp facade, a blasp middleware alias, a @clean Blade directive, and Str/Stringable macros. That is the correct shape for a policy utility — it can be adopted, pinned, and removed without architectural debt.
The important architectural property is that detection is local and deterministic. No content leaves the application boundary, which is a material advantage over hosted moderation APIs (Perspective, AWS Comprehend, OpenAI Moderation) for any product with PII, GDPR, HIPAA-adjacent, or data-residency obligations. That is the strongest strategic argument for this package over the alternatives.
Enforcement surfaces are layered and independently selectable — validation rule, middleware, Eloquent trait, view directive, programmatic facade. That allows a phased rollout where read paths and write paths are hardened on different schedules. The DriverInterface + PendingCheck fluent builder suggests the abstraction is not leaky enough to trap us, but swapping to a hosted provider later would mean abandoning the trait/middleware/rule ergonomics, not just the engine.
Integration feasibility
Low. composer require, publish blasp-config, done. PHP 8.2+ is the only meaningful gate. The API is idiomatic Laravel: fluent builders, enum-based severity, JsonSerializable/Countable/Stringable result objects, a Blasp::fake() test double with assertions. A mid-level Laravel developer can be productive in half a day.
The real integration work is not code — it is policy definition: which fields are checked, which severity threshold applies per surface, sanitize vs. reject, and who owns the wordlist. That is product/legal work disguised as a config file.
Technical risk
| Risk | Severity | Notes |
|---|---|---|
| False positives (Scunthorpe problem) | High | Regex + separator + substitution matching on real names, place names, medical/legal terms, and non-English text. A curated false_positives list exists but is necessarily incomplete and locale-blind. |
| False negatives / evasion | High | Leet-speak mappings and 3-character separator tolerance are enumerable and will be defeated by novel obfuscation, homoglyphs, zero-width joiners, RTL override characters, and non-Latin scripts. README makes no claim about Unicode normalization or ZWJ stripping. |
| Multilingual claims vs. reality | Medium-High | EN/ES/DE/FR dictionaries exist, but the phonetic driver explicitly documents supported_languages => ['english'] and is metaphone()-based. Marketing ("multi-language") and capability (English-centric phonetic) diverge. |
| Global-state toggles under Octane | Medium-High | withoutBlaspChecking() and the middleware's request mutation imply static/global state. In a long-running worker (Octane, Swoole), a leaked toggle or mutated request is a cross-request contamination vector. Must be verified before any Octane deployment. |
| Regex performance / ReDoS | Medium | README asserts complexity is "low enough for PHP-FPM environments" — that is a design claim, not a benchmark. No published p99 latency data at realistic payload sizes, and no adversarial-input analysis. |
| Cache correctness | Medium | cache.results keys on content hash with 24h TTL. Dictionary/config changes leave stale verdicts for up to a day with no described invalidation hook. Content-hash keying also means near-zero hit rate on genuinely novel user text while still writing unbounded keys into the cache store. |
| Eloquent trait mutates on save | Medium | Silent field rewriting breaks auditability, interacts awkwardly with isDirty, observers, updateQuietly, and is bypassed entirely by Model::insert() / query-builder writes. Sanitize-by-default is a surprising default. |
| Middleware defaults | Medium | fields => ['*'] checks every request field; action => 'reject' returns 422. Applied globally, this silently changes API contracts and inspects fields (nested arrays, JSON bodies, file metadata) nobody intended. |
| Opaque severity score | Medium | A 0–100 score with an undocumented formula, exposed as a validation rule (Profanity::maxScore(50)), couples business policy to an internal implementation detail that can shift between minor versions. |
| Adoption / bus factor | Medium | 356 stars, 0 dependents, single-org maintainer, modest composite score. v4 was a ground-up rewrite — evidence that major versions break. No signals of enterprise production usage at scale. |
| Version metadata anomaly | Low | PHP 8.2+ alongside Laravel 8.0+ is internally inconsistent (Laravel 8 predates PHP 8.2 support), and the listed last-release date is future-dated. Verify the actual CI matrix and Packagist history before trusting the compatibility claim. |
Key questions
check() on 1KB, 10KB, and 100KB payloads, and has anyone fuzzed the regex driver with adversarial separator/homoglyph input?withoutBlaspChecking() request-scoped or process-global? Has the package been validated under Laravel Octane?Stack fit
Good fit for a conventional Laravel monolith or API on PHP 8.2+. It is driver-based and config-first, which matches how we already structure environment-specific behavior. No infrastructure dependencies beyond the cache store we already run. It should be treated as a policy library, not a security control — pair it with rate limiting, reporting flows, and human review for high-severity cases; do not present it to legal/compliance as a complete content-safety solution.
Migration path
There is no incumbent to migrate from in most cases, so the path is additive and reversible. Recommended staged adoption:
composer require blaspsoft/blasp with an exact-version constraint; publish config only (not languages) initially.default => 'regex' explicitly rather than relying on env defaults, so behavior cannot shift on deploy.events => true and ProfanityDetected listeners with no enforcement — pure observation for 2–4 weeks to build an internal FP/FN baseline on real traffic.Profanity validation rules on the lowest-risk, highest-value field (e.g., display name).sanitize mode on public-facing comment/bio fields.reject mode and blasp middleware only on surfaces where a 422 is an acceptable UX, and never with fields => ['*'] — enumerate fields explicitly.Blaspable trait model-by-model, defaulting to sanitize, and only after confirming no ETL, import, or admin tooling depends on byte-exact attribute values.Compatibility
Verify against the Laravel versions we actually run. The 8.0+ claim should be treated as aspirational until the CI matrix confirms it; realistically target the current LTS plus one prior. PHP 8.2+ is a hard floor — confirm our runtime images and any shared libraries meet it before committing. Check that our cache store supports the driver and TTL semantics used. If we run Octane, gate adoption on resolving the global-state question first.
Sequencing
Sequence around risk tolerance, not code complexity. The code is a one-day task; the policy is a multi-sprint conversation with Trust & Safety, Legal, and Support. Two ordering constraints matter: (a) observational mode must precede enforcement, or we ship blind; and (b) sanitization on read paths (Blade @clean) should land before rejection on write paths, because masking is recoverable and rejection is not.
Maintenance
Low code maintenance, non-trivial content maintenance. The package itself is a pinned Composer dependency with a changelog and tagged releases; expect to review it at each major Laravel upgrade and on any v5. The ongoing cost is wordlist and false-positive curation — this is an unglamorous, permanent operational commitment that must be assigned to a named owner. Budget for a monthly review of over-mask complaints and a curated allow list per locale.
Treat major versions as breaking: v4 was a ground-up rewrite, so pinning and reading upgrade guides is mandatory. The score() formula and severity categorization are the most likely silent-change surfaces.
Support
No vendor SLA. Support is community + internal. Establish: an internal owner for the package, a documented escalation path for "the filter blocked a legitimate user," and a policy for emergency allow-listing. Because enforcement failures are user-visible and often public (a blocked comment, a mangled name), support load will be driven by false positives, not by bugs.
Scaling
Cost is CPU on the request path, proportional to payload size × driver count. Mitigations, in order of preference:
pattern driver for high-volume, low-obfuscation-risk fields; reserve regex/phonetic/pipeline for high-risk, low-volume surfaces.pipeline across all fields — union-merge semantics mean every driver runs on every check.checkMany() for batch paths to amortize per-call overhead.Set a hard input length cap before check() is invoked — that single control mitigates both cost and adversarial-input risk.
Failure modes
| Failure | Blast radius | Detection | Mitigation |
|---|---|---|---|
| Over-masking legitimate content | User trust, public embarrassment | User reports; event log | Observation phase, curated allow-list, sanitize-before-reject sequencing |
| Under-masking (evasion) | Safety/compliance exposure | Sampled human review; report queue | Layer with reporting + human moderation; do not rely on the filter alone |
ProfanityRejectedException uncaught in a save |
500s, partial writes | Error rate spike on write endpoints | Global exception handler mapping to 422; wrap transactional writes |
| Middleware 422 on unexpected fields | Broken API clients | 4xx spike post-deploy | Explicit field lists, never ['*'] in production |
| Stale verdicts after dictionary update | Inconsistent enforcement | Verdict divergence across nodes | Purge results cache on config deploy; version the cache key |
| Cache store outage or eviction storm | Latency spike on writes | p99 regression | Fail-open to uncached check; alert on cache error rate |
| Regex backtracking on crafted input | Worker pool exhaustion | p99 latency, 504s | Input length caps, driver downgrade on public endpoints, load test with adversarial strings |
| State leak under Octane | Cross-request contamination | Non-deterministic verdicts | Validate before Octane adoption; prefer explicit per-call options over global toggles |
| Model writes bypassing the trait | Policy gaps in ETL/admin paths | Periodic audit of write paths | Enforce at validation/middleware layer too; audit insert()/query-builder usage |
Ramp-up
Very low for engineering — one developer, one day, plus a day for test-double integration and listener wiring. Blasp::fake() materially reduces the cost of writing deterministic tests around enforcement, which is a real quality-of-life win.
The genuine ramp-up cost sits outside engineering: defining severity policy per surface, negotiating acceptable false-positive behavior with Support, and standing up an allow-list governance process. Plan for a two-to-four-week observational runway before any enforcement ships, and treat the wordlist as a living product artifact rather than a config constant.
How can I help you explore Laravel packages today?