Completed
Push — master ( 72048b...aafbe4 )
by Peter
18:50
created

Validator::hasValidators()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 1
crap 2
1
<?php
2
3
/**
4
 * This software package is licensed under AGPL or Commercial license.
5
 *
6
 * @package   maslosoft/mangan
7
 * @licence   AGPL or Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]>
9
 * @copyright Copyright (c) Maslosoft
10
 * @copyright Copyright (c) Others as mentioned in code
11
 * @link      https://maslosoft.com/mangan/
12
 */
13
14
namespace Maslosoft\Mangan;
15
16
use InvalidArgumentException;
17
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
18
use Maslosoft\Mangan\Helpers\Validator\Factory;
19
use Maslosoft\Mangan\Interfaces\EntityManagerInterface;
20
use Maslosoft\Mangan\Interfaces\OwneredInterface;
21
use Maslosoft\Mangan\Interfaces\ValidatableInterface;
22
use Maslosoft\Mangan\Meta\DocumentPropertyMeta;
23
use Maslosoft\Mangan\Meta\DocumentTypeMeta;
24
use Maslosoft\Mangan\Meta\ManganMeta;
25
use Maslosoft\Mangan\Meta\ValidatorMeta;
26
27
/**
28
 * Validator
29
 *
30
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
31
 */
