Schematron in PHP – Ohne Saxon, ohne XSLT 2.0, ohne Schmerzen

Für ein eigenes Projekt habe ich das Repository mmnewmedia/schematron-parser entwickelt. Hierbei handelt es sich um einen Parser, der Schematron-Regeln in PHP-Klassen übersetzt, die ohne weitere Library, wie z.B. Saxon, zum Validieren von komplexen XML Strukturen genutzt werden können.

Wie Schematron-Regeln in native PHP-Klassen übersetzt werden

Schematron ist ein mächtiges Regelwerkzeug für die Validierung von XML-Strukturen – aber für PHP-Entwickler oft ein rotes Tuch. Der Grund ist simpel: Schematron basiert auf XPath 2.0, und PHP unterstützt nur XPath 1.0. Wer ernsthaft Schematron validieren will, landet schnell bei Java-Abhängigkeiten wie Saxon, XSLT-Transformationen oder externen Validatoren.

Genau hier setzt das Open-Source-Projekt mmnewmedia/schematron-parser an: Ein Parser, der Schematron-Dateien automatisch in ausführbare PHP-Klassen übersetzt – ganz ohne Saxon, ohne Java, ohne XSLT 2.0.

Das Ergebnis: Eine vollständig native PHP-Validierungsengine, die Schematron-Regeln direkt ausführt.

Das Problem: XPath 2.0 in PHP existiert nicht

Schematron nutzt XPath 2.0 intensiv:

  • distinct-values()
  • matches()
  • tokenize()
  • avg()
  • sum()
  • for-each-group()
  • komplexe logische Ausdrücke
  • Sequenzen, Typkonvertierungen, String-Funktionen

PHPs DOMXPath kann davon … nichts.
Nur XPath 1.0.

Das führt zu einem Dilemma: Schematron ist Standard. PHP kann den Standard nicht ausführen.

Bisherige Lösungen:

  • Java-Validator über CLI aufrufen
  • Saxon HE/PE/EE einbinden
  • XSLT 2.0 Transformationen nutzen
  • externe Webservices
  • oder: Schematron komplett ignorieren

Für produktive Systeme ist das alles unbefriedigend.

Die Idee: Schematron → PHP Compiler

Der Schematron-Parser löst das Problem nicht durch Emulation von XPath 2.0, sondern durch Übersetzung.

Der Ansatz

  1. Schematron-Datei einlesen
  2. Patterns, Rules und Assertions extrahieren
  3. XPath-Tests analysieren
  4. XPath 2.0 Funktionen erkennen
  5. Equivalenten PHP-Code generieren
  6. Jede Regel wird zu einer eigenen PHP-Klasse

Damit entsteht ein regelbasierter Validator, der exakt das tut, was Schematron verlangt – aber vollständig in PHP.

Beispiel: Eine Schematron Regel

Schematron:

<rule context="ram:TaxSubtotal">
  <assert test="ram:CategoryCode = 'S'">Category must be S</assert>
</rule>

Generierter PHP-Code:

foreach ($xpath->query('//ram:TaxSubtotal') as $ctx) {
    $code = $xpath->evaluate('string(ram:CategoryCode)', $ctx);

    if ($code !== 'S') {
        $errors[] = "Category must be S";
    }
}

Die Logik bleibt identisch – nur die Ausführung erfolgt nativ in PHP.

Beispiel: XPath 2.0 → PHP

Schematron:

<assert test="count(distinct-values(ram:ApplicableTaxRate)) = count(ram:TaxCategory)">
  BR-S-08 violated
</assert>

Generierter PHP-Code:

$rates = [];
foreach ($xpath->query('ram:ApplicableTaxRate', $ctx) as $node) {
    $rates[] = floatval($node->textContent);
}

$categories = iterator_to_array($xpath->query('ram:TaxCategory', $ctx));

if (count(array_unique($rates)) !== count($categories)) {
    $errors[] = "BR-S-08 violated";
}

Der Parser erkennt:

  • distinct-values() → array_unique()
  • count() → count()
  • Node-Listen → Arrays
  • numerische Typen → floatval()

Damit wird XPath 2.0 elegant in PHP übersetzt.

Unterstützte XPath 2.0 Funktionen

Der Parser deckt bereits einen großen Teil der realen Schematron-Praxis ab:

XPath 2.0 FunktionPHP-Äquivalent
abs()abs()
avg()Anonyme PHP Funktion, die alle vorhandenen Node Values summiert und dann durch die Anzahl der vorhandenen Einträge der Node Liste teilt.
boolean()PHP Bedingung, die den übergebenen Wert wandelt und auf true oder false prüft
ceiling()ceil()
concat()PHP Konkatenierung mittels .
contains()str_contains()
count()count()
distinct-values()array_unique()
empty()DOMNodeList->length, count(), instanceof() und Prüfung auf Leere Strings
ends-with()str_ends_with()
exists()DOMNodeList->length, count(), instanceof() und Prüfung auf Leere Strings
false()false
floor()floor()
last()count()
local-name()Anonyme PHP Funktion, die den lokalen Namen eines Elements DOM ELements liefert.
lower-case()mb_strtolower()
matches()preg_match()
max()max()
min()min()
name()Anonyme PHP Funktion, die den Namen einer DOMNode Instanz oder einen leeren String liefert.
normalize-space()preg_replace() als Funktion zum Ersetzen mehrfach aufeinander folgender Whitespaces mit einem einzelnen Whitespace.
not()! (Not-Operator)
number()floatval()
position()X-Path 1.0 Funktion
($xpath->evaluate('position', $ctx))
replace()preg_replace()
round()round()
starts-with()strncmp()
string-length()mb_strlen()
substring-after()Anonyme PHP Funktion, die mittels substr() den Teilstring ab dem ersten Auftreten des übergebenen Vergleichsstrings bis zum Ende des zu durchsuchenden Strings zurück gibt.
substring-before()Anonyme PHP Funktion, die mittels substr() den Teilstring ab Beginn des zu durchsuchenden Strings bis zum Auftreten des übergebenen Vergleichsstrings.
substring()substr()
sum()Summierungslogik in PHP
tokenize()preg_split()
true()true
upper-case()mb_strtoupper()

