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

Profanity Bundle Laravel Package

vangrg/profanity-bundle

vangrg/profanity-bundle adds configurable profanity filtering to PHP applications, detecting and masking offensive words in user input and text. A lightweight way to clean comments, usernames, and messages before storage or display.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the source before you require it. This is a Symfony bundle (4 stars, last release 2019-02-20, zero dependents), not a Laravel package. The bundle is a thin wrapper: almost all behaviour lives in the base library vangrg/profanity, and the bundle just registers one service and maps a dictionary config key to a file path. For a Laravel app you want the library, not the bundle.

composer require vangrg/profanity

Only install vangrg/profanity-bundle if you are genuinely sharing code with a Symfony app — it pulls in symfony/http-kernel and symfony/dependency-injection, and its ProfanityBundle class extends Symfony\Component\HttpKernel\Bundle\Bundle, which means php artisan will never auto-discover it. There is no ServiceProvider, no facade, no publishable config.

First steps in a Laravel app

  1. Open vendor/vangrg/profanity/src/Profanity.php. It is ~200 lines and is the entire documentation. Confirm the method signatures (check(), filter(), addWord(), setDictionary()) and, critically, the boolean sense of check() — verify whether true means "clean" or "contains profanity" before you wire it into a validator.
  2. Look at the shipped dictionary file (a plain newline-delimited word list). You will need to ship your own copy; don't depend on the vendored one surviving composer update.
  3. Pick your first use case: moderating user-generated text — comment bodies, review text, display names, chat messages. Start with masking on output rather than blocking on input; it is reversible and doesn't strand legitimate users.

Bind it once in App\Providers\AppServiceProvider:

$this->app->singleton(\Vangrg\Profanity\Profanity::class, function () {
    return new \Vangrg\Profanity\Profanity(
        config('profanity.dictionary_path')
    );
});

Add config/profanity.php with dictionary_path (default resource_path('profanity/dictionary.txt')), replacement, and a whitelist array. Resolve the path with resource_path()/base_path() at runtime — never bake an absolute path into config, or config:cache on deploy will hand you a stale one.

Implementation Patterns

Custom validation rule (the 90% case). Wrap the checker in a Illuminate\Contracts\Validation\Rule so it composes with the rest of your FormRequest rules:

class CleanText implements ValidationRule
{
    public function __construct(private Profanity $checker) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (is_string($value) && $this->checker->check($value)) {
            $fail("The :attribute contains prohibited language.");
        }
    }
}

Resolve via the container (app(CleanText::class)) so the singleton — and its parsed dictionary — is reused.

Mask on display, don't just reject on input. Store the raw text, render a filtered version. A Blade directive keeps call sites tidy:

Blade::directive('clean', fn ($expr) => "<?php echo e(app(Profanity::class)->filter($expr)); ?>");

Deferred moderation instead of hard failure. Rather than 422-ing every submission, write the row and push a FlagForReview job when check() is true. You keep the audit trail (store the matched term and dictionary version in a moderation_flags table), and you avoid telling an attacker which exact token tripped the filter.

Retroactive scans belong in queue jobs. Dictionary contents change; old rows were validated against an older list. A nightly ModerateExistingContent job using ->chunkById(500) is the right shape — never a synchronous full-table scan in a request.

Tenant/domain dictionaries. For multi-tenant apps, keep the base dictionary shared and call addWord() per tenant inside a scoped binding, or subclass Profanity and setDictionary() to a per-tenant file. Load it lazily; a per-request file parse is measurable on high-RPS endpoints.

Testing. Never let tests hit the real dictionary. Bind a fake in TestCase::setUp():

$this->swap(Profanity::class, new class extends Profanity {
    public function check(string $text): bool { return str_contains($text, 'badword'); }
});

Then assert on your rule and job behaviour, not on the library's word list.

Gotchas and Tips

  • Composer conflict is the most likely blocker. A 2019 Symfony bundle will constrain symfony/* versions that clash with Laravel 10/11's Symfony 6/7 requirements. If composer require explodes, use vangrg/profanity (far fewer constraints) or vendor the single Profanity.php class into app/Support/.
  • PHP 8 compatibility is unverified. A package last released in Feb 2019 predates PHP 8.0. Grep for each(), create_function, unguarded strpos() null returns, and implicit nullable parameters before adopting. Test on your actual PHP version, not just CI's.
  • Naive substring matching produces the Scunthorpe problem. Expect false positives on assassin, class, grape, shiitake, Penistone. Always keep a whitelist and prefer masking over blocking. A blocked user is a lost user; a masked word is a cosmetic bug.
  • Lowercase + MB-safe before matching. Accents, leetspeak (sh1t, f*ck), spacing (f u c k), and Cyrillic/Greek homoglyphs will all slip through plain strtolower() matching. Normalize with mb_strtolower and consider Normalizer::normalize($text, Normalizer::FORM_KC) — but accept that you are doing best-effort filtering, not security.
  • Verify the polarity of check() on day one. Getting it inverted means either a filter that does nothing or one that blocks everything. Write one test asserting a known-clean and a known-dirty string before building anything on top.
  • Cache the parsed dictionary. Re-reading and exploding a word list on every request is the single most common performance mistake with this class. Bind it as a singleton (see above) or wrap the parse in Cache::rememberForever.
  • Log matches in production. Store the term, the input excerpt, and a dictionary version in your moderation table. Without it you cannot tune the word list or defend a decision to a user.
  • Treat the package as unmaintained. 4 stars, 0 dependents, no advisories, no releases in six years. There is no upstream you can rely on for fixes. Budget for the possibility that you own this code — keep the call surface small (one wrapper class, one validation rule) so swapping in a maintained alternative later is a one-file change.
  • Extension points are thin. addWord(), setDictionary(), and the dictionary config key are essentially the whole surface. If you need tokenizer-aware matching, stemming, or per-locale dictionaries, subclass Profanity and keep your additions behind your own interface rather than scattering calls to the vendor class through the codebase.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core