diff --git a/examples/InvoiceXsdInvalid.xml b/examples/InvoiceXsdInvalid.xml new file mode 100644 index 00000000..714fa014 --- /dev/null +++ b/examples/InvoiceXsdInvalid.xml @@ -0,0 +1,30 @@ + + + + + urn:cen.eu:en16931:2017 + + + + 471102 + 380 + + 20180305 + + + Rechnung gemäß Bestellung vom 01.03.2018. + + + Lieferant GmbH + Lieferantenstraße 20 + 80333 München + Deutschland + Geschäftsführer: Hans Muster + Handelsregisternummer: H A 123 + + REG + + + + + diff --git a/examples/XsdValidator.php b/examples/XsdValidator.php new file mode 100644 index 00000000..6268639b --- /dev/null +++ b/examples/XsdValidator.php @@ -0,0 +1,41 @@ +validationFailed()) { + echo "\033[01;31mValidation failed\e[0m\n"; + foreach ($xsdValidator->validationErrors() as $validationError) { + echo $validationError . PHP_EOL; + } + } else { + echo "\033[01;32mValidation passed\e[0m\n"; + } +} + +/** + * Invalid XML + */ + +$document = ZugferdDocumentReader::readAndGuessFromFile(dirname(__FILE__) . "/InvoiceXsdInvalid.xml"); + +$xsdValidator = new ZugferdXsdValidator($document); +$xsdValidator->validate(); + +showValidationResult($xsdValidator); + +/** + * Valid XML + */ + +$document = ZugferdDocumentPdfReader::readAndGuessFromFile(dirname(__FILE__) . "/invoice_1.pdf"); + +$xsdValidator = new ZugferdXsdValidator($document); +$xsdValidator->validate(); + +showValidationResult($xsdValidator); diff --git a/src/ZugferdSettings.php b/src/ZugferdSettings.php index bb91ef69..73dc2fdd 100644 --- a/src/ZugferdSettings.php +++ b/src/ZugferdSettings.php @@ -268,6 +268,16 @@ public static function getValidationDirectory(): string return PathUtils::combineAllPaths(static::getSourceDirectory(), "validation"); } + /** + * Get the directory where all the schema (XSD) files are located + * + * @return string + */ + public static function getSchemaDirectory(): string + { + return PathUtils::combineAllPaths(static::getSourceDirectory(), "schema"); + } + /** * Get the full filename of the ICC profile to use * diff --git a/src/ZugferdValidator.php b/src/ZugferdValidator.php deleted file mode 100644 index 47837aff..00000000 --- a/src/ZugferdValidator.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT - * @link https://github.com/horstoeko/zugferd - */ -class ZugferdValidator -{ - /** - * The invoice document reference - * - * @var ZugferdDocument - */ - private $document; - - /** - * Constructor - * - * @codeCoverageIgnore - * @param ZugferdDocument $document - */ - public function __construct(ZugferdDocument $document) - { - $this->document = $document; - } -} \ No newline at end of file diff --git a/src/ZugferdXsdValidator.php b/src/ZugferdXsdValidator.php new file mode 100644 index 00000000..e6108e00 --- /dev/null +++ b/src/ZugferdXsdValidator.php @@ -0,0 +1,209 @@ + + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/horstoeko/zugferd + */ +class ZugferdXsdValidator +{ + /** + * The invoice document reference + * + * @var ZugferdDocument + */ + private $document; + + /** + * Internal error bag + * + * @var array + */ + private $errorBag = []; + + /** + * Constructor + * + * @codeCoverageIgnore + * @param ZugferdDocument $document + */ + public function __construct(ZugferdDocument $document) + { + $this->document = $document; + } + + /** + * Perform validation of document + * + * @return ZugferdXsdValidator + */ + public function validate(): ZugferdXsdValidator + { + $this->clearErrorBag(); + $this->initLibXml(); + + try { + if (!$this->getDocumentContentAsDomDocument()->schemaValidate($this->getDocumentXsdFilename())) { + $this->pushLibXmlErrorsToErrorBag(); + } + } catch (Exception $exception) { + $this->addToErrorBag($exception); + } finally { + $this->finalizeLibXml(); + } + + return $this; + } + + /** + * Returns true if validation passed otherwise false + * + * @return boolean + */ + public function validationPased(): bool + { + return empty($this->errorBag); + } + + /** + * Returns true if validation failed otherwise false + * + * @return boolean + */ + public function validationFailed(): bool + { + return !$this->validationPased(); + } + + /** + * Returns an array of all validation errors + * + * @return array + */ + public function validationErrors(): array + { + return $this->errorBag; + } + + /** + * Initialize LibXML + * + * @return void + */ + private function initLibXml(): void + { + libxml_use_internal_errors(true); + } + + /** + * Finalize LibXML + * + * @return void + */ + private function finalizeLibXml(): void + { + libxml_clear_errors(); + libxml_use_internal_errors(false); + } + + /** + * Get the content of the document + * + * @return string + */ + private function getDocumentContent(): string + { + return $this->document->serializeAsXml(); + } + + /** + * Get the content of the document as a DOMDocument + * + * @return DOMDocument + */ + private function getDocumentContentAsDomDocument(): DOMDocument + { + $doc = new DOMDocument(); + $doc->loadXML($this->getDocumentContent()); + + return $doc; + } + + /** + * Get the XSD file (schema definition) for the document + * + * @return string + */ + private function getDocumentXsdFilename(): string + { + $xsdFilename = PathUtils::combineAllPaths( + ZugferdSettings::getSchemaDirectory(), + $this->document->getProfileDefinitionParameter('xsdfilename') + ); + + if (!file_exists($xsdFilename)) { + throw new Exception(sprintf("XSD file '%s' not found", $xsdFilename)); + } + + return $xsdFilename; + } + + /** + * Clear the internal error bag + * + * @return void + */ + private function clearErrorBag(): void + { + $this->errorBag = []; + } + + /** + * Add message to error bag + * + * @param string|Exception|LibXMLError $error + * @return void + */ + private function addToErrorBag($error): void + { + if (is_string($error)) { + $this->errorBag[] = $error; + } elseif ($error instanceof Exception) { + $this->errorBag[] = $error->getMessage(); + } elseif ($error instanceof LibXMLError) { + $this->errorBag[] = sprintf('[line %d] %s : %s', $error->line, $error->code, $error->message); + } + } + + /** + * Pushes validation errors to error bag + * + * @return void + */ + private function pushLibXmlErrorsToErrorBag(): void + { + foreach (libxml_get_errors() as $xmlError) { + $this->addToErrorBag($xmlError); + } + } +}