This package allows you to check if your requests would pass validation if you executed them normally.
The Laravel equivalent of --dry-run
in various CLI tools, or what some devs call "preflight requests".
🚀 Hit the endpoint as users are entering information on the form to provide real-time feedback with 100% accuracy.
🚀 Validate only a subset of data of a multi-step form to guarantee success when the form is eventually submitted.
A traditional approach to validating user input in JavaScript applications (Inertia / SPA / Mobile) is using a library such as yup to do the heavy lifting and delegating complex business validations to the server.
However, the client-side can never be trusted, so you can't simply omit the validation rules that ran on the front-end. This means that validation has to live in 2 distinct places and you will have to keep them in sync. This is very tedious and wasteful, so this is where this package comes into play.
You can install the package via composer:
composer require dive-be/laravel-dry-requests
You can publish the config file with:
php artisan vendor:publish --provider="Dive\DryRequests\ServiceProvider" --tag="config"
This is the contents of the published config file:
return [
/*
|--------------------------------------------------------------------------
| Default Dry Validation Behavior
|--------------------------------------------------------------------------
|
| All dry requests are validated against a subset of the defined rules.
| In other words only present fields are checked during the request.
| You may choose to halt validation as soon as a failure occurs,
| or continue validating all fields and return all failures.
|
| Supported: "all", "first"
|
*/
'validation' => 'first',
];
💡 Controller
logic is not executed after a successful validation attempt. 200 OK
is returned upon a successful dry run.
💡 Only present fields are validated to ensure good UX. Other fields are skipped using the sometimes
rule.
This means that you are responsible for only sending the relevant fields for validating e.g. a step of a multi-step wizard.
Assume the following endpoint: POST /users
and Controller
.
Controller
injecting a StoreUserRequest
:
class UserController
{
public function store(StoreUserRequest $request): UserResource
{
$user = User::create($request->validated());
return new UserResource($user);
}
}
Add the DryRunnable
trait to your FormRequest
:
class StoreUserRequest extends FormRequest
{
use DryRunnable;
public function rules(): array
{
return [
'email' => ['required', 'email', 'max:255', 'unique:users'],
'username' => ['required', 'string', 'min:2', 'max:255', 'unique:users'],
'nickname' => ['nullable', 'string', 'min:2', 'max:255'],
];
}
}
That's it 😎.
class UserController
{
public function store(Request $request): UserResource
{
$validated = $request->validate([
'email' => ['required', 'email', 'max:255', 'unique:users'],
'username' => ['required', 'string', 'min:2', 'max:255', 'unique:users'],
'nickname' => ['nullable', 'string', 'min:2', 'max:255'],
]);
$user = User::create($request->validated());
return new UserResource($user);
}
}
You don't have to do anything at all 🤙.
Now, hit the endpoint from the client-side like you normally would.
But with the added X-Dry-Run
header.
// 1. "Username has already been taken" validation error
axios.post('/users', { username: 'Agent007' }, { headers: { 'X-Dry-Run': true } })
.then(response => response.status); // 422 Unprocessable Entity
// 2. Successful unique username check: Controller did not execute
axios.post('/users', { username: 'Asil Kan' }, { headers: { 'X-Dry-Run': true } })
.then(response => response.status); // 200 OK
// 3. Successful unique e-mail check: Controller did not execute
axios.post('/users', { email: 'muhammed@dive.be' }, { headers: { 'X-Dry-Run': true } })
.then(response => response.status); // 200 OK
// 4. Submit entire form: Controller executed
axios.post('/users', { email: 'muhammed@dive.be', username: 'Asil Kan' })
.then(response => response.status); // 201 Created
const { clearErrors, data, errors, setData } = useForm({
email: '',
password: '',
password_confirmation: '',
});
const pick = (obj, fields) => fields.reduce((acc, cur) => (acc[cur] = obj[cur], acc), {});
const validateAsync = (...fields) => () => {
Inertia.post(route('register'), pick(data, fields) , {
headers: { 'X-Dry-Run': 'all' },
onError: setError,
onSuccess() { clearErrors(...fields); },
});
}
// Somewhere in your template
<input onBlur={validateAsync('email')}
type="email"
name="email"
value={data.email}
onChange={setData} />
<input type="password"
name="password"
value={data.password}
onChange={setData} />
<input onBlur={validateAsync('password', 'password_confirmation')}
type="password"
name="password_confirmation"
value={data.password_confirmation}
onChange={setData} />
- The default validation behavior for dry requests is halting validation as soon as an error is found. This is especially useful when handling async validation for a single field.
- The other option is to keep validating even if an error is encountered. This is especially useful for multi-step forms.
You can alter this behavior on 3 distinct levels.
- Change
first
toall
(or vice versa) in thedry-request
config file. This will apply to all of your requests. - FormRequest only - Use the
Dive\DryRequests\Dry
attribute along withDive\DryRequests\Validation
on therules
method to force a specificValidation
behavior for a particularFormRequest
.
#[Dry(Validation::AllFailures)]
public function rules(): array
{
return [...];
}
- Dictate the behavior on the fly from the front-end using the
X-Dry-Run
header. Valid values:all
,first
.
axios.post('/users', { email: '...', username: '...' }, { headers: { 'X-Dry-Run': 'all' } })
.then(response => response.status); // 200 OK
Note: the header value will be ignored if you have explicitly set a validation behavior on the FormRequest
using the Dry
attribute.
The package makes use of the available methods passedValidation
and withValidator
available on FormRequest
classes to enable its behavior.
If you define these in your own requests, you must call the "dry" methods manually:
class CreateUserRequest extends FormRequest
{
protected function passedValidation()
{
$this->stopWhenDry(); // must be called first
// your custom logic
}
protected function withValidator(Validator $instance)
{
$instance = /* your custom logic */;
$this->withDryValidator($instance); // must be called last
}
}
composer test
Please see UPGRADING for details.
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security related issues, please email oss@dive.be instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.