-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Schematron validation using Saxon Home Edition (Java) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
89e5f9e
3d47295
d8eb453
ca43b48
a8455de
b92ac35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Use PHP 8.3 CLI as base image | ||
| FROM php:8.3-cli | ||
|
|
||
| # Install system dependencies and PHP extensions | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends \ | ||
| git \ | ||
| unzip \ | ||
| curl \ | ||
| ca-certificates \ | ||
| libzip-dev \ | ||
| libicu-dev \ | ||
| libxml2-dev \ | ||
| default-jre \ | ||
| wget \ | ||
| && docker-php-ext-install intl zip dom xml \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Install Composer | ||
| COPY --from=composer:2 /usr/bin/composer /usr/bin/composer | ||
|
|
||
| # Set working directory | ||
| WORKDIR /app | ||
|
|
||
| # Install Saxon Home edition for Java | ||
| RUN wget -nv "https://github.com/Saxonica/Saxon-HE/releases/download/SaxonHE12-9/SaxonHE12-9J.zip" -O /tmp/saxon.zip && \ | ||
| unzip /tmp/saxon.zip -d /opt/saxon && \ | ||
| rm /tmp/saxon.zip | ||
|
|
||
| ENV PATH="/opt/saxon/bin:${PATH}" | ||
|
|
||
| ENV SAXON_JAR="/opt/saxon/saxon-he-12.9.jar" | ||
|
|
||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # SaxonJarSchematronValidator | ||
|
|
||
| The `SaxonJarSchematronValidator` is a Schematron validator implementation that uses the Saxon XSLT processor (Java edition) to validate XML documents against Schematron rules compiled as XSLT stylesheets. | ||
|
|
||
| ## Overview | ||
|
|
||
| Schematron is a rule-based validation language for XML documents. Unlike XSD (XML Schema Definition), which validates structure, Schematron validates business rules and constraints using XPath expressions. | ||
|
|
||
| In this implementation: | ||
| 1. Schematron rules are pre-compiled into an XSLT stylesheet | ||
| 2. Saxon XSLT processor transforms the XML document using the XSLT rules | ||
| 3. The output is an SVRL (Schematron Validation Report Language) document | ||
| 4. Failed assertions are extracted and reported as validation errors | ||
|
|
||
| ## Requirements | ||
|
|
||
| ### System Requirements | ||
|
|
||
| - **Java Runtime Environment (JRE)** 8 or higher | ||
| - The `java` command must be available in your system PATH | ||
| - Check with: `java -version` | ||
|
|
||
| - **Saxon-HE JAR file** (Home Edition, version 9.x or higher) | ||
| - Download from: [Saxonica Downloads](https://www.saxonica.com/download/java.xml) | ||
|
|
||
| ### PHP Requirements | ||
|
|
||
| - **PHP 8.3** or higher | ||
| - **Required PHP extensions:** | ||
| - `dom` - for parsing SVRL XML output | ||
| - `libxml` - for XML handling | ||
|
|
||
| ### Composer Dependencies | ||
|
|
||
| ```bash | ||
| composer require symfony/process | ||
| ``` | ||
|
|
||
| The `symfony/process` component is used to execute the Java Saxon processor as a subprocess. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Basic Usage | ||
|
|
||
| ```php | ||
| <?php | ||
|
|
||
| use TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron\SaxonJarSchematronValidator; | ||
| use TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron\ValidationFailedException; | ||
|
|
||
| // Create validator instance with path to Saxon JAR | ||
| $validator = new SaxonJarSchematronValidator( | ||
| saxonJar: '/usr/local/lib/saxon-he.jar' | ||
| ); | ||
|
|
||
| try { | ||
| // Validate XML file against Schematron XSLT rules | ||
| $validator->validate( | ||
| xmlFilepath: '/path/to/document.xml', | ||
| xsltFilepath: '/path/to/schematron-rules.xsl' | ||
| ); | ||
|
|
||
| echo "Validation successful!\n"; | ||
|
|
||
| } catch (ValidationFailedException $e) { | ||
| echo "Validation failed: " . $e->getMessage() . "\n"; | ||
|
|
||
| // Access validation errors | ||
| foreach ($e->errors as $error) { | ||
| echo sprintf( | ||
| "Error [%s]: %s\n Location: %s\n Test: %s\n", | ||
| $error->getId(), | ||
| $error->getText(), | ||
| $error->getLocation(), | ||
| $error->getTest() | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||
| <?php | ||||||||||||||
|
|
||||||||||||||
| declare(strict_types=1); | ||||||||||||||
|
|
||||||||||||||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||||||||||||||
|
|
||||||||||||||
| use Symfony\Component\Process\Process; | ||||||||||||||
|
|
||||||||||||||
| final readonly class SaxonJarSchematronValidator implements SchematronValidatorInterface | ||||||||||||||
| { | ||||||||||||||
| /** | ||||||||||||||
| * @throws \LogicException | ||||||||||||||
| */ | ||||||||||||||
| public function __construct(private string $saxonJar) | ||||||||||||||
| { | ||||||||||||||
| if (false === class_exists(Process::class)) { | ||||||||||||||
| throw new \LogicException('Symfony Process component is required to use SaxonJarSchematronValidator. Run "composer require symfony/process"'); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (false === extension_loaded('dom') || false === extension_loaded('libxml')) { | ||||||||||||||
| throw new \LogicException('DOM and Libxml extensions are required to validate business rules.'); | ||||||||||||||
| } | ||||||||||||||
|
||||||||||||||
| } | |
| } | |
| if (false === is_file($this->saxonJar) || false === is_readable($this->saxonJar)) { | |
| throw new \LogicException(sprintf('Saxon JAR file "%s" does not exist or is not readable.', $this->saxonJar)); | |
| } |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validate method does not check if the input XML file and XSLT file exist and are readable before passing them to the Saxon process. If either file doesn't exist, Saxon will fail with potentially unclear error messages. Consider adding file existence checks at the beginning of the validate method and throwing a clear exception (e.g., \InvalidArgumentException) if files are missing or not readable.
qdequippe marked this conversation as resolved.
Show resolved
Hide resolved
qdequippe marked this conversation as resolved.
Show resolved
Hide resolved
qdequippe marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| interface SchematronValidatorInterface | ||
| { | ||
| /** | ||
| * @param string $xmlFilepath the path to the XML file to validate against the business rules | ||
| * @param string $xsltFilepath the path to the Schematron XSLT file to use for validation | ||
| * | ||
| * @throws ValidationFailedException | ||
| */ | ||
| public function validate(string $xmlFilepath, string $xsltFilepath): void; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| final readonly class ValidationError | ||
qdequippe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| private string $test; | ||
|
|
||
| private string $id; | ||
|
|
||
| private string $flag; | ||
|
|
||
| private string $location; | ||
|
|
||
| private ?string $text; | ||
|
|
||
| public function __construct( | ||
| string $test, | ||
| string $id, | ||
| string $flag, | ||
| string $location, | ||
| ?string $text, | ||
| ) { | ||
| $this->test = $test; | ||
| $this->id = $id; | ||
| $this->flag = $flag; | ||
| $this->location = $location; | ||
| $this->text = $text; | ||
| } | ||
|
Comment on lines
+7
to
+31
|
||
|
|
||
| public function getTest(): string | ||
| { | ||
| return $this->test; | ||
| } | ||
|
|
||
| public function getId(): string | ||
| { | ||
| return $this->id; | ||
| } | ||
|
|
||
| public function getFlag(): string | ||
| { | ||
| return $this->flag; | ||
| } | ||
|
|
||
| public function getLocation(): string | ||
| { | ||
| return $this->location; | ||
| } | ||
|
|
||
| public function getText(): ?string | ||
| { | ||
| return $this->text; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| final class ValidationFailedException extends \RuntimeException | ||
qdequippe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| /** | ||
| * @param ValidationError[] $errors | ||
| */ | ||
| public function __construct( | ||
| public readonly array $errors = [], | ||
| string $message = '', | ||
| int $code = 0, | ||
| ?\Throwable $previous = null, | ||
| ) { | ||
| parent::__construct($message, $code, $previous); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.