xmlvalid/xmlvalid-laravel
A Laravel integration for XML validation that provides validation rules, a facade, and configuration helpers for checking XML documents against XSD schemas. It simplifies adding schema-aware XML checks to uploads, APIs, and form requests in Laravel applications.
xmlvalid/laravel wraps the standalone xmlvalid/xmlvalid-php engine in a Laravel service provider: a facade, a validation rule, a publishable config file, and a testing fake. It is for validating XML documents against a schema (XSD) and, in v1.2, for surfacing structured error metadata returned by the API.
Start here, in this order:
composer require xmlvalid/laravel — then confirm xmlvalid/xmlvalid-php resolves to ^1.2. The v1.2.0 release explicitly requires it; older engine versions cannot hydrate path, sha256, or engine, so a stale lock file will silently give you nulls.php artisan vendor:publish --tag=xmlvalid-config and read config/xmlvalid.php — that is where the endpoint, credentials, and default schema/engine live.First real use case: an uploaded XML document (invoice, e-invoice, UBL, import feed) validated in a Form Request via the rule, with the parsed error list returned to the user.
$request->validate([
'document' => ['required', 'file', 'mimes:xml', new XmlValid(schema: 'invoice-v3')],
]);
Validation rule in Form Requests. Use the rule for the gate, but do not throw away the result — capture it if you need error detail:
$result = Xmlvalid::validate($path, schema: 'invoice-v3');
if (! $result->valid()) {
// $result->errors() — each error now has ->path, ->message, ->code
}
Path-aware error mapping (v1.2). The headline change is path on each error: the location in the document where the business rule fired. Use it to map failures back to form fields or to a line in an editor instead of showing a flat list. Treat it as an opaque locator string — index into it, do not parse it as XPath unless you have verified the engine emits XPath for your schema.
Deterministic caching and dedupe via sha256. The result now carries a content hash. This is the cleanest dedupe key you will get:
$result = Cache::rememberForever("xmlvalid:{$hash}", fn () => Xmlvalid::validate($path, $schema));
Hash first (cheap), then look up, then validate. Include the schema identifier and engine in the cache key — see gotchas.
Audit logging with engine. Persist sha256 and engine alongside each validation outcome. When a document that passed last month fails today, engine is the first thing you want to diff.
Testing with XmlvalidFake. The fake now answers the way the API does, so assertions written against real payloads port over directly:
Xmlvalid::fake([
'invoice-v3' => Xmlvalid::result(valid: false, errors: [
['path' => 'Invoice.Lines[0].Tax', 'message' => 'Tax rate not permitted', 'code' => 'TAX_400'],
]),
]);
// ...
Xmlvalid::assertValidated(fn ($doc) => $doc->schema === 'invoice-v3');
$this->assertSame('TAX_400', $result->errors()[0]->code);
Job pipeline. Validate in a queued job after storage, write the result row (hash, engine, errors as JSON), and let the UI read the stored record rather than re-validating on page load.
The v1.2.0 bump is a two-part upgrade. XmlvalidFake changed and the engine floor moved. Bumping only the Laravel package gives you a fake that emits path/sha256/engine while the underlying models drop them. Bump xmlvalid/xmlvalid-php to ^1.2 in the same commit and run composer update xmlvalid/* together.
Fake/live drift was the actual bug being fixed. If you had assertions that compared the fake's response array to a literal ['message' => ..., 'code' => ...], they will now fail on the extra keys. Prefer asserting on individual properties (->code, ->path) over whole-array equality.
sha256 is only as stable as the bytes. A trailing newline in a test fixture, a BOM, or a different encoding convention changes the hash. If you key caches on it, hash the exact stored file, not a re-serialized copy. Fixture files in tests/ are a common source of "works locally, cold cache in CI."
Do not let engine leak into cache keys implicitly, but do key on it. Two engines can produce the same sha256 and different verdicts. sha256 + schema + engine is the safe key; sha256 alone is a subtle correctness bug that appears only after an engine rollout.
path may be absent. Errors raised before the document is parsed (schema not found, malformed XML) have no location. Type the property as nullable and render accordingly rather than assuming a path exists on every item.
Debugging. Dump the raw transport payload once (Http::preventStrayRequests() + a recorded fixture) before trusting the fake. The fake validates your assumptions, not the API's behaviour, and it will happily agree with a wrong schema name.
Extension points. The fake is swappable and the config exposes a default engine — override the engine per call when you are migrating schemas. If you need custom transport behaviour, bind a client in the container rather than forking the facade.
Test the failing branch with a real path. Assertions on ->path are the highest-value new capability; make at least one integration test assert on a location inside a repeated element (Lines[2]), because index handling in paths is exactly what naive string matching gets wrong.
How can I help you explore Laravel packages today?