Failed Conditions
Push — master ( 2b713c...735d0a )
by Alex
16s queued 13s
created

InterfaceValidator::validateStructure()   C

Complexity

Conditions 16
Paths 131

Size

Total Lines 73
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 36
nc 131
nop 1
dl 0
loc 73
rs 5.3083
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
6
namespace AlgoWeb\ODataMetadata\Edm\Validation\Internal;
7
8
use AlgoWeb\ODataMetadata\Edm\Validation\EdmError;
9
use AlgoWeb\ODataMetadata\Edm\Validation\EdmErrorCode;
10
use AlgoWeb\ODataMetadata\Edm\Validation\Internal\InterfaceValidator\VisitorBase;
11
use AlgoWeb\ODataMetadata\Edm\Validation\Internal\InterfaceValidator\VisitorOfT;
12
use AlgoWeb\ODataMetadata\Edm\Validation\ObjectLocation;
13
use AlgoWeb\ODataMetadata\Edm\Validation\ValidationContext;
14
use AlgoWeb\ODataMetadata\Edm\Validation\ValidationRule;
15
use AlgoWeb\ODataMetadata\Edm\Validation\ValidationRuleSet;
16
use AlgoWeb\ODataMetadata\EdmUtil;
17
use AlgoWeb\ODataMetadata\Exception\InvalidOperationException;
18
use AlgoWeb\ODataMetadata\Interfaces\Annotations\IDirectValueAnnotation;
19
use AlgoWeb\ODataMetadata\Interfaces\ICheckable;
20
use AlgoWeb\ODataMetadata\Interfaces\IEdmElement;
21
use AlgoWeb\ODataMetadata\Interfaces\IEdmValidCoreModelElement;
22
use AlgoWeb\ODataMetadata\Interfaces\ILocatable;
23
use AlgoWeb\ODataMetadata\Interfaces\ILocation;
24
use AlgoWeb\ODataMetadata\Interfaces\IModel;
25
use AlgoWeb\ODataMetadata\Interfaces\IPrimitiveType;
26
use AlgoWeb\ODataMetadata\Interfaces\IPrimitiveTypeReference;
27
use AlgoWeb\ODataMetadata\Interfaces\ITypeReference;
28
use AlgoWeb\ODataMetadata\StringConst;
29
30
class InterfaceValidator
31
{
32
    private static $interfaceVisitors = null;
33
34
    private static $concreteTypeInterfaceVisitors = [];
35
36
    private static function getInterfaceVisitors(): iterable
37
    {
38
        return self::$interfaceVisitors ?? self::$interfaceVisitors = self::createInterfaceVisitorsMap();
39
    }
40
41
    /**
42
     * @var array
43
     */
44
    private $visited = [];
45
    /**
46
     * @var array
47
     */
48
    private $visitedBad = [];
49
    /**
50
     * @var array
51
     */
52
    private $danglingReferences = [];
53
    /**
54
     * @var array|null
55
     */
56
    private $skipVisitation;
57
    /**
58
     * @var bool
59
     */
60
    private $validateDirectValueAnnotations;
61
    /**
62
     * @var IModel|null
63
     */
64
    private $model;
65
66
    private function __construct(?iterable $skipVisitation, ?IModel $model, bool $validateDirectValueAnnotations)
67
    {
68
        $this->skipVisitation                 = iterable_to_array($skipVisitation);
69
        $this->model                          = $model;
70
        $this->validateDirectValueAnnotations = $validateDirectValueAnnotations;
71
    }
72
73
    /**
74
     * @param  IModel               $model
75
     * @param  ValidationRuleSet    $semanticRuleSet
76
     * @throws \ReflectionException
77
     * @return iterable|EdmError[]
78
     */
79
    public static function validateModelStructureAndSemantics(IModel $model, ValidationRuleSet $semanticRuleSet): iterable
80
    {
81
        $modelValidator = new InterfaceValidator(null, $model, true);
82
83
        // Perform structural validation of the root object.
84
        $errors = iterable_to_array($modelValidator->validateStructure($model));
85
86
        // Then check references for structural integrity using separate validator (in order to avoid adding referenced objects to the this.visited).
87
        $referencesValidator              = new InterfaceValidator($modelValidator->visited, $model, false);
88
        $referencesToStructurallyValidate = $modelValidator->danglingReferences;
89
        while (count($referencesToStructurallyValidate) !== 0) {
90
            foreach ($referencesToStructurallyValidate as $reference) {
91
                $errors = array_merge(iterable_to_array($errors), $referencesValidator->validateStructure($reference));
92
            }
93
94
            $referencesToStructurallyValidate = $referencesValidator->danglingReferences;
95
        }
96
        $critical = array_filter($errors, [ValidationHelper::class, 'isInterfaceCritical']);
97
        // If there are any critical structural errors detected, then it is not safe to traverse the root object, so return the errors without further processing.
98
        if (count($critical) > 0) {
99
            return $errors;
100
        }
101
102
        // If the root object is structurally sound, apply validation rules to the visited objects that are not known to be bad.
103
        $semanticValidationContext = new ValidationContext(
104
            $model,
105
            function (IEdmElement $item) use ($modelValidator, $referencesValidator): bool {
106
                return in_array($item, $modelValidator->visitedBad) || in_array($item, $referencesValidator->visitedBad);
107
            }
108
        );
109
        $concreteTypeSemanticInterfaceVisitors = [];
110
        foreach ($modelValidator->visited as $item) {
111
            if (!in_array($item, $modelValidator->visitedBad)) {
112
                /** * @var ValidationRule $rule */
113
                foreach (self::getSemanticInterfaceVisitorsForObject(
114
                    get_class($item),
115
                    $semanticRuleSet,
116
                    $concreteTypeSemanticInterfaceVisitors
117
                ) as $rule) {
118
                    $rule($semanticValidationContext, $item);
119
                }
120
            }
121
        }
122
123
        $errors = array_merge($errors, $semanticValidationContext->getErrors());
124
        return $errors;
125
    }
126
127
    /**
128
     * @param  IEdmElement         $item
129
     * @return iterable|EdmError[]
130
     */
131
    public static function getStructuralErrors(IEdmElement $item): iterable
132
    {
133
        $model               = $item instanceof IModel ? $item : null;
134
        $structuralValidator = new InterfaceValidator(null, $model, $model !== null);
135
        return $structuralValidator->validateStructure($item);
136
    }
137
138
    /**
139
     * @return iterable|array<string, VisitorBase>
140
     */
141
    private static function createInterfaceVisitorsMap(): iterable
142
    {
143
        $map    = [];
144
        $handle = opendir('.');
145
        if (false !== $handle) {
146
            while (false !== ($entry = readdir($handle))) {
147
                /** @var string $name */
148
                $name = substr($entry, 0, -4);
149
                $ext  = substr($entry, -4);
150
                if ($entry === '.' || $entry === '..' || is_dir($entry) || $ext !== '.php' || empty($ext)) {
151
                    continue;
152
                }
153
                if ($name === 'VisitorBase' || $name === 'VisitorOfT') {
154
                    continue;
155
                }
156
                $class = __CLASS__ . '\\' . $name;
157
                /** @var VisitorOfT $instance */
158
                $instance                  = new $class();
159
                $map[$instance->forType()] = $instance;
160
            }
161
        }
162
        return $map;
163
    }
164
165
    /**
166
     * @param  string                 $objectType
167
     * @return iterable|VisitorBase[]
168
     */
169
    private static function computeInterfaceVisitorsForObject(string $objectType): iterable
170
    {
171
        $visitors = [];
172
        foreach (class_implements($objectType) as $type) {
173
            $visitor = null;
174
            if (isset(self::getInterfaceVisitors()[$type])) {
175
                $visitor    = self::getInterfaceVisitors()[$type];
176
                $visitors[] = $visitor;
177
            }
178
        }
179
180
        return $visitors;
181
    }
182
183
    private function getInterfaceVisitorsForObject(string $objectType): iterable
184
    {
185
        $visitors = [];
186
        if (!isset(self::$concreteTypeInterfaceVisitors[$objectType])) {
187
            $visitors                                         = self::computeInterfaceVisitorsForObject($objectType);
188
            self::$concreteTypeInterfaceVisitors[$objectType] = $visitors;
189
        }
190
191
        return $visitors;
192
    }
193
194
    public static function createPropertyMustNotBeNullError($item, string $propertyName): EdmError
195
    {
196
        return new EdmError(
197
            self::getLocation($item),
198
            EdmErrorCode::InterfaceCriticalPropertyValueMustNotBeNull(),
199
            StringConst::EdmModel_Validator_Syntactic_PropertyMustNotBeNull(get_class($item), $propertyName)
200
        );
201
    }
202
203
    public static function createEnumPropertyOutOfRangeError($item, $enumValue, string $propertyName): EdmError
204
    {
205
        return new EdmError(
206
            self::getLocation($item),
207
            EdmErrorCode::InterfaceCriticalEnumPropertyValueOutOfRange(),
208
            StringConst::EdmModel_Validator_Syntactic_EnumPropertyValueOutOfRange(get_class($item), $propertyName, get_class($enumValue), $enumValue)
209
        );
210
    }
211
212
    public static function checkForInterfaceKindValueMismatchError($item, $kind, string $propertyName, string $interface): ?EdmError
213
    {
214
        // If object implements an expected interface, return no error.
215
        if (in_array($interface, class_implements($item))) {
216
            return null;
217
        }
218
219
        return new EdmError(
220
            self::getLocation($item),
221
            EdmErrorCode::InterfaceCriticalKindValueMismatch(),
222
            StringConst::EdmModel_Validator_Syntactic_InterfaceKindValueMismatch($kind, get_class($item), $propertyName, $interface)
223
        );
224
    }
225
226
    public static function createInterfaceKindValueUnexpectedError($item, $kind, string $propertyName): EdmError
227
    {
228
        return new EdmError(
229
            self::getLocation($item),
230
            EdmErrorCode::InterfaceCriticalKindValueUnexpected(),
231
            StringConst::EdmModel_Validator_Syntactic_InterfaceKindValueUnexpected($kind, get_class($item), $propertyName)
232
        );
233
    }
234
235
    public static function createTypeRefInterfaceTypeKindValueMismatchError(ITypeReference $item): EdmError
236
    {
237
        EdmUtil::checkArgumentNull($item->getDefinition(), 'item.Definition');
238
        return new EdmError(
239
            self::getLocation($item),
240
            EdmErrorCode::InterfaceCriticalKindValueMismatch(),
241
            StringConst::EdmModel_Validator_Syntactic_TypeRefInterfaceTypeKindValueMismatch(get_class($item), $item->getDefinition()->getTypeKind()->getKey())
242
        );
243
    }
244
245
    public static function createPrimitiveTypeRefInterfaceTypeKindValueMismatchError(IPrimitiveTypeReference $item): EdmError
246
    {
247
        $definition = $item->getDefinition();
248
        if (!$definition instanceof IPrimitiveType) {
249
            throw new InvalidOperationException('item.Definition is IEdmPrimitiveType');
250
        }
251
        return new EdmError(
252
            self::getLocation($item),
253
            EdmErrorCode::InterfaceCriticalKindValueMismatch(),
254
            StringConst::EdmModel_Validator_Syntactic_TypeRefInterfaceTypeKindValueMismatch(get_class($item), $definition->getPrimitiveKind()->getKey())
255
        );
256
    }
257
258
    public static function processEnumerable($item, ?iterable $enumerable, string $propertyName, array &$targetList, array &$errors): void
259
    {
260
        if (null === $enumerable) {
261
            self::collectErrors(self::createPropertyMustNotBeNullError($item, $propertyName), $errors);
262
        } else {
263
            foreach ($enumerable as $enumMember) {
264
                if (null !== $enumMember) {
265
                    $targetList[] = $enumMember;
266
                } else {
267
                    self::collectErrors(
268
                        new EdmError(
269
                            self::getLocation($item),
270
                            EdmErrorCode::InterfaceCriticalEnumerableMustNotHaveNullElements(),
271
                            StringConst::EdmModel_Validator_Syntactic_EnumerableMustNotHaveNullElements(get_class($item), $propertyName)
272
                        ),
273
                        $errors
274
                    );
275
                    break;
276
                }
277
            }
278
        }
279
    }
280
281
    public static function collectErrors(?EdmError $newError, ?array &$errors): void
282
    {
283
        if ($newError != null) {
284
            if ($errors == null) {
285
                $errors = [];
286
            }
287
288
            $errors[] = $newError;
289
        }
290
    }
291
292
    public static function isCheckableBad($element): bool
293
    {
294
        return $element instanceof ICheckable && null !== $element->getErrors() && count($element->getErrors()) > 0;
295
    }
296
297
    public static function getLocation($item): ILocation
298
    {
299
        return $item instanceof ILocatable && null !== $item->getLocation() ? $item->getLocation() : new ObjectLocation($item);
300
    }
301
302
    /**
303
     * @param  string                    $objectType
304
     * @param  ValidationRuleSet         $ruleSet
305
     * @param  array                     $concreteTypeSemanticInterfaceVisitors
306
     * @return iterable|ValidationRule[]
307
     */
308
    private static function getSemanticInterfaceVisitorsForObject(string $objectType, ValidationRuleSet $ruleSet, array &$concreteTypeSemanticInterfaceVisitors): iterable
309
    {
310
        $visitors = null;
311
        if (!isset($concreteTypeSemanticInterfaceVisitors[$objectType])) {
312
            $visitors = [];
313
            foreach (class_implements($objectType) as $type) {
314
                $visitors = array_merge($visitors, $ruleSet->getRules($type));
315
            }
316
317
            $concreteTypeSemanticInterfaceVisitors[$objectType] = $visitors;
318
        }
319
320
        return $visitors;
321
    }
322
323
    /**
324
     * @param  mixed               $item
325
     * @return iterable|EdmError[]
326
     */
327
    private function validateStructure($item): iterable
328
    {
329
        if ($item instanceof IEdmValidCoreModelElement || in_array($item, $this->visited) || ($this->skipVisitation != null && in_array($item, $this->skipVisitation))) {
330
            // If we already visited this object, then errors (if any) have already been reported.
331
            return [];
332
        }
333
334
        $this->visited[] = $item;
335
        if (in_array($item, $this->danglingReferences)) {
336
            // If this edm element is visited, then it is no longer a dangling reference.
337
            $index = array_search($item, $this->danglingReferences);
338
            unset($this->danglingReferences[$index]);
339
        }
340
341
        //// First pass: collect immediate errors for each interface and collect followup objects for the second pass.
342
343
        $immediateErrors = null;
344
        $followup        = [];
345
        $references      = [];
346
        $visitors        = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $visitors is dead and can be removed.
Loading history...
347
        $visitors        = $this->getInterfaceVisitorsForObject(get_class($item));
348
        /** @var VisitorBase $visitor */
349
        foreach ($visitors as $visitor) {
350
            $errors = $visitor->visit($item, $followup, $references);
351
352
            // For performance reasons some visitors may return null errors enumerator.
353
            if ($errors != null) {
354
                /** @var EdmError $error */
355
                foreach ($errors as $error) {
356
                    if ($immediateErrors == null) {
357
                        $immediateErrors = [];
358
                    }
359
360
                    $immediateErrors[] = $error;
361
                }
362
            }
363
        }
364
365
        // End of the first pass: if there are immediate errors, return them without doing the second pass.
366
        if ($immediateErrors !== null) {
367
            $this->visitedBad[] = $item;
368
            return $immediateErrors;
369
        }
370
371
        //// Second pass: collect errors from followup objects.
372
373
        $followupErrors = [];
374
375
        // An element's direct value annotations are available only through a model,
376
        // and so are not found in a normal traversal.
377
        if ($this->validateDirectValueAnnotations) {
378
            if ($item instanceof IEdmElement) {
379
                $element = $item;
380
                EdmUtil::checkArgumentNull($this->model, 'this->model');
381
                foreach ($this->model->getDirectValueAnnotationsManager()->getDirectValueAnnotations($element) as $annotation) {
382
                    assert($annotation instanceof IDirectValueAnnotation);
383
                    $followupErrors = array_merge(
384
                        iterable_to_array($followupErrors),
385
                        iterable_to_array($this->validateStructure($annotation))
386
                    );
387
                }
388
            }
389
        }
390
391
        foreach ($followup as $followupItem) {
392
            $followupErrors = array_merge($followupErrors, $this->validateStructure($followupItem));
393
        }
394
395
        foreach ($references as $referencedItem) {
396
            $this->collectReference($referencedItem);
397
        }
398
399
        return $followupErrors;
400
    }
401
402
    private function collectReference($reference): void
403
    {
404
        if (!($reference instanceof IEdmValidCoreModelElement) &&
405
            !in_array($reference, $this->visited) &&
406
            ($this->skipVisitation == null || !in_array($reference, $this->skipVisitation))) {
407
            $this->danglingReferences[] = $reference;
408
        }
409
    }
410
}
411