Passed
Pull Request — master (#54)
by
unknown
01:59
created

SchemaValidator::validateCircularDependency()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
ccs 18
cts 18
cp 1
rs 8.439
cc 5
eloc 17
nc 4
nop 3
crap 5
1
<?php
2
namespace Fathomminds\Rest\Schema;
3
4
use Fathomminds\Rest\Schema\TypeValidators\ValidatorFactory;
5
use Fathomminds\Rest\Exceptions\RestException;
6
7
class SchemaValidator
8
{
9
    protected $fields = [];
10
    protected $allowExtraneous = false;
11
    protected $requiredSchemaClass = null;
12
    private $updateMode = false;
13
    private $replaceMode = false;
14
15 58
    public function __construct($requiredSchemaClass = null)
16
    {
17 58
        $this->requiredSchemaClass = $requiredSchemaClass;
18 58
    }
19
20 19
    public function updateMode($updateMode = null)
21
    {
22 19
        if (is_bool($updateMode)) {
23 16
            $this->updateMode = $updateMode;
24
        }
25 19
        return $this->updateMode;
26
    }
27
28 18
    public function replaceMode($replaceMode = null)
29
    {
30 18
        if (is_bool($replaceMode)) {
31 15
            $this->replaceMode = $replaceMode;
32
        }
33 18
        return $this->replaceMode;
34
    }
35
36 20
    public function validate($resource)
37
    {
38 20
        $this->validateResourceType($resource);
39 20
        $this->validateCircularDependency(get_class($resource), $resource->schema());
40 19
        $extraneousCheck = [];
41 19
        if (!$this->allowExtraneous) {
42 17
            $extraneousCheck = $this->validateExtraneousFields($resource);
43
        }
44 19
        $errors = array_merge(
45 19
            $this->validateRequiredFields($resource),
46 19
            $extraneousCheck,
47 19
            $this->validateFieldTypes($resource)
48
        );
49 19
        if (!empty($errors)) {
50 9
            throw new RestException(
51 9
                'Invalid structure',
52
                [
53 9
                    'schema' => get_class($resource),
54 9
                    'errors' => $errors,
55
                ]
56
            );
57
        }
58 12
    }
59
60 20
    private function validateCircularDependency($schemaName, $schemaDefinition, $schemaChain = [])
61
    {
62 20
        $schemaChain[] = $schemaName;
63 20
        foreach ($schemaDefinition as $field => $fieldDetails)
64
        {
65 20
            if (!isset($fieldDetails['type']) || $fieldDetails['type'] !== 'schema') {
66 19
                continue;
67
            }
68 20
            $nestedSchemaName = $fieldDetails['validator']['class'];
69 20
            $nestedSchemaDefinition = ($nestedSchemaName::cast((object)[]))->schema();
70 20
            if (in_array($nestedSchemaName, $schemaChain)) {
71 1
                $schemaChain[] = $nestedSchemaName;
72 1
                throw new RestException(
73 1
                    'Circular dependency found in schema definition',
74
                    [
75 1
                        'schema' => array_shift($schemaChain),
76 1
                        'chain' => $schemaChain,
77
                    ]
78
                );
79
            }
80 20
            $this->validateCircularDependency(
81 20
                $nestedSchemaName,
82 20
                $nestedSchemaDefinition,
83 20
                $schemaChain
84
            );
85
        }
86 19
    }
87
88 45
    public function allowExtraneous($value)
89
    {
90 45
        $this->allowExtraneous = $value;
91 45
    }
92
93 20
    private function validateResourceType($resource)
94
    {
95 20
        $this->expectObject($resource);
96 20
        $this->objectHasSchemaMethod($resource);
97 20
        $this->objectIsValidSchemaClass($resource);
98 20
    }
99
100 21
    private function expectObject($resource)
101
    {
102 21
        if (gettype($resource) !== 'object') {
103 1
            throw new RestException(
104 1
                'Object expected',
105
                [
106 1
                    'schema' => static::class,
107 1
                    'type' => gettype($resource),
108
                ]
109
            );
110
        }
111 20
    }
112
113 20
    private function objectHasSchemaMethod($resource)
114
    {
115 20
        if (!method_exists($resource, 'schema')) {
116 1
            throw new RestException(
117 1
                'Object must be a correct RestSchema object',
118
                [
119 1
                    'schema' => static::class,
120 1
                    'type' => gettype($resource),
121
                ]
122
            );
123
        }
124 20
    }
125
126 20
    private function objectIsValidSchemaClass($resource)
127
    {
128 20
        if ($this->requiredSchemaClass === null) {
129 14
            return;
130
        }
131 10
        if (get_class($resource) !== $this->requiredSchemaClass) {
132 1
            throw new RestException(
133 1
                'Object must be an instance of the defined SchemaClass',
134
                [
135 1
                    'schema' => static::class,
136 1
                    'type' => gettype($resource),
137
                ]
138
            );
139
        }
140 9
    }
141
142 19
    private function validateRequiredFields($resource)
143
    {
144 19
        $errors = [];
145 19
        if ($this->updateMode()) {
146 2
            return $errors;
147
        }
148 17
        $missingFields = array_diff($this->getRequiredFields($resource), array_keys(get_object_vars($resource)));
149 17
        array_walk($missingFields, function($item) use (&$errors) {
150 5
            $errors[$item] = 'Missing required field';
151 17
        });
152 17
        return $errors;
153
    }
154
155 17
    private function validateExtraneousFields($resource)
156
    {
157 17
        $errors = [];
158 17
        $extraFields = array_diff(array_keys(get_object_vars($resource)), array_keys($resource->schema()));
159 17
        array_walk($extraFields, function($item) use (&$errors) {
160 1
            $errors[$item] = 'Extraneous field';
161 17
        });
162 17
        return $errors;
163
    }
164
165 19
    private function validateFieldTypes($resource)
166
    {
167 19
        $validatorFactory = new ValidatorFactory;
168 19
        $errors = [];
169 19
        foreach ($resource->schema() as $fieldName => $rules) {
170 19
            if (property_exists($resource, $fieldName)) {
171
                try {
172
                    $validatorFactory
173 16
                        ->create($rules, $this->updateMode(), $this->replaceMode())
174 16
                        ->validate($resource->{$fieldName});
175 3
                } catch (RestException $ex) {
176 3
                    $errors[$fieldName]['error'] = $ex->getMessage();
177 3
                    $errors[$fieldName]['details'] = $ex->getDetails();
178
                }
179
            }
180
        }
181 19
        return $errors;
182
    }
183
184 28
    private function filterFields($resource, $paramKey, $paramValue, $checkParamValue = true)
185
    {
186 28
        $fields = [];
187 28
        foreach ($resource->schema() as $fieldName => $params) {
188 28
            if (array_key_exists($paramKey, $params) && $this->isMatchedValue(
189 27
                    $checkParamValue,
190 27
                    $params[$paramKey],
191 27
                    $paramValue
192
                )) {
193 27
                $fields[$fieldName] = $params;
194
            }
195
        }
196 28
        return $fields;
197
    }
198
199 27
    private function isMatchedValue($checkRequired, $value, $valueToMatch)
200
    {
201 27
        if (!$checkRequired) {
202 5
            return true;
203
        }
204 26
        return ($value == $valueToMatch);
205
    }
206
207 1
    public function getFields($resource)
208
    {
209 1
        return $resource->schema();
210
    }
211
212 17
    public function getRequiredFields($resource)
213
    {
214 17
        return array_keys($this->filterFields($resource, 'required', true));
215
    }
216
217 12
    public function getUniqueFields($resource)
218
    {
219 12
        return array_merge(
220 12
            array_keys($this->filterFields($resource, 'unique', true)),
221 12
            $this->getNestedUniqueFieldNames($resource)
222
        );
223
    }
224
225 15
    public function getSchemaFieldsWithDetails($resource)
226
    {
227 15
        return $this->filterFields($resource, 'type', 'schema');
228
    }
229
230 12
    private function getNestedUniqueFieldNames($resource) {
231 12
        $result = [];
232 12
        $schemaFields = $this->getSchemaFieldsWithDetails($resource);
233 12
        array_walk($schemaFields, function($fieldDetails, $fieldName) use (&$result, &$resource) {
234 11
            $nestedResourceClass = $fieldDetails['validator']['class'];
235 11
            $nestedResource = property_exists($resource, $fieldName)
236 2
                ? $resource->{$fieldName}
237 11
                : $nestedResourceClass::cast((object)[]);
238 11
            $nestedUniqueFields = (new SchemaValidator($nestedResourceClass))->getUniqueFields($nestedResource);
239 11
            array_walk($nestedUniqueFields, function($nestedFieldName) use (&$result, &$fieldName) {
240 11
                $result[] = $fieldName . '.' . $nestedFieldName;
241 11
            });
242 12
        });
243 12
        return $result;
244
    }
245
246 5
    public function getFieldsWithDefaults($resource)
247
    {
248 5
        return $this->filterFields($resource, 'default', null, false);
249
    }
250
}
251