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

Pure Text Laravel Package

yasser-elgammal/pure-text

Pure Text is a lightweight Laravel package that converts rich HTML content into clean, readable plain text. Strip tags, decode entities, normalize whitespace, and generate search-friendly excerpts for notifications, previews, and indexes through a simple, expressive API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin v2

composer require "yasser-elgammal/pure-text:^2.0"
php artisan vendor:publish --tag=pure-text-config   # re-check the exact tag in the v2 README

v2 is a major bump, so treat this as a migration rather than a patch. The release notes call out three things — Unicode/Arabic support, refactoring, and new features — none of which come with a documented BC-break list. Diff before you commit:

php artisan vendor:publish --tag=pure-text-config --force   # then git diff config/pure-text.php
composer diff    # or diff vendor/.../src between the old and new tag

Where to look first

  1. config/pure-text.php — the most likely place for breaking changes. Expect new or renamed keys around normalization and locale. Re-publish to a temp path and diff against your committed config rather than assuming old key names still resolve.
  2. src/ — v2 was refactored, so class names, namespaces, and strategy constructor signatures may have moved. If you subclassed or type-hinted any package class in v1, grep your codebase for those names before running tests.
  3. The Unicode/normalization layer — this is the headline feature. Find the class that handles Arabic variant folding and read it, because whether it runs automatically or is opt-in determines whether you keep your own normalizer.
  4. The README's word-list section — still a starting point, and now possibly shipped in per-script variants.

First use case: clean user-generated text on write

use YasserElgammal\PureText\Facades\PureText;

$post = Post::create([
    'title' => $request->input('title'),
    'body'  => PureText::filter($request->input('body')),
]);

If the facade alias changed in v2 (refactors often move facades), resolve from the container (app('pure-text')) or use the helper the README documents. String in, filtered string out is the stable contract across both majors.

Migrating from v1

Run your existing sanitizer tests against v2 before touching production. Pay particular attention to Arabic fixtures — if v2 now normalizes input internally, strings that previously passed through unchanged may now match and be masked.

Implementation Patterns

FormRequest: sanitize before validation

protected function prepareForValidation(): void
{
    $this->merge([
        'body' => PureText::filter((string) $this->input('body')),
    ]);
}

Still the best default. Filtering here means max: rules run against the post-filter length, which matters because masking preserves length and removal does not. Keep this scoped to genuine free-text fields — emails, slugs, and base64 payloads should never see the filter.

Model mutator for single-field models

public function setBodyAttribute($value): void
{
    $this->attributes['body'] = PureText::filter((string) $value);
}

Fine for one field. At five fields, move to a FormRequest or an explicit service call.

Blade directive as an escape hatch

@pureText($comment->body)

Use only for legacy rows that predate your filter. Verify echo-escaping semantics against {{ }} before using it inside attributes — and re-verify after the v2 refactor, since directive registration is exactly the kind of thing a refactor touches.

Middleware for route groups

Route::middleware(['auth', 'pure-text'])->group(function () {
    Route::post('/comments', [CommentController::class, 'store']);
});

Confirm the alias is still registered under the same name in v2. Request-rewriting middleware touches every field in the payload by default — test with a request containing a URL and a code snippet to confirm they survive.

Per-locale or per-tenant word lists

config(['pure-text.words' => config('pure-text.words.ar')]);
$clean = PureText::filter($text);

Wrap this in a TextSanitizer service resolving the list from locale or tenant, so the config mutation isn't scattered across controllers. Config mutations are per-request only and are lost or leaked under Octane and long-lived queue workers. If v2 added a first-class locale option, prefer that over mutating config.

Choose the strategy deliberately

  • Mask preserves length and reads as intentional moderation — good for public comments.
  • Remove shifts offsets and can produce run-on words — avoid on text you later diff or highlight.
  • Replace with a neutral token keeps token boundaries intact, which is safest for search indexes.

Set it once at config level. Only switch per call site if v2 exposes a documented per-call option.

Decide whether to keep your own normalizer

This is the main design question v2 forces. If the package now folds Alef variants, ى/ي, ة/ه, harakat, and tatweel before matching, delete your custom normalizer — running both wastes cycles and can double-transform. If it only supports Unicode input without normalizing, keep your layer. Read the source of the normalization path, don't infer it from the changelog line.

Store both raw and filtered when moderation matters

Add a body_raw column if filtered posts may need review or appeal. Filtering is lossy and irreversible. This decision costs one column and saves you a data-loss incident.

Gotchas and Tips

The v2 changelog is not a migration guide

"Unicode/Arabic support, refactoring, and new features" tells you nothing about what broke. The practical checklist: re-publish and diff the config, grep for package class names you referenced, re-run your sanitizer suite, and smoke-test the login and comment flows. Do this on a branch, not on main.

Namespace and strategy class churn from the refactor

Any use statement pointing at package internals is at risk. This is the argument for wrapping the filter in your own TextSanitizer service: one file to fix instead of dozens of call sites. If you were subclassing a strategy class, v2 is a good moment to stop.

Arabic normalization — verify, don't assume

The feature is announced, but "supports Arabic" previously meant only "handles UTF-8 correctly." Test explicitly with:

  • Alef variants: أ إ آ ا
  • ى → ي, ة → ه
  • Diacritics (harakat) and tatweel ـ
  • Presentation forms and NFC/NFD differences

If a blocked word still slips through, it's an unnormalized variant, not a broken regex. Add both normalized and raw forms to the list, or normalize before calling the filter.

The Scunthorpe problem

Substring matching blocks innocent words containing a blocked sequence. Check whether the config exposes whole-word or boundary matching. Whole-word is usually right for Latin; for Arabic, clitics (و, ال, ب) make \b unreliable, so a prefix-aware rule serves better.

Regex escaping and catastrophic backtracking

If you build patterns from the list, preg_quote($word, '/') every entry. One unescaped . or * in a custom word becomes a wildcard, and a pathological entry can blow up on long input. Cap input length before filtering long-form submissions.

Config caching

After editing the word list in production, run php artisan config:cache. And the reverse: when config is cached, runtime config([...]) overrides apply per-request but won't persist — which surprises people under queue workers that reuse state.

Performance: filter once, not per read

Filtering is regex work proportional to text length. In an accessor or Blade directive, every list page and API response pays for it. Filter on write, store the result, reserve render-time filtering for legacy data.

Middleware scope creep

Request-rewriting middleware applies to all input, including password, _token, and file metadata. Scope it to specific routes or verify the package skips non-string and known-safe keys. Re-test registration and login after upgrading — a filtered password field is a support nightmare.

Version pinning

Pin "^2.0" and read the changelog before every minor. With a low-traffic package you are part of the QA surface; the v2 release notes are evidence enough that you can't rely on upstream to enumerate impact.

Testing and extension points

Assert both directions: a blocked word is transformed, and a benign string passes through unchanged. Include at least one Arabic string with a diacritic and one with an Alef variant — those are exactly the cases v2 claims to fix and exactly the cases most likely to regress. Keep the filter behind your own thin service rather than subclassing package internals; the strategy classes are the likeliest thing to change again.

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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin