Глава 110 · От основ к практике
libxml: ошибки XML, настройки парсера и внешние зависимости
От первого XML-элемента до понятной диагностики импорта: все функции libxml, LibXMLError, флаги, загрузчики и границы внешних чтений.
XML от поставщика: понять структуру, затем принять решение
От XML поставщика до решения приложения
Формат, внешние зависимости и предметные данные проверяются на разных этапах.
- <order>42</order>
Текст документа от внешнего источника.
- Парсер и его политика
Проверяет запись, ограничивает доступ к зависимостям.
- Документ или диагностика
DOM хранит структуру; LibXMLError объясняет отказ.
- Проверка заказа
Приложение проверяет номер, полномочия и бизнес-правила.
Успешный XML-разбор ещё не означает, что заказ допустим. Очистка сообщений не исправляет исходный документ.
Нужен понятный отказ импорта: «invoice.xml, строка 2 — файл не принят», а техническое предупреждение не должно попасть в страницу покупателя. Для чтения полей служат SimpleXML и DOM. libxml объединяет их диагностику и правила загрузки XML-зависимостей. Начнём с исправного заказа, затем намеренно повредим его закрывающий тег.
Поставщик прислал <order>42</order>. Приложению нужно получить номер заказа, но сначала убедиться, что документ вообще разбирается. libxml — общий слой XML-библиотек PHP: он предоставляет сообщения, опции парсера и управление внешними зависимостями. DOM и SimpleXML дают удобный доступ к самому содержимому.
- Вход: <order>42</order>
- Парсер проверяет открывающий и закрывающий теги
- DOM: элемент order содержит текст42
- Приложение проверяет номер и сохраняет заказ
XML — текст с явно вложенными элементами: <order>42</order> содержит элемент order и текст42. Парсер проверяет эту запись и строит доступную программе структуру. new DOMDocument() создаёт документ, ->loadXML($text) пытается разобрать строку; === false означает неудачу. Функции libxml_* здесь управляют сообщениями и загрузкой зависимостей парсера, а не заменяют DOM/SimpleXML.
Скобки передают аргументы функции; результат сохраняется через =. try/finally возвращает изменённый режим после операции. Вызов и возврат функции, Ошибки и исключения объясняют эти конструкции отдельно.
<?php
declare(strict_types=1);
$document = new DOMDocument();
if ($document->loadXML('<order>42</order>') === false) {
throw new RuntimeException('XML заказа не разобран');
}
echo 'Получен заказ № ', $document->documentElement->textContent, "\n";
Ожидаемый результат
Получен заказ № 42
new DOMDocument создаёт пустой документ, loadXML разбирает строку. documentElement выбирает корень order, textContent читает его текст. Это ещё не проверка существования заказа или прав отправителя; ниже добавляем обработку отказа парсера.
Нельзя заменить XML-парсер regex для произвольного документа: вложенность, namespace, escaping, CDATA и entities имеют свои правила. JSON и XML — разные форматы интеграций. Выбирает контракт сервиса, а PHP после разбора работает с объектами/значениями, понятными приложению.
Какие инструменты используют libxml
- DOM: дерево с редактированием
- SimpleXML: удобное чтение XML
- XMLReader/Writer: последовательное чтение/запись
- libxml: общие опции, диагностика и загрузчик
Поддержка функций libxml появилась с PHP 5.1. Руководство перечисляет связанные расширенияDOM, SimpleXML, SOAP, XSL, XML, XMLReader и XMLWriter; в историческом списке также WDDX и XMLRPC. Последние не означают наличие этих модулей в современной стандартной сборке. У каждого расширения собственная доступность и отдельная справочная категория.
<?php
declare(strict_types=1);
var_dump(extension_loaded('libxml'), class_exists(DOMDocument::class));
echo LIBXML_DOTTED_VERSION, "\n";
Ожидаемый результат
bool(true)
bool(true)
2.11.9
Это вывод проверенного стенда. Версия PHP 8.5.11 не означает, что вместе с ней загружена libxml 2.13: здесь 2.11.9. Нельзя использовать новую константу без проверки зависимости.
| ВеткаPHP | Минимальнаяlibxml |
|---|---|
| До8.0 | 2.6.0 |
| 8.0–8.3 | 2.9.0 |
| 8.4+ | 2.9.4 |
Для современной сборкиPHP 7.4+ libxml включена по умолчанию; отключение при сборке — --without-libxml. Поиск библиотеки/заголовков/флагов выполняет pkg-config. PKG_CONFIG_PATH задаёт каталоги .pc до configure, например /path/to/libxml2/prefix/lib/pkgconfig:/lib/pkgconfig. Это настройка сборки, не параметр внутриPHP-скрипта.
Историческая установка до PHP 7.4
- --with-libxml-dir указываетDIR
- ПроверяетсяDIR/bin/xml2-config
- Затем/usr/local/bin/xml2-config
- Затем/usr/bin/xml2-config
В старых ветках libxml включалась по умолчанию, отключалась --disable-libxml. Необязательный --with-libxml-dir задавал место установки; если xml2-config не находился там, configure продолжал стандартный поиск. Эти команды нужны для понимания старых сборочных файлов, а не для развёртывания нового сервиса на неподдерживаемойPHP.
LibXMLError: понятная карточка проблемы вместо загадочного предупреждения
LibXMLError содержит сведения о сообщении парсера. Это объект данных, а не Throwable: catch(LibXMLError) не является способом перехвата XML-ошибки. Объект получают через libxml_get_errors или libxml_get_last_error после операции.
- loadXML не разобрал вход
- LibXMLError хранит level/code/line/column
- message объясняет причину, file показывает источник
- Показываем безопасную краткую диагностику
| Публичное свойство | Как читать |
|---|---|
| int level | LIBXML_ERR_WARNING, ERROR или FATAL. Уровни libxml не совпадают с PHP E_WARNING/E_ERROR. |
| int code | Код причины из xmlError API libxml; версию и описание сверяют с движком. |
| int column | Колонка; реализована не везде, часто0. Не рисуйте точный указатель в начало строки на основании 0. |
| string message | Текст сообщения, может заканчиваться переводом строки и содержать входные данные. |
| string file | Имя источника; для XML из строки обычно пусто. Не раскрывайте серверный путь посетителю. |
| int line | Строка ошибки; входные строки обычно нумеруются с 1, аPHP-массивстрок — с 0. |
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$document = new DOMDocument();
$document->load(__DIR__ . '/invoice.xml');
$errors = libxml_get_errors();
$error = $errors[0];
echo basename($error->file), ':', $error->line, ": документ не разобран\n";
$lines = file(__DIR__ . '/invoice.xml', FILE_IGNORE_NEW_LINES);
echo 'Строка исходника: ', $lines[$error->line - 1], "\n";
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
invoice.xml:2: документ не разобран
Строка исходника: <order>42</invoice>
В закрытой локальной лаборатории можно показать известную строку: /invoice закрыл незакрытый order. line начинается с 1, индекс PHP-массива — с 0, поэтому вычитаем 1. Для чужого документа публично не печатайте его содержимое; column=0 не означает надёжно известную позицию символа.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$document = new DOMDocument(); $document->loadXML('<order>');
$error = libxml_get_last_error();
if ($error !== false) {
$level = match ($error->level) {
LIBXML_ERR_WARNING => 'warning', LIBXML_ERR_ERROR => 'error',
LIBXML_ERR_FATAL => 'fatal', default => 'unknown',
};
echo 'XML ', $level, ' at line ', $error->line, "\n";
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
XML fatal at line 1
Сначала было отдельное чтение объекта, теперь небольшое форматирование через match. Не включаем полный XML и секреты в журнал. При выводе в HTML требуется отдельное экранирование.
Все константы: выбираем поведение осознанно
Флаги — целые именованные переключатели. Оператор | объединяет битовые флаги, но только для операции, которая их поддерживает. Числа уровней ошибок и версий к ним прибавлять не нужно. Один и тотже числовой бит может иметь разный смысл при разборе и сохранении.
- Разбор: NOCDATA превращает CDATA в текстовый узел
- Проверка схемы: SCHEMA_CREATE может добавить значения
- Сохранение: NOEMPTYTAG меняет пустой тег
- Задаём опции для конкретного метода
| Константа | Назначение и ограничения |
|---|---|
| LIBXML_VERSION | int: версия заголовков сборки в числовой форме, например21109. Не складывайте/не комбинируйте её как флаг парсинга. |
| LIBXML_DOTTED_VERSION | string: та же версия сборки с точками, например2.11.9. |
| LIBXML_LOADED_VERSION | string: версия реально загруженного ядра libxml; может быть числовой строкой21109. Сравнивайте сборку и runtime отдельно. |
| LIBXML_BIGLINES | Правильные номера строк больше65535; PHP7.0+/libxml 2.9+. Не увеличивает допустимый размер документа сам по себе. |
| LIBXML_COMPACT | Оптимизация размещения небольших узлов, libxml 2.6.21+. Возможный выигрыш измеряют на собственном документе, не обещают ускорение любого XML. |
| LIBXML_DTDATTR | Добавлять атрибуты по умолчанию изDTD. Может потребовать внешние сущности; нужна политика доступа. |
| LIBXML_DTDLOAD | Загружать внешний subsetDTD. Подключает зависимости документа; не включайте автоматически для произвольного XML. |
| LIBXML_DTDVALID | Проверять документ поDTD. Может активировать внешние чтения; синтаксический разбор и валидация разные задачи. |
| LIBXML_HTML_NOIMPLIED | Не добавлять неявные html/body и другие элементы старого HTML-парсера; PHP5.4+/libxml 2.7.7+. Не является режимом XML и не делает парсерHTML5. |
| LIBXML_HTML_NODEFDTD | Не добавлять defaultDOCTYPE старым HTML-парсером; PHP5.4+/libxml 2.7.8+. |
| LIBXML_NOBLANKS | Удалять blank-узлы по правилам парсера. Не считать универсальным trim: в смешанном содержимом пробелы могут быть значимыми. |
| LIBXML_NOCDATA | Представлять CDATA как обычные текстовые узлы. Не декодирует произвольный вложенный XML и не выполняет содержащийся вCDATA PHP. |
| LIBXML_NOEMPTYTAG | При сохранении DOMDocument::save/saveXML разворачивать пустые элементы: <item/> → <item></item>. |
| LIBXML_NOENT | Подставлять entities. Название обманчиво: это НЕ запрет сущностей. Без отдельной политики возможныXXE/чтения внешних данных. |
| LIBXML_NOERROR | Подавлять сообщения об ошибках в поддерживающей опцию операции; это не делает документ правильным. |
| LIBXML_NONET | Ограничить сетевые обращения парсера. Не запрещает чтение локальных файлов и не заменяет контроль пользовательского resolver/всей загрузки. |
| LIBXML_NOWARNING | Подавлять warning-сообщения поддерживающего парсера. Не отключает все предупрежденияPHP и не означает успешную валидацию. |
| LIBXML_NOXMLDECL | Опция сериализации для пропуска XML-декларации; libxml 2.6.21+. Применимость проверяйте у конкретного метода: не каждый PHP wrapper передаёт все save-флаги. |
| LIBXML_NO_XXE | Запрет внешних сущностей при подстановке: PHP 8.4+/libxml 2.13+. На старойlibxml константы может не быть даже вPHP8.5; проверяют defined. |
| LIBXML_NSCLEAN | Удалять избыточные объявления namespace при разборе. Это не удаление пространств имён и не способ игнорировать их при поиске. |
| LIBXML_PARSEHUGE | Поднимать некоторые встроенные пределы глубины/размера/рекурсииentities. Только для доверенного ограниченного источника; большой документ может исчерпать память или стек. |
| LIBXML_PEDANTIC | Более строгая диагностика, PHP5.4+. Дополнительные сообщения не заменяют прикладную схему. |
| LIBXML_RECOVER | Разрешить recovery при разборе, PHP 8.4+. Восстановленная структура может отличаться от намерения отправителя; для заказов нельзя молча принять потерянные поля. |
| LIBXML_XINCLUDE | ПодстановкаXInclude в поддерживающих pull-парсерах, прежде всегоXMLReader. DOM использует собственный методXInclude; это отдельные внешние зависимости. |
| LIBXML_SCHEMA_CREATE | Создавать default/fixed-узлы приXSD-валидации; PHP5.5.2+/libxml 2.6.14+. Проверка схемы может дополнить дерево. |
| LIBXML_ERR_NONE | Уровень0: нет ошибки. |
| LIBXML_ERR_WARNING | Предупреждение: необходимо оценить его смысл для операции. |
| LIBXML_ERR_ERROR | Восстановимая ошибка на уровне libxml; это не разрешение приложению принять неверный документ. |
| LIBXML_ERR_FATAL | Фатальная ошибка парсера; не путать с PHP E_ERROR. |
<?php
declare(strict_types=1);
$xml = '<order><![CDATA[A&B]]></order>';
$withCdata = new DOMDocument(); $withCdata->loadXML($xml);
$asText = new DOMDocument(); $asText->loadXML($xml, LIBXML_NOCDATA);
echo $withCdata->documentElement->firstChild->nodeType, "\n";
echo $asText->documentElement->firstChild->nodeType, "\n";
echo $asText->documentElement->textContent, "\n";
Ожидаемый результат
4
3
A&B
В DOM 4 — CDATA,3 — текст. Содержимое A&B сохранилось. Если внутри CDATA лежит строка с XML, это ещё не узлы вложенного документа; вторичное парсирование должно быть явным условием договора.
<?php
declare(strict_types=1);
$document = new DOMDocument();
$document->loadXML('<order/>');
echo $document->saveXML($document->documentElement), "\n";
echo $document->saveXML($document->documentElement, LIBXML_NOEMPTYTAG), "\n";
Ожидаемый результат
<order/>
<order></order>
XML-смысл двух записей одинаков. Такая опция нужна, например, для совместимости с особенным получателем, а не для изменения данных заказа.
Внешние сущности: документ может запросить ещё один источник
DTD может объявить сущность, которая подставляет текст, а также описать допустимую структуру. Внешний идентификатор может указывать на путь или адрес. При неправильной настройке входной XML способен инициировать чтения от имени процесса PHP — это основа риска XXE.
- Поставщик прислал XML
- В нём есть ссылка на зависимость
- Политика разрешает только известную схему
- Незнакомый ID отклоняется, клиент не выбирает произвольный файл или URL
Для обычного импорта не включайте NOENT/DTDLOAD/DTDATTR/DTDVALID без потребности. NO_XXE работает только при PHP 8.4+/libxml 2.13+; NONET отдельно незащищает от локальных чтений. Для требуемой валидации используйте известные локальные схемы и libxml_set_external_entity_loader. Запрет внешних чтений не отменяет лимиты размера, глубины, времени и числа обрабатываемых записей.
Если приходит <name>A&B</name> с литералом & в XML-исходнике, этот XML повреждён: амперсанд там записывается как & либо корректная секция CDATA. Слепая замена каждого & испортит уже экранированные последовательности. Парсер не обязан угадывать намерение поставщика; сохраняйте ограниченный диагностический образец и проясняйте контракт.
PARSEHUGE и RECOVER не являются универсальными исправлениями. Первый ослабляет лимиты, второй может потерять часть структуры. Для платежа или заказа нельзя молча принимать такой результат. Большой XML лучше обрабатывать поточно через XMLReader, когда задача это позволяет.
Практика: один неверный документ не портит следующий импорт
Перед импортом каталога определите допустимый размер, нужный корневой элемент, обязательные поля, доверенные схемы и поведение при ошибке. Проверьте некорректный XML, затем корректный. У второй попытки не должно оказаться старых сообщений.
- loadXML: можно ли построить структуру?
- root/order: тот ли формат прислал сервис?
- amount/id: допустимы ли значения?
- Сохраняем только после проверок, очищаем диагностику
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
try {
foreach (['<order>', '<invoice>42</invoice>', '<order>42</order>'] as $xml) {
libxml_clear_errors();
$document = new DOMDocument();
if (!$document->loadXML($xml)) { echo "invalid syntax\n"; continue; }
if ($document->documentElement->tagName !== 'order') { echo "wrong document\n"; continue; }
echo 'order text: ', $document->documentElement->textContent, "\n";
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
invalid syntax
wrong document
order text: 42
Следующий шаг — проверить, что 42 является допустимым идентификатором, найти заказ и проверить полномочия. «XML разобрался» — только первая ступень. Проверка внешних значений.
Полный указатель функций libxml
| Функция | Для чего нужна |
|---|---|
| libxml_use_internal_errors | Принять повреждённый XML от поставщика без вывода технических предупреждений посетителю и разобрать причины программно. |
| libxml_get_errors | Получить все диагностические сообщения текущей XML-операции, чтобы объяснить поставщику, что именно не разобралось. |
| libxml_get_last_error | Узнать последнее XML-сообщение для краткой диагностики, не считая его полной историей ошибки. |
| libxml_clear_errors | Закончить обработку XML-диагностики, чтобы следующая попытка не унаследовала прежние сообщения. |
| libxml_get_external_entity_loader | Сохранить чужой XML-загрузчик перед временной настройкой, чтобы после импорта вернуть прежнее поведение приложения. |
| libxml_set_external_entity_loader | Разрешить XML использовать только заранее известную схему или запретить внешние зависимости, вместо произвольного чтения адресов из документа. |
| libxml_disable_entity_loader | Понять старую защитную настройку XML и заменить её осознанной политикой загрузки в современном PHP. |
| libxml_set_streams_context | Задать тайм-аут и заголовки PHP-потока, когда XML-библиотека сама открывает документ по HTTP. |
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
libxml_use_internal_errorsПринять повреждённый XML от поставщика без вывода технических предупреждений посетителю и разобрать причины программно.
libxml_use_internal_errors(?bool $use_errors = null): boolПринять повреждённый XML от поставщика без вывода технических предупреждений посетителю и разобрать причины программно.
| Часть вызова | Смысл и границы |
|---|---|
| use_errors | true включает накопление libxml-ошибок для вашего обработчика, false возвращает стандартный вывод и очищает накопленный список. null/вызов без аргумента только читает текущий режим. |
| Возврат | Предыдущее состояние bool; это не признак успешного разбора документа. Сохраните его и восстановите в finally. |
| Версии / границы | СPHP8 аргумент nullable и default null; ранее default false. Это не общий обработчик PHP Warning, TypeError или исключений других расширений. |
- Включили накопление
- loadXML видит незакрытый order → false
- Читаем список ошибок
- Очищаем список и восстанавливаем режим
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
try {
$document = new DOMDocument();
if (!$document->loadXML('<order>42')) {
echo "Заказ не принят: повреждённый XML\n";
}
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
Ожидаемый результат
Заказ не принят: повреждённый XML
true направляет сообщения libxml во внутренний список. Возврат setter — старый режим, который сохраняем в $previous; успех разбора проверяем отдельно по loadXML. В документе отсутствует закрывающий /order. finally очищает нашу диагностику и возвращает прежний режим, даже если дальнейший код выбросит исключение.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$document = new DOMDocument();
if (!$document->load(__DIR__ . '/invoice.xml')) {
$errors = libxml_get_errors();
echo 'invoice.xml, строка ', $errors[0]->line, ": накладная не принята\n";
}
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
Ожидаемый результат
invoice.xml, строка 2: накладная не принята
Файл открывает invoice, затем order и ошибочно закрывает invoice в строке 2, пока order ещё открыт. Это уже полезная адресация проблемы, без вывода серверного пути и всего документа. Наличие internal_errors не исправляет XML и не перехватывает любые ошибки PHP.
Режим и список ошибок общие для libxml-операций в текущем выполнении PHP. Вложенный helper не может незаметно очистить чужие сообщения, а затем восстановить их этой API: обратной записи списка нет. Определите, кто владеет диагностикой операции. В длительном воркере регулярно очищайте обработанный список, иначе он растёт.
Дальше: libxml_get_errors; libxml_get_last_error; libxml_clear_errors.
libxml_get_errorsПолучить все диагностические сообщения текущей XML-операции, чтобы объяснить поставщику, что именно не разобралось.
libxml_get_errors(): arrayПолучить все диагностические сообщения текущей XML-операции, чтобы объяснить поставщику, что именно не разобралось.
| Часть вызова | Смысл и границы |
|---|---|
| Аргументы | Нет. Читает накопленный libxml-список. Для его заполнения заранее включите internal_errors. |
| Результат | Массив объектов LibXMLError либо пустой массив. Чтение не очищает список и не возвращает объект документа. |
| Ошибки | Сообщения разных операций могут смешаться, если не задать границы очистки. Успех определяется результатом парсера/валидации, не одним count списка. |
- Очистили предыдущую диагностику
- Парсер встретил несовпадающие теги
- get_errors → объекты level/code/line/message
- Сохраняем нужные сведения, затем clear_errors
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$document = new DOMDocument();
if ($document->loadXML('<order>42</order>')) {
echo 'Заказ ', $document->documentElement->textContent, ": XML прочитан\n";
}
if (libxml_get_errors() === []) { echo "Замечаний парсера нет\n"; }
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
Заказ 42: XML прочитан
Замечаний парсера нет
libxml_get_errors не принимает аргументов и возвращает массив сообщений. Пустой массив означает только отсутствие накопленных сообщений: сам факт разбора подтверждён loadXML. Без вызова парсера такой же пустой массив не доказывал бы корректность файла.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$document = new DOMDocument();
$document->loadXML("<invoice>\n<order>42</invoice>");
$errors = libxml_get_errors();
libxml_clear_errors();
echo 'Отчёт импорта 17: первая проблема в строке ', $errors[0]->line, "\n";
echo libxml_get_errors() === [] ? "Очередь для следующей попытки пуста\n" : "Остались сообщения\n";
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
Отчёт импорта 17: первая проблема в строке 2
Очередь для следующей попытки пуста
get_errors возвращает PHP-массив объектов LibXMLError. clear_errors очищает очередь движка, но уже полученный $errors остаётся доступен для отчёта. Чтение get_errors само по себе очередь не очищает. После отправки отчёта не накапливайте бесконечно и собственные копии.
message может включать фрагменты входных данных, file — серверный путь. Перед показом в HTML требуется htmlspecialchars, а публичный ответ обычно ограничивают кодом операции и понятной причиной. Точный текст и количество сообщений могут отличаться между версиями libxml; тестируйте структурные свойства и ожидаемое решение импорта.
Дальше: libxml_clear_errors; libxml_get_last_error; libxml_use_internal_errors.
libxml_get_last_errorУзнать последнее XML-сообщение для краткой диагностики, не считая его полной историей ошибки.
libxml_get_last_error(): LibXMLError|falseУзнать последнее XML-сообщение для краткой диагностики, не считая его полной историей ошибки.
| Часть вызова | Смысл и границы |
|---|---|
| Аргументы | Нет. |
| Возврат | Объект последней libxml-ошибки или false. Сначала проверяйте false, затем обращайтесь к ->message/->line. |
| Нюанс | ВPHP8.5.11 с внутренним списком берётся его последний элемент; без него — native last error. Поэтому get_errors и get_last_error не обязаны давать одинаковую картину при разных режимах. Последний элемент может быть последствием первой причины. |
- Есть или нет LibXMLError?
- false → сообщений нет
- Объект → line/code/message
- Для цепочки причин читаем get_errors
<?php
declare(strict_types=1);
libxml_clear_errors();
$lastError = libxml_get_last_error();
if ($lastError === false) {
echo "Новая попытка: сообщений XML пока нет\n";
}
Ожидаемый результат
Новая попытка: сообщений XML пока нет
false здесь означает отсутствие последнего сообщения. Разбора ещё не было: зелёную отметку «файл проверен» показывать рано. Сначала проверяем результат парсера, затем при необходимости читаем объект ошибки.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$document = new DOMDocument();
if (!$document->loadXML("<order>\n42")) {
$error = libxml_get_last_error();
echo "Импорт заказа отклонён\n";
if ($error !== false) { echo 'Последняя диагностика: строка ', $error->line, "\n"; }
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
Импорт заказа отклонён
Последняя диагностика: строка 2
Сначала проверка !== false, только затем ->line: функция может не вернуть объект. Последнее сообщение не обязательно первопричина. Полную последовательность сообщения разбирайте через get_errors; точные английские формулировки зависят от libxml.
Дальше: libxml_get_errors; libxml_clear_errors.
libxml_clear_errorsЗакончить обработку XML-диагностики, чтобы следующая попытка не унаследовала прежние сообщения.
libxml_clear_errors(): voidЗакончить обработку XML-диагностики, чтобы следующая попытка не унаследовала прежние сообщения.
| Часть вызова | Смысл и границы |
|---|---|
| Аргументы | Нет. |
| Результат | void. Очищает внутренний список и native last error. Режим internal_errors не переключает. |
| Что не делает | Не исправляет повреждённый XML, не очищает сам DOM и не стирает уже сохранённые в переменные копии LibXMLError. |
- Разобрали первый, получили ошибку
- Сообщили результат и clear_errors
- Второй документ проверяется отдельно
- Его диагностика не содержит историю первого
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
try {
foreach (['<order>', "<invoice>\n<order>"] as $text) {
libxml_clear_errors();
$document = new DOMDocument();
$document->loadXML($text);
$error = libxml_get_last_error();
if ($error !== false) { echo 'Отказ текущего документа: строка ', $error->line, "\n"; }
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
Отказ текущего документа: строка 1
Отказ текущего документа: строка 2
После первой очистки режим накопления остаётся включённым: следующий документ снова даёт собственную диагностику. clear_errors возвращает void, не исправляет ни первый, ни второй XML. Следующий пример добавляет успешную попытку после отказа.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
try {
foreach (['<order>', '<order>42</order>'] as $text) {
libxml_clear_errors();
$document = new DOMDocument();
$ok = $document->loadXML($text);
echo $ok ? "accepted\n" : "rejected\n";
echo count(libxml_get_errors()) > 0 ? "has diagnostics\n" : "clean\n";
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
rejected
has diagnostics
accepted
clean
Здесь accepted означает только синтаксически разобранный XML. Наличие нужных полей, допустимый номер заказа и доверие к отправителю проверяются отдельно. Очистка границ особенно важна в импортах тысяч документов.
Дальше: libxml_get_errors; libxml_get_last_error; libxml_use_internal_errors.
libxml_get_external_entity_loaderСохранить чужой XML-загрузчик перед временной настройкой, чтобы после импорта вернуть прежнее поведение приложения.
libxml_get_external_entity_loader(): ?callableСохранить чужой XML-загрузчик перед временной настройкой, чтобы после импорта вернуть прежнее поведение приложения.
| Часть вызова | Смысл и границы |
|---|---|
| Аргументы | Нет. |
| Возврат | Ранее установленный PHP callback либо null, если пользовательский callback не установлен/сброшен. null не означает запрет загрузки: остаётся стандартный загрузчик. |
| Версии | PHP8.2+. Нужен для корректного сохранения/восстановления настройки библиотеки. |
- Получили прежний callback
- Установили свой resolver
- Выполнили XML-операцию
- finally вернул прежний callback
<?php
declare(strict_types=1);
$original = libxml_get_external_entity_loader();
$resolveLocal = static function (?string $public, ?string $system, array $context) {
return $system === 'urn:shop:label' ? __DIR__ . '/label.txt' : null;
};
try {
libxml_set_external_entity_loader($resolveLocal);
$saved = libxml_get_external_entity_loader();
libxml_set_external_entity_loader(static fn() => null);
libxml_set_external_entity_loader($saved);
$document = new DOMDocument();
$document->loadXML('<!DOCTYPE product [<!ENTITY label SYSTEM "urn:shop:label">]><product>&label;</product>', LIBXML_NOENT);
echo 'Восстановленный загрузчик: ', $document->textContent, "\n";
} finally { libxml_set_external_entity_loader($original); }
Ожидаемый результат
Восстановленный загрузчик: Desk
Getter без аргументов сохраняет сам callback, чтобы временная политика не сломала другие XML-операции. После восстановления реальная подстановка читает только известный label.txt. Возврат строки из callback — путь к содержимому, не само содержимое. NOENT включён исключительно в этом контролируемом опыте с заданной зависимостью.
<?php
declare(strict_types=1);
$previous = libxml_get_external_entity_loader();
try {
libxml_set_external_entity_loader(null);
var_dump(libxml_get_external_entity_loader());
libxml_set_external_entity_loader(static fn() => null);
var_dump(is_callable(libxml_get_external_entity_loader()));
} finally { libxml_set_external_entity_loader($previous); }
Ожидаемый результат
NULL
bool(true)
Не вызывайте полученный callback ради проверки: это может открыть ресурс или изменить его состояние. Сохраните и восстановите его как значение.
Дальше: libxml_set_external_entity_loader; libxml_use_internal_errors.
libxml_set_external_entity_loaderРазрешить XML использовать только заранее известную схему или запретить внешние зависимости, вместо произвольного чтения адресов из документа.
libxml_set_external_entity_loader(?callable $resolver_function): trueВнешняя зависимость — отдельные данные, на которые ссылается XML. DTD задаёт допустимую структуру, а entity даёт имя подставляемому фрагменту. В наших опытах supplier просит имя urn:shop:order.dtd, приложение разрешает только свой локальный файл; произвольного открытия имён из документа нет.
Разрешить XML использовать только заранее известную схему или запретить внешние зависимости, вместо произвольного чтения адресов из документа.
| Часть вызова | Смысл и границы |
|---|---|
| resolver_function | PHP callback; null сбрасывает пользовательскую замену и возвращает стандартный загрузчик. Callback получает public_id, system_id и context. |
| public_id / system_id | Публичный и системный идентификаторы зависимости. В официальной сигнатуре system_id — string; taggedPHP8.5.11 передаёт null при отсутствующем native URL, поэтому наш callback принимает ?string для обоих. |
| context | Массив directory, intSubName, extSubURI, extSubSystem; значения могут быть null. Это сведения парсера, не разрешение читать произвольный путь. |
| Результат callback | Поток resource с нужными байтами, строка пути/URI для открытия либо null — отказ. Возвращённая строка XML не считается содержимым: она будет воспринята как адрес. |
| Возврат setter / версии | Всегда true; объявленный тип true сPHP8.5, раньше bool. Функция доступна с5.4. Установка не проверяет заранее результат будущего вызова callback. |
- XML просит urn:shop:order.dtd
- Resolver сравнивает точный идентификатор
- Знакомый → поток с локальной DTD
- Незнакомый → null, без открытия адреса
<?php
declare(strict_types=1);
$loader = libxml_get_external_entity_loader();
$errors = libxml_use_internal_errors(true);
libxml_clear_errors();
$denied = [];
try {
libxml_set_external_entity_loader(static function (?string $public, ?string $system, array $context) use (&$denied) {
$denied[] = $system;
return null;
});
$document = new DOMDocument();
$document->loadXML('<!DOCTYPE product [<!ENTITY label SYSTEM "urn:shop:unknown">]><product>&label;</product>', LIBXML_NOENT);
if ($denied !== []) {
echo 'Импорт отменён: не разрешена зависимость ', $denied[0], "\n";
}
} finally {
libxml_set_external_entity_loader($loader);
libxml_clear_errors(); libxml_use_internal_errors($errors);
}
Ожидаемый результат
Импорт отменён: не разрешена зависимость urn:shop:unknown
return null от callback отказывает в разрешении запрошенной сущности; libxml_set_external_entity_loader(null), напротив, возвращает штатный загрузчик. loadXML может построить дерево с пустым содержимым, поэтому отказ зависимости здесь отдельно запрещает импорт. Идентификатор urn не является адресом сети; чтения внешнего ресурса нет.
<?php
declare(strict_types=1);
$loader = libxml_get_external_entity_loader();
$errors = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
libxml_set_external_entity_loader(static function (?string $public, ?string $system, array $context) {
return $system === 'urn:shop:order.dtd' ? __DIR__ . '/order.dtd' : null;
});
foreach (['<order number="42"><item>Desk</item></order>', '<order number="43"><comment>Desk</comment></order>'] as $xml) {
libxml_clear_errors();
$document = new DOMDocument();
$document->loadXML('<!DOCTYPE order SYSTEM "urn:shop:order.dtd">' . $xml, LIBXML_DTDLOAD);
$number = $document->documentElement->getAttribute('number');
echo 'Заказ ', $number, $document->validate() ? ": формат принят\n" : ": нарушена DTD\n";
}
} finally {
libxml_set_external_entity_loader($loader);
libxml_clear_errors(); libxml_use_internal_errors($errors);
}
Ожидаемый результат
Заказ 42: формат принят
Заказ 43: нарушена DTD
DTD — описание допустимой структуры: order должен содержать хотя бы один item, number обязателен. XML с comment синтаксически разбирается, но не проходит правила формата. Callback связывает один известный urn с собственным order.dtd. Внешние адреса не открываются; разрешение на исходный XML и на зависимость — разные решения.
Не возвращайте system_id безусловно и не делайте is_file(system_id) достаточным разрешением: так документ сам выберет читаемый сервером файл. Белый список связывает точные известные ID с доверенными схемами; разрешение относительных include/import тоже должно следовать этой политике. Доступ к схеме может значительно замедлять каждую валидацию; локальный согласованный каталог снимает повторную сетевую зависимость.
Loader может участвовать и в открытии основного документа через load, а не только сущностей. В примере loadXML получает уже готовую строку, поэтому зависимость отделена от чтения источника. Настройка действует шире одного DOM-объекта: сохраняйте и восстанавливайте её; параллельные логические операции в одном процессе не должны без координации менять общую политику.
Дальше: libxml_get_external_entity_loader; libxml_use_internal_errors; libxml_get_errors.
libxml_disable_entity_loaderПонять старую защитную настройку XML и заменить её осознанной политикой загрузки в современном PHP.
libxml_disable_entity_loader(bool $disable = true): boolПонять старую защитную настройку XML и заменить её осознанной политикой загрузки в современном PHP.
| Часть вызова | Смысл и границы |
|---|---|
| disable | true запрещает загрузку через этот старый переключатель, false разрешает её. Возвращается предыдущее значение. |
| Версии | PHP5.2.11+; deprecated с8.0. Не включайте вызов в новый код по старому совету из форума без необходимости. |
| Граница | Может мешать обычной загрузке документов и другим XML-операциям. Сlibxml2.9 внешняя подстановка по умолчанию отключена, но опасные режимы NOENT/DTDLOAD/DTDVALID меняют поведение. |
- Старый код меняет entity_loader_disabled
- Меняется загрузка для разных XML-инструментов
- Новый код задаёт нужные опции/загрузчик
- Прежнее состояние всегда восстанавливают
<?php
declare(strict_types=1);
$deprecated = 0;
set_error_handler(static function (int $level) use (&$deprecated): bool {
if ($level !== E_DEPRECATED) { return false; }
$deprecated++; return true;
});
try {
$previous = libxml_disable_entity_loader(true);
libxml_disable_entity_loader($previous);
} finally { restore_error_handler(); }
echo $deprecated, "\n";
Ожидаемый результат
2
Оба вызова наPHP8.5.11 помечены устаревшими. Перехват сделан только для наглядного опыта; рабочая миграция убирает причину deprecated, а не скрывает все сообщения.
<?php
declare(strict_types=1);
$options = LIBXML_NONET;
if (defined('LIBXML_NO_XXE')) { $options |= constant('LIBXML_NO_XXE'); }
$document = new DOMDocument();
if (!$document->loadXML('<order number="42"><item>Desk</item></order>', $options)) {
throw new RuntimeException('Заказ не разобран');
}
echo 'Заказ ', $document->documentElement->getAttribute('number'), ': ', $document->textContent, "\n";
Ожидаемый результат
Заказ 42: Desk
Здесь нет DTD, подстановки внешних сущностей и вызова deprecated setter. | объединяет доступные флаги. LIBXML_NO_XXE требует PHP8.4 и libxml2.13+, поэтому проверяем defined; один LIBXML_NONET не закрывает чтение локальных файлов и не задаёт всю политику доступа.
Для требующего сущностей доверенного документа настройте явный resolver. Для недоверенного документа не включайте расширение сущностей ради «починки» ошибки с амперсандом: NOENT означает подстановку entities, а не их запрет. libxml_set_external_entity_loader позволяет возвратить null для запрещённых зависимостей.
Дальше: libxml_set_external_entity_loader; libxml_get_external_entity_loader.
libxml_set_streams_contextЗадать тайм-аут и заголовки PHP-потока, когда XML-библиотека сама открывает документ по HTTP.
libxml_set_streams_context(resource $context): voidРезультат увидим прямо в XML: служба склада возвращает разные остатки по заголовку X-Warehouse. Контекст — набор настроек открытия потока, который PHP прикрепит к загрузке документа. Локальная служба ниже действительно получает этот заголовок; настройка сама по себе запрос не выполняет.
Задать тайм-аут и заголовки PHP-потока, когда XML-библиотека сама открывает документ по HTTP.
| Часть вызова | Смысл и границы |
|---|---|
| context | Ресурс контекста от stream_context_create. Опции выбирают нужный wrapper: http, ssl и другие. Это не CurlHandle. |
| Возврат | void. Не выполняет загрузку и не возвращает XML. |
| Ошибки / версии | СPHP8.4 неподходящий ресурс приводит кTypeError сразу; раньше ошибка могла возникнуть только при использовании. Обычный аргумент не-resource тоже не подходит. |
| Область | Документация говорит о следующей загрузке/записи; вPHP8.5.11 настройка хранится в состоянии libxml и может применяться к последующим операциям. Публичного getter прежнего контекста нет. |
- stream_context_create с http.timeout
- libxml_set_streams_context сохраняет ресурс
- DOMDocument::load открывает адрес
- Парсер разбирает полученные байты
<?php
declare(strict_types=1);
require __DIR__ . '/xml-lab.php';
[$server, $base] = startXmlLab();
try {
$context = stream_context_create(['http' => [
'header' => "X-Warehouse: north\r\n", 'timeout' => 2.0,
]]);
libxml_set_streams_context($context);
$document = new DOMDocument();
if (!$document->load($base . '/stock.xml')) { throw new RuntimeException('Нет складского XML'); }
echo 'Склад ответа: ', $document->documentElement->getAttribute('warehouse'), "\n";
echo 'A1 — остаток ', $document->textContent, "\n";
} finally { stopXmlLab($server); }
Ожидаемый результат
Склад ответа: north
A1 — остаток 3
Это настоящий запрос к локальному PHP HTTP-серверу: он читает X-Warehouse и выбирает XML склада north. Без этого заголовка возвращаются default и 0. В архиве даны сервер и полностью локальный helper запуска/остановки; они нужны только лаборатории. Проверено прохождение заголовка, а не наступление тайм-аута. Setter передаёт ресурс контекста и возвращает void; load выполняет запрос отдельно.
<?php
declare(strict_types=1);
$stream = fopen('php://temp', 'w+');
if ($stream === false) { throw new RuntimeException('No stream'); }
try {
try { libxml_set_streams_context($stream); echo "stream accepted\n"; }
catch (TypeError $error) { echo "context required\n"; }
} finally { fclose($stream); }
Ожидаемый результат
context required
Проверенный PHP8.5.11 отклоняет обычный файловый поток: нужен именно ресурс контекста. Передавайте результат stream_context_create. То, что оба значения имеют тип resource, ещё не означает взаимозаменяемость их назначения.
Контекст общий, без стандартного публичного способа узнать и восстановить чужое значение. В переиспользуемой библиотеке удобнее отдельно получить ограниченное тело HTTP-клиентом, а затем вызвать loadXML над строкой. Так сетевые ошибки, статус, размер и XML-ошибки обрабатываются раздельно. Если именно libxml должен открывать URL, владелец операции задаёт полный контекст и политику доступа.
HTTP timeout потока относится к ожиданию ввода/вывода и не является универсальным ограничением времени всего импорта. Не передавайте Authorization всем адресам XML-зависимостей; whitelist и отдельные контексты resolver могут потребоваться для каждого разрешённого назначения. Контексты и потоки.
Дальше: libxml_set_external_entity_loader; libxml_use_internal_errors.