Упростить PHP DOM XML-анализ - как?

Я провел целые дни с функциями PHP DOM, но я не могу понять, как это работает.:( У меня есть простой XML файл, который выглядит хорошо, но я не могу его использовать, как я думаю, когда я создал его структуру.

Пример фрагмента XML:

-pages //root element
    -page id="1" //we can have any number of pages
        -product id="364826" //we can have any number of products
            -SOME_KIND_OF_VALUE
            -ANOTHER_VALUE
            ...

Моя первоначальная идея заключалась в том, чтобы ускорить работу моего клиентского процесса, поэтому я выбрасываю старые CSV и начинаю использовать XML.

Проблема 1: Когда я группирую продукты на странице, я использую setIdAttribute, чтобы предотвратить сохранение одной и той же страницы в дереве более одного раза. Это работает нормально, пока не происходит чтение, потому что эти идентификаторы привязаны к некоторому DTD (на основе getElementById).

Вопрос 1: Как я могу написать простой DTD, который предоставляет эти необходимые данные, поэтому я могу использовать getElementById на этапе чтения тоже?

Проблема 2: Потому что у меня есть страницы, которые я бы хотел загрузить как можно меньше информации. Вот почему я создал атрибут id на страницах. Теперь я не могу получить доступ к моей странице id = "2" напрямую, потому что проблема 1 выше (getElementById не имеет смысла в настоящее время). Как-то мне удалось получить необходимую информацию о каждом продукте на данной странице, но мой код выглядит страшно:

$doc      = DOMDocument::load('data.xml');
$xpath    = new DOMXPath($doc);
$query    = '/pages/page[' . $page . ']'; //$page is fine: was set earlier
$products = $xpath->query($query);
$_prods   = $doc->getElementsByTagName('product');
foreach($_prods as $product){
    foreach($product->childNodes as $node){
        echo $node->nodeName . ": " . $node->nodeValue . "<br />";
    }
}

Квестон 2: Я думаю, что приведенный выше код является примером того, как не анализировать XML. Но из-за моих ограниченных знаний о функциях PHP DOM я не могу написать чище один сам. Я попробовал какое-то тривиальное решение, но никто из них не работал у меня.

Пожалуйста, помогите мне, если сможете.

Спасибо, Fabrik

Ответ 1

Решение проблемы 1:

W3C определяет: значение атрибута xml:id в качестве атрибута ID в документах XML и определяет обработку этого атрибута для идентификации идентификаторов в отсутствие проверки, без извлечения внешних ресурсов и без использования внутреннего подмножества.

Другими словами, когда вы используете

$element->setAttribute('xml:id', 'test');

вам не нужно вызывать setIdAttribute, а также не указывать DTD или схему. DOM будет распознавать атрибут xml:id при использовании с getElementById без необходимости проверки документа или чего-либо еще. Это подход с наименьшими усилиями. Обратите внимание, что в зависимости от вашей ОС и версии libxml вы не получите getElementById для работы вообще.

Решение проблемы2:

Даже если идентификаторы не были выбраны с помощью getElementById, вы все равно можете получить их с помощью XPath:

$xpath->query('/pages/page[@id=1]');

определенно будет работать. И вы также можете напрямую получить дочерние элементы продукта для конкретной страницы:

$xpath->query('//pages/page[@id=1]/products');

Помимо этого, вы можете сделать очень мало, чтобы код DOM выглядел менее подробным, потому что это действительно сложный интерфейс. Это должно быть, потому что DOM является агностическим интерфейсом языка, снова определяемым W3C.


ИЗМЕНИТЬ после комментария ниже

Он работает, как я объяснил выше. Вот полный тестовый пример для вас. Первая часть предназначена для написания новых файлов XML с DOM. Вот где вам нужно установить атрибут xml:id. Вы используете это вместо обычного атрибута id без имени.

// Setup
$dom = new DOMDocument;
$dom->formatOutput = TRUE;
$dom->preserveWhiteSpace = FALSE;
$dom->loadXML('<pages/>');

// How to set a valid id attribute when not using a DTD or Schema
$page1 = $dom->createElement('page');
$page1->setAttribute('xml:id', 'p1');
$page1->appendChild($dom->createElement('product', 'foo1'));
$page1->appendChild($dom->createElement('product', 'foo2'));

// How to set an ID attribute that requires a DTD or Schema when reloaded
$page2 = $dom->createElement('page');
$page2->setAttribute('id', 'p2');
$page2->setIdAttribute('id', TRUE);
$page2->appendChild($dom->createElement('product', 'bar1'));
$page2->appendChild($dom->createElement('product', 'bar2'));

// Appending pages and saving XML
$dom->documentElement->appendChild($page1);
$dom->documentElement->appendChild($page2);
$xml = $dom->saveXML();
unset($dom, $page1, $page2);
echo $xml;

Это создаст XML файл, подобный этому:

<?xml version="1.0"?>
<pages>
  <page xml:id="p1">
    <product>foo1</product>
    <product>foo2</product>
  </page>
  <page id="p2">
    <product>bar1</product>
    <product>bar2</product>
  </page>
</pages>

Когда вы снова читаете в XML, новый экземпляр DOM больше не знает, что вы объявили атрибут id, не содержащий имен, в качестве атрибута идентификатора с setIdAttribute. Он все равно будет в XML, но атрибут id будет просто регулярным атрибутом. Вы должны знать, что атрибуты идентификатора являются особыми в XML.

// Load the XML we created above
$dom = new DOMDocument;
$dom->loadXML($xml);

Теперь для некоторых тестов:

echo "\n\n GETELEMENTBYID RETURNS ELEMENT WITH XML:ID \n\n";
foreach( $dom->getElementById('p1')->childNodes as $product) {
    echo $product->nodeValue; // Will output foo1 and foo2 with whitespace
}

Вышеописанное работает, поскольку синтаксический анализатор, совместимый с DOM, должен распознавать xml:id атрибут идентификатора независимо от любого DTD или схемы. Это объясняется спецификациями, приведенными выше. Причина, по которой он выводит пробелы, заключается в том, что из-за форматированного вывода есть узлы DOMText между открывающим тегом, двумя тегами продукта и закрывающими тегами, поэтому мы повторяем пять узлов. Концепция node имеет решающее значение для понимания при работе с XML.

echo "\n\n GETELEMENTBYID CANNOT FETCH NORMAL ID \n\n";
foreach( $dom->getElementById('p2')->childNodes as $product) {
    echo $product->nodeValue; // Will output a NOTICE and a WARNING
}

Вышеуказанное не будет работать, потому что id не является атрибутом идентификатора. Чтобы анализатор DOM распознал его как таковой, вам необходимо DTD или Schema, и XML должен быть проверен против него.

echo "\n\n XPATH CAN FETCH NORMAL ID \n\n";
$xPath = new DOMXPath($dom);
$page2 = $xPath->query('/pages/page[@id="p2"]')->item(0);
foreach( $page2->childNodes as $product) {
    echo $product->nodeValue; // Will output bar1 and bar2
}

XPath, с другой стороны, буквально об атрибутах, что означает, что вы можете запросить DOM для элемента страницы с атрибутом id, если getElementById недоступен. Обратите внимание, что для запроса страницы с идентификатором p1 вам нужно будет включить пространство имен, например. @xml:id="p1".

echo "\n\n XPATH CAN FETCH PRODUCTS FOR PAGE WITH ID \n\n";
$xPath = new DOMXPath($dom);
foreach( $xPath->query('/pages/page[@id="p2"]/product') as $product ) {
    echo $product->nodeValue; // Will output bar1 and bar2 w\out whitespace
}

И, как сказано, вы также можете использовать XPath для запроса чего-либо еще в документе. Этот не будет выводить пробелы, потому что он будет возвращать только элементы product под страницей с id p2.

Вы также можете перемещать всю DOM с node. Это древовидная структура. Поскольку DOMNode является самым важным классом в DOM, вы хотите ознакомиться с ним.

echo "\n\n TRAVERSING UP AND DOWN \n\n";
$product = $dom->getElementsByTagName('product')->item(2);
echo $product->tagName; // 'product'
echo $dom->saveXML($product); // '<product>bar1</product>'

// Going from bar1 to foo1
$product = $product->parentNode // Page Node
                   ->parentNode // Pages Node
                   ->childNodes->item(1)  // Page p1
                   ->childNodes->item(1); // 1st Product

echo $product->nodeValue; // 'foo1'

// from foo1 to foo2 it is two(!) nodes because the XML is formatted
echo $product->nextSibling->nodeName; // '#text' with whitespace and linebreak
echo $product->nextSibling->nextSibling->nodeName; // 'product'
echo $product->nextSibling->nextSibling->nodeValue; // 'foo2'

На боковой панели, да, у меня есть опечатка в исходном коде выше. Это product не products. Но я с трудом могу утверждать, что код не работает, когда все, что вам нужно изменить, это s. Это просто слишком похоже на то, что вы хотите быть ложкой.