├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADE.md ├── composer.json └── src ├── CompositeRule.php └── DynamicCompositeRule.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Laravel Composite Validation 2 | ============================ 3 | 4 | 1.2.6, March 6, 2025 5 | -------------------- 6 | 7 | - Enh #12: Added support for "illuminate/support" 12.0 (klimov-paul) 8 | 9 | 10 | 1.2.5, March 25, 2024 11 | --------------------- 12 | 13 | - Enh #11: Added support for "illuminate/support" 11.0 (klimov-paul) 14 | 15 | 16 | 1.2.4, February 24, 2023 17 | ------------------------ 18 | 19 | - Enh #10: Added support for "illuminate/support" 10.0 (klimov-paul) 20 | 21 | 22 | 1.2.3, February 9, 2022 23 | ----------------------- 24 | 25 | - Enh: Added support for "illuminate/support" 9.0 (klimov-paul) 26 | 27 | 28 | 1.2.2, September 9, 2020 29 | ------------------------ 30 | 31 | - Enh #7: Added support for "illuminate/support" 8.0 (JeffreyDavidson, klimov-paul) 32 | 33 | 34 | 1.2.1, March 4, 2020 35 | -------------------- 36 | 37 | - Enh: Added support for "illuminate/support" 7.0 (klimov-paul) 38 | 39 | 40 | 1.2.0, February 19, 2020 41 | ------------------------ 42 | 43 | - Enh #6: Added `CompositeRule::messages()`, for the custom validation messages specification (klimov-paul) 44 | 45 | 46 | 1.1.0, September 6, 2019 47 | ------------------------ 48 | 49 | - Enh #4: Added support for "illuminate/support" 6.0 (klimov-paul) 50 | 51 | 52 | 1.0.1, July 22, 2019 53 | -------------------- 54 | 55 | - Bug #3: Fixed `CompositeRule` skipped on array attributes like 'item_ids.*' or 'items.*.id' (klimov-paul) 56 | 57 | 58 | 1.0.0, June 18, 2019 59 | -------------------- 60 | 61 | - Initial release. 62 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free software. It is released under the terms of the 2 | following BSD License. 3 | 4 | Copyright © 2019 by Illuminatech (https://github.com/illuminatech) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Illuminatech nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Laravel Composite Validation

6 |
7 |

