[php-src] PR #25437: ext/dom: Add Dom\XMLSchema to reuse a compiled XML Schema.
| From: | ArtUkrainskiy | Date: | Fri, 13 Nov 2026 06:18:33 +0000 |
| Subject: | [php-src] PR #25437: ext/dom: Add Dom\XMLSchema to reuse a compiled XML Schema. | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-40611@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/25437
Author: ArtUkrainskiy
This is a proof of concept. I'd like feedback on the API before writing an RFC.
schemaValidate() compiles the XSD on every call, validates, and frees the schema. For a
real schema with imports, that compile is almost the whole cost. Apps that check many invoices or
orders against one schema pay it on every document.
This adds Dom\XMLSchema, which owns the compiled xmlSchemaPtr:
```php
$schema = Dom\XMLSchema::createFromFile('/app/schemas/invoice.xsd'); // once per worker
$schema->validate($doc); // Dom\XMLDocument|DOMDocument, fresh validation context per call
```
schemaValidate() now uses the same compile and validate helpers. Its behaviour
doesn't change.
The test schema is UBL 2.1 Invoice, an OASIS standard for e-invoices. Its main file imports shared
type definitions, so compiling it reads 14 XSD files, 2.8 MB in total. The schema and the 1.2 KB and
20 KB invoices are the official ones from https://docs.oasis-open.org/ubl/os-UBL-2.1/UBL-2.1.zip.
The larger invoices are the 20 KB example with its 5 item lines copied over and over until the
invoice has 100, 1,000 or 10,000 lines. They still pass the schema.
Time to load one document and validate it:
| document | load + schemaValidate() | load + Dom\XMLSchema::validate() |
|---|---:|---:|
| UBL Invoice, 1.2 KB | 50.8 ms | 0.034 ms |
| UBL Invoice, 20 KB | 51.0 ms | 0.31 ms |
| UBL Invoice, 200 KB | 54.2 ms | 3.7 ms |
| UBL Invoice, 1.9 MB | 88.9 ms | 37.2 ms |
| UBL Invoice, 18.7 MB | 421 ms | 367 ms |
Execution time, median of 9 alternating runs. Under callgrind one 20 KB validation is 433M
instructions with schemaValidate() and 1.07M with validate().
schemaValidate() itself stays within noise after the refactor, at −0.06%
instructions.
Tests for ext/dom, ext/libxml and ext/xmlreader pass, and valgrind is clean on the new tests.
A few things I'm not sure about.
- On a broken schema it emits libxml warnings and then throws. Should it throw one exception that
carries the errors instead?
- createFromString() has no base URI, so relative imports resolve against the CWD, same
as schemaValidateSource(). Should it take a ?string $baseUri?
- Should it accept DOMDocument, or only Dom\XMLDocument?
RelaxNG and XMLReader::setSchema() could follow the same pattern later.
I haven't tried other compilers, other real-world schemas or an ASan build yet.