SchemaValidator::validateCircularDependency()   B
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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