8 | 9 | This extension allows uniting several Laravel validation rules into a single one for easy re-usage. 10 | 11 | For license information check the [LICENSE](LICENSE.md)-file. 12 | 13 | [![Latest Stable Version](https://img.shields.io/packagist/v/illuminatech/validation-composite.svg)](https://packagist.org/packages/illuminatech/validation-composite) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/illuminatech/validation-composite.svg)](https://packagist.org/packages/illuminatech/validation-composite) 15 | [![Build Status](https://github.com/illuminatech/validation-composite/workflows/build/badge.svg)](https://github.com/illuminatech/validation-composite/actions) 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 22 | 23 | Either run 24 | 25 | ``` 26 | php composer.phar require --prefer-dist illuminatech/validation-composite 27 | ``` 28 | 29 | or add 30 | 31 | ```json 32 | "illuminatech/validation-composite": "*" 33 | ``` 34 | 35 | to the require section of your composer.json. 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | The same sequence of the validation rules may repeat over the application many times. For example: you may have a set of 42 | restrictions related to the user's password, like it should be at least 8 symbols long, but shorter then 200 to fit the 43 | database field reserved for its storage. Your program may also allow user to upload an image to be his avatar, but in order 44 | to make it safe, you should validate uploaded file mime type and size. 45 | Thus validation for the user profile form may looks like following: 46 | 47 | ```php 48 | validate([ 59 | 'password' => ['required', 'string', 'min:8', 'max:200'], 60 | 'avatar' => ['required', 'file', 'mimes:png,jpg,jpeg', 'max:1024'], 61 | // ... 62 | ]); 63 | 64 | // ... 65 | } 66 | } 67 | ``` 68 | 69 | The problem is: validation for user's password or avatar may appear in several different places. For example: password 70 | can be setup at sign-up process, during password reset and so on. You may also have a separated administration panel, 71 | which allows system administrator to adjust existing user's record or create a new one. Thus you will have to duplicate 72 | all these validation rules many times throughout your project source code. In case requirements change, for example: 73 | we decide that password length should be at least 10 symbols instead of 8, or disallow '*.png' files from avatar - you'll 74 | have to manually changes validation rules at all those places. 75 | 76 | This extension allows uniting several validation rules into a single one for easy re-usage. For the example above you 77 | should create 2 separated validation rule classes extending `Illuminatech\Validation\Composite\CompositeRule`: 78 | 79 | ```php 80 | validate([ 120 | 'password' => ['required', new PasswordRule], 121 | 'avatar' => ['required', new AvatarRule], 122 | // ... 123 | ]); 124 | 125 | // ... 126 | } 127 | } 128 | ``` 129 | 130 | With such approach you can change validation for the 'password' and 'avatar' at the single place. 131 | 132 | In case composite validation rule fails, validator instance will pick up an error message from the particular sub-rule. 133 | For example: 134 | 135 | ```php 136 | 'short'], 143 | [ 144 | 'password' => ['required', new PasswordRule], 145 | ] 146 | ); 147 | 148 | if ($validator->fails()) { 149 | echo $validator->errors()->first('password'); // outputs 'The password must be at least 8 characters.' 150 | } 151 | ``` 152 | 153 | > Note: do not use rules like 'sometimes', 'required', 'required_with', 'required_without' and so on in the composite rule. 154 | These are processed at the different validation level and thus will have no effect or may behave unexpectedly. 155 | 156 | You may define composite validation rules using [validation factory extensions](https://laravel.com/docs/validation#using-extensions) feature. 157 | For such case you may use `Illuminatech\Validation\Composite\DynamicCompositeRule`. For example: 158 | 159 | ```php 160 | app->extend('validator', function (Factory $validatorFactory) { 173 | $validatorFactory->extend('password', function ($attribute, $value) { 174 | return (new DynamicCompositeRule(['string', 'min:8', 'max:200']))->passes($attribute, $value); 175 | }); 176 | 177 | $validatorFactory->extend('avatar', function ($attribute, $value) { 178 | return (new DynamicCompositeRule(['file', 'mimes:png,jpg,jpeg', 'max:1024']))->passes($attribute, $value); 179 | }); 180 | 181 | return $validatorFactory; 182 | }); 183 | 184 | // ... 185 | } 186 | } 187 | ``` 188 | 189 | Note that with such approach automatic pick up of the validation error message becomes impossible, and you will have to setup 190 | it explicitly in language files. 191 | 192 | You may specify [custom error messages](https://laravel.com/docs/validation#custom-error-messages) per each validation rule used in the composite one, 193 | overriding `messages()` method. For example: 194 | 195 | ```php 196 | 'Only string is allowed.', 213 | 'min' => ':attribute is too short.', 214 | 'max' => ':attribute is too long.', 215 | ]; 216 | } 217 | } 218 | ``` 219 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | Upgrading Instructions for Laravel Composite Validation 2 | ======================================================= 3 | 4 | !!!IMPORTANT!!! 5 | 6 | The following upgrading instructions are cumulative. That is, 7 | if you want to upgrade from version A to version C and there is 8 | version B between A and C, you need to following the instructions 9 | for both A and B. 10 | 11 | Upgrade from 1.1.0 12 | ------------------ 13 | 14 | * Signature of the `DynamicCompositeRule` constructor has been changed, adding extra `$messages` parameter. 15 | Make sure to upgrade your code in case you are using `DynamicCompositeRule`. 16 | 17 | 18 | Upgrade from 1.0.1 19 | ------------------ 20 | 21 | * "illuminate/support" package requirements were raised to 6.0. Make sure to upgrade your code accordingly. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminatech/validation-composite", 3 | "description": "Allows uniting several validation rules into a single one for easy re-usage", 4 | "keywords": ["laravel", "validation", "validator", "rule", "composite", "combine"], 5 | "license": "BSD-3-Clause", 6 | "support": { 7 | "issues": "https://github.com/illuminatech/validation-composite/issues", 8 | "wiki": "https://github.com/illuminatech/validation-composite/wiki", 9 | "source": "https://github.com/illuminatech/validation-composite" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Paul Klimov", 14 | "email": "klimov.paul@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0" 19 | }, 20 | "require-dev": { 21 | "illuminate/validation": "*", 22 | "phpunit/phpunit": "^7.5 || ^8.0 || ^9.3 || ^10.5" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Illuminatech\\Validation\\Composite\\": "src" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Illuminatech\\Validation\\Composite\\Test\\": "tests" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "1.0.x-dev" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/CompositeRule.php: -------------------------------------------------------------------------------- 1 | validate([ 42 | * 'password' => ['required', new PasswordRule], 43 | * 'avatar' => ['required', new AvatarRule], 44 | * ]); 45 | * ``` 46 | * 47 | * @author Paul Klimov 48 | * @since 1.0 49 | */ 50 | abstract class CompositeRule implements Rule 51 | { 52 | /** 53 | * @var \Illuminate\Contracts\Validation\Factory validator factory used for slave validator creation. 54 | */ 55 | private $validatorFactory; 56 | 57 | /** 58 | * @var string validation error message from particular underlying validator. 59 | */ 60 | private $message; 61 | 62 | /** 63 | * Constructor. 64 | * 65 | * @param \Illuminate\Contracts\Validation\Factory|null $validatorFactory validator factory used for slave validator creation. 66 | */ 67 | public function __construct(?Factory $validatorFactory = null) 68 | { 69 | if ($validatorFactory !== null) { 70 | $this->setValidatorFactory($validatorFactory); 71 | } 72 | } 73 | 74 | /** 75 | * Defines list of the validation rules, which are combined into this one. 76 | * 77 | * @return array validation rules definition. 78 | */ 79 | abstract protected function rules(): array; 80 | 81 | /** 82 | * Defines custom error messages for the validation rules defined at {@see rules()}. 83 | * @since 1.2.0 84 | * 85 | * @return array error messages. 86 | */ 87 | protected function messages(): array 88 | { 89 | return []; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function passes($attribute, $value): bool 96 | { 97 | $data = []; 98 | 99 | Arr::set($data, $attribute, $value); // ensure correct validation for array attributes like 'item_ids.*' or 'items.*.id' 100 | 101 | $validator = $this->getValidatorFactory()->make( 102 | $data, 103 | [ 104 | $attribute => $this->rules(), 105 | ], 106 | $this->messages() 107 | ); 108 | 109 | if ($validator->fails()) { 110 | $this->message = $validator->getMessageBag()->first(); 111 | 112 | return false; 113 | } 114 | 115 | return true; 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function message() 122 | { 123 | return $this->message; 124 | } 125 | 126 | /** 127 | * @return \Illuminate\Contracts\Validation\Factory validator factory used for slave validator creation. 128 | */ 129 | public function getValidatorFactory(): Factory 130 | { 131 | if ($this->validatorFactory === null) { 132 | $this->validatorFactory = Validator::getFacadeRoot(); 133 | } 134 | 135 | return $this->validatorFactory; 136 | } 137 | 138 | /** 139 | * @param \Illuminate\Contracts\Validation\Factory $validatorFactory validator factory used for slave validator creation. 140 | * @return static self reference. 141 | */ 142 | public function setValidatorFactory(Factory $validatorFactory): self 143 | { 144 | $this->validatorFactory = $validatorFactory; 145 | 146 | return $this; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/DynamicCompositeRule.php: -------------------------------------------------------------------------------- 1 | app->extend('validator', function (Factory $validatorFactory) { 37 | * $validatorFactory->extend('password', function ($attribute, $value) { 38 | * return (new DynamicCompositeRule(['string', 'min:8', 'max:200']))->passes($attribute, $value); 39 | * }); 40 | * 41 | * return $validatorFactory; 42 | * }); 43 | * 44 | * // ... 45 | * } 46 | * } 47 | * ``` 48 | * 49 | * @author Paul Klimov 50 | * @since 1.0 51 | */ 52 | class DynamicCompositeRule extends CompositeRule 53 | { 54 | /** 55 | * @var array list of the validation rules, which should be combined into this one. 56 | */ 57 | private $rules; 58 | 59 | /** 60 | * @var array custom error messages for the validation rules. 61 | */ 62 | private $messages; 63 | 64 | /** 65 | * Constructor. 66 | * 67 | * @param array $rules list of the validation rules, which should be combined into this one. 68 | * @param array $messages custom error messages for the validation rules. 69 | * @param \Illuminate\Contracts\Validation\Factory|null $validatorFactory validator factory used for slave validator creation. 70 | */ 71 | public function __construct(array $rules, array $messages = [], ?Factory $validatorFactory = null) 72 | { 73 | $this->rules = $rules; 74 | $this->messages = $messages; 75 | 76 | parent::__construct($validatorFactory); 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | protected function rules(): array 83 | { 84 | return $this->rules; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | protected function messages(): array 91 | { 92 | return $this->messages; 93 | } 94 | } 95 | --------------------------------------------------------------------------------