xmlvalid / xmlvalid-symfony
Symfony bundle for the xmlvalid API: a validator constraint for XML and e-invoices (Peppol, XRechnung, Factur-X, UBL, XSD, Schematron), webhooks as events, console commands
Package info
github.com/xmlvalid/xmlvalid-symfony
Type:symfony-bundle
pkg:composer/xmlvalid/xmlvalid-symfony
Requires
- php: ^8.2
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- symfony/config: ^6.4 || ^7.4 || ^8.0
- symfony/console: ^6.4 || ^7.4 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.4 || ^8.0
- symfony/event-dispatcher: ^6.4 || ^7.4 || ^8.0
- symfony/http-client: ^6.4 || ^7.4 || ^8.0
- symfony/http-foundation: ^6.4 || ^7.4 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.4 || ^8.0
- symfony/routing: ^6.4 || ^7.4 || ^8.0
- xmlvalid/xmlvalid-php: ^1.2
Requires (Dev)
- laravel/pint: ^1.20
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5 || ^12.0
- symfony/framework-bundle: ^6.4 || ^7.4 || ^8.0
- symfony/translation: ^6.4 || ^7.4 || ^8.0
- symfony/validator: ^6.4 || ^7.4 || ^8.0
Suggests
- symfony/validator: For the #[ValidXml] constraint
Provides
None
Conflicts
None
Replaces
None
README
Symfony bundle for the xmlvalid API: validate uploaded XML and e-invoices in your forms, DTOs and messages 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
#[ValidXml]constraint, with the failing line and business rule in the message - The
Xmlvalid\Clientservice for everything else (built on xmlvalid-php) - Webhooks as events, with the signature checked
bin/console xmlvalid:validatefor deploys and CI- Requests go through Symfony's HTTP client: the profiler shows them,
MockHttpClientfakes them
Symfony 6.4, 7.4 and 8, PHP 8.2+.
Install
composer require xmlvalid/xmlvalid-symfony
Without Flex, add Xmlvalid\Symfony\XmlvalidBundle::class => ['all' => true] to
config/bundles.php. Add an API key from Dashboard → API keys
to .env.local:
XMLVALID_API_KEY=xv_…
The key is read from XMLVALID_API_KEY by default. Everything else is optional, in
config/packages/xmlvalid.yaml:
xmlvalid: api_key: '%env(XMLVALID_API_KEY)%' timeout: 900 # seconds to wait for a result in all wait: 10 # seconds the API holds a new request (0-60) retries: 3 # retries of a 429 rate_limited http_client: http_client webhook_secret: '%env(default::XMLVALID_WEBHOOK_SECRET)%'
Validate uploads
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraints as Assert; use Xmlvalid\Symfony\Validator\ValidXml; final class InvoiceUpload { #[Assert\NotNull] #[Assert\File(maxSize: '10M')] #[ValidXml(schema: 'peppol-bis-3-invoice')] public ?UploadedFile $invoice = null; }
It works the same in a form type ('constraints' => [new ValidXml(schema: 'auto')]) and on
any value: an uploaded file, any file (SplFileInfo) or XML text. An invalid invoice fails with
its first error, and a count of the rest:
This document 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).
This document has 3 more errors.
| Option | Validates against |
|---|---|
schema: 'peppol-bis-3-invoice' |
A catalog entry, with its business rules |
schema: 'auto' |
The catalog profile an e-invoice names (BT-24), for mixed invoices |
schema: 'my-12' |
One of your saved schemas |
schemaFile: 'config/schemas/order.xsd' |
An XSD, DTD or RelaxNG file of yours (relative to the project, or absolute) |
doctype: true |
The DOCTYPE the document declares |
| none | Well-formedness only |
More options: rules (your own Schematron, after the schema check: a .sch file, or
['main.sch' => 'config/rules/main.sch', 'parts/totals.sch' => 'config/rules/parts/totals.sch'] with
includes), rulesVersion: 'peppol-bis-3-ubl@3.0.21' (pins the business rules' release, or a
list of pins: a RulesVersionMismatchException is thrown when one has moved on), errors: 5
(list up to 5 errors, 1 by default), timeout: 30, and the messages
message, moreMessage, notCheckedMessage, notADocumentMessage. Violation codes are the
constants ValidXml::INVALID_ERROR, MORE_ERRORS_ERROR, NOT_CHECKED_ERROR and
NOT_A_DOCUMENT_ERROR. Empty values pass: add NotNull / NotBlank for required fields.
To keep the validation's id or link its report, ValidXmlValidator::lastValidation() (autowire
Xmlvalid\Symfony\Validator\ValidXmlValidator) returns the last one run.
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 violations.
The client
Autowire Xmlvalid\Client for everything the PHP client
does:
use Xmlvalid\Client; use Xmlvalid\Document; public function __construct(private Client $xmlvalid) {} $validation = $this->xmlvalid->validate( Document::file($this->projectDir . '/var/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] …" } $this->xmlvalid->submit(Document::url('https://example.com/feed.xml'), schema: 'auto'); // queue it, answer at once $this->xmlvalid->waitFor($id); $this->xmlvalid->report($id); // the HTML report $this->xmlvalid->share([$id]); // a public link for 30 days (Pro) $this->xmlvalid->usage(); // this month's validations $this->xmlvalid->schemas(); // your saved schemas, then the catalog
Large files are streamed, not loaded into memory. For a long validation in a Messenger handler,
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. Import the route:
# config/routes/xmlvalid.yaml xmlvalid: resource: '@XmlvalidBundle/config/routes.php' prefix: /xmlvalid
Add an endpoint under Dashboard → Webhooks with the URL
https://your-app.com/xmlvalid/webhook, and set webhook_secret to its signing secret. The
controller 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 Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Xmlvalid\Symfony\Event\ValidationCompleted; #[AsEventListener] final class MarkInvoiceValidated { public function __invoke(ValidationCompleted $completed): void { // $completed->validation->id, ->isValid(), ->errors() } }
A retried delivery has the same $event->id. If your firewall covers /xmlvalid, let the
route through without authentication: the signature is the check.
Console
bin/console xmlvalid:validate var/export/*.xml --schema=peppol-bis-3-invoice
bin/console xmlvalid:validate https://example.com/feed.xml --schema-file=config/feed.xsd --warnings
bin/console 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
Point the bundle at a MockHttpClient in the test environment and answer with
XmlvalidMockResponse:
# config/packages/test/xmlvalid.yaml xmlvalid: api_key: xv_test http_client: xmlvalid.mock_client services: xmlvalid.mock_client: class: Symfony\Component\HttpClient\MockHttpClient public: true
use Xmlvalid\Symfony\Testing\XmlvalidMockResponse; static::getContainer()->get('xmlvalid.mock_client')->setResponseFactory([ XmlvalidMockResponse::invalid(['line' => 139, 'rule' => 'BR-CO-16', 'message' => 'Amount due for payment …']), ]);
XmlvalidMockResponse::valid() answers with a passing validation.
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 |
Development
composer install composer test # PHPUnit with a test kernel composer analyse # PHPStan, level max composer lint # Pint (PER)
License
MIT