Volan::validateRequiredFieldIsPresent()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
/*
4
 *  @author Serkin Akexander <[email protected]>
5
 */
6
7
namespace Volan;
8
9
use Volan\Validator\AbstractValidator;
10
use Exception;
11
use Volan\Traits\LoggerTrait;
12
use Volan\Traits\ErrorHandlerTrait;
13
14
class Volan
15
{
16
17
    use ErrorHandlerTrait;
18
    use LoggerTrait;
19
20
    /**
21
     * If set to false all required fields set can be empty
22
     *
23
     * @var bool
24
     */
25
    private $requiredMode = true;
26
27
    /**
28
     * If set to false allows excessive keys in array. Default is true
29
     *
30
     * @var bool
31
     */
32
    private $strictMode;
33
34
    /**
35
     * @var array
36
     */
37
    protected $params = [];
38
39
    /**
40
     * @var string
41
     */
42
    private $currentNode = '';
43
    /**
44
     * @var array
45
     */
46
    private $schema = [];
47
48
49
    /**
50
     * @param array $schema
51
     * @param bool  $strictMode
52
     */
53
    public function __construct($schema, $strictMode = true)
54
    {
55
        $this->schema       = $schema;
56
        $this->strictMode   = $strictMode;
57
58
        $log = new InMemoryLogger();
59
        $this->setLogger($log);
60
    }
61
62
    /**
63
     * Sets required mode
64
     *
65
     * @param bool $mode
66
     */
67
    public function setRequiredMode($mode = true)
68
    {
69
        $this->requiredMode = $mode;
70
    }
71
72
    /**
73
     * Sets custom params
74
     *
75
     * @param array $arr
76
     * @return void
77
     *
78
     */
79
    public function setParams($arr = [])
80
    {
81
        $this->params = $arr;
82
    }
83
84
85
    /**
86
     * @param array $arr
87
     *
88
     * @return ValidatorResult
89
     */
90
    public function validate($arr)
91
    {
92
        $returnValue = new ValidatorResult();
93
        $this->currentNode = 'root';
94
95
        try {
96
            $this->validateSchemaBeginsWithRootKey(new CustomArrayObject($this->schema));
97
98
            if ($this->strictMode) {
99
                $this->validateExcessiveKeysAbsent(new CustomArrayObject($this->schema['root']), $arr);
100
            }
101
102
            $this->validateNode('root', new CustomArrayObject($this->schema), $arr);
103
        } catch (Exception $exc) {
104
            $this->getLogger()->error($exc->getMessage());
105
106
            $returnValue->setError($exc->getCode(), $this->getCurrentNode(), $exc->getMessage());
107
            $returnValue->setLog($this->getLogger()->getLog());
108
        }
109
110
        return $returnValue;
111
    }
112
113
    public function getCurrentNode()
114
    {
115
        return $this->currentNode;
116
    }
117
118
    /**
119
     * @param string            $node
120
     * @param CustomArrayObject $schema
121
     * @param mixed             $element
122
     *
123
     * @throws Exception
124
     */
125
    private function validateNode($node, CustomArrayObject $schema, $element = [])
126
    {
127
        $nodeSchema = new CustomArrayObject($schema[$node]);
128
129
        foreach ($nodeSchema->getArrayKeys() as $key) {
130
            $this->currentNode = $node . '.' . $key;
131
            $this->getLogger()->info("We are in element: {$this->currentNode}");
132
133
            $nodeData = isset($element[$key]) ? $element[$key] : null;
134
135
136
            $this->validateTypeFieldIsPresent($nodeSchema[$key]);
137
138
            $validator = $this->getClassValidator($nodeSchema[$key]);
139
            $validator->setParams($this->params);
140
141
            $isRequired = $this->requiredMode ? $validator->isRequired() : false;
142
143
            if ($isRequired === false && empty($nodeData)) {
144
                $this->getLogger()->info("Element: {$this->currentNode} has empty non-required data. We skip other check");
145
                continue;
146
            }
147
148
            $this->validateRequiredFieldIsPresent($nodeData);
149
150
            $this->validateExcessiveKeys($validator, new CustomArrayObject($nodeSchema[$key]), $nodeData);
151
152
            $this->validateNodeValue($validator, $nodeData);
153
154
            $this->validateNesting($validator, $nodeData);
155
156
            if ($validator->isNested()) {
157
                $this->getLogger()->info("Element: {$this->currentNode} has children");
158
159
                foreach ($nodeData as $record) {
160
                    $this->validateNode($key, $nodeSchema, $record);
161
                }
162
            } else {
163
                $this->validateNode($key, $nodeSchema, $nodeData);
164
            }
165
166
            $this->getLogger()->info("Element: {$this->currentNode} finished checking successfully.");
167
        }
168
    }
169
170
    /**
171
     * @param CustomArrayObject $nodeSchema
172
     * @param array             $nodeData
173
     *
174
     * @return bool
175
     */
176
    private function isChildElementHasStrictKeys(CustomArrayObject $nodeSchema, $nodeData)
177
    {
178
        $returnValue = false;
179
180
        if (!empty($nodeData) && is_array($nodeData)) {
181
            $schemaKeys = $nodeSchema->getArrayKeys();
182
            $dataKeys = count(array_filter(array_keys($nodeData), 'is_string')) ? array_keys($nodeData) : [];
183
            $returnValue = (bool)array_diff($dataKeys, $schemaKeys);
184
        }
185
186
        return $returnValue;
187
    }
188
189
    /**
190
     * @param AbstractValidator $validator
191
     * @param CustomArrayObject $schema
192
     * @param mixed             $nodeData
193
     *
194
     */
195
    private function validateExcessiveKeys(AbstractValidator $validator, CustomArrayObject $schema, $nodeData = null)
196
    {
197
        if ($this->strictMode === false) {
198
            return;
199
        }
200
201
        if (!$validator->isNested()) {
202
            $this->validateExcessiveKeysAbsent($schema, $nodeData);
203
        } else {
204
            foreach ($nodeData as $record) {
205
                $this->validateExcessiveKeysAbsent($schema, $record);
206
            }
207
        }
208
    }
209
210
    /**
211
     * @param CustomArrayObject           $schema
212
     * @param mixed                              $nodeData
213
     *
214
     * @throws Exception
215
     */
216
    private function validateExcessiveKeysAbsent($schema, $nodeData)
217
    {
218
        if ($this->isChildElementHasStrictKeys($schema, $nodeData)) {
219
            throw new Exception("{$this->currentNode} element has excessive keys", ValidatorResult::ERROR_NODE_HAS_EXCESSIVE_KEYS);
220
        }
221
    }
222
223
    /**
224
     * @param array $node
225
     *
226
     * @throws Exception
227
     */
228
    private function validateTypeFieldIsPresent($node)
229
    {
230
        if (empty($node['_type'])) {
231
            throw new Exception("Element: {$this->currentNode} has no compulsory field: _type", ValidatorResult::ERROR_NODE_HAS_NO_FIELD_TYPE);
232
        }
233
234
        $this->getLogger()->info("Element: {$this->currentNode} has field: _type");
235
    }
236
237
    /**
238
239
     * @param mixed $nodeData
240
     *
241
     * @throws Exception
242
     */
243
    private function validateRequiredFieldIsPresent($nodeData = null)
244
    {
245
        if (is_null($nodeData)) {
246
            throw new Exception("{$this->currentNode} element has flag *required*", ValidatorResult::ERROR_REQUIRED_FIELD_IS_EMPTY);
247
        }
248
249
        $this->getLogger()->info('*required* check passed');
250
    }
251
252
    /**
253
     * @param array $node
254
     *
255
     * @return AbstractValidator
256
     *
257
     * @throws Exception
258
     */
259
    private function getClassValidator($node)
260
    {
261
        $classStringName = $node['_type'].'_validator';
262
        $classStringNamespace = '\Volan\Validator\\';
263
264
        $classNames = [];
265
        $classNames[] = $classStringNamespace.$classStringName;
266
        $classNames[] = $classStringNamespace.$this->getPSRCompatibleClassName($classStringName);
267
268
        if (class_exists($classNames[0])) {
269
            $validatorClass = new $classNames[0]();
270
        } elseif (class_exists($classNames[1])) {
271
            $validatorClass = new $classNames[1]();
272
        } else {
273
            throw new Exception("Class validator {$classNames[0]}/{$classNames[1]} not found", ValidatorResult::ERROR_VALIDATOR_CLASS_NOT_FOUND);
274
        }
275
276
        $this->getLogger()->info("Class validator ".get_class($validatorClass)." exists");
277
278
        return $validatorClass;
279
    }
280
281
    /**
282
     * Validate that schema begins with root element
283
     *
284
     * @param CustomArrayObject $schema
285
     *
286
     * @throws Exception
287
     *
288
     * @return void
289
     */
290
    private function validateSchemaBeginsWithRootKey(CustomArrayObject $schema)
291
    {
292
        if (empty($schema['root'])) {
293
            throw new Exception('No root element in schema', ValidatorResult::ERROR_SCHEMA_HAS_NO_ROOT_ELEMENT);
294
        }
295
    }
296
    /*
297
     * Converts string constisting _ to PSR compatible class name
298
     *
299
     * @param string
300
     *
301
     * @return string
302
     */
303
    private function getPSRCompatibleClassName($string)
304
    {
305
        $className = '';
306
        $arr = explode('_', $string);
307
308
        foreach ($arr as $key => $value) {
309
            $className .= ucfirst(strtolower($value));
310
        }
311
312
        return $className;
313
    }
314
315
    /**
316
     * @param AbstractValidator $validator
317
     * @param mixed                              $nodeData
318
     *
319
     * @throws Exception
320
     */
321
    private function validateNodeValue(AbstractValidator $validator, $nodeData = null)
322
    {
323
        if ($validator->isValid($nodeData) === false) {
324
            $error = $this->currentNode . " element has invalid associated data.";
325
            $error .= !is_null($validator->getErrorDescription())
326
                ? $validator->getErrorDescription()
327
                : '';
328
329
            throw new Exception($error, ValidatorResult::ERROR_NODE_IS_NOT_VALID);
330
        }
331
    }
332
333
    /**
334
     * @param AbstractValidator $validator
335
     * @param mixed                              $nodeData
336
     *
337
     * @throws Exception
338
     */
339
    private function validateNesting(AbstractValidator $validator, $nodeData)
340
    {
341
        if ($validator->isNested() && (!isset($nodeData[0]) || !is_array($nodeData[0]))) {
342
            throw new Exception("{$this->currentNode} element supposed to be nested but it is not", ValidatorResult::ERROR_NESTED_ELEMENT_NOT_VALID);
343
        }
344
    }
345
}
346