waavi/sanitizer
Waavi Sanitizer provides a simple, flexible way to clean and transform user input in Laravel before validation. Define reusable sanitizers with built-in filters like trim, escape, cast, and custom filters to keep your data safe and consistent.
Install and verify compatibility first
composer require waavi/sanitizer
Before writing any code, check the resolved version against your PHP / Laravel constraint. This package targets Laravel 7 and PHP 7.x. On Laravel 8+/PHP 8.1+ it may install but can throw deprecation notices or break in edge cases. If composer require pulls a very old tagged release, pin it explicitly and add a note in your upgrade checklist — the package is effectively unmaintained, so treat it as a vendored helper rather than a long-term dependency.
Where to look first
src/Filters/ — the actual filter implementations are small classes implementing Waavi\Sanitizer\Contracts\Filter. Reading two or three of them tells you everything about the apply($value, $options) contract.config/sanitizer.php — custom filter registration and any global settings.SanitizedRequest section — that's the only Laravel-specific integration surface.Minimal standalone use
use Waavi\Sanitizer\Sanitizer;
$data = [
'name' => ' Jane Doe ',
'email' => '[email protected]',
'age' => '042',
];
$filters = [
'name' => 'trim|escape',
'email' => 'trim|lowercase',
'age' => 'cast:integer',
];
$clean = (new Sanitizer())->sanitize($data, $filters);
// ['name' => 'Jane Doe', 'email' => '[email protected]', 'age' => 42]
Minimal form request use
use Waavi\Sanitizer\Laravel\SanitizedRequest;
class StoreUserRequest extends SanitizedRequest
{
protected $filters = [
'name' => 'trim|escape',
'email' => 'trim|lowercase',
'phone' => 'digit',
];
public function rules(): array
{
return [
'name' => 'required|string|max:120',
'email' => 'required|email|unique:users,email',
'phone' => 'nullable|string|min:7',
];
}
}
First real use case: normalizing an email/username before a unique rule runs. This is the classic win — " [email protected] " should be validated as [email protected], otherwise uniqueness and format checks behave inconsistently against stored data.
Prefer the container-bound sanitizer in services/jobs, not just form requests
The Sanitizer class is framework-agnostic. Resolve it once and pass filter maps around:
$sanitizer = app(Sanitizer::class);
$clean = $sanitizer->sanitize($payload, $this->filtersFor('import'));
Use this for CSV imports, queue payloads, console commands, and webhook handlers — anywhere you don't have a FormRequest. Form requests only cover HTTP; most real sanitation leaks past them.
Chain filters deliberately — order is the semantics
'trim|lowercase|escape' and 'escape|trim|lowercase' produce different results. Establish a convention per field type:
trim|strip_tags (escape at render/Blade time, not here).trim|uppercase or trim|lowercase.digit.cast:integer / cast:float after trim.Centralize filter maps for reuse
Form requests tend to duplicate the same $filters across Store* and Update*. Extract:
abstract class SanitizedBaseRequest extends SanitizedRequest
{
protected function userFilters(): array { /* ... */ }
}
or keep a config/sanitizer-fields.php mapping logical entities to filter arrays, then merge in each request. This keeps sanitization consistent between API and web controllers.
Sanitize nested arrays with care
Nested payloads (e.g. items.0.qty) need nested filter arrays or wildcard keys depending on the version:
protected $filters = [
'items' => [
'qty' => 'trim|cast:integer',
'sku' => 'trim|uppercase',
],
];
Wildcard syntax ('items.*.sku' => '...') has varied across releases — verify against the installed Sanitizer source before relying on it, and cover it with a test.
Custom filters are the extension point you'll actually use
namespace App\Sanitizers;
use Waavi\Sanitizer\Contracts\Filter;
class E164Phone implements Filter
{
public function apply($value, $options = [])
{
if ($value === null || $value === '') {
return $value;
}
return preg_replace('/[^\d+]/', '', (string) $value);
}
}
Register in config/sanitizer.php (typically under custom_filters) or via the sanitizer's extend/registration API, then reference as 'phone' => 'trim|e164_phone'. Keep filters pure, single-purpose, and null-safe — they run on raw user input and will occasionally receive null, arrays, or unexpected scalars.
Pair it with validation, don't replace it
Sanitization is a pre-validation transform, not a validation rule. The flow should be:
rules(), unique, exists).$request->validated() — which now contains sanitized values.If a unique check depends on normalization, put that normalization in the sanitizer, not in the rule's callback — otherwise other entry points (imports, API) bypass it.
Testing pattern
Test filters as units ((new E164Phone())->apply(' +1 (555) 010-9999 ')) and test one representative request end-to-end by instantiating the FormRequest and asserting the sanitized payload. Do not rely on feature tests alone; filter-order bugs are best caught in isolation.
required semantics. Input of " " trimmed to "" will fail required, which is usually what you want — but it also means nullable+trim combos can produce "" instead of null. If your DB columns are nullable, add a ConvertEmptyStringsToNull-style step at the end of the chain.escape filter will cause double-escaping when Blade renders it. Prefer strip_tags on input, escape on output.trim on a password silently breaks users; lowercase on a token invalidates it. Whitelist the fields you filter rather than blanket-applying filters.cast is not validation. cast:integer on "12abc" may yield 12, and on "abc" may yield 0 — which then passes integer validation. Combine with numeric/integer rules and consider strict custom casting when data integrity matters.| and :. Values containing : inside options are ambiguous; avoid options with colons, and don't pass user-controlled filter strings into sanitize().$filters property must be protected. A public or missing property silently yields no filtering in some versions — a very common "it does nothing" bug.TrimStrings and ConvertEmptyStringsToNull middleware already trim everything. Stacking the package's trim on top is harmless but hides which layer did the work; if a field is unexpectedly empty, check the middleware before blaming the sanitizer.$sanitizer->sanitize($request->all(), $this->filters) to see exactly what validation receives.all() / validationData() override. SanitizedRequest hooks the request lifecycle; confirm which method it overrides in your version (behavior has shifted between Laravel majors). If authorize() or custom withValidator code reads $this->all() before sanitization, it will see raw input.expectsJson() only changes error responses; the sanitizer still runs. If you have strict JSON contracts (e.g. client sends age as string but expects string back), cast will change the type in validated() output — document that.FormRequest::prepareForValidation() + TrimStrings middleware + explicit casts in models or DTOs. The custom-filter classes port over directly.Filter contract for reusable rules, config/sanitizer.php for registration, and the Sanitizer instance itself if you need to wrap it in your own service with default filters per context. Avoid subclassing SanitizedRequest deeply — the parent's internals are the least stable part of the package.How can I help you explore Laravel packages today?