32
class Validator implements ValidatableInterface
33
{
34
35
	/**
36
	 * Model instance
37
	 * @var AnnotatedInterface
38
	 */
39
	private $model = null;
40
41
	/**
42
	 * Metadata for model
43
	 * @var ManganMeta
44
	 */
45
	private $meta = null;
46
47
	/**
48
	 * Array of error messages.
49
	 * Keys are field names, secondary keys are numeric
50
	 * @var string[][]
51
	 */
52
	private $errors = [];
53
54 140
	public function __construct(AnnotatedInterface $model)
55
	{
56 140
		$this->model = $model;
57 140
		$this->meta = ManganMeta::create($this->model);
58
59
		// Ensure that errors array is initialized - even if does not have validators
60 140
		foreach (array_keys($this->meta->fields()) as $name)
61
		{
62 140
			$this->errors[$name] = [];
63
		}
64 140
	}
65
66
	/**
67
	 * Validate model, optionally only selected fields
68
	 * @param string[] $fields
69
	 * @return boolean
70
	 */
71 104
	public function validate($fields = [])
72
	{
73 104
		$valid = [];
74 104
		if (empty($fields))
75
		{
76 104
			$fields = array_keys($this->meta->fields());
77
		}
78
79 104
		$meta = $this->meta->type();
80
		// Model validators
81 104
		if ($this->hasValidators($meta))
82
		{
83 1
			$typeName = $meta->name;
84
			// Reset errors
85 1
			$this->errors[$typeName] = [];
86 1
			$valid[] = (int)$this->validateField($typeName, $meta->validators);
87
		}
88
89 104
		foreach ($fields as $name)
90
		{
91 104
			$fieldMeta = $this->meta->field($name);
92
93
			// Reset errors
94 104
			$this->errors[$name] = [];
95
96
			// Check if meta for field exists
97 104
			assert(!empty($fieldMeta), new InvalidArgumentException(sprintf("Unknown field `%s` in model `%s`", $name, get_class($this->model))));
98
99
			// Validate if it is applicable
100 104
			if ($this->hasValidators($fieldMeta))
101
			{
102 21
				$valid[] = (int)$this->validateField($name, $fieldMeta->validators);
103
			}
104
105
			// Validate sub documents
106 104
			if ($this->haveSubObjects($fieldMeta) && $this->shouldValidate($this->model, $fieldMeta))
107
			{
108 26
				if (is_array($this->model->$name))
109
				{
110
					// Handle arrays of documents
111 11
					foreach ($this->model->$name as $fieldIndex => $model)
112
					{
113 11
						$errors = [];
114 11
						$valid[] = (int)$isValid = $this->validateEntity($model, $errors);
115 11
						if (!$isValid)
116
						{
117
							$errors = [
118
								$name => [
119 3
									$fieldIndex => $errors
120
								]
121
							];
122 11
							$this->setErrors($errors);
123
						}
124
					}
125
				}
126 19
				elseif (!empty($this->model->$name))
127
				{
128 18
					$model = $this->model->$name;
129 18
					$errors = [];
130 18
					$valid[] = (int)$isValid = $this->validateEntity($model, $errors);
131 18
					if (!$isValid)
132
					{
133
						$errors = [
134 3
							$name => $errors
135
						];
136 104
						$this->setErrors($errors);
137
					}
138
				}
139
			}
140
		}
141
142 104
		$areAllValid = count($valid) === array_sum($valid);
143
144
		// For easier debug
145 104
		if ($areAllValid)
146
		{
147 101
			return true;
148
		}
149 22
		return false;
150
	}
151
152
	/**
153
	 * Whether field has any validators
154
	 * @param DocumentTypeMeta|DocumentPropertyMeta $meta
155
	 * @return bool
156
	 */
157 104
	private function hasValidators($meta)
158
	{
159 104
		assert($meta instanceof DocumentTypeMeta || $meta instanceof DocumentPropertyMeta);
160 104
		return !empty($meta->validators);
161
	}
162
163
	/**
164
	 * Check whether field has sub object or sub objects
165
	 * @param DocumentPropertyMeta $fieldMeta
166
	 * @return bool
167
	 */
168 104
	private function haveSubObjects(DocumentPropertyMeta $fieldMeta)
169
	{
170 104
		return $fieldMeta->owned;
171
	}
172
173
	/**
174
	 * Check whether field should be validated.
175
	 *
176
	 * @param AnnotatedInterface   $model
177
	 * @param DocumentPropertyMeta $fieldMeta
178
	 * @return bool
179
	 */
180 30
	private function shouldValidate(AnnotatedInterface $model, DocumentPropertyMeta $fieldMeta)
181
	{
182
		// Skip fields that are owned...
183 30
		if ($fieldMeta->owned)
184
		{
185
			// ... and not updatable
186 30
			if (!$fieldMeta->updatable)
187
			{
188 4
				return false;
189
			}
190
		}
191
192
		// Check if should validate when saving and removing...
193 26
		if (AspectManager::hasAspect($model, EntityManagerInterface::AspectSaving) || AspectManager::hasAspect($model, EntityManagerInterface::AspectRemoving))
194
		{
195
			// ... when field is not persistent.
196
			// This is to speed up validation and prevent false
197
			// positive validations.
198 11
			if (!$fieldMeta->persistent)
199
			{
200
				return false;
201
			}
202
		}
203 26
		return true;
204
	}
205
206
	/**
207
	 * Validate single sub object entity. The `$errors`
208
	 * parameter will contain error messages passed by
209
	 * reference from this method.
210
	 *
211
	 * @param AnnotatedInterface $model
212
	 * @param string[]           $errors
213
	 * @return bool
214
	 */
215 25
	private function validateEntity(AnnotatedInterface $model, &$errors = [])
216
	{
217
		// Ensure owner, as validation might rely on it
218 25
		if ($model instanceof OwneredInterface)
219
		{
220 10
			$model->setOwner($this->model);
221
		}
222 25
		assert($model instanceof AnnotatedInterface);
223
		// Handle single documents
224 25
		$validator = new Validator($model);
225 25
		$isValid = $validator->validate();
226 25
		$errors = $validator->getErrors();
227 25
		return $isValid;
228
	}
229
230
	/**
231
	 * Validate single, simple field
232
	 * @param string          $name
233
	 * @param ValidatorMeta[] $validators
234
	 * @return bool
235
	 */
236 22
	private function validateField($name, $validators)
237
	{
238 22
		$valid = [];
239 22
		foreach ($validators as $validatorMeta)
240
		{
241
			// Filter out validators based on scenarios
242 22
			if (!empty($validatorMeta->on))
243
			{
244 2
				$on = (array)$validatorMeta->on;
245 2
				$enabled = false;
246 2
				foreach ($on as $scenario)
247
				{
248 2
					if ($scenario === ScenarioManager::getScenario($this->model))
249
					{
250 2
						$enabled = true;
251 2
						break;
252
					}
253
				}
254 2
				if (!$enabled)
255
				{
256 2
					continue;
257
				}
258
			}
259 22
			if (!empty($validatorMeta->except))
260
			{
261 1
				$except = (array)$validatorMeta->except;
262 1
				$enabled = true;
263 1
				foreach ($except as $scenario)
264
				{
265 1
					if ($scenario === ScenarioManager::getScenario($this->model))
266
					{
267 1
						$enabled = false;
268 1
						break;
269
					}
270
				}
271 1
				if (!$enabled)
272
				{
273 1
					continue;
274
				}
275
			}
276
277
278
			// Create validator and validate
279 22
			$validator = Factory::create($this->model, $validatorMeta, $name);
280 22
			if ($validator->isValid($this->model, $name))
281
			{
282 18
				$valid[] = true;
283
			}
284
			else
285
			{
286 22
				$valid[] = false;
287 22
				$this->errors[$name] = array_merge($this->errors[$name], $validator->getErrors());
288
289
				// Set errors to model instance if it implements ValidatableInterface
290 22
				if ($this->model instanceof ValidatableInterface)
291
				{
292 22
					$this->model->setErrors($this->errors);
293
				}
294
			}
295
		}
296 22
		return count($valid) === array_sum($valid);
297
	}
298
299 28
	public function getErrors()
300
	{
301 28
		return $this->errors;
302
	}
303
304 12
	public function setErrors($errors)
305
	{
306 12
		foreach ($errors as $field => $fieldErrors)
307
		{
308 12
			$this->errors[$field] = $fieldErrors;
309
		}
310 12
	}
311
312
}
313