Глава 118 · От основ к практике
XSL: преобразование XML, отчёты и полный справочник XSLTProcessor
Все методы XSLTProcessor, параметры, callbacks, свойства, ограничения, профилирование и примеры XML → отчёт.
Данные отдельно, правила отдельно: catalog.xml → отчёт
Одни данные товара — отдельные правила отчёта
PHP связывает XML-каталог и XSL-шаблон. До transform они только подготовлены.
- $xml: product A → Tea
DOMDocument разбирает каталог; название Tea находится в элементе title первого товара.
- $xsl: catalog/product[1]/title
Правило xsl:value-of выбирает название. xsl:output method=text задаёт текстовый результат.
- $processor->transformToXml($xml)
Процессор применяет ранее импортированные правила к переданному дереву данных.
- $result = 'Tea'
Строка возвращается в место вызова. Теперь PHP может вывести её или сохранить в отчёт.
XSLT строит результат по правилам; он не запускает PHP-файл из каталога. Для большого XML учитывают память исходного и результирующего деревьев.
Магазин получает один XML-каталог, а отделам нужны разные представления: список остатков, HTML-отчёт, XML другого формата. XSLT позволяет менять правила представления отдельно от данных. Сначала извлечём одно название Desk из реального файла; затем тот же каталог превратится в список товаров в report.html.
catalog.xml<catalog>
<product sku="A1"><name>Desk</name><stock>3</stock></product>
</catalog>
first.xsl<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/">
<xsl:value-of select="catalog/product[1]/name"/>
</xsl:template>
</xsl:stylesheet>
- catalog.xml: product/name содержит Desk
- first.xsl: правило / выбирает catalog/product[1]/name
- DOMDocument разбирает оба XML-файла
- XSLTProcessor импортирует правила и применяет к данным
- transformToXml возвращает строку Desk
| Часть XSL | Смысл |
|---|---|
| xsl:stylesheet и xmlns:xsl | Корень документа правил; URI связывает префикс xsl с языком XSLT. version="1.0" — версия языка правил, не PHP. |
| xsl:output method="text" | Результат будет обычным текстом. Для будущего отчёта выберем html, для другого XML — xml. |
| xsl:template match="/" | Правило начинается с целого документа. / — корневой узел документа, а не папка на диске. |
| xsl:value-of select="catalog/product[1]/name" | XPath выбирает первый product и его name. Позиции XPath начинаются с 1. value-of выводит текст выбранного значения. |
<?php
declare(strict_types=1);
$data = new DOMDocument();
$data->load(__DIR__ . '/catalog.xml');
$rules = new DOMDocument();
$rules->load(__DIR__ . '/first.xsl');
$processor = new XSLTProcessor();
$processor->importStylesheet($rules);
echo $processor->transformToXml($data), "\n";
Ожидаемый результат
Desk
Оба файла входят в архив и заранее корректны. new DOMDocument создаёт дерево, load читает файл; new XSLTProcessor создаёт исполнитель. importStylesheet загружает правила, а transformToXml отдельно применяет их к $data. Стрелка вызывает действие объекта; скобки передают вход; echo выводит возвращённую строку. Обработка отказов добавлена в полном примере ниже.
XSLT полезен, когда XML-контракт уже существует и нужны отдельные правила преобразования. Если есть PHP-массив и обычный HTML-шаблон, создавать промежуточный XML только ради XSL часто не нужно. Это другой язык, не CSS и не PHP. Он работает с XML-документами, элементами, атрибутами и текстом, которые вводятся в начале XML-маршрута.
Установка, libxslt и версия языка преобразования
Расширению xsl нужны libxml и libxslt версии не ниже 1.1.0. При сборке PHP используют --with-xsl[=DIR]; готовые пакеты устанавливают и включают по правилам ОС/дистрибутива. CLI и PHP-FPM могут читать разные ini: проверяйте тот процесс, который выполняет отчёт. В Windows DLL должна соответствовать сборке PHP по версии, архитектуре и thread safety.
<?php
declare(strict_types=1);
var_dump(extension_loaded('xsl'), class_exists(XSLTProcessor::class));
echo LIBXSLT_DOTTED_VERSION, "\n";
Ожидаемый результат
bool(true)
bool(true)
1.1.43
Версия 1.1.43 — результат проверенного стенда PHP 8.5.11, а не требование точного совпадения для каждого сервера. При отсутствии модуля константу читать нельзя; сначала проверяют extension_loaded.
libxslt реализует XSLT 1.0 с рядом расширений EXSLT. Запись version="2.0" или "3.0" в шаблоне не добавляет поддержку этих стандартов. Если контракт действительно требует другой версии XSLT, выбирают отдельный совместимый процессор и явно организуют его запуск/интеграцию. XSLTProcessor::hasExsltSupport проверяет библиотеку расширений, не новую версию XSLT.
Разобрать минимальный XSL по частям
| Запись | Что происходит |
|---|---|
| xsl:stylesheet version="1.0" | Корень документа правил. Префикс xsl связан с URI http://www.w3.org/1999/XSL/Transform. |
| xsl:output method="text" | Вывод — текст; возможны также xml и html. encoding выбирает байты результата. |
| xsl:template match="/" | Правило для корневого узла документа, с которого начинается обработка. |
| xsl:value-of select="catalog/product[1]/title" | Взять текст title первого product. В XPath позиции начинаются с 1. |
| xsl:for-each select="catalog/product" | Выполнить вложенные инструкции для каждого выбранного товара. |
| @sku | Атрибут sku текущего товара. title — его дочерний элемент. |
| xsl:text | Явно записать пробел/разделитель/перевод строки. Форматирование самого файла XSL не всегда равно желаемому выводу. |
| xsl:apply-templates | Передать выбранные узлы соответствующим шаблонам; это другой способ организовать обработку вместо одного большого for-each. |
| xsl:param name="heading" | Объявить настройку, которую можно передать из PHP. Значение читается как $heading в XPath. |
В XML символ & в исходном тексте записывается как &, а < внутри текста — как <. PHP-строка, XML-атрибут и XPath-выражение имеют разные правила кавычек. Nowdoc <<<'XSL' хранит шаблон без интерполяции PHP-переменных, поэтому $heading в нём остаётся именем параметра XSL. Сначала разберите строки и структуру XML, если эти уровни пока смешиваются.
Тот же каталог → HTML-список остатков
Теперь результат нужен человеку: заголовок «Склад на сегодня», ниже два товара и их остатки. Изменим только XSL-правила и параметр heading. Данные в catalog.xml останутся теми же.
report.xsl<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="UTF-8" indent="no"/>
<xsl:param name="heading" select="'Остатки'"/>
<xsl:template match="/">
<section><h1><xsl:value-of select="$heading"/></h1><ul>
<xsl:for-each select="catalog/product">
<li><xsl:value-of select="@sku"/><xsl:text>: </xsl:text><xsl:value-of select="name"/><xsl:text> — </xsl:text><xsl:value-of select="stock"/></li>
</xsl:for-each>
</ul></section>
</xsl:template>
</xsl:stylesheet>
xsl:for-each select="catalog/product" повторяет вложенную запись li для каждого товара. Внутри этого повтора @sku — атрибут текущего product, name и stock — его дочерние элементы. xsl:text задаёт точные пробелы и разделители. xsl:param объявляет заголовок, а $heading в XSL читает параметр, переданный из PHP.
<?php
declare(strict_types=1);
$data = new DOMDocument();
$rules = new DOMDocument();
if (!$data->load(__DIR__ . '/catalog.xml', LIBXML_NONET)
|| !$rules->load(__DIR__ . '/report.xsl', LIBXML_NONET)) {
throw new RuntimeException('Исходные файлы не прочитаны');
}
$processor = new XSLTProcessor();
$processor->setSecurityPrefs(XSL_SECPREF_DEFAULT | XSL_SECPREF_READ_FILE | XSL_SECPREF_READ_NETWORK);
if (!$processor->importStylesheet($rules)) { throw new RuntimeException('Шаблон не принят'); }
$processor->setParameter('', 'heading', 'Склад на сегодня');
if ($processor->transformToUri($data, __DIR__ . '/report.html') < 0) {
throw new RuntimeException('Отчёт не сохранён');
}
echo file_get_contents(__DIR__ . '/report.html');
Ожидаемый результат
<section><h1>Склад на сегодня</h1><ul><li>A1: Desk — 3</li><li>B2: Lamp — 0</li></ul></section>
report.html — готовый HTML-фрагмент, который можно открыть или включить в страницу отчёта. Он сгенерирован серверным XSLT, браузеру не требуется выполнять XSL. В этом наборе файлы известны и исправны; полная граница libxml-диагностики показана в следующем разделе «Полный пример». Запреты transform не заменяют контроль исходных XSL-файлов при импорте.
Поменяйте stock в catalog.xml на 7 — изменится соответствующая строка отчёта. Поменяйте heading через setParameter — изменится заголовок. Чтобы показывать только доступные товары, замените путь в for-each на catalog/product[number(stock) > 0]; Lamp с нулём исчезнет. Такая фильтрация представления не проверяет достоверность складских данных.
Полный пример: два файла данных и правил, затем PHP-запуск
Представьте ежедневный отчёт по каталогу. В catalog.xml лежат данные; в report.xsl — только правила текста; run.php связывает их. Это тот же принцип разделения XML/XSL, что в базовом руководстве, с небольшим прикладным каталогом вместо неявных файлов.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$xml = new DOMDocument();
$xsl = new DOMDocument();
if (!$xml->load(__DIR__ . '/catalog.xml', LIBXML_NONET)
|| !$xsl->load(__DIR__ . '/report.xsl', LIBXML_NONET)) {
throw new RuntimeException('Некорректные исходные файлы');
}
$processor = new XSLTProcessor();
$processor->setSecurityPrefs(XSL_SECPREF_DEFAULT | XSL_SECPREF_READ_FILE | XSL_SECPREF_READ_NETWORK);
if (!$processor->importStylesheet($xsl)) {
throw new RuntimeException('Некорректный XSL-шаблон');
}
$result = $processor->transformToXml($xml);
if ($result === false) {
throw new RuntimeException('Не удалось сформировать результат');
}
echo $result ?? '';
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
Ожидаемый результат
A: Tea
B: Coffee
Все файлы примера находятся рядом и предоставлены вместе с кодом. load читает файл, loadXML — уже готовый текст. NONET не означает общий запрет локальных файлов; приложение выбирает доверенные пути и шаблон.
Для HTTP-ответа добавляют подходящий Content-Type до первого echo. Для отправки в другую систему проверяют кодировку и контракт её формата. Внешний пользователь не должен выбирать произвольный путь report.xsl через GET; выбирайте шаблон из собственного списка.
Почему дерево есть, а нужный текст не находится
У поставщика может быть корень <catalog xmlns="urn:shop">. Эти элементы принадлежат пространству urn:shop. В XPath 1.0 простая запись catalog/product обозначает элементы без пространства и их не найдёт. Объявите свой префикс в XSL и используйте его в пути. Префикс не обязан совпадать с префиксом исходного XML; совпадать должен URI.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog xmlns="urn:shop"><product><title>Tea</title></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML('<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s="urn:shop"><xsl:output method="text"/><xsl:template match="/"><xsl:value-of select="s:catalog/s:product/s:title"/></xsl:template></xsl:stylesheet>');
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Tea
URI urn:shop связывает путь с элементами. Удаление xmlns из входных данных ради совпадения меняет их смысл и может скрыть ошибку интеграции.
Пустой вывод также бывает из-за неправильного контекста: внутри for-each текущим узлом становится товар, и путь title относится к нему; абсолютный /catalog/product/title начинается с документа. Сначала проверьте вход, namespace и XPath; не пытайтесь лечить всё htmlspecialchars или перекодировкой.
Все свойства XSLTProcessor и время применения настроек
| Свойство | Тип и смысл | Когда учитывать |
|---|---|---|
| doXInclude | bool, по умолчанию false. Включает обработку XInclude в контексте преобразования. | Дополнительные включения могут читать ресурсы; это не обычный xsl:include. Явное объявление свойства с PHP 8.4. |
| cloneDocument | bool, по умолчанию false. Просит выполнять преобразование на копии входного дерева. | Задайте до importStylesheet: в PHP 8.5.11 там определяется режим копирования. xsl:key может потребовать копию автоматически. Явно типизировано с PHP 8.4. |
| maxTemplateDepth | int, максимальная глубина вложения шаблонов. С PHP 8.4. | Начальное значение берётся из библиотеки libxslt при создании объекта. Это не глубина XML-дерева и не лимит секунд. |
| maxTemplateVars | int, лимит переменных/параметров шаблонов в контексте. С PHP 8.4. | Защита от чрезмерного выполнения; не лимит количества полей PHP-массива. |
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
$processor->cloneDocument = true;
$processor->doXInclude = false;
$processor->maxTemplateDepth = 1000;
$processor->maxTemplateVars = 5000;
var_dump($processor->cloneDocument, $processor->doXInclude);
echo $processor->maxTemplateDepth, '/', $processor->maxTemplateVars, "\n";
Ожидаемый результат
bool(true)
bool(false)
1000/5000
Это выбранные ограничения примера, не универсальные рекомендуемые числа. Подбирают по размеру и устройству своего шаблона, сохраняя ограничения ресурсов процесса.
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
$processor->maxTemplateDepth = 1000;
try {
$processor->maxTemplateDepth = -1;
} catch (ValueError $error) {
echo "Отрицательный лимит запрещён\n";
}
echo $processor->maxTemplateDepth, "\n";
Ожидаемый результат
Отрицательный лимит запрещён
1000
PHP 8.5.11 отвергает отрицательное значение и восстанавливает старое. Ноль допускается проверкой свойства, но не следует понимать его как документированную команду «снять все лимиты».
Для maxTemplateDepth/maxTemplateVars запрещены unset и косвенная модификация через ссылку: назначайте новое целое непосредственно свойству. cloneDocument увеличивает потребление памяти, но false не обещает полного отсутствия копий. Не используйте XSL_CLONE_NEVER как булево false: историческая константа имеет значение -1.
Все константы XSL: версии, клонирование и запреты
| Константа | Тип | Назначение |
|---|---|---|
| XSL_CLONE_AUTO | int | Историческое имя режима автоматического копирования, значение 0. Современное cloneDocument имеет тип bool; используйте false/true и учитывайте автоматическое копирование для xsl:key. |
| XSL_CLONE_NEVER | int | Историческое значение -1. Не присваивайте его bool-свойству с ожиданием запрета клонирования: -1 приводится к true при слабом приведении и не выражает «никогда». |
| XSL_CLONE_ALWAYS | int | Историческое значение 1. В современном API явно задают cloneDocument = true до импорта шаблона. |
| LIBXSLT_VERSION | int | Версия libxslt числом, например 10143. Это версия библиотеки, не PHP и не версия XSLT. |
| LIBXSLT_DOTTED_VERSION | string | Человекочитаемая версия libxslt, например 1.1.43. |
| LIBEXSLT_VERSION | int | Числовая версия EXSLT-библиотеки, если собрана соответствующая поддержка. |
| LIBEXSLT_DOTTED_VERSION | string | Версия EXSLT в строковой форме; наличие проверяется defined/hasExsltSupport. |
| XSL_SECPREF_NONE | int | Снять все эти ограничения libxslt. Не безопасный режим для чужого шаблона. |
| XSL_SECPREF_READ_FILE | int | Запретить чтение файлов при transform. |
| XSL_SECPREF_WRITE_FILE | int | Запретить запись файлов из XSLT. |
| XSL_SECPREF_CREATE_DIRECTORY | int | Запретить создание каталогов из XSLT. |
| XSL_SECPREF_READ_NETWORK | int | Запретить сетевое чтение из XSLT. |
| XSL_SECPREF_WRITE_NETWORK | int | Запретить сетевую запись из XSLT. |
| XSL_SECPREF_DEFAULT | int | WRITE_NETWORK | CREATE_DIRECTORY | WRITE_FILE: стандартный запрет записи. Не включает запрет чтения. |
Константы расширения доступны после его загрузки. Не фиксируйте числовые маски в своём коде: имена XSL_SECPREF_* объясняют намерение. Версия библиотеки помогает разбирать отличие двух серверов, но не заменяет проверку конкретной доступной функции. XSLTProcessor::getSecurityPrefs и XSLTProcessor::hasExsltSupport.
Где возникла ошибка: XML, шаблон, обработчик или запись результата
- load/loadXML: разобрать XML
- importStylesheet: скомпилировать XSL
- transform: выполнить XPath и callbacks
- Сериализовать/записать результат
libxml_use_internal_errors(true) позволяет забрать диагностику через libxml_get_errors, не выводя предупреждения посетителю. Очистите старый буфер перед операцией, после неё сохраните нужные сообщения в журнал и восстановите предыдущий режим в finally. Ошибки PHP-callback и неверные типы аргументов могут быть исключениями, а не только элементами массива libxml.
| Симптом | Что проверить |
|---|---|
| loadXML вернул true, importStylesheet false | Корректность XPath, namespace XSLT, допустимость инструкций верхнего уровня. |
| Нет шаблона у процессора | Не игнорировали ли false при импорте; не вызвали ли transform раньше импорта. |
| Пустой результат/null | Выбор узлов, контекст XPath, пространства имён, намеренно пустой шаблон. |
| Запрет document() | Нужен ли дополнительный ресурс; адрес, базовый URI и политика XSL_SECPREF. |
| Непонятный текст из .php | Чтение файла не запускает PHP через веб-сервер. Подготовьте данные явно, а не подставляйте путь к скрипту. |
| transformToUri вернул -1 | Права/путь/ошибка преобразования; 0 байтов сам по себе не ошибка. |
Сообщения, номера строк и пути предназначены разработчику. Клиенту отдавайте понятную ошибку с идентификатором операции. Не используйте @, чтобы сделать отчёт «рабочим»: оно скрывает предупреждение, но не исправляет результат.
Когда преобразование становится дорогим
Основные расходы: построение исходного дерева, компиляция XSL, обходы XPath, дерево результата и его сериализация. Повторный глобальный поиск //product внутри обработки каждого товара способен многократно обходить документ. Для повторного поиска по ключу изучают xsl:key/key(), но индекс и автоматическая копия также стоят памяти. Сначала измеряют реальную задачу, затем выбирают алгоритм.
Повторно используйте скомпилированный шаблон, если правила и границы доверия одинаковы; сбрасывайте параметры прошлого задания. XSLTProcessor::setProfiling показывает затраты шаблонов. Не вызывайте БД или HTTP внутри callback для каждого узла: заранее загрузите справочник и работайте с ним в памяти в разумных пределах.
| Задача | Подход |
|---|---|
| Известный XML → другое представление | XSLT отделяет правила преобразования от данных. |
| Огромный файл → одна запись за раз | XMLReader; при необходимости преобразовывать отдельные независимые фрагменты. |
| Массив → простой XML-экспорт | XMLWriter без дерева всех результатов. |
| Небольшой XML → простые значения | SimpleXML или DOM могут быть проще отдельного XSL. |
Вход и шаблон могут быть маленькими, а результат очень большим из-за циклов/рекурсии. Ограничивайте размер входа, время задания, память и объём вывода на уровне рабочего процесса. XSL_SECPREF не ограничивает вычислительную сложность.
Все методы XSLTProcessor
| Метод | Для чего нужен |
|---|---|
| XSLTProcessor::__construct | Создать отдельный процессор с настройками для преобразования XML. |
| XSLTProcessor::importStylesheet | Скомпилировать XSL-шаблон перед преобразованием одного или нескольких XML-документов. |
| XSLTProcessor::transformToXml | Получить готовый результат строкой: XML, HTML или текст в зависимости от xsl:output. |
| XSLTProcessor::transformToDoc | Получить дерево результата, чтобы продолжить работу с узлами вместо повторного разбора строки. |
| XSLTProcessor::transformToUri | Сформировать выгрузку сразу в выбранный приложением файл и получить число записанных байтов. |
| XSLTProcessor::setParameter | Передать заголовок, язык или другой параметр из PHP в доверенный XSL-шаблон. |
| XSLTProcessor::getParameter | Прочитать установленное PHP-кодом значение параметра процессора. |
| XSLTProcessor::removeParameter | Удалить внешнее значение и снова дать шаблону использовать собственное значение по умолчанию. |
| XSLTProcessor::getSecurityPrefs | Узнать, какие чтения и записи запрещены при выполнении XSLT. |
| XSLTProcessor::setSecurityPrefs | Установить запреты на дополнительные файловые и сетевые операции внутри преобразования. |
| XSLTProcessor::hasExsltSupport | Проверить, собрана ли поддержка библиотеки расширений EXSLT. |
| XSLTProcessor::setProfiling | Записать профиль выполнения XSL-шаблонов, чтобы найти дорогие участки преобразования. |
| XSLTProcessor::registerPHPFunctions | Разрешить шаблону вызывать конкретные PHP-функции через пространство http://php.net/xsl. |
| XSLTProcessor::registerPHPFunctionNS | Дать XSL понятную прикладную функцию вроде shop:label без перечисления глобальных PHP-имён. |
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
XSLTProcessor::__constructСоздать отдельный процессор с настройками для преобразования XML.
new XSLTProcessor()Создать отдельный процессор с настройками для преобразования XML.
| Часть вызова | Что передать или получить |
|---|---|
| Аргументы | Конструктор не принимает параметров. |
| Результат new | Объект XSLTProcessor. Шаблон, входные данные и результат ещё не созданы. |
| Жизненный цикл | Импортируйте XSL, установите параметры, затем преобразуйте XML. Один объект хранит настройки между вызовами. |
- new: пустой процессор
- importStylesheet: правила
- transformToXml: вход → результат
<?php
declare(strict_types=1);
$data = new DOMDocument();
$data->load(__DIR__ . '/catalog.xml');
$rules = new DOMDocument();
$rules->load(__DIR__ . '/first.xsl');
$processor = new XSLTProcessor();
$processor->importStylesheet($rules);
echo 'Первый товар в отчёте: ', $processor->transformToXml($data), "\n";
Ожидаемый результат
Первый товар в отчёте: Desk
Конструктор не принимает аргументы и создаёт только процессор. Файлы данных и правил подготовлены отдельно; два следующих вызова импортируют правила и запускают преобразование. Второй пример ниже показывает конкретный Error, если этап импорта пропустить.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog/>');
$processor = new XSLTProcessor();
try {
$processor->transformToXml($xml);
} catch (Error $error) {
echo "Сначала импортируйте XSL\n";
}
Ожидаемый результат
Сначала импортируйте XSL
В актуальном PHP отсутствие импортированного шаблона приводит к Error. Это ошибка порядка вызовов, а не повод повторять запрос к поставщику.
В длительно работающем обработчике параметры предыдущего задания сохраняются в объекте. Либо устанавливайте полный набор и удаляйте прежние значения, либо создавайте отдельный процессор для независимого задания. Присваивание объекта другой переменной не создаёт независимую копию его состояния.
Связанные инструменты: XSLTProcessor::importStylesheet; XSLTProcessor::removeParameter.
XSLTProcessor::importStylesheetСкомпилировать XSL-шаблон перед преобразованием одного или нескольких XML-документов.
public importStylesheet(object $stylesheet): boolСкомпилировать XSL-шаблон перед преобразованием одного или нескольких XML-документов.
| Часть вызова | Что передать или получить |
|---|---|
| stylesheet | XML-объект с правилами XSLT: например DOMDocument, SimpleXMLElement; с PHP 8.4 поддерживается также современный Dom\Document. |
| Возврат | true: шаблон импортирован; false: компиляция не удалась. Ошибки libxml нужно забрать отдельно. |
| Ошибки | Неподходящий объект даёт TypeError в PHP 8.4+; ранее ValueError. Успешный loadXML ещё не доказывает корректность XSLT. |
- XSL-текст
- XML-дерево шаблона
- Компиляция правил
- Процессор готов к transform
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="catalog/product[1]/title"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Tea
loadXML понимает текст XML. importStylesheet понимает назначения xsl:template и xsl:value-of. Это разные этапы; разделяйте их ошибки.
<?php
declare(strict_types=1);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$rules = new DOMDocument();
if ($rules->loadXML('<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template match="/"><xsl:value-of select="["/></xsl:template></xsl:stylesheet>')) {
echo "XML шаблона разобран\n";
}
$processor = new XSLTProcessor();
if (!$processor->importStylesheet($rules)) {
echo "Отчёт не запускаем: незавершённый XPath в select\n";
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
XML шаблона разобран
Отчёт не запускаем: незавершённый XPath в select
В известном fixture одна [ допустима как текст XML-атрибута, но не как законченное XPath-выражение. Поэтому loadXML может быть успешным, а importStylesheet отказывает. Диагностика libxml содержит подробности; её не превращают в публичный HTML без ограничения/экранирования.
Импортируйте один доверенный шаблон и применяйте к нескольким документам: это убирает повторную компиляцию. При замене файла шаблона переимпортируйте его. Изменение исходного DOM после импорта не следует считать изменением уже скомпилированных правил. Относительные xsl:include/import зависят от базового URI: загрузка строки и загрузка файла имеют разные исходные адреса. Политика setSecurityPrefs для этапа transform не заменяет ограничения чтений при импорте.
Связанные инструменты: XSLTProcessor::__construct; XSLTProcessor::transformToXml; XSLTProcessor::setSecurityPrefs.
XSLTProcessor::transformToXmlПолучить готовый результат строкой: XML, HTML или текст в зависимости от xsl:output.
public transformToXml(object $document): string|null|falseПолучить готовый результат строкой: XML, HTML или текст в зависимости от xsl:output.
| Часть вызова | Что передать или получить |
|---|---|
| document | XML-объект с исходными данными; обычно DOMDocument или другой поддерживаемый XML-объект. |
| Строка | Сериализованный результат. Название метода не заставляет использовать XML: method="text" выдаёт текст. |
| null | Успешная сериализация пустого результата может дать null. Не смешивайте с ошибкой false. |
| false / исключение | false при отказе преобразования/сериализации; отсутствие шаблона, неверный XML-объект и ошибки PHP-callback имеют собственные Error/TypeError. |
- Дерево данных
- Правила XSLT
- Дерево результата
- Сериализованная строка
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:for-each select="catalog/product"><xsl:value-of select="@sku"/><xsl:text>: </xsl:text><xsl:value-of select="title"/><xsl:text> </xsl:text></xsl:for-each></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$result = $processor->transformToXml($xml);
if ($result === false) {
throw new RuntimeException('Не удалось сформировать отчёт');
}
echo $result ?? '';
Ожидаемый результат
A: Tea
B: Coffee
for-each перебирает выбранные product; @sku — атрибут; xsl:text явно сохраняет разделители и перевод строки. Итог можно вернуть HTTP-ответом или положить в письмо.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$result = $processor->transformToXml($xml);
if ($result === false) { echo "Ошибка формирования отчёта\n"; }
elseif ($result === null || $result === '') { echo "Шаблон не вывел строк: отчёт пуст\n"; }
else { echo $result; }
Ожидаемый результат
Шаблон не вывел строк: отчёт пуст
Шаблон намеренно пуст: преобразование не равно отказу. Условие if (!$result) смешало бы пустой успешный результат и false. В каталожном отчёте неожиданную пустоту стоит проверять по пути XPath и namespace.
encoding в xsl:output определяет байты результата. Если передаёте HTTP-ответ, Content-Type и charset должны соответствовать им. Метод создаёт строку целиком; большой DOM + дерево результата + строка могут одновременно занимать память. Для простого поточного экспорта рассмотрите XMLWriter, а не автоматическое увеличение memory_limit.
Связанные инструменты: XSLTProcessor::transformToDoc; XSLTProcessor::transformToUri.
XSLTProcessor::transformToDocПолучить дерево результата, чтобы продолжить работу с узлами вместо повторного разбора строки.
public transformToDoc(object $document, ?string $returnClass = null): object|falseПолучить дерево результата, чтобы продолжить работу с узлами вместо повторного разбора строки.
| Часть вызова | Что передать или получить |
|---|---|
| document | Исходное XML-дерево. |
| returnClass | Необязательное имя совместимого класса результата; типичный случай — свой наследник DOMDocument. null использует создание DOM-объекта по умолчанию. |
| Возврат | Объект дерева или false при отказе. Несовместимый/неизвестный класс даёт TypeError. Не обещает автоматически тот же класс, что у входа. |
- XML + XSL
- Новое дерево
- Чтение/редактирование узлов
- Отдельная сериализация при необходимости
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="xml" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><summary><first><xsl:value-of select="catalog/product[1]/title"/></first></summary></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$result = $processor->transformToDoc($xml);
if (!$result instanceof DOMDocument) { throw new RuntimeException('Нет дерева отчёта'); }
$result->documentElement->setAttribute('checked', 'yes');
echo $result->saveXML($result->documentElement), "\n";
Ожидаемый результат
<summary checked="yes"><first>Tea</first></summary>
transformToDoc даёт новое дерево, поэтому сразу меняем его атрибут, без transformToXml→строка→loadXML. saveXML в конце нужен лишь для показа фактической структуры. Отметка checked здесь демонстрирует редактирование, не доказывает реальную складскую проверку.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="xml" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><summary>ready</summary></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
class ExportDocument extends DOMDocument
{
public function status(): string
{
return $this->documentElement->textContent;
}
}
$result = $processor->transformToDoc($xml, ExportDocument::class);
if (!$result instanceof ExportDocument) {
throw new RuntimeException('Нет документа экспорта');
}
echo $result->status(), "\n";
Ожидаемый результат
ready
::class передаёт имя класса, а не создаёт объект. Пример показывает расширение DOMDocument; не рассчитывайте, что фабрика вызовет ваш пользовательский конструктор и его побочные действия.
Современные Dom\XMLDocument и старый DOMDocument — разные семейства API. Поддержка современных входов с PHP 8.4 не означает взаимозаменяемость всех классов и методов результата. Для переносимого кода явно фиксируйте семейство входа/выхода; пример выше целиком использует DOMDocument. Дерево текстового результата может не иметь обычного documentElement — выбирайте XML-структуру, когда нужны элементы.
Связанные инструменты: XSLTProcessor::transformToXml; XSLTProcessor::importStylesheet.
XSLTProcessor::transformToUriСформировать выгрузку сразу в выбранный приложением файл и получить число записанных байтов.
public transformToUri(object $document, string $uri): intСформировать выгрузку сразу в выбранный приложением файл и получить число записанных байтов.
| Часть вызова | Что передать или получить |
|---|---|
| document | Исходное XML-дерево. |
| uri | Назначение результата: например абсолютный путь в разрешённом каталоге экспорта. Не путь из произвольного запроса клиента. |
| Возврат PHP 8.5.11 | Число байтов при успехе, -1 при отказе преобразования/сохранения. 0 может быть успешным пустым результатом. Старый текст справочника с false не соответствует int-сигнатуре и текущей реализации. |
| Ошибки | Неверный вход/отсутствие шаблона могут бросить исключение; ошибки записи могут сопровождаться предупреждениями. |
- Дерево входа
- Преобразование в памяти
- Сохранение в uri
- Количество байтов или -1
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:for-each select="catalog/product"><xsl:value-of select="@sku"/><xsl:text>: </xsl:text><xsl:value-of select="title"/><xsl:text> </xsl:text></xsl:for-each></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$path = __DIR__ . '/export.txt';
if ($processor->transformToUri($xml, $path) < 0) {
throw new RuntimeException('Ведомость не сохранена');
}
echo file_get_contents($path);
Ожидаемый результат
A: Tea
B: Coffee
Числовой возврат сообщает запись/отказ, содержимое находится в export.txt. Выводим саму ведомость. Файл остаётся в учебной папке; этот метод всё равно строит результат преобразования в памяти libxslt и не является потоковым XMLReader.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$path = __DIR__ . '/empty.txt';
$written = $processor->transformToUri($xml, $path);
if ($written < 0) { echo "Ошибка сохранения\n"; }
elseif ($written === 0) { echo "empty.txt создан пустым: проверьте, ожидается ли пустой отчёт\n"; }
Ожидаемый результат
empty.txt создан пустым: проверьте, ожидается ли пустой отчёт
0 байтов допустим для успешного пустого преобразования. Отказ текущего PHP8.5.11 — отрицательное число. Существование файла не делает его подходящим для бизнес-задачи: наш оператор отдельно решает, почему отчёт не содержит строк.
Метод избегает дополнительной PHP-строки результата, но libxslt всё равно строит результат в памяти: это не потоковая обработка гигабайтного XML. setSecurityPrefs регулирует операции внутри XSLT; он не является разрешением для переданного PHP-кодом выходного пути и не заменяет контроль каталога/прав ОС. Сетевые URI и wrapper зависят от сборки и окружения; эти примеры проверены только с локальным файлом.
Связанные инструменты: XSLTProcessor::transformToXml; XSLTProcessor::transformToDoc; XSLTProcessor::setSecurityPrefs.
XSLTProcessor::setParameterПередать заголовок, язык или другой параметр из PHP в доверенный XSL-шаблон.
public setParameter(string $namespace, string $name, string $value): bool
public setParameter(string $namespace, array $options): boolПередать заголовок, язык или другой параметр из PHP в доверенный XSL-шаблон.
| Часть вызова | Что передать или получить |
|---|---|
| namespace | URI пространства параметра; пустая строка для обычного непредикатного имени. С PHP 8.5 значение действительно учитывается; в более ранних версиях игнорировалось. |
| name / value | Имя объявленного xsl:param и строковое значение. Передавайте PHP-переменную $heading, а не строку '$heading'. Это значение, не исполняемое XPath-выражение. |
| options | Альтернативная форма: массив имя => строка для нескольких параметров одного пространства. |
| Возврат / ошибки | bool; неверные типы TypeError. С PHP 8.4 NUL в строковых аргументах ValueError; в 8.5 при непустом namespace нельзя одновременно задавать имя QName/Clark notation. |
| Версия кавычек | С PHP 8.4 корректно поддерживаются строки одновременно с одинарной и двойной кавычками; старые советы по их замене меняли исходный текст. |
- PHP $heading
- setParameter: сохраняется строка
- XSL $heading
- value-of выводит результат
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="heading" select="'Catalog'"/><xsl:template match="/"><xsl:value-of select="$heading"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$heading = 'Products';
var_dump($processor->setParameter('', 'heading', $heading));
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
bool(true)
Products
PHP-переменная содержит строку Products. XSL-переменная с тем же именем получает её через явный мост setParameter. Вставка $heading в кавычках как текста не выполнит PHP-код.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="heading" select="'Catalog'"/><xsl:param name="currency" select="'RUB'"/><xsl:template match="/"><xsl:value-of select="$heading"/><xsl:text>: </xsl:text><xsl:value-of select="$currency"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setParameter('', ['heading' => 'Price list', 'currency' => 'EUR']);
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Price list: EUR
Второй аргумент — массив настроек, поэтому третий не передают. Неизвестный шаблону параметр не создаёт новый вывод автоматически: правила должны его использовать.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="heading" select="'Catalog'"/><xsl:template match="/"><xsl:value-of select="$heading"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setParameter('', 'heading', 'Alice\'s "Tea"');
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Alice's "Tea"
Обратная косая черта защищает одинарную кавычку в PHP-литерале. Не нужно заменять реальные кавычки на другие символы или включать disable-output-escaping.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:report="urn:report"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="report:heading" select="'Default'"/><xsl:template match="/"><xsl:value-of select="$report:heading"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setParameter('urn:report', 'heading', 'Namespaced title');
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Namespaced title
PHP передаёт URI urn:report и локальное имя heading; префикс report объявлен в XSL и может быть другим. Этот контракт namespace требует PHP 8.5, его нельзя переносить на 8.4 без проверки.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="position" select="'1'"/><xsl:template match="/"><xsl:value-of select="catalog/product[position() = number($position)]/title"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setParameter('', 'position', '2');
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Coffee
В XPath 1.0 product[$position] при строковом непустом $position проверяет истинность строки и может выбрать все товары. position() = number($position) выражает намерение явно. Внешнее значение позиции ещё надо проверить как допустимое целое.
Не путайте экранирование XML, строковый параметр и доверие к ссылке. value-of экранирует текст при XML/HTML-сериализации, но если параметр используется как href/src или путь document(), приложение отдельно ограничивает допустимые схемы и адреса. Параметры остаются в процессоре между преобразованиями; удаляйте настройки прошлого клиента.
Связанные инструменты: XSLTProcessor::getParameter; XSLTProcessor::removeParameter; XSLTProcessor::transformToXml.
XSLTProcessor::getParameterПрочитать установленное PHP-кодом значение параметра процессора.
public getParameter(string $namespace, string $name): string|falseПрочитать установленное PHP-кодом значение параметра процессора.
| Часть вызова | Что передать или получить |
|---|---|
| namespace / name | Та же пара URI и имени, что при setParameter. Пустой namespace для обычного параметра. С PHP 8.5 пространство учитывается. |
| Строка | Последнее сохранённое значение; пустая строка также допустима. |
| false | У процессора нет такого параметра. Метод не извлекает select/default из xsl:param внутри шаблона. |
| Ошибки | NUL в аргументе ValueError с PHP 8.4; неоднозначное квалифицированное имя при непустом namespace ValueError с PHP 8.5. |
- PHP сохранённые параметры
- Поиск URI + имя
- Строка либо false
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="heading" select="'Catalog'"/><xsl:template match="/"><xsl:value-of select="$heading"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
var_dump($processor->getParameter('', 'heading'));
echo $processor->transformToXml($xml), "\n";
$processor->setParameter('', 'heading', 'Orders');
echo $processor->getParameter('', 'heading'), "\n";
Ожидаемый результат
bool(false)
Catalog
Orders
Шаблон уже знает Catalog, но процессор не имеет внешнего override. getParameter показывает только назначение через PHP; это удобно для журнала конфигурации отчёта.
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
$processor->setParameter('', 'heading', '');
if ($processor->getParameter('', 'heading') === '') {
echo "Заголовок явно оставлен пустым\n";
}
if ($processor->getParameter('', 'missing') === false) {
echo "Внешняя настройка missing не задана\n";
}
Ожидаемый результат
Заголовок явно оставлен пустым
Внешняя настройка missing не задана
getParameter читает параметры, назначенные PHP, а не select/default внутри XSL. Пустая строка существует как значение; false означает отсутствие. Для возвращения default используют removeParameter, а не пустую строку.
Связанные инструменты: XSLTProcessor::setParameter; XSLTProcessor::removeParameter.
XSLTProcessor::removeParameterУдалить внешнее значение и снова дать шаблону использовать собственное значение по умолчанию.
public removeParameter(string $namespace, string $name): boolУдалить внешнее значение и снова дать шаблону использовать собственное значение по умолчанию.
| Часть вызова | Что передать или получить |
|---|---|
| namespace / name | Пара URI и имени внешнего параметра; правила имён такие же, как у setParameter. |
| true | Сохранённый параметр удалён. |
| false | Удалять было нечего. Это не ошибка компиляции и не удаление объявления xsl:param из XSL. |
- Override из PHP
- Удаление записи
- Следующий transform использует default XSL
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="heading" select="'Catalog'"/><xsl:template match="/"><xsl:value-of select="$heading"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setParameter('', 'heading', 'Special');
echo $processor->transformToXml($xml), "\n";
var_dump($processor->removeParameter('', 'heading'));
echo $processor->transformToXml($xml), "\n";
var_dump($processor->removeParameter('', 'heading'));
Ожидаемый результат
Special
bool(true)
Catalog
bool(false)
Повторное удаление возвращает false, потому что первого назначения уже нет. Это удобно в рабочем процессе, где один шаблон служит нескольким экспортам.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:param name="heading" select="'Catalog'"/><xsl:template match="/"><xsl:value-of select="$heading"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setParameter('', 'heading', '');
var_dump($processor->transformToXml($xml));
$processor->removeParameter('', 'heading');
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
NULL
Catalog
Пустой override просит вывести пустое значение. Удаление возвращает Catalog. Не пытайтесь очистить конфигурацию заменой всех параметров на пустые строки.
Связанные инструменты: XSLTProcessor::getParameter; XSLTProcessor::setParameter.
XSLTProcessor::getSecurityPrefsУзнать, какие чтения и записи запрещены при выполнении XSLT.
public getSecurityPrefs(): intУзнать, какие чтения и записи запрещены при выполнении XSLT.
| Часть вызова | Что передать или получить |
|---|---|
| Аргументы | Нет. |
| Результат | Целочисленная битовая маска запрещённых действий. Наличие флага означает запрет, а не разрешение. |
| По умолчанию | XSL_SECPREF_DEFAULT запрещает запись в файлы, создание каталогов и запись в сеть; чтение файла/сети нужно запрещать дополнительно при необходимости. |
- Процессор
- Маска ограничений
- Проверка отдельного бита через &
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
$mask = $processor->getSecurityPrefs();
echo ($mask & XSL_SECPREF_WRITE_FILE) !== 0 ? "Запись из XSLT запрещена\n" : "Запись из XSLT разрешена\n";
echo ($mask & XSL_SECPREF_READ_FILE) !== 0 ? "Чтение из XSLT запрещено\n" : "Чтение из XSLT не запрещено этой маской\n";
Ожидаемый результат
Запись из XSLT запрещена
Чтение из XSLT не запрещено этой маской
& проверяет отдельный бит маски. Это диагностический первый шаг: по умолчанию запреты записи не означают запрет чтения. Они относятся к transform libxslt, а не ко всем операциям PHP; рабочий пример отказа document() приведён в setter.
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
$processor->setSecurityPrefs(XSL_SECPREF_DEFAULT | XSL_SECPREF_READ_NETWORK);
$blocked = ($processor->getSecurityPrefs() & XSL_SECPREF_READ_NETWORK) !== 0;
echo $blocked ? "XSLT network read blocked\n" : "XSLT network read not blocked\n";
Ожидаемый результат
XSLT network read blocked
Это проверка настройки, без реального сетевого запроса. Она не утверждает, что PHP-callback, DOM load или импорт шаблона охватывается той же границей.
Связанные инструменты: XSLTProcessor::setSecurityPrefs; XSLTProcessor::registerPHPFunctions.
XSLTProcessor::setSecurityPrefsУстановить запреты на дополнительные файловые и сетевые операции внутри преобразования.
public setSecurityPrefs(int $preferences): intУстановить запреты на дополнительные файловые и сетевые операции внутри преобразования.
| Часть вызова | Что передать или получить |
|---|---|
| preferences | Биты XSL_SECPREF_* объединяют через |. XSL_SECPREF_NONE равен отсутствию этих ограничений, а не самому строгому режиму. |
| Возврат | Предыдущая маска, удобная для восстановления в finally; это не bool успеха. |
| Граница действия | Политика libxslt на этапе transform. Не полноценная песочница PHP, не лимит CPU/памяти, не общий запрет файлов для приложения. |
- Выбрать запреты
- Запомнить прежнюю маску
- Запустить transform
- Восстановить настройку
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
$denyAll = XSL_SECPREF_READ_FILE | XSL_SECPREF_WRITE_FILE
| XSL_SECPREF_CREATE_DIRECTORY | XSL_SECPREF_READ_NETWORK
| XSL_SECPREF_WRITE_NETWORK;
$previous = $processor->setSecurityPrefs($denyAll);
var_dump($previous === XSL_SECPREF_DEFAULT);
var_dump($processor->getSecurityPrefs() === $denyAll);
$processor->setSecurityPrefs($previous);
Ожидаемый результат
bool(true)
bool(true)
Полезно для доверенного шаблона, которому не нужны document() и дополнительные файлы. Сам XML и XSL должны также загружаться с подходящими ограничениями libxml.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="document('extra.xml')/extra"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->setSecurityPrefs(XSL_SECPREF_DEFAULT | XSL_SECPREF_READ_FILE);
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
if ($processor->transformToXml($xml) === false) {
echo "Отчёт отклонён: шаблону запрещено дополнительное чтение\n";
}
} finally { libxml_clear_errors(); libxml_use_internal_errors($previous); }
Ожидаемый результат
Отчёт отклонён: шаблону запрещено дополнительное чтение
extra.xml реально находится в учебной папке. Причина отказа — настройка READ_FILE, а не отсутствующий fixture. Запрет трансформации не заменяет контроль xsl:include/import при компиляции и не ограничивает произвольный PHP callback.
Не лечите ошибку внешнего чтения вызовом setSecurityPrefs(XSL_SECPREF_NONE). Сначала выясните, нужен ли этот ресурс, кто управляет его адресом и на каком этапе он читается. xsl:include/import обрабатываются при компиляции шаблона; загружать XML нужно отдельно с политикой libxml. PHP-callback способен обращаться к файлам и сети от имени процесса независимо от этих флагов. Внешние ресурсы libxml.
Связанные инструменты: XSLTProcessor::getSecurityPrefs; XSLTProcessor::importStylesheet; XSLTProcessor::registerPHPFunctionNS.
XSLTProcessor::hasExsltSupportПроверить, собрана ли поддержка библиотеки расширений EXSLT.
public hasExsltSupport(): boolПроверить, собрана ли поддержка библиотеки расширений EXSLT.
| Часть вызова | Что передать или получить |
|---|---|
| Аргументы | Нет. |
| true / false | true — сборка PHP использует библиотеку EXSLT; false — такой поддержки нет. Это не результат конкретного преобразования. |
| Ограничение | EXSLT расширяет XSLT 1.0 отдельными функциями, но не превращает libxslt в процессор XSLT 2.0/3.0. |
- Проверить сборку
- Выбрать совместимый шаблон
- Использовать конкретную EXSLT-функцию
<?php
declare(strict_types=1);
$processor = new XSLTProcessor();
echo $processor->hasExsltSupport() ? "EXSLT available\n" : "EXSLT unavailable\n";
Ожидаемый результат
EXSLT available
Ожидаемый результат относится к проверенной сборке PHP 8.5.11. На другом сервере ветка unavailable — допустимый результат проверки, а не поломка кода.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:variable name="temporary"><item>Tea</item><item>Coffee</item></xsl:variable><xsl:for-each select="exsl:node-set($temporary)/item"><xsl:value-of select="."/><xsl:text> </xsl:text></xsl:for-each></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
if (!$processor->hasExsltSupport()) { throw new RuntimeException('Этот шаблон требует EXSLT'); }
echo $processor->transformToXml($xml);
Ожидаемый результат
Tea
Coffee
В XSLT1.0 временный фрагмент результата ещё не node-set для такого обхода. exsl:node-set превращает его в доступный набор узлов; теперь foreach выдаёт названия. Поддержка EXSLT не означает XSLT2/3; конкретную функцию также можно проверить в XSL через function-available.
Связанные инструменты: XSLTProcessor::importStylesheet; XSLTProcessor::transformToXml.
XSLTProcessor::setProfilingЗаписать профиль выполнения XSL-шаблонов, чтобы найти дорогие участки преобразования.
public setProfiling(?string $filename): trueЗаписать профиль выполнения XSL-шаблонов, чтобы найти дорогие участки преобразования.
| Часть вызова | Что передать или получить |
|---|---|
| filename | Путь файла профиля; null отключает профилирование. Каталог выбирает приложение, файл не публикуют в webroot. |
| Возврат | Всегда true при принятии настройки. Файл открывается позже во время transform, поэтому это не подтверждение его успешной записи. |
| Ошибки | NUL в пути даёт ValueError; отсутствие прав/open_basedir могут помешать записи на этапе выполнения. Точные времена зависят от системы. |
- Настроить имя файла
- Выполнить transform
- Прочитать статистику шаблонов
- Отключить profiling
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="catalog/product[1]/title"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$profile = __DIR__ . '/xsl-profile.txt';
$processor->setProfiling($profile);
echo 'Результат: ', $processor->transformToXml($xml), "\n";
$processor->setProfiling(null);
$text = file_get_contents($profile);
if ($text === false || !preg_match('/^\s*0\s+\/\s+(\d+)\s+/m', $text, $match)) {
throw new RuntimeException('Не найдена строка корневого шаблона в профиле');
}
echo 'Профиль: шаблон / выполнен ', $match[1], " раз\n";
Ожидаемый результат
Результат: Tea
Профиль: шаблон / выполнен 1 раз
Полный xsl-profile.txt остаётся рядом и содержит также меняющиеся времена. Для точного учебного вывода извлечена стабильная строка имени/числа вызовов из профиля проверенной libxslt. Это доказывает реальное выполнение шаблона, а не только наличие пустого файла. При другой форме профиля используйте сам файл; настройка пути возвращает true до записи.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="catalog/product[1]/title"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$profile = __DIR__ . '/disabled-profile.txt';
$processor->setProfiling($profile);
$processor->setProfiling(null);
echo $processor->transformToXml($xml), "\n";
var_dump(is_file($profile));
Ожидаемый результат
Tea
bool(false)
Настройка пути сама не создаёт файл. Профилирование имеет накладные расходы и обычно включается на время измерения конкретного медленного отчёта.
Повторное преобразование с тем же путём может перезаписать файл профиля. Для параллельных заданий нужны отдельные имена. Измеряйте также загрузку XML, компиляцию XSL и сериализацию: профиль шаблонов не описывает весь HTTP-запрос или SQL-подготовку данных.
Связанные инструменты: XSLTProcessor::transformToXml; XSLTProcessor::importStylesheet.
XSLTProcessor::registerPHPFunctionsРазрешить шаблону вызывать конкретные PHP-функции через пространство http://php.net/xsl.
public registerPHPFunctions(array|string|null $functions = null): voidРазрешить шаблону вызывать конкретные PHP-функции через пространство http://php.net/xsl.
| Часть вызова | Что передать или получить |
|---|---|
| functions: string | Одно разрешённое имя, например strtoupper. |
| functions: array | Список имён либо, с PHP 8.4, словарь публичное имя => callable. Выбирайте только необходимые обработчики. |
| functions: null | Разрешает все PHP-функции. Вызов без аргумента имеет именно этот смысл; это широкое разрешение. |
| Возврат | void: метод только регистрирует обработчики. Их результат появляется при последующем вызове из XSLT. |
| Ошибки с PHP 8.4 | Некорректное имя ValueError; невызываемый обработчик TypeError. Ошибка внутри callback распространяется из transform. |
- Зарегистрировать разрешённое имя
- XSL php:function вызывает обработчик
- PHP получает аргументы
- Результат возвращается в XPath
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="php:function('strtoupper', string(catalog/product[1]/title))"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->registerPHPFunctions('strtoupper');
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
TEA
В XSL объявлен префикс php. Первый аргумент php:function — разрешённое имя. string(...) преобразует набор узлов в текст до входа в PHP; strtoupper здесь работает с ASCII-названием.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="php:function('stockLabel', number(catalog/product[1]/price))"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->registerPHPFunctions([
'stockLabel' => static function (float $price): string {
return $price >= 400 ? 'premium' : 'regular';
},
]);
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
regular
Именем stockLabel шаблон обращается к callable, а не обязательно к глобальной одноимённой PHP-функции. XPath 1.0 передаёт числа как float; поэтому контракт callback должен это учитывать.
php:function передаёт node-set как массив DOM-узлов; явный string(...) подходит, если нужен текст. Вариант php:functionString преобразует узловые аргументы в строковую форму; выбирайте по контракту обработчика. Результаты должны быть совместимы с мостом XPath: число, строка, bool либо поддерживаемый DOM-узел. Произвольный объект приложения не становится XML автоматически.
Регистрация не предназначена для открытия всех PHP-функций недоверенному шаблону. Даже «только чтение XML» через callback может превратиться в запись файла, SQL или сеть. Используйте новый процессор с явным минимальным набором для каждого профиля доверия; не считайте повторную регистрацию пустого массива универсальным сбросом прежних разрешений.
Связанные инструменты: XSLTProcessor::registerPHPFunctionNS; XSLTProcessor::setSecurityPrefs.
XSLTProcessor::registerPHPFunctionNSДать XSL понятную прикладную функцию вроде shop:label без перечисления глобальных PHP-имён.
public registerPHPFunctionNS(string $namespaceURI, string $name, callable $callable): voidДать XSL понятную прикладную функцию вроде shop:label без перечисления глобальных PHP-имён.
| Часть вызова | Что передать или получить |
|---|---|
| namespaceURI | URI собственного пространства, например urn:shop. Пространство http://php.net/xsl зарезервировано и даёт ValueError. |
| name | Локальное допустимое имя XML-функции без двоеточия, например label; неверное имя ValueError. |
| callable | Функция, замыкание или метод, вызываемый из XSL. Невызываемое значение TypeError. |
| Входные значения | Node-set превращается в массив DOM-узлов; number в XPath — float, string — строка. Не предполагайте, что один узел автоматически пришёл строкой. |
| Возврат | void при регистрации. Сам callback возвращает значение в выражение XSL; доступно с PHP 8.4. |
- urn:shop + label → PHP callable
- XSL shop:label(...)
- Аргументы → тело PHP
- return → исходное выражение XSL
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:shop="urn:shop"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="shop:label(string(catalog/product[1]/title))"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->registerPHPFunctionNS('urn:shop', 'label',
static fn (string $title): string => 'Product: ' . $title
);
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
Product: Tea
Префикс shop связывается с urn:shop внутри XSL. registerPHPFunctionNS использует URI и локальное label. PHP return возвращает строку в value-of, после чего продолжается тот же шаблон.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:shop="urn:shop"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="shop:countExpensive(catalog/product)"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->registerPHPFunctionNS('urn:shop', 'countExpensive',
static function (array $products): int {
$count = 0;
foreach ($products as $product) {
$price = (int) $product->getElementsByTagName('price')->item(0)->textContent;
if ($price >= 400) {
$count++;
}
}
return $count;
}
);
echo $processor->transformToXml($xml), "\n";
Ожидаемый результат
1
Здесь массив содержит DOMElement, не строки. Форма XML полностью известна, поэтому price есть у каждого товара; для внешнего каталога нужны проверки отсутствия/формата. Простой порог можно выразить XPath; callback полезен, когда требуется уже существующее правило PHP, а не ради усложнения.
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalog><product sku="A"><title>Tea</title><price>250</price></product><product sku="B"><title>Coffee</title><price>450</price></product></catalog>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:shop="urn:shop"><xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/><xsl:template match="/"><xsl:value-of select="shop:fail()"/></xsl:template></xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
$processor->registerPHPFunctionNS('urn:shop', 'fail', static function (): string {
throw new DomainException('Unknown customer');
});
$previous = libxml_use_internal_errors(true);
try {
$processor->transformToXml($xml);
} catch (DomainException $error) {
echo $error->getMessage(), "\n";
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
Ожидаемый результат
Unknown customer
Исключение из PHP не нужно превращать в «товаров нет». Обработчик задания решает, можно ли повторить операцию или надо исправить данные.
Не выполняйте SQL/HTTP для каждой строки шаблона: сто тысяч узлов могут создать сто тысяч запросов. Загрузите нужный справочник заранее и передайте чистый быстрый обработчик. Такой callback проще тестировать, повторять и профилировать. Рекурсивные обращения к тому же процессору также требуют осторожности; разделяйте независимые преобразования.
Связанные инструменты: XSLTProcessor::registerPHPFunctions; XSLTProcessor::setProfiling.