- 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
.
- 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
laravel-validation-provider | PHP Version | Laravel Version |
---|---|---|
2.x | 8.1+ | 10.x |
3.x | 8.2+ | 11.x |
You can install the package via composer:
composer require indexzer0/laravel-validation-provider
- Create granular representations of domain concepts in validation provider classes.
- Should extend
AbstractValidationProvider
.
- Should extend
$rules
, $messages
, $attributes
class AddressValidationProvider extends AbstractValidationProvider
{
protected array $rules = [
'post_code' => ['required', 'string', 'between:1,20'],
];
protected array $messages = [];
protected array $attributes = [];
}
- 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 []; }
}
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'],
// ]
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'],
// ]
$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'],
// ]
- 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'],
// ]
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);
You can use validation providers in your form requests via two methods.
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;
}
}
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();
}
}
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
- 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.
// ]
- 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.
// ]
- 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.
// ]
- 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'],
// ]
- 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'],
// ]
- 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'],
// ]
Method | Returns |
---|---|
nested(string $nestedKey) |
NestedValidationProvider |
nestedArray(string $nestedKey) |
ArrayValidationProvider |
with(string|ValidationProvider $validationProvider) |
AggregateValidationProvider |
exclude(array $attributes) |
ExcludeAttributesValidationProvider |
map(array $attributes) |
MapAttributesValidationProvider |
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
}
- 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.
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
]
]);
}
}
- 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"
// ]
// ]
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.
}
// 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 {}
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
The MIT License (MIT). Please see License File for more information.