Search by

xmlvalid / xmlvalid-symfony

alexbabintsev

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

Homepage

Documentation

Type:symfony-bundle

pkg:composer/xmlvalid/xmlvalid-symfony

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.2.0 2026-10-19 19:13 UTC

This package is auto-updated.

Last update: 2026-10-19 19:20:04 UTC


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\Client service for everything else (built on xmlvalid-php)
  • Webhooks as events, with the signature checked
  • bin/console xmlvalid:validate for deploys and CI
  • Requests go through Symfony's HTTP client: the profiler shows them, MockHttpClient fakes 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