xmlvalid / xmlvalid-laravel
Laravel integration for the xmlvalid API: a validation rule for XML and e-invoices (Peppol, XRechnung, Factur-X, UBL, XSD, Schematron), webhooks as events, Artisan commands
Requires
- php: ^8.2
- illuminate/console: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/routing: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- illuminate/validation: ^12.0 || ^13.0
- xmlvalid/xmlvalid-php: ^1.2
Requires (Dev)
- guzzlehttp/guzzle: ^7.9
- larastan/larastan: ^3.0
- laravel/pint: ^1.20
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel integration for the xmlvalid API: validate uploaded XML and e-invoices in your forms and jobs against XSD, DTD, RelaxNG and Schematron business rules. Peppol BIS, XRechnung, Factur-X / ZUGFeRD, EN 16931, UBL, CII and national rules (France, Italy, Croatia, Romania and more) are in the schema catalog; your own schemas work too.
- A
ValidXmlvalidation rule, with the failing line and business rule in the message - The
Xmlvalidfacade for everything else (built on xmlvalid-php) - Webhooks as Laravel events, with the signature checked
php artisan xmlvalid:validatefor deploys and CI- Requests go through Laravel's HTTP client:
Http::fake()works in your tests
Laravel 12 and 13, PHP 8.2+.
Install
composer require xmlvalid/xmlvalid-laravel
Add an API key from Dashboard → API keys to .env:
XMLVALID_API_KEY=xv_…
To change the defaults: php artisan vendor:publish --tag=xmlvalid-config.
Validate uploads
use Xmlvalid\Laravel\Rules\ValidXml; $request->validate([ 'invoice' => ['required', 'file', 'max:10240', ValidXml::schema('peppol-bis-3-invoice')], ]);
An invalid invoice fails with its first error, and a count of the rest:
The invoice is not valid: Line 139: [BR-CO-16] Amount due for payment (BT-115) = Invoice total amount with VAT (BT-112) - Paid amount (BT-113) + Rounding amount (BT-114).
The invoice has 3 more errors.
| Rule | Validates against |
|---|---|
ValidXml::schema('peppol-bis-3-invoice') |
A catalog entry, with its business rules |
ValidXml::schema('auto') |
The catalog profile an e-invoice names (BT-24), for mixed invoices |
ValidXml::schema('my-12') |
One of your saved schemas |
ValidXml::schemaFile(resource_path('schemas/order.xsd')) |
An XSD, DTD or RelaxNG file of yours |
ValidXml::doctype() |
The DOCTYPE the document declares |
new ValidXml() |
Well-formedness only |
Options:
ValidXml::schema('xrechnung-3-ubl') ->withRules(resource_path('rules/our-rules.sch')) // your own Schematron, after the schema check ->rulesVersion('xrechnung-3-ubl@2.4.0') // throw RulesVersionMismatchException when the rules move on ->errors(5) // list up to 5 errors (1 by default) ->timeout(30); // seconds to wait (config: xmlvalid.timeout)
The value can be an uploaded file, any file (SplFileInfo) or XML text. The validation itself
is kept, to store its id or link its report:
$rule = ValidXml::schema('auto'); $request->validate(['invoice' => ['required', 'file', $rule]]); $invoice->update(['validation_id' => $rule->validation()->id]);
Errors of the API itself (a revoked key, an unknown schema, the monthly quota used up) are thrown as exceptions (see below), not reported as an invalid field.
Messages are in lang/vendor/xmlvalid after php artisan vendor:publish --tag=xmlvalid-lang.
The facade
Everything the PHP client does:
use Xmlvalid\Document; use Xmlvalid\Laravel\Facades\Xmlvalid; $validation = Xmlvalid::validate( Document::file(storage_path('app/invoices/INV-12.xml')), schema: 'peppol-bis-3-invoice', ); $validation->isValid(); foreach ($validation->errors() as $error) { $error->line; $error->rule; $error->message; // (string) $error: "Line 139: [BR-CO-16] …" } Xmlvalid::submit(Document::url('https://example.com/feed.xml'), schema: 'auto'); // queue it, answer at once Xmlvalid::waitFor($id); Xmlvalid::report($id); // the HTML report Xmlvalid::share([$id]); // a public link for 30 days (Pro) Xmlvalid::usage(); // this month's validations Xmlvalid::schemas(); // your saved schemas, then the catalog
Or inject Xmlvalid\Client. Large files are streamed, not loaded into memory. For a long
validation in a queued job, submit() and a webhook save keeping a worker busy.
Webhooks
On Pro, xmlvalid can call your app when a validation finishes or a URL monitor changes state.
Add an endpoint under Dashboard → Webhooks with the URL
https://your-app.com/xmlvalid/webhook, and its signing secret to .env:
XMLVALID_WEBHOOK_SECRET=whsec_…
The route is registered once the secret is set (outside the web group, so without CSRF). It
checks the signature and dispatches events:
| Event | When | Properties |
|---|---|---|
ValidationCompleted |
An API validation finished | $validation, $event |
MonitorFailed |
A monitored URL started failing | $monitor, $check, $event |
MonitorRecovered |
A monitored URL passes again | $monitor, $check, $event |
WebhookReceived |
Any of them, and test pings | $event |
use Illuminate\Support\Facades\Event; use Xmlvalid\Laravel\Events\ValidationCompleted; Event::listen(function (ValidationCompleted $completed) { Invoice::where('validation_id', $completed->validation->id) ->update(['valid' => $completed->validation->isValid()]); });
A retried delivery has the same $event->id. The path, the middleware and the accepted clock
skew are in the config.
Artisan
php artisan xmlvalid:validate storage/app/export/*.xml --schema=peppol-bis-3-invoice
php artisan xmlvalid:validate https://example.com/feed.xml --schema-file=resources/feed.xsd --warnings
php artisan xmlvalid:schemas xrechnung
xmlvalid:validate lists the errors and exits with 1 when a document is not valid. It names the
business rule releases used (Rules: Peppol BIS Billing 3.0 rules 3.0.21); pin them with
--rules-version=peppol-bis-3-ubl@3.0.21 (repeatable) to exit with 2 when one has moved on.
xmlvalid:schemas lists each schema's rule sets as pins.
Testing
Requests go through Laravel's HTTP client, so fake them. XmlvalidFake builds the answers:
use Illuminate\Support\Facades\Http; use Xmlvalid\Laravel\Testing\XmlvalidFake; Http::fake(['xmlvalid.com/*' => XmlvalidFake::valid()]); Http::fake(['xmlvalid.com/*' => XmlvalidFake::invalid( ['line' => 139, 'rule' => 'BR-CO-16', 'message' => 'Amount due for payment …'], )]);
Errors
Every exception extends Xmlvalid\Exception\XmlvalidException:
| Exception | When |
|---|---|
AuthenticationException |
401: no key, a revoked key |
PlanLimitException |
403: not in your plan |
NotFoundException |
404: no such validation, or it expired (24 hours) |
RulesVersionMismatchException |
409: a pinned rule set runs another release; $ruleSet, $pinned, $current |
DocumentTooLargeException |
413: over your plan's size limit |
InvalidRequestException |
422: unknown schema, a bad URL, … |
QuotaExceededException |
429: this month's validations are used up |
RateLimitedException |
429: still limited after the retries |
ServerException |
5xx |
TransportException |
xmlvalid could not be reached |
TimeoutException |
Still running after the timeout; it carries the validation |
Configuration
XMLVALID_API_KEY=xv_… XMLVALID_TIMEOUT=900 # seconds to wait for a result in all XMLVALID_WAIT=10 # seconds the API holds a new request (0-60) XMLVALID_RETRIES=3 # retries of a 429 rate_limited XMLVALID_WEBHOOK_SECRET=whsec_… XMLVALID_WEBHOOK_PATH=xmlvalid/webhook
Development
composer install composer test # PHPUnit with Orchestra Testbench composer analyse # PHPStan (Larastan), level max composer lint # Pint (PER)
License
MIT