indexzer0 / laravel-validation-provider
Simple reusable composable validation providers
Fund package maintenance!
IndexZer0
Requires
- php: ^8.2
- illuminate/contracts: ^11.0
- illuminate/support: ^11.0
Requires (Dev)
- larastan/larastan: ^2.0.1
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.1
- orchestra/testbench: ^9.0
- pestphp/pest: ^2.34
- pestphp/pest-plugin-arch: ^2.7
- pestphp/pest-plugin-laravel: ^2.0
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^1.0
- phpstan/phpstan-phpunit: ^1.0
- spatie/laravel-ray: ^1.26
This package is auto-updated.
Last update: 2024-12-17 06:42:15 UTC
README
- Write all your validation rules in clean reusable composable providers.
- Standardise the way you define and use validation in
Form Requests
and elsewhere. - Easily compose validation rules using multiple validation providers.
- Conveniently create and validate data straight from the
ValidationProvider
.
Simple example
- Nesting domain model rules within arrays.
use IndexZer0\LaravelValidationProvider\Facades\ValidationProvider; class AuthorValidationProvider extends AbstractValidationProvider { protected array $rules = ['name' => ['required']]; } class BookValidationProvider extends AbstractValidationProvider { protected array $rules = ['title' => ['required']]; } $validationProvider = ValidationProvider::make([ 'author' => [ AuthorValidationProvider::class, new ArrayValidationProvider('books', new BookValidationProvider()), ], ]); $validationProvider->rules(); // [ // 'author.name' => ['required'], // 'author.books.*.title' => ['required'], // ]
- Simple Example
- Requirements
- Installation
- Usage
- Package Offering
- Changelog
- Contributing
Requirements
Installation
You can install the package via composer:
composer require indexzer0/laravel-validation-provider
Usage
Defining Validation Providers
- Create granular representations of domain concepts in validation provider classes.
- Should extend
AbstractValidationProvider
.
- Should extend
Via Properties
$rules
, $messages
, $attributes
class AddressValidationProvider extends AbstractValidationProvider { protected array $rules = [ 'post_code' => ['required', 'string', 'between:1,20'], ]; protected array $messages = []; protected array $attributes = []; }
Via Methods
- You can also define methods
rules()
,messages()
,attributes()
.- Sometimes you need to dynamically define rules, messages and attributes.
- You are using a dependent rule.
- See Dependent Rules for more info.
class AddressValidationProvider extends AbstractValidationProvider { public function rules(): array { return [ 'post_code' => ['required', 'string', "regex:" . RegexHelper::getPostCodeRegex()], 'zip_code' => ["same:{$this->dependentField('post_code')}"] ]; } public function messages(): array { return []; } public function attributes(): array { return []; } }
Creating Validation Providers
There are 3 ways to create validation providers. Facade
, Manual Creation
, and Fluent API
.
In all 3 examples, were going to use the following two defined validation providers along-side this packages core validation providers to achieve validation rules of:
class AuthorValidationProvider extends AbstractValidationProvider { protected array $rules = ['name' => ['required'],]; } class BookValidationProvider extends AbstractValidationProvider { protected array $rules = ['title' => ['required',],]; } // Desired validation rules: // [ // 'author.name' => ['required'], // 'author.books' => ['required', 'array', 'min:1', 'max:2'], // 'author.books.*.title' => ['required'], // ]
Facade
use IndexZer0\LaravelValidationProvider\Facades\ValidationProvider; $validationProvider = ValidationProvider::make([ 'author' => [ AuthorValidationProvider::class, new CustomValidationProvider(['books' => ['required', 'array', 'min:1', 'max:2']]), new ArrayValidationProvider('books', new BookValidationProvider()), ], ]); $validationProvider->rules(); // [ // 'author.name' => ['required'], // 'author.books' => ['required', 'array', 'min:1', 'max:2'], // 'author.books.*.title' => ['required'], // ]
Manual Creation
$validationProvider = new NestedValidationProvider( 'author', new AggregateValidationProvider( new AuthorValidationProvider(), new CustomValidationProvider(['books' => ['required', 'array', 'min:1', 'max:2']]), new ArrayValidationProvider('books', new BookValidationProvider()) ) ); $validationProvider->rules(); // [ // 'author.name' => ['required'], // 'author.books' => ['required', 'array', 'min:1', 'max:2'], // 'author.books.*.title' => ['required'], // ]
Fluent API
- For the fluent API, you compose your validation providers from bottom up.
$validationProvider = (new BookValidationProvider()) ->nestedArray('books') ->with(new CustomValidationProvider(['books' => ['required', 'array', 'min:1', 'max:2']])) ->with(AuthorValidationProvider::class) ->nested('author'); $validationProvider->rules(); // [ // 'author.name' => ['required'], // 'author.books' => ['required', 'array', 'min:1', 'max:2'], // 'author.books.*.title' => ['required'], // ]
Service/Action Class Usage
In your services and actions ->createValidator()
and ->validate()
methods are provided for convenience.
$addressValidationProvider = new AddressValidationProvider(); /** @var Illuminate\Validation\Validator $validator */ $validator = $addressValidationProvider->createValidator($data); /** @var array $validated */ $validated = $addressValidationProvider->validate($data);
Form Requests Usage
You can use validation providers in your form requests via two methods.
Extending Abstract
ValidationProviderFormRequest
is provided to extend your form requests from.
Using prepareForValidation
hook to instantiate validation provider.
class StoreAddressRequest extends ValidationProviderFormRequest { public function prepareForValidation() { $this->validationProvider = new AddressValidationProvider(); } }
Or using dependency injection.
// In a service provider. $this->app->when(StoreAddressRequest::class) ->needs(ValidationProvider::class) ->give(AddressValidationProvider::class); class StoreAddressRequest extends ValidationProviderFormRequest { public function __construct(ValidationProvider $validationProvider) { $this->validationProvider = $validationProvider; } }
Decorate With Trait
HasValidationProvider
is provided to decorate your existing form requests.
Sometimes you don't have the ability to extend ValidationProviderFormRequest
. You can instead use the HasValidationProvider
trait in your existing form request.
class StoreAddressRequest extends YourOwnExistingFormRequest { use HasValidationProvider; public function prepareForValidation() { $this->validationProvider = new AddressValidationProvider(); } }
Available Validation Providers
This package provides core classes that give you the ability to compose your validation providers.
- Aggregate Validation Provider
- Nested Validation Provider
- Array Validation Provider
- Custom Validation Provider
- Exclude Attributes Validation Provider
- Map Attributes Validation Provider
Aggregate Validation Provider
- Used when composing validation providers next to each other.
class AggregateValidationProvider extends AbstractValidationProvider {} $validationProvider = new AggregateValidationProvider( new AuthorValidationProvider(), new BookValidationProvider(), ); $validationProvider->rules(); // [ // 'name' => ['required'], // From AuthorValidationProvider. // 'title' => ['required'], // From BookValidationProvider. // ]
Nested Validation Provider
- Used when wanting to nest a validation provider inside an array.
class NestedValidationProvider extends AbstractValidationProvider {} $validationProvider = new NestedValidationProvider( 'author', new AuthorValidationProvider(), ); $validationProvider->rules(); // [ // 'author.name' => ['required'], // From AuthorValidationProvider. // ]
Array Validation Provider
- Used when validating an array of domain models.
- https://laravel.com/docs/10.x/validation#validating-nested-array-input
class ArrayValidationProvider extends NestedValidationProvider {} $validationProvider = new ArrayValidationProvider('books', new BookValidationProvider()); $validationProvider->rules(); // [ // 'books.*.title' => ['required'], // From BookValidationProvider. // ]
Custom Validation Provider
- Used when wanting to validate data without creating a dedicated ValidationProvider class.
class CustomValidationProvider extends AbstractValidationProvider {} $customRules = [ 'books' => ['required', 'array', 'min:1', 'max:2'], ]; $customMessages = [ 'books.required' => 'Provide :attribute' ]; $customAttributes = [ 'books' => 'BOOKS' ]; $validationProvider = new CustomValidationProvider($customRules, $customMessages, $customAttributes); $validationProvider->rules(); // [ // 'books' => ['required', 'array', 'min:1', 'max:2'], // ]
Exclude Attributes Validation Provider
- Sometimes you may want to remove certain attributes from a validation provider.
class ExcludeAttributesValidationProvider extends AbstractValidationProvider {} $validationProvider = new ExcludeAttributesValidationProvider( ['one'], new CustomValidationProvider([ 'one' => ['required'], 'two' => ['required'] ]) ); $validationProvider->rules(); // [ // 'two' => ['required'], // ]
Map Attributes Validation Provider
- Sometimes you may want to rename an attribute.
class MapAttributesValidationProvider extends AbstractValidationProvider {} $validationProvider = new MapAttributesValidationProvider( ['one' => 'two'], new CustomValidationProvider([ 'one' => ['required'], ]) ); $validationProvider->rules(); // [ // 'two' => ['required'], // ]
Digging Deeper
Using Fluent API
Using Facade
ValidationProvider::make(ValidationProviderInterface|string|array $config): ValidationProviderInterface
- Can use fully qualified class name strings.
// Returns AuthorValidationProvider $validationProvider = ValidationProvider::make(AuthorValidationProvider::class);
- Invalid class string throws exception.
// throws ValidationProviderExceptionInterface try { $validationProvider = ValidationProvider::make('This is an invalid fqcn string'); } catch (\IndexZer0\LaravelValidationProvider\Contracts\ValidationProviderExceptionInterface $exception) { $exception->getMessage(); // Class must be a ValidationProvider }
- Can use validation provider objects. Essentially does nothing.
// Returns AuthorValidationProvider (same object) $validationProvider = ValidationProvider::make(new AuthorValidationProvider());
- Can use arrays (fully qualified class name strings and objects).
// Returns AuthorValidationProvider $validationProvider = ValidationProvider::make([ AuthorValidationProvider::class, ]); // Returns AggregateValidationProvider $validationProvider = ValidationProvider::make([ AuthorValidationProvider::class, new BookValidationProvider() ]);
- Array string keys create
NestedValidationProvider
.
// Returns NestedValidationProvider $validationProvider = ValidationProvider::make([ 'author' => [ AuthorValidationProvider::class, ], ]);
- Empty array is invalid.
// throws ValidationProviderExceptionInterface try { $validationProvider = ValidationProvider::make([]); } catch (\IndexZer0\LaravelValidationProvider\Contracts\ValidationProviderExceptionInterface $exception) { $exception->getMessage(); // Empty array provided }
Composing Validation Providers
Use case:
- You may have parts of your application that need to validate data for multiple domain concepts.
- You may want to validate data in nested arrays without introducing duplication in your rule definitions.
Example:
Let's look at the example of 3 routes and how you could reuse your Validation Providers.
- Route: address
- Stores address information
- Uses
AddressValidationProvider
/* * ------------------ * Address * ------------------ */ Route::post('address', StoreAddress::class); class StoreAddress extends Controller { public function __invoke(StoreAddressRequest $request) {} } class StoreAddressRequest extends ValidationProviderFormRequest { public function prepareForValidation() { $this->validationProvider = new AddressValidationProvider(); } }
- Route: contact-details
- Stores contact information
- Uses
ContactValidationProvider
/* * ------------------ * Contact * ------------------ */ Route::post('contact-details', StoreContactDetails::class); class StoreContactDetails extends Controller { public function __invoke(StoreContactDetailsRequest $request) {} } class StoreContactDetailsRequest extends ValidationProviderFormRequest { public function prepareForValidation() { $this->validationProvider = new ContactValidationProvider(); } }
- Route: profile
- Stores address and contact information.
- Uses
AddressValidationProvider
ContactValidationProvider
NestedValidationProvider
(under the hood from Facade)AggregateValidationProvider
(under the hood from Facade)
/* * ------------------ * Profile * ------------------ */ Route::post('profile', StoreProfile::class); class StoreProfile extends Controller { public function __invoke(StoreProfileRequest $request) {} } class StoreProfileRequest extends ValidationProviderFormRequest { public function prepareForValidation() { $this->validationProvider = ValidationProvider::make([ 'profile' => [ 'address' => AddressValidationProvider::class 'contact' => ContactValidationProvider::class ] ]); } }
Dependent Rules
- When using any of the dependent rules, you should use the
$this->dependentField()
helper.- This ensures that when using the
NestedValidationProvider
andArrayValidationProvider
, the dependent field will have the correct nesting.
- This ensures that when using the
class PriceRangeValidationProvider extends AbstractValidationProvider { public function rules(): array { return [ 'min_price' => ["lt:{$this->dependentField('max_price')}"], 'max_price' => ["gt:{$this->dependentField('min_price')}"], ]; } } $validationProvider = new NestedValidationProvider( 'product', new PriceRangeValidationProvider() ); $validationProvider->rules(); // [ // "product.min_price" => [ // "lt:product.max_price" // ] // "product.max_price" => [ // "gt:product.min_price" // ] // ]
Error Handling
All exceptions thrown by the package implement \IndexZer0\LaravelValidationProvider\Contracts\ValidationProviderExceptionInterface
.
How-ever it doesn't harm to also catch \Throwable
.
try { $validationProvider = ValidationProvider::make('This is an invalid fqcn string'); } catch (\IndexZer0\LaravelValidationProvider\Contracts\ValidationProviderExceptionInterface $exception) { $exception->getMessage(); // Class must be a ValidationProvider } catch (\Throwable $t) { // Shouldn't happen - but failsafe. }
Package Offering
// Interface interface ValidationProvider {} // Validation Providers abstract class AbstractValidationProvider implements ValidationProvider {} class AggregateValidationProvider extends AbstractValidationProvider {} class NestedValidationProvider extends AbstractValidationProvider {} class ArrayValidationProvider extends NestedValidationProvider {} class CustomValidationProvider extends AbstractValidationProvider {} class ExcludeAttributesValidationProvider extends AbstractValidationProvider {} class MapAttributesValidationProvider extends AbstractValidationProvider {} // Form Request class ValidationProviderFormRequest extends \Illuminate\Foundation\Http\FormRequest {} trait HasValidationProvider {} // Facade class ValidationProvider extends \Illuminate\Support\Facades\Facade {}
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Credits
License
The MIT License (MIT). Please see License File for more information.