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