Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.78% covered (warning)
77.78%
56 / 72
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Validator
77.78% covered (warning)
77.78%
56 / 72
50.00% covered (danger)
50.00%
4 / 8
35.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 loadSchemas
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
5.07
 isValid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrors
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 extractErrors
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 getCustomMessage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getOriginPointer
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getTranslatedPointer
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BO\Zmsentities\Schema;
4
5use Opis\JsonSchema\{Validator as OpisValidator, ValidationResult, Schema as OpisSchema};
6use Opis\JsonSchema\Resolvers\FormatResolver;
7use Opis\JsonSchema\Errors\ValidationError as OpisValidationError;
8
9class Validator
10{
11    protected Schema $schemaObject;
12    protected mixed $schemaData;
13    protected mixed $locale;
14    protected OpisValidator $validator;
15    protected ValidationResult $validationResult;
16
17    private static bool $schemasLoaded = false;
18    private static ?OpisValidator $validatorInstance = null;
19
20    public function __construct(mixed $data, Schema $schemaObject, mixed $locale)
21    {
22        $this->schemaData = $data;
23        $this->schemaObject = $schemaObject;
24        $this->locale = $locale;
25
26        // Use static validator instance if available
27        if (self::$validatorInstance === null) {
28            self::$validatorInstance = new OpisValidator();
29            $formats = self::$validatorInstance->parser()->getFormatResolver();
30            if ($formats !== null) {
31                $formats->registerCallable("array", "sameValues", function (array $data): bool {
32                    return count($data) === 2 && $data[0] === $data[1];
33                });
34            }
35        }
36        $this->validator = self::$validatorInstance;
37
38        // Load schemas only once for each process
39        if (!self::$schemasLoaded) {
40            self::$schemasLoaded = $this->loadSchemas();
41        }
42
43        $schemaJson = json_decode((string) json_encode($schemaObject->toJsonObject()));
44        $data = json_decode((string) json_encode($data));
45        $this->validationResult = $this->validator->validate($data, $schemaJson);
46    }
47
48    private function loadSchemas(): bool
49    {
50        $schemaDir = realpath(dirname(__FILE__) . '/../../../schema');
51        $schemaPath = ($schemaDir !== false ? $schemaDir : '') . '/';
52        $resolver = $this->validator->resolver();
53        if ($resolver === null) {
54            return false;
55        }
56        $resolver->registerPrefix('schema://', $schemaPath);
57        $schemaFiles = glob($schemaPath . '*.json');
58        if ($schemaFiles === false) {
59            return false;
60        }
61
62        // TODO: Implement persistent caching for schema file reads to reduce redundant disk I/O and improve application performance. Not just for each process.
63
64        foreach ($schemaFiles as $schemaFile) {
65            $schemaContent = file_get_contents($schemaFile);
66            $schemaName = 'schema://' . basename($schemaFile);
67            $resolver->registerRaw($schemaContent, $schemaName);
68        }
69        return true;
70    }
71
72    public function isValid(): mixed
73    {
74        return $this->validationResult->isValid();
75    }
76
77    public function getErrors(): mixed
78    {
79        if ($this->validationResult->isValid()) {
80            return [];
81        }
82
83        $errorsReducedList = [];
84        $error = $this->validationResult->error();
85
86        if ($error) {
87            $errorsReducedList = $this->extractErrors($error);
88        }
89
90        return $errorsReducedList;
91    }
92
93    private function extractErrors(OpisValidationError $error): array
94    {
95        $errors = [];
96
97        $errors[] = new OpisValidationError(
98            $error->keyword(),
99            $error->schema(),
100            $error->data(),
101            $this->getCustomMessage($error),
102            $error->args(),
103            []
104        );
105
106        foreach ($error->subErrors() as $subError) {
107            if ($subError instanceof OpisValidationError) {
108                $errors = array_merge($errors, $this->extractErrors($subError));
109            }
110        }
111
112        return $errors;
113    }
114
115    public function getCustomMessage(OpisValidationError $error): mixed
116    {
117        $schemaData = $error->schema()->info()->data();
118        if (is_object($schemaData)) {
119            $schemaData = (array) $schemaData;
120        }
121        $property = new \BO\Zmsentities\Helper\Property($schemaData);
122
123        if (
124            isset($property['x-locale'][$this->locale]->messages[$error->keyword()])
125            && $property['x-locale'][$this->locale]->messages[$error->keyword()] !== null
126        ) {
127            return $property['x-locale'][$this->locale]->messages[$error->keyword()]->get();
128        }
129
130        return $error->message();
131    }
132
133    public static function getOriginPointer(OpisValidationError $error): string
134    {
135        $dataInfo = $error->data();
136
137        if (empty($dataInfo->path())) {
138            return '/';
139        }
140
141        $pointer = '/' . implode('/', array_map('strval', $dataInfo->path()));
142
143        return $pointer;
144    }
145
146    public function getTranslatedPointer(OpisValidationError $error): mixed
147    {
148        $schemaData = $error->schema()->info()->data();
149        if (is_object($schemaData)) {
150            $schemaData = (array) $schemaData;
151        }
152        $property = new \BO\Zmsentities\Helper\Property($schemaData);
153
154        if (
155            isset($property['x-locale'][$this->locale]->pointer)
156            && $property['x-locale'][$this->locale]->pointer !== null
157        ) {
158            return $property['x-locale'][$this->locale]->pointer->get(self::getOriginPointer($error));
159        }
160
161        return self::getOriginPointer($error);
162    }
163}