dive-be / laravel-dry-requests
Dry run your Laravel requests
Installs: 244 018
Dependents: 0
Suggesters: 0
Security: 0
Stars: 209
Watchers: 2
Forks: 5
Open Issues: 0
Requires
- php: ^8.1
- dive-be/php-enum-utils: ^1.1
- dive-be/php-utils: ^0.1.0
- laravel/framework: ^10.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.8
- nunomaduro/larastan: ^2.6
- orchestra/testbench: ^8.5
- pestphp/pest: ^2.6
- pestphp/pest-plugin-laravel: ^2.0
README
Warning two months after the release of our package, Taylor Otwell announced an almost identical functionality as a core package. Since this package has pretty much been made obsolete, we have decided to stop maintaining it.
So, please consider migrating to Laravel Precognition.
X-Dry-Run your requests
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.
Showcase
What problem does this package solve?
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.
Installation
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', ];
Behavior
💡 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.
Usage
Assume the following endpoint: POST /users
and Controller
.
Option 1 - using FormRequest
s
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 😎.
Option 2 - using validate
method on the Request
object
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 🤙.
Front-end execution
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
Inertia.js example
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} />
Fine-tuning Dry Validations: AllFailures
/ FirstFailure
- 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.
Conflicting FormRequest
methods
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 } }
Testing
composer test
Upgrading
Please see UPGRADING for details.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security related issues, please email oss@dive.be instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.