Passed
Push — master ( 78f140...0de278 )
by Paul
04:49
created

Validator::translator()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 0
cts 23
cp 0
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 19
nc 6
nop 4
crap 20
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use BadMethodCallException;
6
use GeminiLabs\SiteReviews\Helper;
7
use GeminiLabs\SiteReviews\Modules\Validator\ValidationRules;
8
use InvalidArgumentException;
9
10
/**
11
 * Much of the code in this class is derived from \Illuminate\Validation\Validator (5.3)
12
 */
13
class Validator
14
{
15
	use ValidationRules;
16
17
	/**
18
	 * @var array
19
	 */
20
	public $errors = [];
21
22
	/**
23
	 * The data under validation.
24
	 * @var array
25
	 */
26
	protected $data = [];
27
28
	/**
29
	 * The failed validation rules.
30
	 * @var array
31
	 */
32
	protected $failedRules = [];
33
34
	/**
35
	 * The rules to be applied to the data.
36
	 * @var array
37
	 */
38
	protected $rules = [];
39
40
	/**
41
	 * The size related validation rules.
42
	 * @var array
43
	 */
44
	protected $sizeRules = [
45
		'Between', 'Max', 'Min',
46
	];
47
48
	/**
49
	 * The validation rules that imply the field is required.
50
	 * @var array
51
	 */
52
	protected $implicitRules = [
53
		'Required',
54
	];
55
56
	/**
57
	 * The numeric related validation rules.
58
	 * @var array
59
	 */
60
	protected $numericRules = [
61
		'Numeric',
62
	];
63
64
	/**
65
	 * Run the validator's rules against its data.
66
	 * @param mixed $data
67
	 * @return array
68
	 */
69
	public function validate( $data, array $rules = [] )
70
	{
71
		$this->normalizeData( $data );
72
		$this->setRules( $rules );
73
		foreach( $this->rules as $attribute => $rules ) {
74
			foreach( $rules as $rule ) {
75
				$this->validateAttribute( $attribute, $rule );
76
				if( $this->shouldStopValidating( $attribute ))break;
77
			}
78
		}
79
		return $this->errors;
80
	}
81
82
	/**
83
	 * Validate a given attribute against a rule.
84
	 * @param string $attribute
85
	 * @param string $rule
86
	 * @return void
87
	 * @throws BadMethodCallException
88
	 */
89
	public function validateAttribute( $attribute, $rule )
90
	{
91
		list( $rule, $parameters ) = $this->parseRule( $rule );
92
		if( $rule == '' )return;
93
		$value = $this->getValue( $attribute );
94
		$this->validateRequired( $attribute, $value ) || in_array( $rule, $this->implicitRules );
95
		if( !method_exists( $this, $method = 'validate'.$rule )) {
96
			throw new BadMethodCallException( "Method [$method] does not exist." );
97
		}
98
		if( !$this->$method( $attribute, $value, $parameters )) {
99
			$this->addFailure( $attribute, $rule, $parameters );
100
		}
101
	}
102
103
	/**
104
	 * Add an error message to the validator's collection of errors.
105
	 * @param string $attribute
106
	 * @param string $rule
107
	 * @return void
108
	 */
109
	protected function addError( $attribute, $rule, array $parameters )
110
	{
111
		$message = $this->getMessage( $attribute, $rule, $parameters );
112
		$this->errors[$attribute]['errors'][] = $message;
113
		if( !isset( $this->errors[$attribute]['value'] )) {
114
			$this->errors[$attribute]['value'] = $this->getValue( $attribute );
115
		}
116
	}
117
118
	/**
119
	 * Add a failed rule and error message to the collection.
120
	 * @param string $attribute
121
	 * @param string $rule
122
	 * @return void
123
	 */
124
	protected function addFailure( $attribute, $rule, array $parameters )
125
	{
126
		$this->addError( $attribute, $rule, $parameters );
127
		$this->failedRules[$attribute][$rule] = $parameters;
128
	}
129
130
	/**
131
	 * Get the data type of the given attribute.
132
	 * @param string $attribute
133
	 * @return string
134
	 */
135
	protected function getAttributeType( $attribute )
136
	{
137
		return $this->hasRule( $attribute, $this->numericRules )
138
			? 'numeric'
139
			: 'string';
140
	}
141
142
	/**
143
	 * Get the validation message for an attribute and rule.
144
	 * @param string $attribute
145
	 * @param string $rule
146
	 * @return string|null
147
	 */
148
	protected function getMessage( $attribute, $rule, array $parameters )
149
	{
150
		if( in_array( $rule, $this->sizeRules )) {
151
			return $this->getSizeMessage( $attribute, $rule, $parameters );
152
		}
153
		$lowerRule = glsr( Helper::class )->snakeCase( $rule );
154
		return $this->translator( $lowerRule, $rule, $attribute, $parameters );
155
	}
156
157
	/**
158
	 * Get a rule and its parameters for a given attribute.
159
	 * @param string $attribute
160
	 * @param string|array $rules
161
	 * @return array|null
162
	 */
163
	protected function getRule( $attribute, $rules )
164
	{
165
		if( !array_key_exists( $attribute, $this->rules ))return;
166
		$rules = (array) $rules;
167
		foreach( $this->rules[$attribute] as $rule ) {
168
			list( $rule, $parameters ) = $this->parseRule( $rule );
169
			if( in_array( $rule, $rules )) {
170
				return [$rule, $parameters];
171
			}
172
		}
173
	}
174
175
	/**
176
	 * Get the size of an attribute.
177
	 * @param string $attribute
178
	 * @param mixed $value
179
	 * @return mixed
180
	 */
181
	protected function getSize( $attribute, $value )
182
	{
183
		$hasNumeric = $this->hasRule( $attribute, $this->numericRules );
184
		if( is_numeric( $value ) && $hasNumeric ) {
185
			return $value;
186
		}
187
		elseif( is_array( $value )) {
188
			return count( $value );
189
		}
190
		return mb_strlen( $value );
191
	}
192
193
	/**
194
	 * Get the proper error message for an attribute and size rule.
195
	 * @param string $attribute
196
	 * @param string $rule
197
	 * @return string|null
198
	 */
199
	protected function getSizeMessage( $attribute, $rule, array $parameters )
200
	{
201
		$lowerRule = glsr( Helper::class )->snakeCase( $rule );
202
		$type = $this->getAttributeType( $attribute );
203
		$lowerRule .= '.'.$type;
204
		return $this->translator( $lowerRule, $rule, $attribute, $parameters );
205
	}
206
207
	/**
208
	 * Get the value of a given attribute.
209
	 * @param string $attribute
210
	 * @return mixed
211
	 */
212
	protected function getValue( $attribute )
213
	{
214
		if( isset( $this->data[$attribute] )) {
215
			return $this->data[$attribute];
216
		}
217
	}
218
219
	/**
220
	 * Determine if the given attribute has a rule in the given set.
221
	 * @param string $attribute
222
	 * @param string|array $rules
223
	 * @return bool
224
	 */
225
	protected function hasRule( $attribute, $rules )
226
	{
227
		return !is_null( $this->getRule( $attribute, $rules ));
228
	}
229
230
	/**
231
	 * Normalize the provided data to an array.
232
	 * @param mixed $data
233
	 * @return $this
234
	 */
235
	protected function normalizeData( $data )
236
	{
237
		$this->data = is_object( $data )
238
			? get_object_vars( $data )
239
			: $data;
240
		return $this;
241
	}
242
243
	/**
244
	 * Parse a parameter list.
245
	 * @param string $rule
246
	 * @param string $parameter
247
	 * @return array
248
	 */
249
	protected function parseParameters( $rule, $parameter )
250
	{
251
		if( strtolower( $rule ) == 'regex' ) {
252
			return [$parameter];
253
		}
254
		return str_getcsv( $parameter );
255
	}
256
257
	/**
258
	 * Extract the rule name and parameters from a rule.
259
	 * @param string $rule
260
	 * @return array
261
	 */
262
	protected function parseRule( $rule )
263
	{
264
		$parameters = [];
265
		// example: {rule}:{parameters}
1 ignored issue
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
266
		if( strpos( $rule, ':' ) !== false ) {
267
			list( $rule, $parameter ) = explode( ':', $rule, 2 );
268
			// example: {parameter1,parameter2,...}
1 ignored issue
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
269
			$parameters = $this->parseParameters( $rule, $parameter );
270
		}
271
		$rule = ucwords( str_replace( ['-', '_'], ' ', trim( $rule )));
272
		$rule = str_replace( ' ', '', $rule );
273
		return [$rule, $parameters];
274
	}
275
276
	/**
277
	 * Set the validation rules.
278
	 * @return $this
279
	 */
280
	protected function setRules( array $rules )
281
	{
282
		foreach( $rules as $key => $rule ) {
283
			$rules[$key] = is_string( $rule )
284
				? explode( '|', $rule )
285
				: $rule;
286
		}
287
		$this->rules = $rules;
288
		return $this;
289
	}
290
291
	/**
292
	 * Check if we should stop further validations on a given attribute.
293
	 * @param string $attribute
294
	 * @return bool
295
	 */
296
	protected function shouldStopValidating( $attribute )
297
	{
298
		return $this->hasRule( $attribute, $this->implicitRules )
299
			&& isset( $this->failedRules[$attribute] )
300
			&& array_intersect( array_keys( $this->failedRules[$attribute] ), $this->implicitRules );
301
	}
302
303
	/**
304
	 * Returns a translated message for the attribute
305
	 * @param string $key
306
	 * @param string $rule
307
	 * @param string $attribute
308
	 * @return string|null
309
	 */
310
	protected function translator( $key, $rule, $attribute, array $parameters )
311
	{
312
		$strings = [
313
			'accepted' => _x( 'The :attribute must be accepted.', ':attribute is a placeholder and should not be translated.', 'site-reviews' ),
314
			'between.numeric' => _x( 'The :attribute must be between :min and :max.', ':attribute, :min, and :max are placeholders and should not be translated.', 'site-reviews' ),
315
			'between.string' => _x( 'The :attribute must be between :min and :max characters.', ':attribute, :min, and :max are placeholders and should not be translated.', 'site-reviews' ),
316
			'email' => _x( 'The :attribute must be a valid email address.', ':attribute is a placeholder and should not be translated.', 'site-reviews' ),
317
			'max.numeric' => _x( 'The :attribute may not be greater than :max.', ':attribute and :max are placeholders and should not be translated.', 'site-reviews' ),
318
			'max.string' => _x( 'The :attribute may not be greater than :max characters.', ':attribute and :max are placeholders and should not be translated.', 'site-reviews' ),
319
			'min.numeric' => _x( 'The :attribute must be at least :min.', ':attribute and :min are placeholders and should not be translated.', 'site-reviews' ),
320
			'min.string' => _x( 'The :attribute must be at least :min characters.', ':attribute and :min are placeholders and should not be translated.', 'site-reviews' ),
321
			'regex' => _x( 'The :attribute format is invalid.', ':attribute is a placeholder and should not be translated.', 'site-reviews' ),
322
			'required' => _x( 'The :attribute field is required.', ':attribute is a placeholder and should not be translated.', 'site-reviews' ),
323
		];
324
		$message = isset( $strings[$key] )
325
			? $strings[$key]
326
			: false;
327
		if( !$message )return;
328
		$message = str_replace( ':attribute', $attribute, $message );
329
		if( method_exists( $this, $replacer = 'replace'.$rule )) {
330
			$message = $this->$replacer( $message, $parameters );
331
		}
332
		return $message;
333
	}
334
}
335