eclipxe/xmlschemavalidator
A lightweight PHP library for validating XML documents against XSD schemas using DOMDocument and libxml. It provides simple APIs for validating XML strings or files, collects detailed schema errors, supports exception-based handling, and integrates easily into Composer-based PHP projects.
eclipxe/xmlschemavalidator is a thin, exception-first wrapper around PHP's DOMDocument::schemaValidate(). It exists because raw libxml error reporting is noisy and easy to misuse — this package surfaces clean, iterable errors instead of E_WARNING soup.
Install:
composer require eclipxe/xmlschemavalidator
Requires PHP 8.0+ and ext-dom / ext-libxml.
First place to look: src/XmlSchemaValidator.php for the static constructors (fromFile, fromString, fromDomDocument) and src/Exceptions/ for the exception hierarchy. The README is short; the source is the real documentation.
First use case — validate an uploaded XML against a bundled XSD:
use XmlSchemaValidator\XmlSchemaValidator;
$validator = XmlSchemaValidator::fromFile(
storage_path('app/invoices/incoming.xml'),
resource_path('schemas/invoice-v2.xsd')
);
try {
$validator->validate();
// valid
} catch (\XmlSchemaValidator\Exceptions\XmlSchemaValidatorException $e) {
$errors = $validator->getErrors(); // array of human-readable strings
}
Where to store schemas: resources/schemas/*.xsd, referenced via resource_path(). Never via storage_path() for anything shipped with the app — deployments won't have it.
Wrapping it in a service. Don't call the validator from controllers. Bind a small service that knows the schema location:
final class XmlSchemaService
{
public function __construct(private string $schemaDir = '') {}
public function validate(string $xmlPath, string $schema): array
{
$validator = XmlSchemaValidator::fromFile(
$xmlPath,
$this->schemaDir . '/' . $schema
);
try { $validator->validate(); return []; }
catch (XmlSchemaValidatorException) { return $validator->getErrors(); }
}
}
A Form Request rule. Uploaded XML is the most common use case:
use Illuminate\Contracts\Validation\Rule;
class ValidXml implements Rule
{
public function __construct(
private string $schema,
private XmlSchemaService $service
) {}
public function passes($attribute, $value): bool
{
$path = $value instanceof UploadedFile ? $value->getRealPath() : $value;
return empty($this->service->validate($path, $this->schema));
}
public function message(): string
{
return 'The :attribute is not a valid :schema document.';
}
}
Then $request->validate(['document' => ['required', 'file', new ValidXml('invoice.xsd', app(XmlSchemaService::class))]]).
Validating strings from webhooks / queues. fromString($xml, $xsdPath) skips writing to disk. Combine with a Job for large batches — XSD validation is CPU-heavy on big documents (thousands of nodes), so offload it.
Parse only after validation. The pattern is: validate → then simplexml_load_string() / DOMDocument to extract fields. Never trust structure before validation, and never trust content after it (schema validates shape, not business rules).
Version dispatch. When you support multiple schema versions (e.g. invoice v1/v2), read the root element's version attribute first, select the matching XSD, then validate — the schema itself often can't tell you which one you meant.
ext-dom is not installed by default in many slim Docker images. Add docker-php-ext-install dom (or apt install php8.x-xml) in your Dockerfile and make sure CI checks it. composer install alone doesn't guarantee the extension is present.
XSD import/include uses relative paths resolved from the schema file's directory. If you copy schemas around or load them from a stream, imports silently fail. Always keep the schema tree intact and reference it by absolute path (resource_path() is fine).
Namespace errors are the #1 source of confusion. A root element in the wrong namespace gives you Element '{...}root': No matching global declaration available for the validation root. — this means "your XML is in the wrong namespace for this schema", not "the file is broken". Check xmlns before debugging anything else.
schemaLocation hints inside the XML are ignored — the package validates against the schema you pass, which is what you want. Don't let the XML's own hints trick you into thinking a different schema is being used.
No LIBXML_PARSEHUGE escape hatch. Documents with >10M nodes (rare) or deeply nested structures will hit libxml limits. If you need that, fall back to raw DOMDocument::schemaValidate() and set libxml_use_internal_errors(true) yourself.
The exception doesn't flatten errors. getErrors() returns an array — log it whole; don't implode() and lose the line numbers that make debugging feasible.
Cache compiled schemas where possible. For high-throughput endpoints, libxml re-parses the XSD on every validation. If the schema is static, validate a warm DOMDocument once at boot and reuse the same schema file path, or batch validations so schema parsing amortises.
Test with fixtures under tests/fixtures/. Use Storage::fake('local') in feature tests to simulate uploads, then Storage::disk('local')->put('doc.xml', $invalidFixture) — this catches the getRealPath() gotcha on UploadedFile immediately.
Do not enable external entity loading. The package doesn't surface the XXE toggle; if you ever drop to libxml directly, keep libxml_disable_entity_loader(true) (PHP < 8) or rely on PHP 8's defaults. Never validate untrusted XML with DOCTYPE loading allowed.
Extension point: the static constructors. fromDomDocument, fromSimpleXmlElement, and fromString all funnel into the same XmlSchemaValidator. If you need pre-processing (e.g. stripping a signature before validation, or wrapping a fragment), do it on a DOMDocument you build yourself, then pass it in — that's the intended seam.
How can I help you explore Laravel packages today?