Главная

Проверка XML на соответствие XSD в PHP

Проверка XML на соответствие XSD в PHP

На дворе 2022 год, а вы по-прежнему предпочитаете использовать XML (расширяемый язык разметки) JSON-ну (нотация объектов JavaScript) для обмена данными? Ну не в этом суть статьи. Все, что вы хотите сказать о компромиссах любого из них, было рассмотрено здесь. Но сегодня все еще существует множество систем, использующих XML, и я могу заверить вас, что это будет так и через X лет.
Проверка XML на соответствие XSD может быть первым шагом, который необходимо предпринять, особенно при создании модуля Reader/Ingester. Для начинающих любой файл, подобный приведенному ниже образцу, представляет собой правильно сформированный XML-файл.

а ниже приведен пример файла XSD (определение схемы XML):

Написать XSD для вашего XML на самом деле легко сделать. На простарах интернета очень много документции и различных статей по описанию XSD-схемы.
Теперь мы готовы проверить наш файл XML на соответствие XSD с помощью DOMDocument или XMLReader.

Во-первых, убедитесь, что эти расширения включены при установке PHP.

Проверка с помощью DOMDocument

Сервис валидации XML согласно XSD, использующий DOMDocument

class XmlValidatorService
{
    private string $schema;
    private DOMDocument $handler;
    private string $xml;

    public function __construct()
    {
        $this->handler = new DOMDocument('1.0', 'utf-8');
    }

    /**
     * @return XmlErrorDto[]
     * @throws Exception
     */
    public function run(): array
    {
        $this->checkSchema();
        $this->setLibXmlUserInternalErrors();

        $this->handler->loadXML($this->xml);
        if (!$this->handler->schemaValidate($this->schema)) {
            return $this->getErrors();
        }

        return [];
    }

    public function setXml(string $filePath): self
    {
        $this->xml = $xml;
        return $this;
    }

    public function setSchema(string $filePath): self
    {
        $this->schema = $filePath;
        return $this;
    }

    private function setLibXmlUserInternalErrors(): void
    {
        libxml_use_internal_errors(true);
    }

    /**
     * @return void
     * @throws Exception
     */
    private function checkSchema(): void
    {
        if (!file_exists($this->schema)) {
            throw new Exception('Не найден XSD-файл');
        }
    }

    /**
     * @return XmlErrorDto[]
     */
    private function getErrors(): array
    {
        $errors = libxml_get_errors();
        libxml_clear_errors();

        return array_map(
            fn (LibXMLError $libXMLError) => $this->buildErrorDto($libXMLError),
            $errors
        );
    }

    private function buildErrorDto(LibXMLError $libXMLError): XmlErrorDto
    {
        return (new XmlErrorDto())
            ->setCode($libXMLError->code)
            ->setColumn($libXMLError->column)
            ->setLevel($libXMLError->level)
            ->setLine($libXMLError->line)
            ->setMessage($libXMLError->message);
    }
}

Класс DTO ошибки

class XmlErrorDto
{
    private int $level;
    private int $code;
    private int $column;
    private string $message;
    private int $line;

    public function setLevel(int $level): self
    {
        $this->level = $level;
        return $this;
    }

    public function getLevel(): int
    {
        return $this->level;
    }

    public function setCode(int $code): self
    {
        $this->code = $code;
        return $this;
    }

    public function getCode(): int
    {
        return $this->code;
    }

    public function setColumn(int $column): self
    {
        $this->column = $column;
        return $this;
    }

    public function getColumn(): int
    {
        return $this->column;
    }

    public function setMessage(string $message): self
    {
        $this->message = $message;
        return $this;
    }

    public function getMessage(): string
    {
        return $this->message;
    }

    public function setLine(int $line): self
    {
        $this->line = $line;
        return $this;
    }

    public function getLine(): int
    {
        return $this->line;
    }
}

Этот XmlValidatorService можно легко использовать следующим образом:

$errors = (new XmlValidatorService())
    ->setSchema('schema.xsd')
    ->setXml('sample.xml')
    ->run();
if ($errors === []) {
    // XML соответствует XSD, ошибок нет
} else {
    // XML не соответствует XSD, ошибки $errors
}

Проверка с помощью XMLReader

Преимуществом использования XMLReader вместо DomDocument является масштабируемость. XMLReader может обрабатывать очень большие файлы лучше, чем DomDocument. Наш класс будет очень похож на класс DomDocument. Также обратите внимание, что ваша версия libxml выше 2.6.

class XmlValidatorService
{
    private string $schema;
    private DOMDocument $handler;
    private string $xml;

    public function __construct()
    {
        $this->handler = new XMLReader();
    }

    /**
     * @return XmlErrorDto[]
     * @throws Exception
     */
    public function run(): array
    {
        $this->fileExists($this->schema);
        $this->fileExists($this->xml);
        $this->setLibXmlUserInternalErrors();

        $this->handler->open($this->xml);
        $this->handler->setSchema($this->schema);

        $errors = [];
        while ($this->handler->read()) {
            if ($this->handler->isValid()) {
                continue;
            }

            $errors = array_merge($errors, $this->getErrors());
        }

        return $errors;
    }

    /**
     * @return XmlErrorDto[]
     */
    private function getErrors(): array
    {
        $errors = libxml_get_errors();
        libxml_clear_errors();

        return array_map(
            fn (LibXMLError $libXMLError) => $this->buildErrorDto($libXMLError),
            $errors
        );
    }

    private function setLibXmlUserInternalErrors(): void
    {
        libxml_use_internal_errors(true);
    }

    /**
     * @param string $path
     * @return void
     * @throws Exception
     */
    private function fileExists(string $path): void
    {
        if (!file_exists($path)) {
            throw new Exception('Не найден файл');
        }
    }

    private function buildErrorDto(LibXMLError $libXMLError): XmlErrorDto
    {
        return (new XmlErrorDto())
            ->setCode($libXMLError->code)
            ->setColumn($libXMLError->column)
            ->setLevel($libXMLError->level)
            ->setLine($libXMLError->line)
            ->setMessage($libXMLError->message);
    }
}
Роберт Фатхуллин

Статья Роберт Фатхуллин

Backend Developer