michael-rubel/laravel-safe-request
A Laravel package for safer request handling: define validation rules and access only validated input, keeping unvalidated fields out of controllers and services. It helps reduce mass-assignment and untrusted-data risks while keeping form requests simple.
Install the package, pinning the 2.x line, and wire the trait into a single FormRequest:
composer require michael-rubel/laravel-safe-request:^2.0
Requires Laravel 10 as of 2.0.1 (2.0.0 topped out at Laravel 9). If you're on Laravel 11/12, see the compatibility gotcha below before committing to the dependency.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use MichaelRubel\SafeRequest\SafeRequest;
class StoreUserRequest extends FormRequest
{
use SafeRequest;
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
];
}
}
The trait's job is to make the validated payload the default source for the request accessors, so $request->input('name'), $request->all(), $request->only(...), $request->collect() etc. return only data that passed rules() — instead of the raw, unvalidated bag. That means the controller no longer needs $request->validated() sprinkled everywhere:
public function store(StoreUserRequest $request)
{
User::create($request->all()); // already constrained to validated keys
}
Where to look first: read the trait source itself (src/SafeRequest.php) — it's small and is the whole contract. Then check composer.json for the illuminate/* constraint (this is what 2.0.1 changed) and skim the README for which accessor methods it overrides and whether it ships a config file (config/safe-request.php). Only after that decide whether the override list matches the methods your codebase actually calls.
First use case: retrofit one noisy controller. Pick a FormRequest with a long validated() call chain, add the trait, delete the manual validated() extraction, and confirm the response shape is unchanged.
Progressive rollout via a base request. Rather than editing every FormRequest, make an App\Http\Requests\SafeFormRequest that extends FormRequest and uses the trait, then extend that for requests you want to opt in. Requests still on plain FormRequest keep their old semantics, so migration is per-endpoint and reversible. This also gives you one file to patch if you later need to vendor the trait for framework compatibility.
PATCH/partial updates. This is where the pattern earns its keep. With sometimes rules, validated() only contains keys actually present in the payload. So:
'name' => ['sometimes', 'string'],
combined with $request->input('name', $user->name) gives you sane defaults without an array_key_exists dance — and unvalidated keys still can't leak through.
Dot-notation for nested arrays. $request->input('address.city') works against the validated array, which is exactly what you want for nested resources. Keep rules flat with address.city style or nested arrays; both round-trip through the trait consistently.
Push extraction into actions/services. Because the accessors are "safe" everywhere, you can type-hint the FormRequest in a controller and hand the request itself (or $request->validated(), which now equals $request->all()) to an action class without re-validating or whitelisting.
API resources / DTOs. $request->collect() returning validated-only data makes JsonResource and spatie/laravel-data style DTO construction cleaner:
$dto = UserData::from($request->collect()->toArray());
Testing. In feature tests, $this->post(route(...), $payload) still exercises real validation, so the safe accessors behave the same as in production. Assert on the persisted model rather than on $request->all() internals to avoid coupling tests to the trait.
Composition with other request traits. If you already use traits for authorization, filters, or prepared data, add SafeRequest last in the use block and resolve any method collisions explicitly — see gotchas.
Maintenance mode — 2.0.1 is a compatibility bump, not a feature release. The only change in 2.0.1 is Laravel 10 support in the illuminate/* constraint (PR #4); nothing in the trait's behaviour moved between 2.0.0 and 2.0.1. The project has seen only sporadic maintenance since its early 2023 releases, so budget for the possibility that it goes quiet again. The trait is small enough that vendoring it into app/ and owning it is a cheap, low-risk alternative — especially if you're on Laravel 11 or 12, where FormRequest internals have kept moving and the overrides may go stale. Decide with composer.json open, not by assumption.
all() stops meaning "all". Anything relying on raw input breaks deliberately: _token, _method, extra analytics fields, honeypot values, or a field you log but don't validate. If you need the raw bag, call $request->getInputSource()->all() (or the underlying Request accessors) explicitly so the escape hatch is visible in diffs.
Query strings aren't automatically safe. ?page=3&sort=name only survives if there's a rule for page/sort. Pagination is the classic trap: $request->input('page') returns null once the trait is on, silently resetting to page 1. Add ['sometimes','integer','min:1'] rules, or read pagination from the raw request in a per_page-aware helper.
Uploaded files aren't in validated(). $request->file('avatar') and $request->hasFile() go through UploadedFile handling, not the validated array — but if you call $request->all(), files won't be there. Keep file access on the dedicated methods; don't try $request->input('avatar').
Route parameters are untouched. $request->route('user') and route-model binding are unaffected — the trait only touches input accessors. Don't expect $request->input('id') to pick up {id}.
Validation-not-yet-run behavior. The safe accessors depend on a validator having executed (normally via the FormRequest lifecycle). Manually instantiating the request, calling $request->validate() with an inline array, or resolving the request outside the container pipeline can leave you without validated data — check what the trait falls back to (raw all() vs. throwing) and make sure that fallback is acceptable in your context. Prefer always going through controller injection.
Mutation after validation. $request->merge([...]) and $request->replace(...) are easy to reach for; verify in your version of the trait whether they feed back into the validated array or bypass it. If in doubt, mutate after extraction ($data = $request->validated(); $data['x'] = ...;) rather than relying on merge semantics.
Method collisions in the use block. Traits that also override all()/input() will conflict; PHP fatals on ambiguity. Resolve with insteadof or, cleaner, make the safe base request the single place that owns those overrides.
Debugging checklist. When a field "disappears," compare dump($request->validated(), $request->getInputSource()->all()) side by side — the diff is almost always a missing or misnamed rule (nested rules especially: items.*.sku vs items.sku). Log both once during the rollout of each endpoint.
Extension point. The right place to add behavior is a SafeFormRequest base class: override prepareForValidation() for normalization, add a helper like safeOnly(string ...$keys) wrapping $this->validated(), and document that raw access requires an explicit escape hatch. That keeps the "validated by default" invariant discoverable to the next developer.
How can I help you explore Laravel packages today?