Damit lassen sich z.B. die EN16931-Regeln problemlos abbilden – inklusive X-Rechnung.

Architektur: Wie der Parser funktioniert

Der Schematron-Parser besteht aus drei Kernkomponenten:

1. Schematron Parser

Extrahiert:

  • Patterns
  • Rules
  • Contexts
  • Assertions
  • Messages

2. XPath-Compiler

Erkennt:

  • Funktionen
  • Operatoren
  • Node-Selektoren
  • Sequenzen
  • numerische und string-basierte Ausdrücke

Und erzeugt daraus ausführbaren PHP-Code.

3. Rule Engine

Jede Regel wird zu einer Klasse:

<?php

declare(strict_types=1);

namespace MMNewmedia\Schematron\Rules\Cii;

use DOMNode;
use DOMXPath;

final class BR01Rule
{
    public static function validate(DOMXPath $xpath, DOMNode $context): bool
    {
        return (
            (
                trim(
                    preg_replace('/\s+/', ' ', $xpath->evaluate("string(rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)", $context))
                )
            ) != ('')
        ) !== false;
    }

    public static function message(): string
    {
        return "[BR-01]-An Invoice shall have a Specification identifier (BT-24).";
    }

    public static function context(DOMXPath $xpath, DOMNode $context): iterable
    {
        return ($xpath->evaluate("/rsm:CrossIndustryInvoice", $context));
    }
}

Ein Validator orchestriert alle Regeln:

$validator = new SchematronValidator();
/** var bool $result */
$result = $validator->validate($xml);

Warum das wichtig ist

  • Keine Java-Abhängigkeiten
  • Keine Saxon-Lizenz
  • Keine XSLT 2.0 Transformation
  • Vollständig native PHP-Validierung
  • Ideal für EN16931 / XRechnung / UBL / Custom Schematron
  • Deployment-freundlich (Docker, Shared Hosting, CI / CD)
  • Performant und erweiterbar

Für PHP-Teams, die XML-Validierung ernst nehmen müssen, ist das ein echter Gamechanger.

Repository und Einstieg

Das Projekt ist Open Source und auf GitHub verfügbar: https://github.com/ezkimo/schematron-parser

Der Einstieg ist einfach:

composer require mmnewmedia/schematron-parser

Erstellen von Rule-Klassen über CLI:

php ./vendor/bin/compile-schematron.php <source.sch> <outputDir> <namespace>

Erstellen von Rule-Klassen mit PHP:

use MMNewmedia\Schematron\Ast\XPathTokenizer;
use MMNewmedia\Schematron\Compiler\ExpressionCompiler;
use MMNewmedia\Schematron\Compiler\ExpressionNormalizer;
use MMNewmedia\Schematron\Compiler\FunctionLoader;
use MMNewmedia\Schematron\Compiler\XPathAstCompiler;
use MMNewmedia\Schematron\Parser\XPathAstParser;
use MMNewmedia\Schematron\SchematronCompilerService;

$functionLoader = new FunctionLoader(
    dirname(__DIR__) . '/vendor/mmnewmedia/schematron-parser/src/Schematron/Compiler/Functions',
    'MMNewmedia\Schematron\Compiler\Functions',
);

$compiler = new ExpressionCompiler(
    new XPathAstParser(new XPathTokenizer()),
    new XPathAstCompiler($functionLoader->load()),
    new ExpressionNormalizer(),
);

// XPath -> PHP expression evaluating against a context node ($context by default)
$php = $compiler->compile('substring-before(ram:ID, "-")', '$node');

// XPath -> PHP expression evaluating to a node-set (DOMNodeList)
$nodeSet = $compiler->compileNodeSet('.//ram:SpecifiedLineTradeTransaction', '$node');

$service = new SchematronCompilerService($compiler, '/path/to/output/Rules/Cii');
$service->compile(
    '/path/to/EN16931-CII-validation-preprocessed.sch', 
    'MyProject\Schematron\Rules\Cii'
);

Fazit

Der Schematron-Parser zeigt, dass Schematron-Validierung in PHP nicht kompliziert sein muss. Durch die automatische Übersetzung von Schematron-Regeln in native PHP-Klassen entsteht eine Lösung, die:

  • leichtgewichtig
  • performant
  • standardkonform
  • und komplett ohne externe Abhängigkeiten

auskommt.

Ein Werkzeug, dass die Lücke zwischen Schematron-Standard und PHP-Ökosystem endlich schließt.

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.