graham-campbell/binput
Binput is a Laravel input cleaner that sanitises user input via HTMLPurifier, stripping dangerous markup while keeping safe content. It handles strings and arrays, supports cleaning types such as default, html, url and query, and integrates through a simple facade for safer request data.
Architecture fit. Binput is a thin, framework-native facade over Laravel's input retrieval — a single service provider, a Binput alias, and a wrapper that reads query strings, form bodies, and JSON payloads through one accessor. Architecturally it slots in at the HTTP boundary as a reader, not a transformer: controllers, Form Requests, and API resources call Binput::get('key', $default) instead of mixing request()->input(), request()->query(), and json_decode($request->getContent(), true). That is a real, if modest, consistency win on endpoints that accept both form-encoded and JSON clients. It is not middleware, it does not touch every request, and it does not sanitize, validate, or neutralize anything. The "Input Protector" framing in the package description and in the supplied PM notes is a positioning error we should correct before it reaches a roadmap review — treating this as a security control would create false confidence in exactly the way that matters least and costs most.
Integration feasibility. Low effort. composer require graham-campbell/binput, auto-discovery registers the provider and alias, php artisan about confirms it booted. Total surface is small enough to read end-to-end in an afternoon, which is the right pre-adoption gate for anything that will appear in request-handling code. No config publishing, no migrations, no bootstrapping, no queue or scheduler coupling. The realistic adoption unit is one vertical slice: convert one endpoint's input reads, add characterization tests for application/x-www-form-urlencoded, application/json, and query-string access, review the diff, then expand.
Technical risk. Three distinct risks, in order of severity:
null vs '', interaction with ConvertEmptyStringsToNull and required/nullable, nested JSON ({"user":{"name":"x"}}) and dot-notation (Binput::get('user.name')). Each is cheap to test and expensive to discover in production. Also: it does not read uploaded files — $request->file() remains the path — and the Binput alias can collide with a model or DTO of the same name.Key questions.
Binput as a sanitizer.composer.json constrain illuminate/* and php in a way that fits our app, and have we confirmed Laravel 12 behaviour rather than assumed it?Stack fit. Fits Laravel 8–11 apps on the matching release line; v11.1.0 implies PHP 8.2+ and Laravel 11. It is framework-native — no custom container bindings, no config surface, no opinion about how we structure controllers. It composes cleanly with Form Requests (prepareForValidation() can trim via Binput::get() before rules run), with API Resources, and with queued jobs and console commands, since a facade has no HTTP-boundary limitation. It does not fit alongside a parallel input-sanitization layer; pick output escaping or input sanitization for any given field, never both, or we produce double-encoded output (<) that corrupts legitimate content and generates a support queue about mangled apostrophes.
Migration path. Incremental by design, and it should stay that way. Do not rewrite every request()->input() call in one PR. Sequence: (1) read README.md, src/, and composer.json end-to-end; (2) pick one endpoint with genuine mixed-content-type traffic; (3) convert reads to Binput::get() with explicit defaults; (4) add characterization tests asserting response shape for form-encoded, JSON, and query-string; (5) add a test for a missing key and an empty-string value; (6) review the diff; (7) expand only after the first slice is quiet in staging. Roll out behind a flag or per-route, not globally — this is a reader, so a global cutover has no safety benefit and multiplies blast radius if a semantic surprise exists.
Compatibility. The gating item is version, not code. Pin the exact release. Add "re-run input characterization tests on framework major upgrade" to the upgrade checklist, because the failure mode is a silent boot failure or a changed default, not a loud exception. If we are standardizing on Laravel 12, treat this as unverified until our own suite passes — do not extend the L11 claim forward by optimism. File uploads stay on $request->file()/hasFile() and are out of scope. Verify Binput alias absence in our own namespaces before install.
Sequencing. Do the policy decision before the technical install. The order that works: (a) write the house rule for input handling and output escaping and get it into team docs; (b) confirm the demonstrated problem this solves; (c) decide vendor/fork vs. depend; (d) install and convert one slice; (e) expand. Doing (d) before (a) is how a consistency utility becomes an accidental security theater artifact. If the honest answer at step (b) is "nothing is broken; we just want tidier controllers," this is a refactor ticket, not a hardening ticket, and should be scheduled and resourced as such.
Maintenance. Effectively zero ongoing cost if it stays a reader — one facade, no config, no schema, no background work. The real maintenance burden is the fork/vendor decision: with a single maintainer and zero dependents, we should treat this as source we own. Vendoring it into the repo, pinning the version, and wrapping it in our own characterization tests converts "abandoned package causes an outage" into "routine maintenance chore." That is the trade I would recommend — the package is small enough that owning it is cheaper than the governance cost of tracking an unmaintained dependency.
Support. No vendor, no SLA, no community to escalate to. Internal support means one or two engineers who have read src/ and can debug a surprise value — reproduce with curl, log Binput::get('key') and $request->input('key') side by side, and the difference (content type, dot-notation, or default) is immediately visible. That is a fast path, but it depends on someone having done the reading. Budget one afternoon for that person up front, and document the debugging recipe alongside the house rule.
Scaling. Negligible technical scaling concern — it is a synchronous in-process read with no I/O, no cache, no state. It scales exactly as Laravel's request object does. The scaling concern is organizational: every additional endpoint converted adds a small surface where a developer might believe validation has already happened. The mitigation is the written contract at the call site — Binput::get() → Form Request validation → DTO/Model → output escaping — repeated in review until it is reflexive.
Failure modes.
Binput with tag-stripping or HTML escaping, legitimate content corrupts on output. Mitigate by choosing one policy and enforcing it in review.Binput model or service in our codebase fails to resolve. Mitigate with a namespace check pre-install.$fillable/$guarded, policies, and scoped queries.Ramp-up. Very low for an engineer already fluent in Laravel — an afternoon of reading plus one converted endpoint is sufficient. Non-trivial for the organization, which must absorb: a corrected mental model of what the package does, a written input-handling policy, a vendor/fork decision, an upgrade-checklist line, and a review habit. That organizational work is where the actual cost sits, and it is worth doing regardless of whether we adopt this package — which is arguably the strongest argument for the assessment: the policy work has standalone value, the library is optional, and the two should be evaluated separately rather than bundled into a single "should we adopt Binput" decision.
How can I help you explore Laravel packages today?