├── .gitignore
├── src
├── TranslatorInterface.php
├── PresenceVerifierInterface.php
├── helpers.php
├── Translator.php
├── Factory.php
├── MessageBag.php
└── Validator.php
├── composer.json
├── .php_cs
├── phpunit.xml.dist
├── LICENSE
├── README.md
└── tests
└── ValidatorTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | /vendor
3 | sftp-config.json
4 | /*.php
5 | /.idea
6 | /coverage
7 | /.split
8 | /composer.lock
9 |
--------------------------------------------------------------------------------
/src/TranslatorInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | interface TranslatorInterface
15 | {
16 | /**
17 | * translator.
18 | *
19 | * @param $key message key.
20 | *
21 | * @return string
22 | */
23 | public function trans($key);
24 | }
25 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "overtrue/validation",
3 | "description": "Laralve Validation 简化无依赖版",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Carlos",
8 | "email": "anzhengchao@gmail.com"
9 | }
10 | ],
11 | "require": {
12 | "php": ">=5.4.0"
13 | },
14 |
15 | "autoload": {
16 | "psr-4": {
17 | "Overtrue\\Validation\\": "src/"
18 | },
19 | "files":["src/helpers.php"]
20 | },
21 |
22 | "minimum-stability": "dev",
23 | "require-dev": {
24 | "phpunit/phpunit": "^5.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 |
7 |
8 | This source file is subject to the MIT license that is bundled
9 | with this source code in the file LICENSE.
10 | EOF;
11 |
12 | Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header);
13 |
14 | return Symfony\CS\Config\Config::create()
15 | // use default SYMFONY_LEVEL and extra fixers:
16 | ->fixers(array(
17 | 'header_comment',
18 | 'short_array_syntax',
19 | 'ordered_use',
20 | 'php_unit_construct',
21 | 'strict_param',
22 | ))
23 | ->finder(
24 | Symfony\CS\Finder\DefaultFinder::create()
25 | ->in(__DIR__.'/src')
26 | )
27 | ;
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/PresenceVerifierInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | interface PresenceVerifierInterface
15 | {
16 | /**
17 | * Count the number of objects in a collection having the given value.
18 | *
19 | * @param string $collection
20 | * @param string $column
21 | * @param string $value
22 | * @param int $excludeId
23 | * @param string $idColumn
24 | * @param array $extra
25 | *
26 | * @return int
27 | */
28 | public function getCount($collection, $column, $value, $excludeId = null, $idColumn = null, array $extra = []);
29 |
30 | /**
31 | * Count the number of objects in a collection with the given values.
32 | *
33 | * @param string $collection
34 | * @param string $column
35 | * @param array $values
36 | * @param array $extra
37 | *
38 | * @return int
39 | */
40 | public function getMultiCount($collection, $column, array $values, array $extra = []);
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Original work Copyright (c) 2016 Taylor Otwell
4 | Modified work Copyright (c) 2016 overtrue
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Validation
2 | ==========
3 |
4 | Validation 是从Laravel的验证模块提取简化而来,旨在让你更方便的在非laravel项目中便捷的完成数据验证。
5 |
6 | 更多验证规则请阅读:http://laravel.com/docs/4.2/validation#available-validation-rules
7 |
8 | # Usage
9 |
10 | ```php
11 | 'required|min:5',
23 | 'password' => 'confirmed',
24 | ///...
25 | ];
26 |
27 | $validator = $factory->make($input, $rules);
28 |
29 | //判断验证是否通过
30 | if ($validator->passes()) {
31 | //通过
32 | } else {
33 | //未通过
34 | //输出错误消息
35 | print_r($validator->messages()->all()); // 或者 $validator->errors();
36 | }
37 |
38 | ```
39 |
40 | ## 自定义消息语言:
41 |
42 | > 语言列表可以从这里拿:https://github.com/caouecs/Laravel-lang
43 |
44 | 以中文为例:
45 |
46 | ```php
47 | $messages = [
48 | 'accepted' => ':attribute 必须接受。',
49 | 'active_url' => ':attribute 不是一个有效的网址。',
50 | 'after' => ':attribute 必须是一个在 :date 之后的日期。',
51 | 'alpha' => ':attribute 只能由字母组成。',
52 | 'alpha_dash' => ':attribute 只能由字母、数字和斜杠组成。',
53 | 'alpha_num' => ':attribute 只能由字母和数字组成。',
54 | // ...
55 | ];
56 |
57 | //初始化工厂对象
58 | $factory = new ValidatorFactory(new Translator($messages));
59 |
60 | ```
61 |
62 | ## 设置属性名称
63 |
64 | ```php
65 | $attributes = [
66 | 'username' => '用户名',
67 | 'password' => '密码',
68 | ];
69 |
70 | $rules = [
71 | 'username' => 'required|min:5',
72 | 'password' => 'confirmed',
73 | ///...
74 | ];
75 |
76 | $messages = [...]; // 自定义消息,如果你在初始化 factory 的时候已经设置了消息,则留空即可
77 |
78 | $validator = $factory->make($input, $rules, $messages, $attributes);
79 | ```
80 |
81 | ## PHP 扩展包开发
82 |
83 | > 想知道如何从零开始构建 PHP 扩展包?
84 | >
85 | > 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package)
86 |
87 | # License
88 |
89 | MIT
90 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | use ArrayAccess;
15 | use Closure;
16 |
17 | /**
18 | * Return array specific item.
19 | *
20 | * @param array $array
21 | * @param string $key
22 | * @param mixed $default
23 | *
24 | * @return mixed
25 | */
26 | function array_get($array, $key, $default = null)
27 | {
28 | if (!array_accessible($array)) {
29 | return value($default);
30 | }
31 |
32 | if (is_null($key)) {
33 | return $array;
34 | }
35 |
36 | if (array_exists($array, $key)) {
37 | return $array[$key];
38 | }
39 |
40 | foreach (explode('.', $key) as $segment) {
41 | if (array_accessible($array) && array_exists($array, $segment)) {
42 | $array = $array[$segment];
43 | } else {
44 | return value($default);
45 | }
46 | }
47 |
48 | return $array;
49 | }
50 |
51 | /**
52 | * Flatten a multi-dimensional associative array with dots.
53 | *
54 | * @param array $array
55 | * @param string $prepend
56 | * @return array
57 | */
58 | function array_dot($array, $prepend = '')
59 | {
60 | $results = [];
61 |
62 | foreach ($array as $key => $value) {
63 | if (is_array($value)) {
64 | $results = array_merge($results, array_dot($value, $prepend.$key.'.'));
65 | } else {
66 | $results[$prepend.$key] = $value;
67 | }
68 | }
69 |
70 | return $results;
71 | }
72 |
73 | /**
74 | * Check input is array accessable.
75 | *
76 | * @param mixed $value
77 | *
78 | * @return bool
79 | */
80 | function array_accessible($value)
81 | {
82 | return is_array($value) || $value instanceof ArrayAccess;
83 | }
84 |
85 | /**
86 | * Check array key exists.
87 | *
88 | * @param array $array
89 | * @param string $key
90 | *
91 | * @return bool
92 | */
93 | function array_exists($array, $key)
94 | {
95 | if ($array instanceof ArrayAccess) {
96 | return $array->offsetExists($key);
97 | }
98 |
99 | return array_key_exists($key, $array);
100 | }
101 |
102 | /**
103 | * Convert a string to snake case.
104 | *
105 | * @param string $string
106 | * @param string $delimiter
107 | *
108 | * @return string
109 | */
110 | function snake_case($string, $delimiter = '_')
111 | {
112 | $replace = '$1'.$delimiter.'$2';
113 |
114 | return ctype_lower($string) ? $string : strtolower(preg_replace('/(.)([A-Z])/', $replace, $string));
115 | }
116 |
117 | /**
118 | * Convert a value to studly caps case.
119 | *
120 | * @param string $string
121 | *
122 | * @return string
123 | */
124 | function studly_case($string)
125 | {
126 | $string = ucwords(str_replace(['-', '_'], ' ', $string));
127 |
128 | return str_replace(' ', '', $string);
129 | }
130 |
131 | /**
132 | * Return the default value of the given value.
133 | *
134 | * @param mixed $value
135 | * @return mixed
136 | */
137 | function value($value)
138 | {
139 | return $value instanceof Closure ? $value() : $value;
140 | }
--------------------------------------------------------------------------------
/src/Translator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | /**
15 | * 验证类使用的默认消息翻译工具.
16 | */
17 | class Translator implements TranslatorInterface
18 | {
19 | protected $messages = [];
20 |
21 | /**
22 | * 默认消息.
23 | *
24 | * @var array
25 | */
26 | protected $defaultMessages = [
27 | 'accepted' => 'The :attribute must be accepted.',
28 | 'active_url' => 'The :attribute is not a valid URL.',
29 | 'after' => 'The :attribute must be a date after :date.',
30 | 'alpha' => 'The :attribute may only contain letters.',
31 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
32 | 'alpha_num' => 'The :attribute may only contain letters and numbers.',
33 | 'array' => 'The :attribute must be an array.',
34 | 'before' => 'The :attribute must be a date before :date.',
35 | 'between' => [
36 | 'numeric' => 'The :attribute must be between :min and :max.',
37 | 'file' => 'The :attribute must be between :min and :max kilobytes.',
38 | 'string' => 'The :attribute must be between :min and :max characters.',
39 | 'array' => 'The :attribute must have between :min and :max items.',
40 | ],
41 | 'boolean' => 'The :attribute field must be true or false',
42 | 'confirmed' => 'The :attribute confirmation does not match.',
43 | 'date' => 'The :attribute is not a valid date.',
44 | 'date_format' => 'The :attribute does not match the format :format.',
45 | 'different' => 'The :attribute and :other must be different.',
46 | 'digits' => 'The :attribute must be :digits digits.',
47 | 'digits_between' => 'The :attribute must be between :min and :max digits.',
48 | 'email' => 'The :attribute must be a valid email address.',
49 | 'filled' => 'The :attribute field is required.',
50 | 'exists' => 'The selected :attribute is invalid.',
51 | 'image' => 'The :attribute must be an image.',
52 | 'in' => 'The selected :attribute is invalid.',
53 | 'integer' => 'The :attribute must be an integer.',
54 | 'ip' => 'The :attribute must be a valid IP address.',
55 | 'max' => [
56 | 'numeric' => 'The :attribute may not be greater than :max.',
57 | 'file' => 'The :attribute may not be greater than :max kilobytes.',
58 | 'string' => 'The :attribute may not be greater than :max characters.',
59 | 'array' => 'The :attribute may not have more than :max items.',
60 | ],
61 | 'mimes' => 'The :attribute must be a file of type: :values.',
62 | 'min' => [
63 | 'numeric' => 'The :attribute must be at least :min.',
64 | 'file' => 'The :attribute must be at least :min kilobytes.',
65 | 'string' => 'The :attribute must be at least :min characters.',
66 | 'array' => 'The :attribute must have at least :min items.',
67 | ],
68 | 'not_in' => 'The selected :attribute is invalid.',
69 | 'numeric' => 'The :attribute must be a number.',
70 | 'regex' => 'The :attribute format is invalid.',
71 | 'required' => 'The :attribute field is required.',
72 | 'required_if' => 'The :attribute field is required when :other is :value.',
73 | 'required_with' => 'The :attribute field is required when :values is present.',
74 | 'required_with_all' => 'The :attribute field is required when :values is present.',
75 | 'required_without' => 'The :attribute field is required when :values is not present.',
76 | 'required_without_all' => 'The :attribute field is required when none of :values are present.',
77 | 'same' => 'The :attribute and :other must match.',
78 | 'size' => [
79 | 'numeric' => 'The :attribute must be :size.',
80 | 'file' => 'The :attribute must be :size kilobytes.',
81 | 'string' => 'The :attribute must be :size characters.',
82 | 'array' => 'The :attribute must contain :size items.',
83 | ],
84 | 'unique' => 'The :attribute has already been taken.',
85 | 'url' => 'The :attribute format is invalid.',
86 | 'timezone' => 'The :attribute must be a valid zone.',
87 | ];
88 |
89 | /**
90 | * 设置验证消息列表.
91 | *
92 | * @param $messages
93 | */
94 | public function __construct(array $messages = [])
95 | {
96 | $this->messages = ['validation' => array_merge($this->defaultMessages, $messages)];
97 | }
98 |
99 | /**
100 | * 翻译.
101 | *
102 | * @param string $key 点拼接的key
103 | *
104 | * @return string
105 | */
106 | public function trans($key)
107 | {
108 | return $this->arrayGet($this->messages, $key, $key);
109 | }
110 |
111 | /**
112 | * 使用点字符串获取.
113 | *
114 | * @param array $array
115 | * @param string $key
116 | * @param mixed $default
117 | *
118 | * @return mixed
119 | */
120 | protected function arrayGet($array, $key, $default = null)
121 | {
122 | if (is_null($key)) {
123 | return $array;
124 | }
125 |
126 | if (isset($array[$key])) {
127 | return $array[$key];
128 | }
129 |
130 | foreach (explode('.', $key) as $segment) {
131 | if (!is_array($array) || !array_key_exists($segment, $array)) {
132 | return value($default);
133 | }
134 |
135 | $array = $array[$segment];
136 | }
137 |
138 | return $array;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Factory.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | use Closure;
15 |
16 | class Factory
17 | {
18 | /**
19 | * The Translator implementation.
20 | *
21 | * @var \Overtrue\Validation\TranslatorInterface
22 | */
23 | protected $translator;
24 |
25 | /**
26 | * The Presence Verifier implementation.
27 | *
28 | * @var \Overtrue\Validation\PresenceVerifierInterface
29 | */
30 | protected $verifier;
31 |
32 | /**
33 | * All of the custom validator extensions.
34 | *
35 | * @var array
36 | */
37 | protected $extensions = [];
38 |
39 | /**
40 | * All of the custom implicit validator extensions.
41 | *
42 | * @var array
43 | */
44 | protected $implicitExtensions = [];
45 |
46 | /**
47 | * All of the custom validator message replacers.
48 | *
49 | * @var array
50 | */
51 | protected $replacers = [];
52 |
53 | /**
54 | * All of the fallback messages for custom rules.
55 | *
56 | * @var array
57 | */
58 | protected $fallbackMessages = [];
59 |
60 | /**
61 | * The Validator resolver instance.
62 | *
63 | * @var Closure
64 | */
65 | protected $resolver;
66 |
67 | /**
68 | * Create a new Validator factory instance.
69 | *
70 | * @param \Overtrue\Validation\TranslatorInterface $translator
71 | *
72 | * @return \Overtrue\Validation\Factory
73 | */
74 | public function __construct(TranslatorInterface $translator = null)
75 | {
76 | $this->translator = $translator ?: new Translator();
77 | }
78 |
79 | /**
80 | * Create a new Validator instance.
81 | *
82 | * @param array $data
83 | * @param array $rules
84 | * @param array $messages
85 | * @param array $customAttributes
86 | *
87 | * @return \Overtrue\Validation\Validator
88 | */
89 | public function make(array $data, array $rules, array $messages = [], array $customAttributes = [])
90 | {
91 | // The presence verifier is responsible for checking the unique and exists data
92 | // for the validator. It is behind an interface so that multiple versions of
93 | // it may be written besides database. We'll inject it into the validator.
94 | $validator = $this->resolve($data, $rules, $messages, $customAttributes);
95 |
96 | if (!is_null($this->verifier)) {
97 | $validator->setPresenceVerifier($this->verifier);
98 | }
99 |
100 | $this->addExtensions($validator);
101 |
102 | return $validator;
103 | }
104 |
105 | /**
106 | * Add the extensions to a validator instance.
107 | *
108 | * @param \Overtrue\Validation\Validator $validator
109 | */
110 | protected function addExtensions(Validator $validator)
111 | {
112 | $validator->addExtensions($this->extensions);
113 |
114 | // Next, we will add the implicit extensions, which are similar to the required
115 | // and accepted rule in that they are run even if the attributes is not in a
116 | // array of data that is given to a validator instances via instantiation.
117 | $implicit = $this->implicitExtensions;
118 |
119 | $validator->addImplicitExtensions($implicit);
120 |
121 | $validator->addReplacers($this->replacers);
122 |
123 | $validator->setFallbackMessages($this->fallbackMessages);
124 | }
125 |
126 | /**
127 | * Resolve a new Validator instance.
128 | *
129 | * @param array $data
130 | * @param array $rules
131 | * @param array $messages
132 | * @param array $customAttributes
133 | *
134 | * @return \Overtrue\Validation\Validator
135 | */
136 | protected function resolve(array $data, array $rules, array $messages, array $customAttributes)
137 | {
138 | if (is_null($this->resolver)) {
139 | return new Validator($this->translator, $data, $rules, $messages, $customAttributes);
140 | } else {
141 | return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
142 | }
143 | }
144 |
145 | /**
146 | * Register a custom validator extension.
147 | *
148 | * @param string $rule
149 | * @param \Closure|string $extension
150 | * @param string $message
151 | */
152 | public function extend($rule, $extension, $message = null)
153 | {
154 | $this->extensions[$rule] = $extension;
155 |
156 | if ($message) {
157 | $this->fallbackMessages[snake_case($rule)] = $message;
158 | }
159 | }
160 |
161 | /**
162 | * Register a custom implicit validator extension.
163 | *
164 | * @param string $rule
165 | * @param \Closure|string $extension
166 | * @param string $message
167 | */
168 | public function extendImplicit($rule, $extension, $message = null)
169 | {
170 | $this->implicitExtensions[$rule] = $extension;
171 |
172 | if ($message) {
173 | $this->fallbackMessages[snake_case($rule)] = $message;
174 | }
175 | }
176 |
177 | /**
178 | * Register a custom implicit validator message replacer.
179 | *
180 | * @param string $rule
181 | * @param \Closure|string $replacer
182 | */
183 | public function replacer($rule, $replacer)
184 | {
185 | $this->replacers[$rule] = $replacer;
186 | }
187 |
188 | /**
189 | * Set the Validator instance resolver.
190 | *
191 | * @param \Closure $resolver
192 | */
193 | public function resolver(Closure $resolver)
194 | {
195 | $this->resolver = $resolver;
196 | }
197 |
198 | /**
199 | * Get the Translator implementation.
200 | *
201 | * @return \Overtrue\Validation\TranslatorInterface
202 | */
203 | public function getTranslator()
204 | {
205 | return $this->translator;
206 | }
207 |
208 | /**
209 | * Get the Presence Verifier implementation.
210 | *
211 | * @return \Overtrue\Validation\PresenceVerifierInterface
212 | */
213 | public function getPresenceVerifier()
214 | {
215 | return $this->verifier;
216 | }
217 |
218 | /**
219 | * Set the Presence Verifier implementation.
220 | *
221 | * @param \Overtrue\Validation\PresenceVerifierInterface $presenceVerifier
222 | */
223 | public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier)
224 | {
225 | $this->verifier = $presenceVerifier;
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/MessageBag.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | use Countable;
15 | use JsonSerializable;
16 |
17 | class MessageBag implements Countable, JsonSerializable
18 | {
19 | /**
20 | * All of the registered messages.
21 | *
22 | * @var array
23 | */
24 | protected $messages = [];
25 |
26 | /**
27 | * Default format for message output.
28 | *
29 | * @var string
30 | */
31 | protected $format = ':message';
32 |
33 | /**
34 | * Create a new message bag instance.
35 | *
36 | * @param array $messages
37 | *
38 | * @return \Overtrue\Validation\MessageBag
39 | */
40 | public function __construct(array $messages = [])
41 | {
42 | foreach ($messages as $key => $value) {
43 | $this->messages[$key] = (array) $value;
44 | }
45 | }
46 |
47 | /**
48 | * Add a message to the bag.
49 | *
50 | * @param string $key
51 | * @param string $message
52 | *
53 | * @return $this
54 | */
55 | public function add($key, $message)
56 | {
57 | if ($this->isUnique($key, $message)) {
58 | $this->messages[$key][] = $message;
59 | }
60 |
61 | return $this;
62 | }
63 |
64 | /**
65 | * Merge a new array of messages into the bag.
66 | *
67 | * @param array $messages
68 | *
69 | * @return $this
70 | */
71 | public function merge($messages)
72 | {
73 | $this->messages = array_merge_recursive($this->messages, $messages);
74 |
75 | return $this;
76 | }
77 |
78 | /**
79 | * Determine if a key and message combination already exists.
80 | *
81 | * @param string $key
82 | * @param string $message
83 | *
84 | * @return bool
85 | */
86 | protected function isUnique($key, $message)
87 | {
88 | $messages = (array) $this->messages;
89 |
90 | return !isset($messages[$key]) || !in_array($message, $messages[$key], true);
91 | }
92 |
93 | /**
94 | * Determine if messages exist for a given key.
95 | *
96 | * @param string $key
97 | *
98 | * @return bool
99 | */
100 | public function has($key = null)
101 | {
102 | return $this->first($key) !== '';
103 | }
104 |
105 | /**
106 | * Get the first message from the bag for a given key.
107 | *
108 | * @param string $key
109 | * @param string $format
110 | *
111 | * @return string
112 | */
113 | public function first($key = null, $format = null)
114 | {
115 | $messages = is_null($key) ? $this->all($format) : $this->get($key, $format);
116 |
117 | return (count($messages) > 0) ? $messages[0] : '';
118 | }
119 |
120 | /**
121 | * Get all of the messages from the bag for a given key.
122 | *
123 | * @param string $key
124 | * @param string $format
125 | *
126 | * @return array
127 | */
128 | public function get($key, $format = null)
129 | {
130 | $format = $this->checkFormat($format);
131 |
132 | // If the message exists in the container, we will transform it and return
133 | // the message. Otherwise, we'll return an empty array since the entire
134 | // methods is to return back an array of messages in the first place.
135 | if (array_key_exists($key, $this->messages)) {
136 | return $this->transform($this->messages[$key], $format, $key);
137 | }
138 |
139 | return [];
140 | }
141 |
142 | /**
143 | * Get all of the messages for every key in the bag.
144 | *
145 | * @param string $format
146 | *
147 | * @return array
148 | */
149 | public function all($format = null)
150 | {
151 | $format = $this->checkFormat($format);
152 |
153 | $all = [];
154 |
155 | foreach ($this->messages as $key => $messages) {
156 | $all = array_merge($all, $this->transform($messages, $format, $key));
157 | }
158 |
159 | return $all;
160 | }
161 |
162 | /**
163 | * Format an array of messages.
164 | *
165 | * @param array $messages
166 | * @param string $format
167 | * @param string $messageKey
168 | *
169 | * @return array
170 | */
171 | protected function transform($messages, $format, $messageKey)
172 | {
173 | $messages = (array) $messages;
174 |
175 | // We will simply spin through the given messages and transform each one
176 | // replacing the :message place holder with the real message allowing
177 | // the messages to be easily formatted to each developer's desires.
178 | foreach ($messages as $key => &$message) {
179 | $replace = [':message', ':key'];
180 |
181 | $message = str_replace($replace, [$message, $messageKey], $format);
182 | }
183 |
184 | return $messages;
185 | }
186 |
187 | /**
188 | * Get the appropriate format based on the given format.
189 | *
190 | * @param string $format
191 | *
192 | * @return string
193 | */
194 | protected function checkFormat($format)
195 | {
196 | return ($format === null) ? $this->format : $format;
197 | }
198 |
199 | /**
200 | * Get the raw messages in the container.
201 | *
202 | * @return array
203 | */
204 | public function getMessages()
205 | {
206 | return $this->messages;
207 | }
208 |
209 | /**
210 | * Get the messages for the instance.
211 | *
212 | * @return \Overtrue\Validation\MessageBag
213 | */
214 | public function getMessageBag()
215 | {
216 | return $this;
217 | }
218 |
219 | /**
220 | * Get the default message format.
221 | *
222 | * @return string
223 | */
224 | public function getFormat()
225 | {
226 | return $this->format;
227 | }
228 |
229 | /**
230 | * Set the default message format.
231 | *
232 | * @param string $format
233 | *
234 | * @return \Overtrue\Validation\MessageBag
235 | */
236 | public function setFormat($format = ':message')
237 | {
238 | $this->format = $format;
239 |
240 | return $this;
241 | }
242 |
243 | /**
244 | * Determine if the message bag has any messages.
245 | *
246 | * @return bool
247 | */
248 | public function isEmpty()
249 | {
250 | return !$this->any();
251 | }
252 |
253 | /**
254 | * Determine if the message bag has any messages.
255 | *
256 | * @return bool
257 | */
258 | public function any()
259 | {
260 | return $this->count() > 0;
261 | }
262 |
263 | /**
264 | * Get the number of messages in the container.
265 | *
266 | * @return int
267 | */
268 | public function count()
269 | {
270 | return count($this->messages, COUNT_RECURSIVE) - count($this->messages);
271 | }
272 |
273 | /**
274 | * Get the instance as an array.
275 | *
276 | * @return array
277 | */
278 | public function toArray()
279 | {
280 | return $this->getMessages();
281 | }
282 |
283 | /**
284 | * Convert the object into something JSON serializable.
285 | *
286 | * @return array
287 | */
288 | public function jsonSerialize()
289 | {
290 | return $this->toArray();
291 | }
292 |
293 | /**
294 | * Convert the object to its JSON representation.
295 | *
296 | * @param int $options
297 | *
298 | * @return string
299 | */
300 | public function toJson($options = 0)
301 | {
302 | return json_encode($this->toArray(), $options);
303 | }
304 |
305 | /**
306 | * Convert the message bag to its string representation.
307 | *
308 | * @return string
309 | */
310 | public function __toString()
311 | {
312 | return $this->toJson();
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/tests/ValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new Factory();
12 | }
13 |
14 | /**
15 | * @dataProvider cases
16 | *
17 | * @return
18 | */
19 | public function testCases($input, $rule, $status, $message = null)
20 | {
21 | $validator = $this->validator->make($input, $rule);
22 |
23 | $this->assertEquals($status, $validator->passes());
24 |
25 | if (!$status) {
26 | $this->assertSame($message, $validator->messages()->first());
27 | }
28 | }
29 |
30 | public function cases()
31 | {
32 | return [
33 | // sometimes
34 | [
35 | ['foo' => 'foo'],
36 | ['foo' => 'sometimes'],
37 | true,
38 | ],
39 | [
40 | ['foo' => false],
41 | ['foo' => 'sometimes'],
42 | true,
43 | ],
44 |
45 | // required
46 | [
47 | ['foo' => 'foo', 'bar' => 1, 'baz' => 0, 'overtrue' => array('0')],
48 | ['foo' => 'required', 'bar' => 'required', 'baz' => 'required', 'overtrue' => 'required'],
49 | true,
50 | ],
51 | [
52 | ['foo' => ' ', 'bar' => array()],
53 | ['foo' => 'required'],
54 | false,
55 | 'The foo field is required.'
56 | ],
57 | [
58 | ['bar' => array()],
59 | ['bar' => 'required'],
60 | false,
61 | 'The bar field is required.'
62 | ],
63 |
64 | // filled
65 | [
66 | [],
67 | ['foo' => 'filled'],
68 | true,
69 | ],
70 | [
71 | ['bar' => ''],
72 | ['bar' => 'required|filled'],
73 | false,
74 | 'The bar field is required.'
75 | ],
76 |
77 | // required_with
78 | [
79 | ['bar' => ''],
80 | ['bar' => 'required_with:foo'],
81 | true,
82 | ],
83 | [
84 | ['foo' => 'overtrue'],
85 | ['bar' => 'required_with:foo'],
86 | false,
87 | 'The bar field is required when foo is present.',
88 | ],
89 |
90 | // required_with_all
91 | [
92 | ['baz' => ''],
93 | ['bar' => 'required_with_all:foo,baz'],
94 | true,
95 | ],
96 | [
97 | ['foo' => 'overtrue', 'baz' => 123],
98 | ['bar' => 'required_with_all:foo,baz'],
99 | false,
100 | 'The bar field is required when foo / baz is present.',
101 | ],
102 |
103 | // required_without
104 | [
105 | ['foo' => 'overtrue'],
106 | ['bar' => 'required_without:foo'],
107 | true,
108 | ],
109 | [
110 | ['foo' => '', 'baz' => 123],
111 | ['bar' => 'required_without:foo'],
112 | false,
113 | 'The bar field is required when foo is not present.',
114 | ],
115 |
116 | // required_without_all
117 | [
118 | ['bar' => 123],
119 | ['bar' => 'required_without_all:foo,baz'],
120 | true,
121 | ],
122 | [
123 | ['foo' => '', 'baz' => ''],
124 | ['bar' => 'required_without_all:foo,baz'],
125 | false,
126 | 'The bar field is required when none of foo / baz are present.',
127 | ],
128 |
129 | // required_if
130 | [
131 | ['foo' => 1],
132 | ['bar' => 'required_if:foo,2'],
133 | true
134 | ],
135 | [
136 | ['foo' => 1],
137 | ['bar' => 'required_if:foo,1'],
138 | false,
139 | 'The bar field is required when foo is 1.',
140 | ],
141 |
142 | // comfirmed
143 | [
144 | ['password' => 'foo', 'password_confirmation' => 'foo'],
145 | ['password' => 'confirmed'],
146 | true,
147 | ],
148 |
149 | [
150 | ['password' => 'foo'],
151 | ['password' => 'confirmed'],
152 | false,
153 | 'The password confirmation does not match.',
154 | ],
155 | [
156 | ['password' => 'foo', 'password_confirmation' => 'foo'],
157 | ['password' => 'confirmed'],
158 | true,
159 | ],
160 |
161 | // same
162 | [
163 | ['foo' => 'hello', 'bar' => 'hello'],
164 | ['foo' => 'same:bar'],
165 | true,
166 | ],
167 | [
168 | ['foo' => 'overtrue', 'bar' => 'hello'],
169 | ['foo' => 'same:bar'],
170 | false,
171 | 'The foo and bar must match.',
172 | ],
173 | [
174 | ['foo' => 'overtrue', 'bar' => ''],
175 | ['foo' => 'same:bar'],
176 | false,
177 | 'The foo and bar must match.',
178 | ],
179 |
180 | // different
181 | [
182 | ['foo' => 'overtrue', 'bar' => 'hello'],
183 | ['foo' => 'different:bar'],
184 | true,
185 | ],
186 | [
187 | ['foo' => 'a', 'bar' => 'a'],
188 | ['foo' => 'different:bar'],
189 | false,
190 | 'The foo and bar must be different.',
191 | ],
192 |
193 | // accepted
194 | [
195 | ['foo' => 'yes', 'bar' => 'on', 'one' => 1, 'two' => true, 'three' => 'true'],
196 | [
197 | 'foo' => 'accepted',
198 | 'bar' => 'accepted',
199 | 'one' => 'accepted',
200 | 'two' => 'accepted',
201 | 'three' => 'accepted',
202 | ],
203 | true,
204 | ],
205 | [
206 | ['foo' => 'onn'],
207 | ['foo' => 'accepted'],
208 | false,
209 | 'The foo must be accepted.',
210 | ],
211 | [
212 | ['foo' => 'off'],
213 | ['foo' => 'accepted'],
214 | false,
215 | 'The foo must be accepted.',
216 | ],
217 |
218 | // boolean
219 | [
220 | [
221 | 'one' => false,
222 | 'two' => 1,
223 | 'three' => '1',
224 | 'four' => true,
225 | 'five' => 0,
226 | 'six' => '0',
227 | ],
228 | [
229 | 'one' => 'boolean',
230 | 'two' => 'boolean',
231 | 'three' => 'boolean',
232 | 'four' => 'boolean',
233 | 'five' => 'boolean',
234 | 'six' => 'boolean',
235 | ],
236 | true,
237 | ],
238 | [
239 | ['foo' => 'onn'],
240 | ['foo' => 'boolean'],
241 | false,
242 | 'The foo field must be true or false',
243 | ],
244 | [
245 | ['foo' => 'off'],
246 | ['foo' => 'boolean'],
247 | false,
248 | 'The foo field must be true or false',
249 | ],
250 |
251 | // array
252 | [
253 | ['foo' => []],
254 | ['foo' => 'array'],
255 | true
256 | ],
257 | [
258 | ['foo' => [1,2,3]],
259 | ['foo' => 'array'],
260 | true
261 | ],
262 | [
263 | ['foo' => 'string'],
264 | ['foo' => 'array'],
265 | false,
266 | 'The foo must be an array.',
267 | ],
268 | // numeric
269 | [
270 | ['foo' => 1],
271 | ['foo' => 'numeric'],
272 | true
273 | ],
274 | [
275 | ['foo' => '2356'],
276 | ['foo' => 'numeric'],
277 | true
278 | ],
279 | [
280 | ['foo' => '1234string'],
281 | ['foo' => 'numeric'],
282 | false,
283 | 'The foo must be a number.',
284 | ],
285 | [
286 | ['foo' => 'string1234'],
287 | ['foo' => 'numeric'],
288 | false,
289 | 'The foo must be a number.',
290 | ],
291 |
292 | // integer
293 | [
294 | ['foo' => 0, 'bar' => 1, 'baz' => -3],
295 | ['foo' => 'integer'],
296 | true
297 | ],
298 | [
299 | ['foo' => 1234.5],
300 | ['foo' => 'integer'],
301 | false,
302 | 'The foo must be an integer.',
303 | ],
304 | [
305 | ['foo' => '000'],
306 | ['foo' => 'integer'],
307 | false,
308 | 'The foo must be an integer.',
309 | ],
310 |
311 | // digits
312 | [
313 | ['foo' => 0, 'bar' => 1, 'baz' => '3', 'baba' => '0'],
314 | ['foo' => 'digits:1'],
315 | true
316 | ],
317 | [
318 | ['foo' => 999999999999],
319 | ['foo' => 'digits:12'],
320 | true,
321 | ],
322 | [
323 | ['foo' => '1234string'],
324 | ['foo' => 'digits:4'],
325 | false,
326 | 'The foo must be 4 digits.',
327 | ],
328 |
329 | // digits_between
330 | [
331 | ['foo' => 0, 'bar' => 134, 'baz' => '32345', 'baba' => '0'],
332 | [
333 | 'foo' => 'digits_between:1,5',
334 | 'bar' => 'digits_between:1,5',
335 | 'baz' => 'digits_between:1,5',
336 | 'baba' => 'digits_between:1,5',
337 | ],
338 | true
339 | ],
340 | [
341 | ['foo' => 123],
342 | ['foo' => 'digits_between:1,3'],
343 | true,
344 | ],
345 | [
346 | ['foo' => '1234'],
347 | ['foo' => 'digits_between:2,3'],
348 | false,
349 | 'The foo must be between 2 and 3 digits.',
350 | ],
351 |
352 | // size
353 | [
354 | ['foo' => 12, 'bar' => [3,45], 'one' => 'ab'],
355 | ['foo' => 'size:2', 'bar' => 'size:2', 'one' => 'size:2'],
356 | true
357 | ],
358 | [
359 | ['foo' => 123],
360 | ['foo' => 'size:1'],
361 | false,
362 | 'The foo must be 1 characters.'
363 | ],
364 |
365 | // between
366 | [
367 | ['foo' => 12, 'bar' => [3,45], 'one' => 'ab'],
368 | ['foo' => 'between:2,12', 'bar' => 'between:2,4', 'one' => 'between:1,2'],
369 | true
370 | ],
371 | [
372 | ['foo' => 123],
373 | ['foo' => 'numeric|between:1,2'],
374 | false,
375 | 'The foo must be between 1 and 2.'
376 | ],
377 | [
378 | ['foo' => [1,2,3,4]],
379 | ['foo' => 'array|between:1,2'],
380 | false,
381 | 'The foo must have between 1 and 2 items.'
382 | ],
383 | [
384 | ['foo' => 'item'],
385 | ['foo' => 'between:2,3'],
386 | false,
387 | 'The foo must be between 2 and 3 characters.'
388 | ],
389 |
390 | // min
391 | [
392 | ['foo' => 1, 'bar' => [1,2], 'baz' => 'abc'],
393 | ['foo' => 'numeric|min:0', 'bar' => 'array|min:2', 'baz' => 'min:3'],
394 | true,
395 | ],
396 | [
397 | ['foo' => 1,],
398 | ['foo' => 'numeric|min:2'],
399 | false,
400 | 'The foo must be at least 2.'
401 | ],
402 | [
403 | ['foo' => [1],],
404 | ['foo' => 'array|min:2'],
405 | false,
406 | 'The foo must have at least 2 items.'
407 | ],
408 |
409 | // max
410 | [
411 | ['foo' => 1, 'bar' => [1,2], 'baz' => 'abc'],
412 | ['foo' => 'numeric|max:1', 'bar' => 'array|max:2', 'baz' => 'max:4'],
413 | true,
414 | ],
415 | [
416 | ['foo' => 1,],
417 | ['foo' => 'numeric|max:0'],
418 | false,
419 | 'The foo may not be greater than 0.'
420 | ],
421 | [
422 | ['foo' => [1,5],],
423 | ['foo' => 'array|max:1'],
424 | false,
425 | 'The foo may not have more than 1 items.'
426 | ],
427 |
428 | // in
429 | [
430 | ['foo' => 1, 'bar' => 'abc'],
431 | ['foo' => 'in:1,2,3', 'bar' => 'in:abc,def'],
432 | true,
433 | ],
434 | [
435 | ['foo' => 1 ],
436 | ['foo' => 'in:2,3'],
437 | false,
438 | 'The selected foo is invalid.',
439 | ],
440 | [
441 | ['foo' => 'abc' ],
442 | ['foo' => 'in:cde,def'],
443 | false,
444 | 'The selected foo is invalid.',
445 | ],
446 |
447 | // not_in
448 | [
449 | ['foo' => 1, 'bar' => 'abc'],
450 | ['foo' => 'not_in:2,3', 'bar' => 'not_in:bcd,def'],
451 | true,
452 | ],
453 | [
454 | ['foo' => 1 ],
455 | ['foo' => 'not_in:1,2'],
456 | false,
457 | 'The selected foo is invalid.',
458 | ],
459 | [
460 | ['foo' => 'abc' ],
461 | ['foo' => 'not_in:abc,def'],
462 | false,
463 | 'The selected foo is invalid.',
464 | ],
465 |
466 | // ip
467 | [
468 | ['ip_address' => '127.0.0.1'],
469 | ['ip_address' => 'ip'],
470 | true,
471 | ],
472 | [
473 | ['ip_address' => '127.0.0.1.1'],
474 | ['ip_address' => 'ip'],
475 | false,
476 | 'The ip address must be a valid IP address.',
477 | ],
478 |
479 | // email
480 | [
481 | ['email_address' => 'foo@bar.com', 'foo' => 'f.bar@bar.com'],
482 | ['email_address' => 'email', 'foo' => 'email'],
483 | true,
484 | ],
485 | [
486 | ['email_address' => '127.0.0.1.1'],
487 | ['email_address' => 'email'],
488 | false,
489 | 'The email address must be a valid email address.',
490 | ],
491 |
492 | // url
493 | [
494 | ['foo' => 'http://abc.m', 'bar' => 'https://f.bar.com', 'ftp' => 'ftp://a.c.com'],
495 | ['foo' => 'url', 'bar' => 'url', 'ftp' => 'url'],
496 | true,
497 | ],
498 | [
499 | ['foo' => 'abc.com'],
500 | ['foo' => 'url'],
501 | false,
502 | 'The foo format is invalid.',
503 | ],
504 |
505 | // active_url
506 | [
507 | ['foo' => 'http://google.com', 'bar' => 'https://ip.com'],
508 | ['foo' => 'active_url', 'bar' => 'active_url'],
509 | true,
510 | ],
511 | [
512 | ['foo' => 'thisisanonexistswebsite.com'],
513 | ['foo' => 'active_url'],
514 | false,
515 | 'The foo is not a valid URL.',
516 | ],
517 |
518 | // alpha
519 | [
520 | ['foo' => 'abcd', 'bar' => '汉字', 'baz' => 'Пароль'],
521 | ['foo' => 'alpha', 'bar' => 'alpha', 'baz' => 'alpha'],
522 | true,
523 | ],
524 |
525 | [
526 | ['foo' => '123abcd'],
527 | ['foo' => 'alpha'],
528 | false,
529 | 'The foo may only contain letters.',
530 | ],
531 |
532 | // alpha_num
533 | [
534 | ['foo' => 'a13bcd', 'bar' => '汉2字', 'baz' => '123Пароль3'],
535 | ['foo' => 'alpha_num', 'bar' => 'alpha_num', 'baz' => 'alpha_num'],
536 | true,
537 | ],
538 |
539 | [
540 | ['foo' => '123abcd.w'],
541 | ['foo' => 'alpha_num'],
542 | false,
543 | 'The foo may only contain letters and numbers.',
544 | ],
545 |
546 | // alpha_dash
547 | [
548 | ['foo' => 'a13b_cd', 'bar' => '汉2字_', 'baz' => '123П__ароль3'],
549 | ['foo' => 'alpha_dash', 'bar' => 'alpha_dash', 'baz' => 'alpha_dash'],
550 | true,
551 | ],
552 |
553 | [
554 | ['foo' => '123ab_cd.w'],
555 | ['foo' => 'alpha_dash'],
556 | false,
557 | 'The foo may only contain letters, numbers, and dashes.',
558 | ],
559 |
560 | // regex
561 | [
562 | ['foo' => 'a13b_cd', 'bar' => '汉2字_', 'baz' => '123П__ароль3'],
563 | ['foo' => 'regex:/^[a-z_13]+$/', 'bar' => 'regex:/^[_2\p{Han}]+$/u', 'baz' => 'regex:/^[\p{L}_0-9]+$/u'],
564 | true,
565 | ],
566 |
567 | [
568 | ['foo' => '123ab_cd.w'],
569 | ['foo' => 'regex:/^[a-z]+$/'],
570 | false,
571 | 'The foo format is invalid.',
572 | ],
573 |
574 | // date
575 | [
576 | ['foo' => '2016-03-06', 'bar' => '1970/09/08 12:23:00'],
577 | ['foo' => 'date', 'bar' => 'date'],
578 | true,
579 | ],
580 | [
581 | ['foo' => '+1 days'],
582 | ['foo' => 'date'],
583 | false,
584 | 'The foo is not a valid date.',
585 | ],
586 | [
587 | ['foo' => '2016年 4月27日 星期三 22时08分39秒 CST'],
588 | ['foo' => 'date'],
589 | false,
590 | 'The foo is not a valid date.',
591 | ],
592 | [
593 | ['foo' => '2016 12'],
594 | ['foo' => 'date'],
595 | false,
596 | 'The foo is not a valid date.',
597 | ],
598 |
599 | // date_format
600 | [
601 | ['foo' => '2016-03-06', 'bar' => '1970/09/08 12:23:00'],
602 | ['foo' => 'date_format:Y-m-d', 'bar' => 'date_format:Y/m/d H:i:s'],
603 | true,
604 | ],
605 | [
606 | ['foo' => '1970/09/08 12:23:00'],
607 | ['foo' => 'date_format:Y-m-d'],
608 | false,
609 | 'The foo does not match the format Y-m-d.',
610 | ],
611 |
612 | // before & after
613 | [
614 | ['foo' => '2016-03-04', 'bar' => '2016-05-06 12:06', 'baz' => '2016-05-06 12:06:02'],
615 | ['foo' => 'before:2016-03-04 01:00:00', 'bar' => 'before:2016-05-06 12:06:01', 'baz' => 'before:2016-05-06 12:06:03'],
616 | true,
617 | ],
618 | [
619 | ['start' => '2013-01-02', 'end' => '2014-02-02'],
620 | ['start' => 'before:end', 'end' => 'after:start'],
621 | true,
622 | ],
623 | [
624 | ['foo' => '2016-03-04'],
625 | ['foo' => 'before:2016-03-04 00:00:00'],
626 | false,
627 | 'The foo must be a date before 2016-03-04 00:00:00.',
628 | ],
629 |
630 | // after
631 | [
632 | ['foo' => '2016-03-04', 'bar' => '2016-05-06 12:06', 'baz' => '2016-05-06 12:06:02'],
633 | ['foo' => 'after:2016-03-03 23:59:59', 'bar' => 'after:2016-05-06 12:05:59', 'baz' => 'after:2016-05-06 12:06:01'],
634 | true,
635 | ],
636 | [
637 | ['foo' => '2016-03-04'],
638 | ['foo' => 'after:2016-03-04 00:00:00'],
639 | false,
640 | 'The foo must be a date after 2016-03-04 00:00:00.',
641 | ],
642 |
643 | // timezone
644 | [
645 | ['zone' => 'PRC', 'zone' => 'asia/chongqing', 'UTC'],
646 | ['zone' => 'timezone'],
647 | true,
648 | ],
649 | [
650 | ['zone' => 'not_a_time_zone'],
651 | ['zone' => 'timezone'],
652 | false,
653 | 'The zone must be a valid zone.',
654 | ],
655 | ];
656 | }
657 | }
--------------------------------------------------------------------------------
/src/Validator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Overtrue\Validation;
13 |
14 | use Closure;
15 | use DateTime;
16 | use DateTimeZone;
17 |
18 | class Validator
19 | {
20 | /**
21 | * The Translator implementation.
22 | *
23 | * @var \Overtrue\Validation\TranslatorInterface
24 | */
25 | protected $translator;
26 |
27 | /**
28 | * The Presence Verifier implementation.
29 | *
30 | * @var \Overtrue\Validation\PresenceVerifierInterface
31 | */
32 | protected $presenceVerifier;
33 |
34 | /**
35 | * The failed validation rules.
36 | *
37 | * @var array
38 | */
39 | protected $failedRules = [];
40 |
41 | /**
42 | * The messages.
43 | *
44 | * @var \Overtrue\Validation\MessageBag
45 | */
46 | protected $messages;
47 |
48 | /**
49 | * The data under validation.
50 | *
51 | * @var array
52 | */
53 | protected $data;
54 |
55 | /**
56 | * The files under validation.
57 | *
58 | * @var array
59 | */
60 | protected $files = [];
61 |
62 | /**
63 | * The rules to be applied to the data.
64 | *
65 | * @var array
66 | */
67 | protected $rules;
68 |
69 | /**
70 | * All of the registered "after" callbacks.
71 | *
72 | * @var array
73 | */
74 | protected $after = [];
75 |
76 | /**
77 | * The array of custom error messages.
78 | *
79 | * @var array
80 | */
81 | protected $customMessages = [];
82 |
83 | /**
84 | * The array of fallback error messages.
85 | *
86 | * @var array
87 | */
88 | protected $fallbackMessages = [];
89 |
90 | /**
91 | * The array of custom attribute names.
92 | *
93 | * @var array
94 | */
95 | protected $customAttributes = [];
96 |
97 | /**
98 | * The array of custom displayabled values.
99 | *
100 | * @var array
101 | */
102 | protected $customValues = [];
103 |
104 | /**
105 | * All of the custom validator extensions.
106 | *
107 | * @var array
108 | */
109 | protected $extensions = [];
110 |
111 | /**
112 | * All of the custom replacer extensions.
113 | *
114 | * @var array
115 | */
116 | protected $replacers = [];
117 |
118 | /**
119 | * The size related validation rules.
120 | *
121 | * @var array
122 | */
123 | protected $sizeRules = ['Size', 'Between', 'Min', 'Max'];
124 |
125 | /**
126 | * The numeric related validation rules.
127 | *
128 | * @var array
129 | */
130 | protected $numericRules = ['Numeric', 'Integer'];
131 |
132 | /**
133 | * The validation rules that imply the field is required.
134 | *
135 | * @var array
136 | */
137 | protected $implicitRules = [
138 | 'Required', 'Filled', 'RequiredWith', 'RequiredWithAll', 'RequiredWithout', 'RequiredWithoutAll', 'RequiredIf', 'Accepted',
139 | ];
140 |
141 | /**
142 | * Create a new Validator instance.
143 | *
144 | * @param \Overtrue\Validation\TranslatorInterface $translator
145 | * @param array $data
146 | * @param array $rules
147 | * @param array $messages
148 | * @param array $customAttributes
149 | *
150 | * @return \Overtrue\Validation\Validator
151 | */
152 | public function __construct(TranslatorInterface $translator, array $data, array $rules, array $messages = [], array $customAttributes = [])
153 | {
154 | $this->translator = $translator;
155 | $this->customMessages = $messages;
156 | $this->data = $this->parseData($data);
157 | $this->rules = $this->explodeRules($rules);
158 | $this->customAttributes = $customAttributes;
159 | }
160 |
161 | /**
162 | * Parse the data and hydrate the files array.
163 | *
164 | * @param array $data
165 | *
166 | * @return array
167 | */
168 | protected function parseData(array $data)
169 | {
170 | $this->files = [];
171 |
172 | foreach ($data as $key => $value) {
173 | // If this value is an instance of the HttpFoundation File class we will
174 | // remove it from the data array and add it to the files array, which
175 | // we use to conveniently separate out these files from other data.
176 | if (in_array($value, $_FILES, true)) {
177 | $this->files[$key] = $value;
178 |
179 | unset($data[$key]);
180 | }
181 | }
182 |
183 | return $data;
184 | }
185 |
186 | /**
187 | * Explode the rules into an array of rules.
188 | *
189 | * @param string|array $rules
190 | *
191 | * @return array
192 | */
193 | protected function explodeRules($rules)
194 | {
195 | foreach ($rules as $key => &$rule) {
196 | $rule = (is_string($rule)) ? explode('|', $rule) : $rule;
197 | }
198 |
199 | return $rules;
200 | }
201 |
202 | /**
203 | * After an after validation callback.
204 | *
205 | * @param callable|string $callback
206 | *
207 | * @return $this
208 | */
209 | public function after($callback)
210 | {
211 | $this->after[] = function () use ($callback) {
212 | return call_user_func($callback, [], 'validate');
213 | };
214 |
215 | return $this;
216 | }
217 |
218 | /**
219 | * Add conditions to a given field based on a Closure.
220 | *
221 | * @param string $attribute
222 | * @param string|array $rules
223 | * @param callable $callback
224 | */
225 | public function sometimes($attribute, $rules, callable $callback)
226 | {
227 | $payload = array_merge($this->data, $this->files);
228 |
229 | if (call_user_func($callback, $payload)) {
230 | foreach ((array) $attribute as $key) {
231 | $this->mergeRules($key, $rules);
232 | }
233 | }
234 | }
235 |
236 | /**
237 | * Define a set of rules that apply to each element in an array attribute.
238 | *
239 | * @param string $attribute
240 | * @param string|array $rules
241 | *
242 | * @throws \InvalidArgumentException
243 | */
244 | public function each($attribute, $rules)
245 | {
246 | $data = array_get($this->data, $attribute);
247 |
248 | if (!is_array($data)) {
249 | if ($this->hasRule($attribute, 'Array')) {
250 | return;
251 | }
252 |
253 | throw new \InvalidArgumentException('Attribute for each() must be an array.');
254 | }
255 |
256 | foreach ($data as $dataKey => $dataValue) {
257 | foreach ($rules as $ruleValue) {
258 | $this->mergeRules("$attribute.$dataKey", $ruleValue);
259 | }
260 | }
261 | }
262 |
263 | /**
264 | * Merge additional rules into a given attribute.
265 | *
266 | * @param string $attribute
267 | * @param string|array $rules
268 | */
269 | public function mergeRules($attribute, $rules)
270 | {
271 | $current = isset($this->rules[$attribute]) ? $this->rules[$attribute] : [];
272 |
273 | $merge = head($this->explodeRules([$rules]));
274 |
275 | $this->rules[$attribute] = array_merge($current, $merge);
276 | }
277 |
278 | /**
279 | * Determine if the data passes the validation rules.
280 | *
281 | *
282 | * @return bool
283 | */
284 | public function passes()
285 | {
286 | $this->messages = new MessageBag();
287 |
288 | // We'll spin through each rule, validating the attributes attached to that
289 | // rule. Any error messages will be added to the containers with each of
290 | // the other error messages, returning true if we don't have messages.
291 | foreach ($this->rules as $attribute => $rules) {
292 | foreach ($rules as $rule) {
293 | $this->validate($attribute, $rule);
294 | }
295 | }
296 |
297 | // Here we will spin through all of the "after" hooks on this validator and
298 | // fire them off. This gives the callbacks a chance to perform all kinds
299 | // of other validation that needs to get wrapped up in this operation.
300 | foreach ($this->after as $after) {
301 | call_user_func($after);
302 | }
303 |
304 | return count($this->messages->all()) === 0;
305 | }
306 |
307 | /**
308 | * Determine if the data fails the validation rules.
309 | *
310 | *
311 | * @return bool
312 | */
313 | public function fails()
314 | {
315 | return !$this->passes();
316 | }
317 |
318 | /**
319 | * Validate a given attribute against a rule.
320 | *
321 | * @param string $attribute
322 | * @param string $rule
323 | */
324 | protected function validate($attribute, $rule)
325 | {
326 | list($rule, $parameters) = $this->parseRule($rule);
327 |
328 | if ($rule === '') {
329 | return;
330 | }
331 |
332 | // We will get the value for the given attribute from the array of data and then
333 | // verify that the attribute is indeed validatable. Unless the rule implies
334 | // that the attribute is required, rules are not run for missing values.
335 | $value = $this->getValue($attribute);
336 |
337 | $validatable = $this->isValidatable($rule, $attribute, $value);
338 |
339 | $method = "validate{$rule}";
340 |
341 | if ($validatable && !$this->$method($attribute, $value, $parameters, $this)) {
342 | $this->addFailure($attribute, $rule, $parameters);
343 | }
344 | }
345 |
346 | /**
347 | * Get the value of a given attribute.
348 | *
349 | * @param string $attribute
350 | *
351 | * @return mixed
352 | */
353 | protected function getValue($attribute)
354 | {
355 | if (!is_null($value = array_get($this->data, $attribute))) {
356 | return $value;
357 | } elseif (!is_null($value = array_get($this->files, $attribute))) {
358 | return $value;
359 | }
360 | }
361 |
362 | /**
363 | * Determine if the attribute is validatable.
364 | *
365 | * @param string $rule
366 | * @param string $attribute
367 | * @param mixed $value
368 | *
369 | * @return bool
370 | */
371 | protected function isValidatable($rule, $attribute, $value)
372 | {
373 | return $this->presentOrRuleIsImplicit($rule, $attribute, $value) &&
374 | $this->passesOptionalCheck($attribute);
375 | }
376 |
377 | /**
378 | * Determine if the field is present, or the rule implies required.
379 | *
380 | * @param string $rule
381 | * @param string $attribute
382 | * @param mixed $value
383 | *
384 | * @return bool
385 | */
386 | protected function presentOrRuleIsImplicit($rule, $attribute, $value)
387 | {
388 | return $this->validateRequired($attribute, $value) || $this->isImplicit($rule);
389 | }
390 |
391 | /**
392 | * Determine if the attribute passes any optional check.
393 | *
394 | * @param string $attribute
395 | *
396 | * @return bool
397 | */
398 | protected function passesOptionalCheck($attribute)
399 | {
400 | if ($this->hasRule($attribute, ['Sometimes'])) {
401 | return array_key_exists($attribute, array_dot($this->data))
402 | || array_key_exists($attribute, $this->files);
403 | }
404 |
405 | return true;
406 | }
407 |
408 | /**
409 | * Determine if a given rule implies the attribute is required.
410 | *
411 | * @param string $rule
412 | *
413 | * @return bool
414 | */
415 | protected function isImplicit($rule)
416 | {
417 | return in_array($rule, $this->implicitRules, true);
418 | }
419 |
420 | /**
421 | * Add a failed rule and error message to the collection.
422 | *
423 | * @param string $attribute
424 | * @param string $rule
425 | * @param array $parameters
426 | */
427 | protected function addFailure($attribute, $rule, $parameters)
428 | {
429 | $this->addError($attribute, $rule, $parameters);
430 |
431 | $this->failedRules[$attribute][snake_case($rule)] = $parameters;
432 | }
433 |
434 | /**
435 | * Add an error message to the validator's collection of messages.
436 | *
437 | * @param string $attribute
438 | * @param string $rule
439 | * @param array $parameters
440 | */
441 | protected function addError($attribute, $rule, $parameters)
442 | {
443 | $message = $this->getMessage($attribute, $rule);
444 |
445 | $message = $this->doReplacements($message, $attribute, $rule, $parameters);
446 |
447 | $this->messages->add($attribute, $message);
448 | }
449 |
450 | /**
451 | * "Validate" optional attributes.
452 | *
453 | * Always returns true, just lets us put sometimes in rules.
454 | *
455 | *
456 | * @return bool
457 | */
458 | protected function validateSometimes()
459 | {
460 | return true;
461 | }
462 |
463 | /**
464 | * Validate that a required attribute exists.
465 | *
466 | * @param string $attribute
467 | * @param mixed $value
468 | *
469 | * @return bool
470 | */
471 | protected function validateRequired($attribute, $value)
472 | {
473 | if (is_null($value)) {
474 | return false;
475 | } elseif (is_string($value) && trim($value) === '') {
476 | return false;
477 | } elseif (is_array($value) && count($value) < 1) {
478 | return false;
479 | } elseif (in_array($value, $_FILES, true)) {
480 | return (string) $value['tmp_name'] !== '';
481 | }
482 |
483 | return true;
484 | }
485 |
486 | /**
487 | * Validate the given attribute is filled if it is present.
488 | *
489 | * @param string $attribute
490 | * @param mixed $value
491 | *
492 | * @return bool
493 | */
494 | protected function validateFilled($attribute, $value)
495 | {
496 | if (array_key_exists($attribute, $this->data) || array_key_exists($attribute, $this->files)) {
497 | return $this->validateRequired($attribute, $value);
498 | } else {
499 | return true;
500 | }
501 | }
502 |
503 | /**
504 | * Determine if any of the given attributes fail the required test.
505 | *
506 | * @param array $attributes
507 | *
508 | * @return bool
509 | */
510 | protected function anyFailingRequired(array $attributes)
511 | {
512 | foreach ($attributes as $key) {
513 | if (!$this->validateRequired($key, $this->getValue($key))) {
514 | return true;
515 | }
516 | }
517 |
518 | return false;
519 | }
520 |
521 | /**
522 | * Determine if all of the given attributes fail the required test.
523 | *
524 | * @param array $attributes
525 | *
526 | * @return bool
527 | */
528 | protected function allFailingRequired(array $attributes)
529 | {
530 | foreach ($attributes as $key) {
531 | if ($this->validateRequired($key, $this->getValue($key))) {
532 | return false;
533 | }
534 | }
535 |
536 | return true;
537 | }
538 |
539 | /**
540 | * Validate that an attribute exists when any other attribute exists.
541 | *
542 | * @param string $attribute
543 | * @param mixed $value
544 | * @param mixed $parameters
545 | *
546 | * @return bool
547 | */
548 | protected function validateRequiredWith($attribute, $value, $parameters)
549 | {
550 | if (!$this->allFailingRequired($parameters)) {
551 | return $this->validateRequired($attribute, $value);
552 | }
553 |
554 | return true;
555 | }
556 |
557 | /**
558 | * Validate that an attribute exists when all other attributes exists.
559 | *
560 | * @param string $attribute
561 | * @param mixed $value
562 | * @param mixed $parameters
563 | *
564 | * @return bool
565 | */
566 | protected function validateRequiredWithAll($attribute, $value, $parameters)
567 | {
568 | if (!$this->anyFailingRequired($parameters)) {
569 | return $this->validateRequired($attribute, $value);
570 | }
571 |
572 | return true;
573 | }
574 |
575 | /**
576 | * Validate that an attribute exists when another attribute does not.
577 | *
578 | * @param string $attribute
579 | * @param mixed $value
580 | * @param mixed $parameters
581 | *
582 | * @return bool
583 | */
584 | protected function validateRequiredWithout($attribute, $value, $parameters)
585 | {
586 | if ($this->anyFailingRequired($parameters)) {
587 | return $this->validateRequired($attribute, $value);
588 | }
589 |
590 | return true;
591 | }
592 |
593 | /**
594 | * Validate that an attribute exists when all other attributes do not.
595 | *
596 | * @param string $attribute
597 | * @param mixed $value
598 | * @param mixed $parameters
599 | *
600 | * @return bool
601 | */
602 | protected function validateRequiredWithoutAll($attribute, $value, $parameters)
603 | {
604 | if ($this->allFailingRequired($parameters)) {
605 | return $this->validateRequired($attribute, $value);
606 | }
607 |
608 | return true;
609 | }
610 |
611 | /**
612 | * Validate that an attribute exists when another attribute has a given value.
613 | *
614 | * @param string $attribute
615 | * @param mixed $value
616 | * @param mixed $parameters
617 | *
618 | * @return bool
619 | */
620 | protected function validateRequiredIf($attribute, $value, $parameters)
621 | {
622 | $this->requireParameterCount(2, $parameters, 'required_if');
623 |
624 | $data = array_get($this->data, $parameters[0]);
625 |
626 | $values = array_slice($parameters, 1);
627 |
628 | if (in_array($data, $values)) {
629 | return $this->validateRequired($attribute, $value);
630 | }
631 |
632 | return true;
633 | }
634 |
635 | /**
636 | * Get the number of attributes in a list that are present.
637 | *
638 | * @param array $attributes
639 | *
640 | * @return int
641 | */
642 | protected function getPresentCount($attributes)
643 | {
644 | $count = 0;
645 |
646 | foreach ($attributes as $key) {
647 | if (array_get($this->data, $key) || array_get($this->files, $key)) {
648 | ++$count;
649 | }
650 | }
651 |
652 | return $count;
653 | }
654 |
655 | /**
656 | * Validate that an attribute has a matching confirmation.
657 | *
658 | * @param string $attribute
659 | * @param mixed $value
660 | *
661 | * @return bool
662 | */
663 | protected function validateConfirmed($attribute, $value)
664 | {
665 | return $this->validateSame($attribute, $value, [$attribute.'_confirmation']);
666 | }
667 |
668 | /**
669 | * Validate that two attributes match.
670 | *
671 | * @param string $attribute
672 | * @param mixed $value
673 | * @param array $parameters
674 | *
675 | * @return bool
676 | */
677 | protected function validateSame($attribute, $value, $parameters)
678 | {
679 | $this->requireParameterCount(1, $parameters, 'same');
680 |
681 | $other = array_get($this->data, $parameters[0]);
682 |
683 | return isset($other) && $value === $other;
684 | }
685 |
686 | /**
687 | * Validate that an attribute is different from another attribute.
688 | *
689 | * @param string $attribute
690 | * @param mixed $value
691 | * @param array $parameters
692 | *
693 | * @return bool
694 | */
695 | protected function validateDifferent($attribute, $value, $parameters)
696 | {
697 | $this->requireParameterCount(1, $parameters, 'different');
698 |
699 | $other = array_get($this->data, $parameters[0]);
700 |
701 | return isset($other) && $value !== $other;
702 | }
703 |
704 | /**
705 | * Validate that an attribute was "accepted".
706 | *
707 | * This validation rule implies the attribute is "required".
708 | *
709 | * @param string $attribute
710 | * @param mixed $value
711 | *
712 | * @return bool
713 | */
714 | protected function validateAccepted($attribute, $value)
715 | {
716 | $acceptable = ['yes', 'on', '1', 1, true, 'true'];
717 |
718 | return $this->validateRequired($attribute, $value) && in_array($value, $acceptable, true);
719 | }
720 |
721 | /**
722 | * Validate that an attribute is a boolean.
723 | *
724 | * @param string $attribute
725 | * @param mixed $value
726 | *
727 | * @return bool
728 | */
729 | protected function validateBoolean($attribute, $value)
730 | {
731 | $acceptable = [true, false, 0, 1, '0', '1'];
732 |
733 | return in_array($value, $acceptable, true);
734 | }
735 |
736 | /**
737 | * Validate that an attribute is an array.
738 | *
739 | * @param string $attribute
740 | * @param mixed $value
741 | *
742 | * @return bool
743 | */
744 | protected function validateArray($attribute, $value)
745 | {
746 | return is_array($value);
747 | }
748 |
749 | /**
750 | * Validate that an attribute is numeric.
751 | *
752 | * @param string $attribute
753 | * @param mixed $value
754 | *
755 | * @return bool
756 | */
757 | protected function validateNumeric($attribute, $value)
758 | {
759 | return is_numeric($value);
760 | }
761 |
762 | /**
763 | * Validate that an attribute is an integer.
764 | *
765 | * @param string $attribute
766 | * @param mixed $value
767 | *
768 | * @return bool
769 | */
770 | protected function validateInteger($attribute, $value)
771 | {
772 | return filter_var($value, FILTER_VALIDATE_INT) !== false;
773 | }
774 |
775 | /**
776 | * Validate that an attribute has a given number of digits.
777 | *
778 | * @param string $attribute
779 | * @param mixed $value
780 | * @param array $parameters
781 | *
782 | * @return bool
783 | */
784 | protected function validateDigits($attribute, $value, $parameters)
785 | {
786 | $this->requireParameterCount(1, $parameters, 'digits');
787 |
788 | return $this->validateNumeric($attribute, $value)
789 | && strlen((string) $value) == $parameters[0];
790 | }
791 |
792 | /**
793 | * Validate that an attribute is between a given number of digits.
794 | *
795 | * @param string $attribute
796 | * @param mixed $value
797 | * @param array $parameters
798 | *
799 | * @return bool
800 | */
801 | protected function validateDigitsBetween($attribute, $value, $parameters)
802 | {
803 | $this->requireParameterCount(2, $parameters, 'digits_between');
804 |
805 | $length = strlen((string) $value);
806 |
807 | return $length >= $parameters[0] && $length <= $parameters[1];
808 | }
809 |
810 | /**
811 | * Validate the size of an attribute.
812 | *
813 | * @param string $attribute
814 | * @param mixed $value
815 | * @param array $parameters
816 | *
817 | * @return bool
818 | */
819 | protected function validateSize($attribute, $value, $parameters)
820 | {
821 | $this->requireParameterCount(1, $parameters, 'size');
822 |
823 | return $this->getSize($attribute, $value) == $parameters[0];
824 | }
825 |
826 | /**
827 | * Validate the size of an attribute is between a set of values.
828 | *
829 | * @param string $attribute
830 | * @param mixed $value
831 | * @param array $parameters
832 | *
833 | * @return bool
834 | */
835 | protected function validateBetween($attribute, $value, $parameters)
836 | {
837 | $this->requireParameterCount(2, $parameters, 'between');
838 |
839 | $size = $this->getSize($attribute, $value);
840 |
841 | return $size >= $parameters[0] && $size <= $parameters[1];
842 | }
843 |
844 | /**
845 | * Validate the size of an attribute is greater than a minimum value.
846 | *
847 | * @param string $attribute
848 | * @param mixed $value
849 | * @param array $parameters
850 | *
851 | * @return bool
852 | */
853 | protected function validateMin($attribute, $value, $parameters)
854 | {
855 | $this->requireParameterCount(1, $parameters, 'min');
856 |
857 | return $this->getSize($attribute, $value) >= $parameters[0];
858 | }
859 |
860 | /**
861 | * Validate the size of an attribute is less than a maximum value.
862 | *
863 | * @param string $attribute
864 | * @param mixed $value
865 | * @param array $parameters
866 | *
867 | * @return bool
868 | */
869 | protected function validateMax($attribute, $value, $parameters)
870 | {
871 | $this->requireParameterCount(1, $parameters, 'max');
872 |
873 | if (!empty($value['tmp_name']) && is_uploaded_file($value['tmp_name']) && $value['error']) {
874 | return false;
875 | }
876 |
877 | return $this->getSize($attribute, $value) <= $parameters[0];
878 | }
879 |
880 | /**
881 | * Get the size of an attribute.
882 | *
883 | * @param string $attribute
884 | * @param mixed $value
885 | *
886 | * @return mixed
887 | */
888 | protected function getSize($attribute, $value)
889 | {
890 | $hasNumeric = $this->hasRule($attribute, $this->numericRules);
891 |
892 | // This method will determine if the attribute is a number, string, or file and
893 | // return the proper size accordingly. If it is a number, then number itself
894 | // is the size. If it is a file, we take kilobytes, and for a string the
895 | // entire length of the string will be considered the attribute size.
896 | if (is_numeric($value) && $hasNumeric) {
897 | return array_get($this->data, $attribute);
898 | } elseif (is_array($value)) {
899 | return count($value);
900 | } elseif (in_array($value, $_FILES, true)) {
901 | return $value->getSize() / 1024;
902 | } else {
903 | return $this->getStringSize($value);
904 | }
905 | }
906 |
907 | /**
908 | * Get the size of a string.
909 | *
910 | * @param string $value
911 | *
912 | * @return int
913 | */
914 | protected function getStringSize($value)
915 | {
916 | if (function_exists('mb_strlen')) {
917 | return mb_strlen($value);
918 | }
919 |
920 | return strlen($value);
921 | }
922 |
923 | /**
924 | * Validate an attribute is contained within a list of values.
925 | *
926 | * @param string $attribute
927 | * @param mixed $value
928 | * @param array $parameters
929 | *
930 | * @return bool
931 | */
932 | protected function validateIn($attribute, $value, $parameters)
933 | {
934 | return in_array((string) $value, $parameters, true);
935 | }
936 |
937 | /**
938 | * Validate an attribute is not contained within a list of values.
939 | *
940 | * @param string $attribute
941 | * @param mixed $value
942 | * @param array $parameters
943 | *
944 | * @return bool
945 | */
946 | protected function validateNotIn($attribute, $value, $parameters)
947 | {
948 | return !$this->validateIn($attribute, $value, $parameters);
949 | }
950 |
951 | /**
952 | * Validate the uniqueness of an attribute value on a given database table.
953 | *
954 | * If a database column is not specified, the attribute will be used.
955 | *
956 | * @param string $attribute
957 | * @param mixed $value
958 | * @param array $parameters
959 | *
960 | * @return bool
961 | */
962 | protected function validateUnique($attribute, $value, $parameters)
963 | {
964 | $this->requireParameterCount(1, $parameters, 'unique');
965 |
966 | $table = $parameters[0];
967 |
968 | // The second parameter position holds the name of the column that needs to
969 | // be verified as unique. If this parameter isn't specified we will just
970 | // assume that this column to be verified shares the attribute's name.
971 | $column = isset($parameters[1]) ? $parameters[1] : $attribute;
972 |
973 | list($idColumn, $id) = [null, null];
974 |
975 | if (isset($parameters[2])) {
976 | list($idColumn, $id) = $this->getUniqueIds($parameters);
977 |
978 | if (strtolower($id) === 'null') {
979 | $id = null;
980 | }
981 | }
982 |
983 | // The presence verifier is responsible for counting rows within this store
984 | // mechanism which might be a relational database or any other permanent
985 | // data store like Redis, etc. We will use it to determine uniqueness.
986 | $verifier = $this->getPresenceVerifier();
987 |
988 | $extra = $this->getUniqueExtra($parameters);
989 |
990 | return $verifier->getCount(
991 |
992 | $table, $column, $value, $id, $idColumn, $extra
993 |
994 | ) === 0;
995 | }
996 |
997 | /**
998 | * Get the excluded ID column and value for the unique rule.
999 | *
1000 | * @param array $parameters
1001 | *
1002 | * @return array
1003 | */
1004 | protected function getUniqueIds($parameters)
1005 | {
1006 | $idColumn = isset($parameters[3]) ? $parameters[3] : 'id';
1007 |
1008 | return [$idColumn, $parameters[2]];
1009 | }
1010 |
1011 | /**
1012 | * Get the extra conditions for a unique rule.
1013 | *
1014 | * @param array $parameters
1015 | *
1016 | * @return array
1017 | */
1018 | protected function getUniqueExtra($parameters)
1019 | {
1020 | if (isset($parameters[4])) {
1021 | return $this->getExtraConditions(array_slice($parameters, 4));
1022 | } else {
1023 | return [];
1024 | }
1025 | }
1026 |
1027 | /**
1028 | * Validate the existence of an attribute value in a database table.
1029 | *
1030 | * @param string $attribute
1031 | * @param mixed $value
1032 | * @param array $parameters
1033 | *
1034 | * @return bool
1035 | */
1036 | protected function validateExists($attribute, $value, $parameters)
1037 | {
1038 | $this->requireParameterCount(1, $parameters, 'exists');
1039 |
1040 | $table = $parameters[0];
1041 |
1042 | // The second parameter position holds the name of the column that should be
1043 | // verified as existing. If this parameter is not specified we will guess
1044 | // that the columns being "verified" shares the given attribute's name.
1045 | $column = isset($parameters[1]) ? $parameters[1] : $attribute;
1046 |
1047 | $expected = (is_array($value)) ? count($value) : 1;
1048 |
1049 | return $this->getExistCount($table, $column, $value, $parameters) >= $expected;
1050 | }
1051 |
1052 | /**
1053 | * Get the number of records that exist in storage.
1054 | *
1055 | * @param string $table
1056 | * @param string $column
1057 | * @param mixed $value
1058 | * @param array $parameters
1059 | *
1060 | * @return int
1061 | */
1062 | protected function getExistCount($table, $column, $value, $parameters)
1063 | {
1064 | $verifier = $this->getPresenceVerifier();
1065 |
1066 | $extra = $this->getExtraExistConditions($parameters);
1067 |
1068 | if (is_array($value)) {
1069 | return $verifier->getMultiCount($table, $column, $value, $extra);
1070 | } else {
1071 | return $verifier->getCount($table, $column, $value, null, null, $extra);
1072 | }
1073 | }
1074 |
1075 | /**
1076 | * Get the extra exist conditions.
1077 | *
1078 | * @param array $parameters
1079 | *
1080 | * @return array
1081 | */
1082 | protected function getExtraExistConditions(array $parameters)
1083 | {
1084 | return $this->getExtraConditions(array_values(array_slice($parameters, 2)));
1085 | }
1086 |
1087 | /**
1088 | * Get the extra conditions for a unique / exists rule.
1089 | *
1090 | * @param array $segments
1091 | *
1092 | * @return array
1093 | */
1094 | protected function getExtraConditions(array $segments)
1095 | {
1096 | $extra = [];
1097 |
1098 | $count = count($segments);
1099 |
1100 | for ($i = 0; $i < $count; $i = $i + 2) {
1101 | $extra[$segments[$i]] = $segments[$i + 1];
1102 | }
1103 |
1104 | return $extra;
1105 | }
1106 |
1107 | /**
1108 | * Validate that an attribute is a valid IP.
1109 | *
1110 | * @param string $attribute
1111 | * @param mixed $value
1112 | *
1113 | * @return bool
1114 | */
1115 | protected function validateIp($attribute, $value)
1116 | {
1117 | return filter_var($value, FILTER_VALIDATE_IP) !== false;
1118 | }
1119 |
1120 | /**
1121 | * Validate that an attribute is a valid e-mail address.
1122 | *
1123 | * @param string $attribute
1124 | * @param mixed $value
1125 | *
1126 | * @return bool
1127 | */
1128 | protected function validateEmail($attribute, $value)
1129 | {
1130 | return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
1131 | }
1132 |
1133 | /**
1134 | * Validate that an attribute is a valid URL.
1135 | *
1136 | * @param string $attribute
1137 | * @param mixed $value
1138 | *
1139 | * @return bool
1140 | */
1141 | protected function validateUrl($attribute, $value)
1142 | {
1143 | return filter_var($value, FILTER_VALIDATE_URL) !== false;
1144 | }
1145 |
1146 | /**
1147 | * Validate that an attribute is an active URL.
1148 | *
1149 | * @param string $attribute
1150 | * @param mixed $value
1151 | *
1152 | * @return bool
1153 | */
1154 | protected function validateActiveUrl($attribute, $value)
1155 | {
1156 | $url = str_replace(['http://', 'https://', 'ftp://'], '', strtolower($value));
1157 |
1158 | return checkdnsrr($url);
1159 | }
1160 |
1161 | /**
1162 | * Validate the MIME type of a file is an image MIME type.
1163 | *
1164 | * @param string $attribute
1165 | * @param mixed $value
1166 | *
1167 | * @return bool
1168 | */
1169 | protected function validateImage($attribute, $value)
1170 | {
1171 | return $this->validateMimes($attribute, $value, ['jpeg', 'png', 'gif', 'bmp']);
1172 | }
1173 |
1174 | /**
1175 | * Validate the MIME type of a file upload attribute is in a set of MIME types.
1176 | *
1177 | * @param string $attribute
1178 | * @param array $value
1179 | * @param array $parameters
1180 | *
1181 | * @return bool
1182 | */
1183 | protected function validateMimes($attribute, $value, $parameters)
1184 | {
1185 | if (!in_array($value, $_FILES, true)) {
1186 | return false;
1187 | }
1188 |
1189 | if (!empty($value['tmp_name']) && is_uploaded_file($value['tmp_name']) && $value['error']) {
1190 | return false;
1191 | }
1192 |
1193 | // The Symfony File class should do a decent job of guessing the extension
1194 | // based on the true MIME type so we'll just loop through the array of
1195 | // extensions and compare it to the guessed extension of the files.
1196 | if ($value['tmp_name'] !== '') {
1197 | return in_array(pathinfo($value['name'], PATHINFO_EXTENSION), $parameters, true);
1198 | } else {
1199 | return false;
1200 | }
1201 | }
1202 |
1203 | /**
1204 | * Validate that an attribute contains only alphabetic characters.
1205 | *
1206 | * @param string $attribute
1207 | * @param mixed $value
1208 | *
1209 | * @return bool
1210 | */
1211 | protected function validateAlpha($attribute, $value)
1212 | {
1213 | return preg_match('/^[\pL\pM]+$/u', $value);
1214 | }
1215 |
1216 | /**
1217 | * Validate that an attribute contains only alpha-numeric characters.
1218 | *
1219 | * @param string $attribute
1220 | * @param mixed $value
1221 | *
1222 | * @return bool
1223 | */
1224 | protected function validateAlphaNum($attribute, $value)
1225 | {
1226 | return preg_match('/^[\pL\pM\pN]+$/u', $value);
1227 | }
1228 |
1229 | /**
1230 | * Validate that an attribute contains only alpha-numeric characters, dashes, and underscores.
1231 | *
1232 | * @param string $attribute
1233 | * @param mixed $value
1234 | *
1235 | * @return bool
1236 | */
1237 | protected function validateAlphaDash($attribute, $value)
1238 | {
1239 | return preg_match('/^[\pL\pM\pN_-]+$/u', $value);
1240 | }
1241 |
1242 | /**
1243 | * Validate that an attribute passes a regular expression check.
1244 | *
1245 | * @param string $attribute
1246 | * @param mixed $value
1247 | * @param array $parameters
1248 | *
1249 | * @return bool
1250 | */
1251 | protected function validateRegex($attribute, $value, $parameters)
1252 | {
1253 | $this->requireParameterCount(1, $parameters, 'regex');
1254 |
1255 | return preg_match($parameters[0], $value);
1256 | }
1257 |
1258 | /**
1259 | * Validate that an attribute is a valid date.
1260 | *
1261 | * @param string $attribute
1262 | * @param mixed $value
1263 | *
1264 | * @return bool
1265 | */
1266 | protected function validateDate($attribute, $value)
1267 | {
1268 | if ($value instanceof DateTime) {
1269 | return true;
1270 | }
1271 |
1272 | if (strtotime($value) === false) {
1273 | return false;
1274 | }
1275 |
1276 | $date = date_parse($value);
1277 |
1278 | return checkdate($date['month'], $date['day'], $date['year']);
1279 | }
1280 |
1281 | /**
1282 | * Validate that an attribute matches a date format.
1283 | *
1284 | * @param string $attribute
1285 | * @param mixed $value
1286 | * @param array $parameters
1287 | *
1288 | * @return bool
1289 | */
1290 | protected function validateDateFormat($attribute, $value, $parameters)
1291 | {
1292 | $this->requireParameterCount(1, $parameters, 'date_format');
1293 |
1294 | $parsed = date_parse_from_format($parameters[0], $value);
1295 |
1296 | return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
1297 | }
1298 |
1299 | /**
1300 | * Validate the date is before a given date.
1301 | *
1302 | * @param string $attribute
1303 | * @param mixed $value
1304 | * @param array $parameters
1305 | *
1306 | * @return bool
1307 | */
1308 | protected function validateBefore($attribute, $value, $parameters)
1309 | {
1310 | $this->requireParameterCount(1, $parameters, 'before');
1311 |
1312 | if ($format = $this->getDateFormat($attribute)) {
1313 | return $this->validateBeforeWithFormat($format, $value, $parameters);
1314 | }
1315 |
1316 | if (!($date = strtotime($parameters[0]))) {
1317 | return strtotime($value) < strtotime($this->getValue($parameters[0]));
1318 | } else {
1319 | return strtotime($value) < $date;
1320 | }
1321 | }
1322 |
1323 | /**
1324 | * Validate the date is before a given date with a given format.
1325 | *
1326 | * @param string $format
1327 | * @param mixed $value
1328 | * @param array $parameters
1329 | *
1330 | * @return bool
1331 | */
1332 | protected function validateBeforeWithFormat($format, $value, $parameters)
1333 | {
1334 | $param = $this->getValue($parameters[0]) ?: $parameters[0];
1335 |
1336 | return $this->checkDateTimeOrder($format, $value, $param);
1337 | }
1338 |
1339 | /**
1340 | * Validate the date is after a given date.
1341 | *
1342 | * @param string $attribute
1343 | * @param mixed $value
1344 | * @param array $parameters
1345 | *
1346 | * @return bool
1347 | */
1348 | protected function validateAfter($attribute, $value, $parameters)
1349 | {
1350 | $this->requireParameterCount(1, $parameters, 'after');
1351 |
1352 | if ($format = $this->getDateFormat($attribute)) {
1353 | return $this->validateAfterWithFormat($format, $value, $parameters);
1354 | }
1355 |
1356 | if (!($date = strtotime($parameters[0]))) {
1357 | return strtotime($value) > strtotime($this->getValue($parameters[0]));
1358 | } else {
1359 | return strtotime($value) > $date;
1360 | }
1361 | }
1362 |
1363 | /**
1364 | * Validate the date is after a given date with a given format.
1365 | *
1366 | * @param string $format
1367 | * @param mixed $value
1368 | * @param array $parameters
1369 | *
1370 | * @return bool
1371 | */
1372 | protected function validateAfterWithFormat($format, $value, $parameters)
1373 | {
1374 | $param = $this->getValue($parameters[0]) ?: $parameters[0];
1375 |
1376 | return $this->checkDateTimeOrder($format, $param, $value);
1377 | }
1378 |
1379 | /**
1380 | * Given two date/time strings, check that one is after the other.
1381 | *
1382 | * @param string $format
1383 | * @param string $before
1384 | * @param string $after
1385 | *
1386 | * @return bool
1387 | */
1388 | protected function checkDateTimeOrder($format, $before, $after)
1389 | {
1390 | $before = $this->getDateTimeWithOptionalFormat($format, $before);
1391 |
1392 | $after = $this->getDateTimeWithOptionalFormat($format, $after);
1393 |
1394 | return ($before && $after) && ($after > $before);
1395 | }
1396 |
1397 | /**
1398 | * Get a DateTime instance from a string.
1399 | *
1400 | * @param string $format
1401 | * @param string $value
1402 | *
1403 | * @return \DateTime|null
1404 | */
1405 | protected function getDateTimeWithOptionalFormat($format, $value)
1406 | {
1407 | $date = DateTime::createFromFormat($format, $value);
1408 |
1409 | if ($date) {
1410 | return $date;
1411 | }
1412 |
1413 | try {
1414 | return new DateTime($value);
1415 | } catch (\Exception $e) {
1416 | return;
1417 | }
1418 | }
1419 |
1420 | /**
1421 | * Validate that an attribute is a valid timezone.
1422 | *
1423 | * @param string $attribute
1424 | * @param mixed $value
1425 | *
1426 | * @return bool
1427 | */
1428 | protected function validateTimezone($attribute, $value)
1429 | {
1430 | try {
1431 | new DateTimeZone($value);
1432 | } catch (\Exception $e) {
1433 | return false;
1434 | }
1435 |
1436 | return true;
1437 | }
1438 |
1439 | /**
1440 | * Get the date format for an attribute if it has one.
1441 | *
1442 | * @param string $attribute
1443 | *
1444 | * @return string|null
1445 | */
1446 | protected function getDateFormat($attribute)
1447 | {
1448 | if ($result = $this->getRule($attribute, 'DateFormat')) {
1449 | return $result[1][0];
1450 | }
1451 | }
1452 |
1453 | /**
1454 | * Get the validation message for an attribute and rule.
1455 | *
1456 | * @param string $attribute
1457 | * @param string $rule
1458 | *
1459 | * @return string
1460 | */
1461 | protected function getMessage($attribute, $rule)
1462 | {
1463 | $lowerRule = snake_case($rule);
1464 |
1465 | $inlineMessage = $this->getInlineMessage($attribute, $lowerRule);
1466 |
1467 | // First we will retrieve the custom message for the validation rule if one
1468 | // exists. If a custom validation message is being used we'll return the
1469 | // custom message, otherwise we'll keep searching for a valid message.
1470 | if (!is_null($inlineMessage)) {
1471 | return $inlineMessage;
1472 | }
1473 |
1474 | $customKey = "validation.custom.{$attribute}.{$lowerRule}";
1475 |
1476 | $customMessage = $this->translator->trans($customKey);
1477 |
1478 | // First we check for a custom defined validation message for the attribute
1479 | // and rule. This allows the developer to specify specific messages for
1480 | // only some attributes and rules that need to get specially formed.
1481 | if ($customMessage !== $customKey) {
1482 | return $customMessage;
1483 | }
1484 |
1485 | // If the rule being validated is a "size" rule, we will need to gather the
1486 | // specific error message for the type of attribute being validated such
1487 | // as a number, file or string which all have different message types.
1488 | elseif (in_array($rule, $this->sizeRules, true)) {
1489 | return $this->getSizeMessage($attribute, $rule);
1490 | }
1491 |
1492 | // Finally, if no developer specified messages have been set, and no other
1493 | // special messages apply for this rule, we will just pull the default
1494 | // messages out of the translator service for this validation rule.
1495 | $key = "validation.{$lowerRule}";
1496 |
1497 | if ($key !== ($value = $this->translator->trans($key))) {
1498 | return $value;
1499 | }
1500 |
1501 | return $this->getInlineMessage(
1502 | $attribute, $lowerRule, $this->fallbackMessages
1503 | ) ?: $key;
1504 | }
1505 |
1506 | /**
1507 | * Get the inline message for a rule if it exists.
1508 | *
1509 | * @param string $attribute
1510 | * @param string $lowerRule
1511 | * @param array $source
1512 | *
1513 | * @return string
1514 | */
1515 | protected function getInlineMessage($attribute, $lowerRule, $source = null)
1516 | {
1517 | $source = $source ?: $this->customMessages;
1518 |
1519 | $keys = ["{$attribute}.{$lowerRule}", $lowerRule];
1520 |
1521 | // First we will check for a custom message for an attribute specific rule
1522 | // message for the fields, then we will check for a general custom line
1523 | // that is not attribute specific. If we find either we'll return it.
1524 | foreach ($keys as $key) {
1525 | if (isset($source[$key])) {
1526 | return $source[$key];
1527 | }
1528 | }
1529 | }
1530 |
1531 | /**
1532 | * Get the proper error message for an attribute and size rule.
1533 | *
1534 | * @param string $attribute
1535 | * @param string $rule
1536 | *
1537 | * @return string
1538 | */
1539 | protected function getSizeMessage($attribute, $rule)
1540 | {
1541 | $lowerRule = snake_case($rule);
1542 |
1543 | // There are three different types of size validations. The attribute may be
1544 | // either a number, file, or string so we will check a few things to know
1545 | // which type of value it is and return the correct line for that type.
1546 | $type = $this->getAttributeType($attribute);
1547 |
1548 | $key = "validation.{$lowerRule}.{$type}";
1549 |
1550 | return $this->translator->trans($key);
1551 | }
1552 |
1553 | /**
1554 | * Get the data type of the given attribute.
1555 | *
1556 | * @param string $attribute
1557 | *
1558 | * @return string
1559 | */
1560 | protected function getAttributeType($attribute)
1561 | {
1562 | // We assume that the attributes present in the file array are files so that
1563 | // means that if the attribute does not have a numeric rule and the files
1564 | // list doesn't have it we'll just consider it a string by elimination.
1565 | if ($this->hasRule($attribute, $this->numericRules)) {
1566 | return 'numeric';
1567 | } elseif ($this->hasRule($attribute, ['Array'])) {
1568 | return 'array';
1569 | } elseif (array_key_exists($attribute, $this->files)) {
1570 | return 'file';
1571 | }
1572 |
1573 | return 'string';
1574 | }
1575 |
1576 | /**
1577 | * Replace all error message place-holders with actual values.
1578 | *
1579 | * @param string $message
1580 | * @param string $attribute
1581 | * @param string $rule
1582 | * @param array $parameters
1583 | *
1584 | * @return string
1585 | */
1586 | protected function doReplacements($message, $attribute, $rule, $parameters)
1587 | {
1588 | $value = $this->getAttribute($attribute);
1589 |
1590 | $message = str_replace(
1591 | [':ATTRIBUTE', ':Attribute', ':attribute'],
1592 | [strtoupper($value), ucfirst($value), $value],
1593 | $message
1594 | );
1595 |
1596 | if (isset($this->replacers[snake_case($rule)])) {
1597 | $message = $this->callReplacer($message, $attribute, snake_case($rule), $parameters);
1598 | } elseif (method_exists($this, $replacer = "replace{$rule}")) {
1599 | $message = $this->$replacer($message, $attribute, $rule, $parameters);
1600 | }
1601 |
1602 | return $message;
1603 | }
1604 |
1605 | /**
1606 | * Transform an array of attributes to their displayable form.
1607 | *
1608 | * @param array $values
1609 | *
1610 | * @return array
1611 | */
1612 | protected function getAttributeList(array $values)
1613 | {
1614 | $attributes = [];
1615 |
1616 | // For each attribute in the list we will simply get its displayable form as
1617 | // this is convenient when replacing lists of parameters like some of the
1618 | // replacement functions do when formatting out the validation message.
1619 | foreach ($values as $key => $value) {
1620 | $attributes[$key] = $this->getAttribute($value);
1621 | }
1622 |
1623 | return $attributes;
1624 | }
1625 |
1626 | /**
1627 | * Get the displayable name of the attribute.
1628 | *
1629 | * @param string $attribute
1630 | *
1631 | * @return string
1632 | */
1633 | protected function getAttribute($attribute)
1634 | {
1635 | // The developer may dynamically specify the array of custom attributes
1636 | // on this Validator instance. If the attribute exists in this array
1637 | // it takes precedence over all other ways we can pull attributes.
1638 | if (isset($this->customAttributes[$attribute])) {
1639 | return $this->customAttributes[$attribute];
1640 | }
1641 |
1642 | $key = "validation.attributes.{$attribute}";
1643 |
1644 | // We allow for the developer to specify language lines for each of the
1645 | // attributes allowing for more displayable counterparts of each of
1646 | // the attributes. This provides the ability for simple formats.
1647 | if (($line = $this->translator->trans($key)) !== $key) {
1648 | return $line;
1649 | }
1650 |
1651 | // If no language line has been specified for the attribute all of the
1652 | // underscores are removed from the attribute name and that will be
1653 | // used as default versions of the attribute's displayable names.
1654 | else {
1655 | return str_replace('_', ' ', snake_case($attribute));
1656 | }
1657 | }
1658 |
1659 | /**
1660 | * Get the displayable name of the value.
1661 | *
1662 | * @param string $attribute
1663 | * @param mixed $value
1664 | *
1665 | * @return string
1666 | */
1667 | public function getDisplayableValue($attribute, $value)
1668 | {
1669 | if (isset($this->customValues[$attribute][$value])) {
1670 | return $this->customValues[$attribute][$value];
1671 | }
1672 |
1673 | $key = "validation.values.{$attribute}.{$value}";
1674 |
1675 | if (($line = $this->translator->trans($key)) !== $key) {
1676 | return $line;
1677 | } else {
1678 | return $value;
1679 | }
1680 | }
1681 |
1682 | /**
1683 | * Replace all place-holders for the between rule.
1684 | *
1685 | * @param string $message
1686 | * @param string $attribute
1687 | * @param string $rule
1688 | * @param array $parameters
1689 | *
1690 | * @return string
1691 | */
1692 | protected function replaceBetween($message, $attribute, $rule, $parameters)
1693 | {
1694 | return str_replace([':min', ':max'], $parameters, $message);
1695 | }
1696 |
1697 | /**
1698 | * Replace all place-holders for the digits rule.
1699 | *
1700 | * @param string $message
1701 | * @param string $attribute
1702 | * @param string $rule
1703 | * @param array $parameters
1704 | *
1705 | * @return string
1706 | */
1707 | protected function replaceDigits($message, $attribute, $rule, $parameters)
1708 | {
1709 | return str_replace(':digits', $parameters[0], $message);
1710 | }
1711 |
1712 | /**
1713 | * Replace all place-holders for the digits (between) rule.
1714 | *
1715 | * @param string $message
1716 | * @param string $attribute
1717 | * @param string $rule
1718 | * @param array $parameters
1719 | *
1720 | * @return string
1721 | */
1722 | protected function replaceDigitsBetween($message, $attribute, $rule, $parameters)
1723 | {
1724 | return str_replace([':min', ':max'], $parameters, $message);
1725 | }
1726 |
1727 | /**
1728 | * Replace all place-holders for the size rule.
1729 | *
1730 | * @param string $message
1731 | * @param string $attribute
1732 | * @param string $rule
1733 | * @param array $parameters
1734 | *
1735 | * @return string
1736 | */
1737 | protected function replaceSize($message, $attribute, $rule, $parameters)
1738 | {
1739 | return str_replace(':size', $parameters[0], $message);
1740 | }
1741 |
1742 | /**
1743 | * Replace all place-holders for the min rule.
1744 | *
1745 | * @param string $message
1746 | * @param string $attribute
1747 | * @param string $rule
1748 | * @param array $parameters
1749 | *
1750 | * @return string
1751 | */
1752 | protected function replaceMin($message, $attribute, $rule, $parameters)
1753 | {
1754 | return str_replace(':min', $parameters[0], $message);
1755 | }
1756 |
1757 | /**
1758 | * Replace all place-holders for the max rule.
1759 | *
1760 | * @param string $message
1761 | * @param string $attribute
1762 | * @param string $rule
1763 | * @param array $parameters
1764 | *
1765 | * @return string
1766 | */
1767 | protected function replaceMax($message, $attribute, $rule, $parameters)
1768 | {
1769 | return str_replace(':max', $parameters[0], $message);
1770 | }
1771 |
1772 | /**
1773 | * Replace all place-holders for the in rule.
1774 | *
1775 | * @param string $message
1776 | * @param string $attribute
1777 | * @param string $rule
1778 | * @param array $parameters
1779 | *
1780 | * @return string
1781 | */
1782 | protected function replaceIn($message, $attribute, $rule, $parameters)
1783 | {
1784 | foreach ($parameters as &$parameter) {
1785 | $parameter = $this->getDisplayableValue($attribute, $parameter);
1786 | }
1787 |
1788 | return str_replace(':values', implode(', ', $parameters), $message);
1789 | }
1790 |
1791 | /**
1792 | * Replace all place-holders for the not_in rule.
1793 | *
1794 | * @param string $message
1795 | * @param string $attribute
1796 | * @param string $rule
1797 | * @param array $parameters
1798 | *
1799 | * @return string
1800 | */
1801 | protected function replaceNotIn($message, $attribute, $rule, $parameters)
1802 | {
1803 | return $this->replaceIn($message, $attribute, $rule, $parameters);
1804 | }
1805 |
1806 | /**
1807 | * Replace all place-holders for the mimes rule.
1808 | *
1809 | * @param string $message
1810 | * @param string $attribute
1811 | * @param string $rule
1812 | * @param array $parameters
1813 | *
1814 | * @return string
1815 | */
1816 | protected function replaceMimes($message, $attribute, $rule, $parameters)
1817 | {
1818 | return str_replace(':values', implode(', ', $parameters), $message);
1819 | }
1820 |
1821 | /**
1822 | * Replace all place-holders for the required_with rule.
1823 | *
1824 | * @param string $message
1825 | * @param string $attribute
1826 | * @param string $rule
1827 | * @param array $parameters
1828 | *
1829 | * @return string
1830 | */
1831 | protected function replaceRequiredWith($message, $attribute, $rule, $parameters)
1832 | {
1833 | $parameters = $this->getAttributeList($parameters);
1834 |
1835 | return str_replace(':values', implode(' / ', $parameters), $message);
1836 | }
1837 |
1838 | /**
1839 | * Replace all place-holders for the required_with_all rule.
1840 | *
1841 | * @param string $message
1842 | * @param string $attribute
1843 | * @param string $rule
1844 | * @param array $parameters
1845 | *
1846 | * @return string
1847 | */
1848 | protected function replaceRequiredWithAll($message, $attribute, $rule, $parameters)
1849 | {
1850 | return $this->replaceRequiredWith($message, $attribute, $rule, $parameters);
1851 | }
1852 |
1853 | /**
1854 | * Replace all place-holders for the required_without rule.
1855 | *
1856 | * @param string $message
1857 | * @param string $attribute
1858 | * @param string $rule
1859 | * @param array $parameters
1860 | *
1861 | * @return string
1862 | */
1863 | protected function replaceRequiredWithout($message, $attribute, $rule, $parameters)
1864 | {
1865 | return $this->replaceRequiredWith($message, $attribute, $rule, $parameters);
1866 | }
1867 |
1868 | /**
1869 | * Replace all place-holders for the required_without_all rule.
1870 | *
1871 | * @param string $message
1872 | * @param string $attribute
1873 | * @param string $rule
1874 | * @param array $parameters
1875 | *
1876 | * @return string
1877 | */
1878 | protected function replaceRequiredWithoutAll($message, $attribute, $rule, $parameters)
1879 | {
1880 | return $this->replaceRequiredWith($message, $attribute, $rule, $parameters);
1881 | }
1882 |
1883 | /**
1884 | * Replace all place-holders for the required_if rule.
1885 | *
1886 | * @param string $message
1887 | * @param string $attribute
1888 | * @param string $rule
1889 | * @param array $parameters
1890 | *
1891 | * @return string
1892 | */
1893 | protected function replaceRequiredIf($message, $attribute, $rule, $parameters)
1894 | {
1895 | $parameters[1] = $this->getDisplayableValue($parameters[0], array_get($this->data, $parameters[0]));
1896 |
1897 | $parameters[0] = $this->getAttribute($parameters[0]);
1898 |
1899 | return str_replace([':other', ':value'], $parameters, $message);
1900 | }
1901 |
1902 | /**
1903 | * Replace all place-holders for the same rule.
1904 | *
1905 | * @param string $message
1906 | * @param string $attribute
1907 | * @param string $rule
1908 | * @param array $parameters
1909 | *
1910 | * @return string
1911 | */
1912 | protected function replaceSame($message, $attribute, $rule, $parameters)
1913 | {
1914 | return str_replace(':other', $this->getAttribute($parameters[0]), $message);
1915 | }
1916 |
1917 | /**
1918 | * Replace all place-holders for the different rule.
1919 | *
1920 | * @param string $message
1921 | * @param string $attribute
1922 | * @param string $rule
1923 | * @param array $parameters
1924 | *
1925 | * @return string
1926 | */
1927 | protected function replaceDifferent($message, $attribute, $rule, $parameters)
1928 | {
1929 | return $this->replaceSame($message, $attribute, $rule, $parameters);
1930 | }
1931 |
1932 | /**
1933 | * Replace all place-holders for the date_format rule.
1934 | *
1935 | * @param string $message
1936 | * @param string $attribute
1937 | * @param string $rule
1938 | * @param array $parameters
1939 | *
1940 | * @return string
1941 | */
1942 | protected function replaceDateFormat($message, $attribute, $rule, $parameters)
1943 | {
1944 | return str_replace(':format', $parameters[0], $message);
1945 | }
1946 |
1947 | /**
1948 | * Replace all place-holders for the before rule.
1949 | *
1950 | * @param string $message
1951 | * @param string $attribute
1952 | * @param string $rule
1953 | * @param array $parameters
1954 | *
1955 | * @return string
1956 | */
1957 | protected function replaceBefore($message, $attribute, $rule, $parameters)
1958 | {
1959 | if (!(strtotime($parameters[0]))) {
1960 | return str_replace(':date', $this->getAttribute($parameters[0]), $message);
1961 | } else {
1962 | return str_replace(':date', $parameters[0], $message);
1963 | }
1964 | }
1965 |
1966 | /**
1967 | * Replace all place-holders for the after rule.
1968 | *
1969 | * @param string $message
1970 | * @param string $attribute
1971 | * @param string $rule
1972 | * @param array $parameters
1973 | *
1974 | * @return string
1975 | */
1976 | protected function replaceAfter($message, $attribute, $rule, $parameters)
1977 | {
1978 | return $this->replaceBefore($message, $attribute, $rule, $parameters);
1979 | }
1980 |
1981 | /**
1982 | * Determine if the given attribute has a rule in the given set.
1983 | *
1984 | * @param string $attribute
1985 | * @param string|array $rules
1986 | *
1987 | * @return bool
1988 | */
1989 | protected function hasRule($attribute, $rules)
1990 | {
1991 | return !is_null($this->getRule($attribute, $rules));
1992 | }
1993 |
1994 | /**
1995 | * Get a rule and its parameters for a given attribute.
1996 | *
1997 | * @param string $attribute
1998 | * @param string|array $rules
1999 | *
2000 | * @return array|null
2001 | */
2002 | protected function getRule($attribute, $rules)
2003 | {
2004 | if (!array_key_exists($attribute, $this->rules)) {
2005 | return;
2006 | }
2007 |
2008 | $rules = (array) $rules;
2009 |
2010 | foreach ($this->rules[$attribute] as $rule) {
2011 | list($rule, $parameters) = $this->parseRule($rule);
2012 |
2013 | if (in_array($rule, $rules, true)) {
2014 | return [$rule, $parameters];
2015 | }
2016 | }
2017 | }
2018 |
2019 | /**
2020 | * Extract the rule name and parameters from a rule.
2021 | *
2022 | * @param array|string $rules
2023 | *
2024 | * @return array
2025 | */
2026 | protected function parseRule($rules)
2027 | {
2028 | if (is_array($rules)) {
2029 | return $this->parseArrayRule($rules);
2030 | }
2031 |
2032 | return $this->parseStringRule($rules);
2033 | }
2034 |
2035 | /**
2036 | * Parse an array based rule.
2037 | *
2038 | * @param array $rules
2039 | *
2040 | * @return array
2041 | */
2042 | protected function parseArrayRule(array $rules)
2043 | {
2044 | return [studly_case(trim(array_get($rules, 0))), array_slice($rules, 1)];
2045 | }
2046 |
2047 | /**
2048 | * Parse a string based rule.
2049 | *
2050 | * @param string $rules
2051 | *
2052 | * @return array
2053 | */
2054 | protected function parseStringRule($rules)
2055 | {
2056 | $parameters = [];
2057 |
2058 | // The format for specifying validation rules and parameters follows an
2059 | // easy {rule}:{parameters} formatting convention. For instance the
2060 | // rule "Max:3" states that the value may only be three letters.
2061 | if (strpos($rules, ':') !== false) {
2062 | list($rules, $parameter) = explode(':', $rules, 2);
2063 |
2064 | $parameters = $this->parseParameters($rules, $parameter);
2065 | }
2066 |
2067 | return [studly_case(trim($rules)), $parameters];
2068 | }
2069 |
2070 | /**
2071 | * Parse a parameter list.
2072 | *
2073 | * @param string $rule
2074 | * @param string $parameter
2075 | *
2076 | * @return array
2077 | */
2078 | protected function parseParameters($rule, $parameter)
2079 | {
2080 | if (strtolower($rule) === 'regex') {
2081 | return [$parameter];
2082 | }
2083 |
2084 | return str_getcsv($parameter);
2085 | }
2086 |
2087 | /**
2088 | * Get the array of custom validator extensions.
2089 | *
2090 | *
2091 | * @return array
2092 | */
2093 | public function getExtensions()
2094 | {
2095 | return $this->extensions;
2096 | }
2097 |
2098 | /**
2099 | * Register an array of custom validator extensions.
2100 | *
2101 | * @param array $extensions
2102 | */
2103 | public function addExtensions(array $extensions)
2104 | {
2105 | if ($extensions) {
2106 | $keys = array_map('Overtrue\Validation\snake_case', array_keys($extensions));
2107 |
2108 | $extensions = array_combine($keys, array_values($extensions));
2109 | }
2110 |
2111 | $this->extensions = array_merge($this->extensions, $extensions);
2112 | }
2113 |
2114 | /**
2115 | * Register an array of custom implicit validator extensions.
2116 | *
2117 | * @param array $extensions
2118 | */
2119 | public function addImplicitExtensions(array $extensions)
2120 | {
2121 | $this->addExtensions($extensions);
2122 |
2123 | foreach ($extensions as $rule => $extension) {
2124 | $this->implicitRules[] = studly_case($rule);
2125 | }
2126 | }
2127 |
2128 | /**
2129 | * Register a custom validator extension.
2130 | *
2131 | * @param string $rule
2132 | * @param \Closure|string $extension
2133 | */
2134 | public function addExtension($rule, $extension)
2135 | {
2136 | $this->extensions[snake_case($rule)] = $extension;
2137 | }
2138 |
2139 | /**
2140 | * Register a custom implicit validator extension.
2141 | *
2142 | * @param string $rule
2143 | * @param \Closure|string $extension
2144 | */
2145 | public function addImplicitExtension($rule, $extension)
2146 | {
2147 | $this->addExtension($rule, $extension);
2148 |
2149 | $this->implicitRules[] = studly_case($rule);
2150 | }
2151 |
2152 | /**
2153 | * Get the array of custom validator message replacers.
2154 | *
2155 | *
2156 | * @return array
2157 | */
2158 | public function getReplacers()
2159 | {
2160 | return $this->replacers;
2161 | }
2162 |
2163 | /**
2164 | * Register an array of custom validator message replacers.
2165 | *
2166 | * @param array $replacers
2167 | */
2168 | public function addReplacers(array $replacers)
2169 | {
2170 | if ($replacers) {
2171 | $keys = array_map('Overtrue\Validation\snake_case', array_keys($replacers));
2172 |
2173 | $replacers = array_combine($keys, array_values($replacers));
2174 | }
2175 |
2176 | $this->replacers = array_merge($this->replacers, $replacers);
2177 | }
2178 |
2179 | /**
2180 | * Register a custom validator message replacer.
2181 | *
2182 | * @param string $rule
2183 | * @param \Closure|string $replacer
2184 | */
2185 | public function addReplacer($rule, $replacer)
2186 | {
2187 | $this->replacers[snake_case($rule)] = $replacer;
2188 | }
2189 |
2190 | /**
2191 | * Get the data under validation.
2192 | *
2193 | *
2194 | * @return array
2195 | */
2196 | public function getData()
2197 | {
2198 | return $this->data;
2199 | }
2200 |
2201 | /**
2202 | * Set the data under validation.
2203 | *
2204 | * @param array $data
2205 | */
2206 | public function setData(array $data)
2207 | {
2208 | $this->data = $this->parseData($data);
2209 | }
2210 |
2211 | /**
2212 | * Get the validation rules.
2213 | *
2214 | *
2215 | * @return array
2216 | */
2217 | public function getRules()
2218 | {
2219 | return $this->rules;
2220 | }
2221 |
2222 | /**
2223 | * Set the validation rules.
2224 | *
2225 | * @param array $rules
2226 | *
2227 | * @return $this
2228 | */
2229 | public function setRules(array $rules)
2230 | {
2231 | $this->rules = $this->explodeRules($rules);
2232 |
2233 | return $this;
2234 | }
2235 |
2236 | /**
2237 | * Set the custom attributes on the validator.
2238 | *
2239 | * @param array $attributes
2240 | *
2241 | * @return $this
2242 | */
2243 | public function setAttributeNames(array $attributes)
2244 | {
2245 | $this->customAttributes = $attributes;
2246 |
2247 | return $this;
2248 | }
2249 |
2250 | /**
2251 | * Set the custom values on the validator.
2252 | *
2253 | * @param array $values
2254 | *
2255 | * @return $this
2256 | */
2257 | public function setValueNames(array $values)
2258 | {
2259 | $this->customValues = $values;
2260 |
2261 | return $this;
2262 | }
2263 |
2264 | /**
2265 | * Get the files under validation.
2266 | *
2267 | *
2268 | * @return array
2269 | */
2270 | public function getFiles()
2271 | {
2272 | return $this->files;
2273 | }
2274 |
2275 | /**
2276 | * Set the files under validation.
2277 | *
2278 | * @param array $files
2279 | *
2280 | * @return $this
2281 | */
2282 | public function setFiles(array $files)
2283 | {
2284 | $this->files = $files;
2285 |
2286 | return $this;
2287 | }
2288 |
2289 | /**
2290 | * Get the Presence Verifier implementation.
2291 | *
2292 | *
2293 | * @return \Overtrue\Validation\PresenceVerifierInterface
2294 | *
2295 | * @throws \RuntimeException
2296 | */
2297 | public function getPresenceVerifier()
2298 | {
2299 | if (!isset($this->presenceVerifier)) {
2300 | throw new \RuntimeException('Presence verifier has not been set.');
2301 | }
2302 |
2303 | return $this->presenceVerifier;
2304 | }
2305 |
2306 | /**
2307 | * Set the Presence Verifier implementation.
2308 | *
2309 | * @param \Overtrue\Validation\PresenceVerifierInterface $presenceVerifier
2310 | */
2311 | public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier)
2312 | {
2313 | $this->presenceVerifier = $presenceVerifier;
2314 | }
2315 |
2316 | /**
2317 | * Get the Translator implementation.
2318 | *
2319 | *
2320 | * @return \Overtrue\Validation\TranslatorInterface
2321 | */
2322 | public function getTranslator()
2323 | {
2324 | return $this->translator;
2325 | }
2326 |
2327 | /**
2328 | * Set the Translator implementation.
2329 | *
2330 | * @param \Overtrue\Validation\TranslatorInterface $translator
2331 | */
2332 | public function setTranslator(TranslatorInterface $translator)
2333 | {
2334 | $this->translator = $translator;
2335 | }
2336 |
2337 | /**
2338 | * Get the custom messages for the validator.
2339 | *
2340 | *
2341 | * @return array
2342 | */
2343 | public function getCustomMessages()
2344 | {
2345 | return $this->customMessages;
2346 | }
2347 |
2348 | /**
2349 | * Set the custom messages for the validator.
2350 | *
2351 | * @param array $messages
2352 | */
2353 | public function setCustomMessages(array $messages)
2354 | {
2355 | $this->customMessages = array_merge($this->customMessages, $messages);
2356 | }
2357 |
2358 | /**
2359 | * Get the custom attributes used by the validator.
2360 | *
2361 | *
2362 | * @return array
2363 | */
2364 | public function getCustomAttributes()
2365 | {
2366 | return $this->customAttributes;
2367 | }
2368 |
2369 | /**
2370 | * Add custom attributes to the validator.
2371 | *
2372 | * @param array $customAttributes
2373 | *
2374 | * @return $this
2375 | */
2376 | public function addCustomAttributes(array $customAttributes)
2377 | {
2378 | $this->customAttributes = array_merge($this->customAttributes, $customAttributes);
2379 |
2380 | return $this;
2381 | }
2382 |
2383 | /**
2384 | * Get the custom values for the validator.
2385 | *
2386 | *
2387 | * @return array
2388 | */
2389 | public function getCustomValues()
2390 | {
2391 | return $this->customValues;
2392 | }
2393 |
2394 | /**
2395 | * Add the custom values for the validator.
2396 | *
2397 | * @param array $customValues
2398 | *
2399 | * @return $this
2400 | */
2401 | public function addCustomValues(array $customValues)
2402 | {
2403 | $this->customValues = array_merge($this->customValues, $customValues);
2404 |
2405 | return $this;
2406 | }
2407 |
2408 | /**
2409 | * Get the fallback messages for the validator.
2410 | *
2411 | *
2412 | * @return array
2413 | */
2414 | public function getFallbackMessages()
2415 | {
2416 | return $this->fallbackMessages;
2417 | }
2418 |
2419 | /**
2420 | * Set the fallback messages for the validator.
2421 | *
2422 | * @param array $messages
2423 | */
2424 | public function setFallbackMessages(array $messages)
2425 | {
2426 | $this->fallbackMessages = $messages;
2427 | }
2428 |
2429 | /**
2430 | * Get the failed validation rules.
2431 | *
2432 | *
2433 | * @return array
2434 | */
2435 | public function failed()
2436 | {
2437 | if (!$this->messages) {
2438 | $this->passes();
2439 | }
2440 |
2441 | return $this->failedRules;
2442 | }
2443 |
2444 | /**
2445 | * Get the message container for the validator.
2446 | *
2447 | *
2448 | * @return \Overtrue\Validation\MessageBag
2449 | */
2450 | public function messages()
2451 | {
2452 | if (!$this->messages) {
2453 | $this->passes();
2454 | }
2455 |
2456 | return $this->messages;
2457 | }
2458 |
2459 | /**
2460 | * An alternative more semantic shortcut to the message container.
2461 | *
2462 | *
2463 | * @return \Overtrue\Validation\MessageBag
2464 | */
2465 | public function errors()
2466 | {
2467 | return $this->messages();
2468 | }
2469 |
2470 | /**
2471 | * Call a custom validator extension.
2472 | *
2473 | * @param string $rule
2474 | * @param array $parameters
2475 | *
2476 | * @return bool
2477 | */
2478 | protected function callExtension($rule, $parameters)
2479 | {
2480 | $callback = $this->extensions[$rule];
2481 |
2482 | if ($callback instanceof Closure) {
2483 | return call_user_func_array($callback, $parameters);
2484 | } elseif (is_string($callback)) {
2485 | return $this->callClassBasedExtension($callback, $parameters);
2486 | }
2487 | }
2488 |
2489 | /**
2490 | * Call a class based validator extension.
2491 | *
2492 | * @param string $callback
2493 | * @param array $parameters
2494 | *
2495 | * @return bool
2496 | */
2497 | protected function callClassBasedExtension($callback, $parameters)
2498 | {
2499 | list($class, $method) = explode('@', $callback);
2500 |
2501 | return call_user_func_array([$class, $method], $parameters);
2502 | }
2503 |
2504 | /**
2505 | * Call a custom validator message replacer.
2506 | *
2507 | * @param string $message
2508 | * @param string $attribute
2509 | * @param string $rule
2510 | * @param array $parameters
2511 | *
2512 | * @return string
2513 | */
2514 | protected function callReplacer($message, $attribute, $rule, $parameters)
2515 | {
2516 | $callback = $this->replacers[$rule];
2517 |
2518 | if ($callback instanceof Closure) {
2519 | return call_user_func_array($callback, func_get_args());
2520 | } elseif (is_string($callback)) {
2521 | return $this->callClassBasedReplacer($callback, $message, $attribute, $rule, $parameters);
2522 | }
2523 | }
2524 |
2525 | /**
2526 | * Call a class based validator message replacer.
2527 | *
2528 | * @param string $callback
2529 | * @param string $message
2530 | * @param string $attribute
2531 | * @param string $rule
2532 | * @param array $parameters
2533 | *
2534 | * @return string
2535 | */
2536 | protected function callClassBasedReplacer($callback, $message, $attribute, $rule, $parameters)
2537 | {
2538 | list($class, $method) = explode('@', $callback);
2539 |
2540 | return call_user_func_array([$class, $method], array_slice(func_get_args(), 1));
2541 | }
2542 |
2543 | /**
2544 | * Require a certain number of parameters to be present.
2545 | *
2546 | * @param int $count
2547 | * @param array $parameters
2548 | * @param string $rule
2549 | *
2550 | * @throws \InvalidArgumentException
2551 | */
2552 | protected function requireParameterCount($count, $parameters, $rule)
2553 | {
2554 | if (count($parameters) < $count) {
2555 | throw new \InvalidArgumentException("Validation rule $rule requires at least $count parameters.");
2556 | }
2557 | }
2558 |
2559 | /**
2560 | * Handle dynamic calls to class methods.
2561 | *
2562 | * @param string $method
2563 | * @param array $parameters
2564 | *
2565 | * @return mixed
2566 | *
2567 | * @throws \BadMethodCallException
2568 | */
2569 | public function __call($method, $parameters)
2570 | {
2571 | $rule = snake_case(substr($method, 8));
2572 |
2573 | if (isset($this->extensions[$rule])) {
2574 | return $this->callExtension($rule, $parameters);
2575 | }
2576 |
2577 | throw new \BadMethodCallException("Method [$method] does not exist.");
2578 | }
2579 | }
2580 |
--------------------------------------------------------------------------------