Passed
Push — master ( add283...7568bf )
by Carlos C
02:10 queued 11s
created

SchemaValidator::getLastError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Eclipxe\XmlSchemaValidator;
6
7
use DOMAttr;
8
use DOMDocument;
9
use DOMNodeList;
10
use DOMXPath;
11
use Eclipxe\XmlSchemaValidator\Exceptions\SchemaLocationPartsNotEvenException;
12
use Eclipxe\XmlSchemaValidator\Exceptions\ValidationFailException;
13
use Eclipxe\XmlSchemaValidator\Exceptions\XmlContentIsEmptyException;
14
use Eclipxe\XmlSchemaValidator\Exceptions\XmlContentIsInvalidException;
15
use Eclipxe\XmlSchemaValidator\Exceptions\XmlSchemaValidatorException;
16
use Eclipxe\XmlSchemaValidator\Internal\LibXmlException;
17
18
/**
19
 * This class is an XML schema validator
20
 * It is needed because some XML can contain more than one external schema and DOM library fails to load it.
21
 */
22
class SchemaValidator
23
{
24
    /** @var DOMDocument */
25
    private $document;
26
27
    /** @var string */
28
    private $lastError = '';
29
30
    /**
31
     * SchemaValidator constructor.
32
     *
33
     * @param DOMDocument $document
34
     */
35 13
    public function __construct(DOMDocument $document)
36
    {
37 13
        $this->document = $document;
38 13
    }
39
40
    /**
41
     * Create a SchemaValidator instance based on a XML string
42
     *
43
     * @param string $contents
44
     * @return self
45
     * @throws XmlContentIsEmptyException when the xml contents is an empty string
46
     * @throws XmlContentIsInvalidException when the xml contents cannot be loaded
47
     */
48 14
    public static function createFromString(string $contents): self
49
    {
50
        // do not allow empty string
51 14
        if ('' === $contents) {
52 1
            throw XmlContentIsEmptyException::create();
53
        }
54
55
        // create and load contents throwing specific exception
56
        try {
57
            /** @var DOMDocument $document */
58 13
            $document = LibXmlException::useInternalErrors(
59
                function () use ($contents): DOMDocument {
60 13
                    $document = new DOMDocument();
61 13
                    $document->loadXML($contents);
62 13
                    return $document;
63 13
                }
64
            );
65 1
        } catch (LibXmlException $exception) {
66 1
            throw XmlContentIsInvalidException::create($exception);
67
        }
68
69 12
        return new self($document);
70
    }
71
72
    /**
73
     * Validate the content by:
74
     * - Create the Schemas collection from the document
75
     * - Validate using validateWithSchemas
76
     * - Populate the error property
77
     *
78
     * @return bool
79
     * @see validateWithSchemas
80
     */
81 9
    public function validate(): bool
82
    {
83 9
        $this->lastError = '';
84
        try {
85
            // create the schemas collection
86 9
            $schemas = $this->buildSchemas();
87
            // validate the document using the schema collection
88 8
            $this->validateWithSchemas($schemas);
89 4
        } catch (XmlSchemaValidatorException $ex) {
90 4
            $this->lastError = $ex->getMessage();
91 4
            return false;
92
        }
93 5
        return true;
94
    }
95
96
    /**
97
     * Retrieve the last error message captured on the last validate operation
98
     *
99
     * @return string
100
     */
101 5
    public function getLastError(): string
102
    {
103 5
        return $this->lastError;
104
    }
105
106
    /**
107
     * Validate against a list of schemas (if any)
108
     *
109
     * @param Schemas $schemas
110
     * @return void
111
     *
112
     * @throws ValidationFailException when schema validation fails
113
     */
114 11
    public function validateWithSchemas(Schemas $schemas): void
115
    {
116
        // early exit, do not validate if schemas collection is empty
117 11
        if (0 === $schemas->count()) {
118 2
            return;
119
        }
120
121
        // build the unique importing schema
122 9
        $xsd = $schemas->getImporterXsd();
123
124
        // validate and trap LibXmlException
125
        try {
126
            LibXmlException::useInternalErrors(function () use ($xsd): void {
127 9
                $this->document->schemaValidateSource($xsd);
128 9
            });
129 4
        } catch (LibXmlException $exception) {
130 4
            throw ValidationFailException::create($exception);
131
        }
132 5
    }
133
134
    /**
135
     * Retrieve a list of namespaces based on the schemaLocation attributes
136
     *
137
     * @return Schemas
138
     * @throws SchemaLocationPartsNotEvenException when the schemaLocation attribute does not have even parts
139
     */
140 9
    public function buildSchemas(): Schemas
141
    {
142 9
        $schemas = new Schemas();
143 9
        $xpath = new DOMXPath($this->document);
144
145
        // get the http://www.w3.org/2001/XMLSchema-instance namespace (it could not be 'xsi')
146 9
        $xsi = $this->document->lookupPrefix('http://www.w3.org/2001/XMLSchema-instance');
147 9
        if (! $xsi) { // the namespace is not registered, no need to continue
148 1
            return $schemas;
149
        }
150
151
        // get all the xsi:schemaLocation attributes in the document
152
        /** @var DOMNodeList<DOMAttr> $schemasList */
153 8
        $schemasList = $xpath->query("//@$xsi:schemaLocation") ?: new DOMNodeList();
154
155
        // process every schemaLocation and import them into schemas
156 8
        foreach ($schemasList as $node) {
157 7
            $schemas->import($this->buildSchemasFromSchemaLocationValue($node->nodeValue));
158
        }
159
160 7
        return $schemas;
161
    }
162
163
    /**
164
     * Create a schemas collection from the content of a schema location
165
     *
166
     * @param string $content
167
     * @return Schemas
168
     * @throws SchemaLocationPartsNotEvenException when the schemaLocation attribute does not have even parts
169
     */
170 7
    public function buildSchemasFromSchemaLocationValue(string $content): Schemas
171
    {
172
        // get parts without inner spaces
173 7
        $parts = preg_split('/\s+/', $content) ?: [];
174 7
        $partsCount = count($parts);
175
176
        // check that the list count is an even number
177 7
        if (0 !== $partsCount % 2) {
178 1
            throw SchemaLocationPartsNotEvenException::create($parts);
179
        }
180
181
        // insert the uris pairs into the schemas
182 6
        $schemas = new Schemas();
183 6
        for ($k = 0; $k < $partsCount; $k = $k + 2) {
184 6
            $schemas->create($parts[$k], $parts[$k + 1]);
185
        }
186 6
        return $schemas;
187
    }
188
}
189