Product Decisions This Supports
- Build vs. buy for content sanitization: HTML Purifier answers the "do we write our own sanitizer?" question with a firm no. Any product surface accepting untrusted HTML — comments, forum posts, support tickets, profile bios, CMS-authored rich text, pasted email bodies, imported content — needs XSS defense, and whitelist-based parsing is a solved problem we should not re-solve. The buy decision is one Composer line; the build decision is the integration layer around it.
- Roadmap unlock for rich-text and user-generated content features: If the roadmap includes WYSIWYG editing, markdown-plus-inline-HTML, user-generated embeds, or any "paste from Word/Google Docs" experience, this package is a prerequisite rather than an enhancement. It converts a feature we would otherwise defer behind a security review into one we can scope and ship.
- Scope control on what "safe HTML" means per surface: Because the library supports named, per-context configurations, we can make a product decision — not an engineering one — about how much formatting each surface allows. A support-ticket body might permit tables and inline CSS; a public comment might permit a half-dozen tags; a profile bio might permit a subset of styling. Those become documented product rules with a test corpus behind them, rather than implicit behavior buried in a controller.
- Trust-tier segmentation: The library's whitelist model lets us treat "content from staff," "content from verified users," and "content from anonymous sources" as distinct trust tiers with distinct sanitization policies. That is a product lever (how much expression do we grant each tier?) that the package makes cheap to operate.
- Data-import and migration features: Any backlog item involving ingesting third-party HTML — legacy CMS migration, RSS/Atom ingestion, partner content feeds, Zendesk/Intercom transcript import — becomes tractable, because aggressive parsing normalizes malformed input before filtering.
- De-risking an existing liability: If any current surface stores and renders user-supplied HTML with ad-hoc escaping or regex stripping, this is a remediation decision, not a new feature. It competes for the same sprint capacity as growth work, and the decision framing should be "cost of a breach vs. cost of a sprint."
When to Consider This Package
Adopt when:
- You store or render HTML from users, tenants, importers, or partners, and that HTML is expected to retain formatting rather than be reduced to plain text.
- Output standards compliance matters — accessibility audits, email rendering, PDF generation, syndication, or downstream consumers that break on malformed markup.
- You need CSS-level filtering, not just tag stripping. Most lightweight alternatives either drop all
style attributes or pass them through largely unfiltered; neither is acceptable if you promise styled user content.
- You have per-surface policy differences and want them expressed as configuration rather than as parallel code paths.
- You can accept a wrapper layer as a real workstream. The library is framework-agnostic and Laravel-adjacent rather than Laravel-native — no service provider, facade, or publishable config ships with it. Plan for a small internal package: singleton binding, named config profiles, a
HtmlSanitizer service, and a writable definition-cache path.
- Legal has cleared LGPL-2.1 for your distribution model. This is the gating item, not a footnote. Unmodified use as a Composer dependency is generally workable for hosted SaaS; obligations change if you ship a bundled, on-prem, or appliance artifact. Get sign-off before the sprint is committed, because a late reversal means rebuilding against a permissive alternative and re-running the entire content-regression suite.
Look elsewhere when:
- You only ever render plain text.
htmlspecialchars on output plus input validation is sufficient, and Purifier's CPU cost buys you nothing.
- You need this in a hot path at high volume with untrusted input. Purifier is CPU-bound by design and materially slower than
strip_tags(). If sanitization sits on a synchronous request path for large payloads, either cap payload size and time-box the request, or move sanitization to a queue and store both raw and sanitized forms.
- You need Markdown, not HTML. A Markdown parser with HTML disabled at the renderer is a cleaner and faster answer.
- You require a permissive license for redistribution.
symfony/html-sanitizer and HTMLawed are the usual permissive fallbacks; budget a rewrite of the wrapper's internals, not its interface.
- You need a maintained-by-many, vendor-backed dependency with an SLA. This project tracks PHP 8.4/8.5 with semantic-release cadence, which signals genuine health, but it is effectively a single-maintainer effort. Note that the "0 dependents" figure is almost certainly a data artifact — this library is transitively ubiquitous in the PHP ecosystem — so treat it as a sourcing quirk, not a signal about adoption.
- You need a Laravel-native developer experience out of the box. You are buying the engine, not the ergonomics; the ergonomics are yours to build.
How to Pitch It (Stakeholders)
For executives: This is a security control we can adopt for effectively zero license cost, and it is the reason we don't have to block rich-text features behind six weeks of custom sanitizer development. It is an established, standards-focused filter that has been tracking modern PHP releases continuously through 8.5, with a steady release cadence. The cost is not the library — it is roughly one sprint of integration work to wrap it properly so we can swap it later without touching product code. The one item needing executive attention is legal: the license is LGPL-2.1, which is fine for our hosted product but must be reviewed before we commit to any on-prem or bundled distribution. Approving that review early is the cheapest way to avoid a rebuild later.
For engineering: We are adopting the engine and owning the wrapper. Concretely: a singleton binding, per-context configuration profiles (comment, rich text, profile bio), a HtmlSanitizer service that controllers, form requests, jobs, and API resources call, and a writable, build-time-warmed definition cache at storage/app/htmlpurifier so the first request after deploy isn't a latency spike. Three rules to enforce in review: sanitization is not validation — Purifier mutates content rather than rejecting it, so it does not belong in FormRequest::rules(); nothing calls HTMLPurifier directly from Blade or a controller, or the swap becomes expensive; and every configuration change ships with a curated payload corpus and a golden-file diff in CI, because this filter's job is removal and it will happily delete legitimate content (tables, embeds, custom attributes, CSS properties) without raising an error. Budget payload size caps and request timeouts as requirements — we are parsing hostile input, and recent releases have patched both catastrophic backtracking and lexer deprecations, which tells you the attack surface is real. Store raw input alongside sanitized output so policy changes are re-appliable to history rather than permanently lossy.