Статьи

PHP DOM: Работа с XML

SimpleXML позволяет вам быстро и легко работать с XML-документами, и в большинстве случаев SimpleXML достаточно. Но если вы работаете с XML в любом серьезном качестве, вам в конечном итоге понадобится функция, которая не поддерживается SimpleXML, и именно здесь появляется PHP DOM (Document Object Model).

PHP DOM является реализацией стандарта W3C DOM и больше привязывается к объектной модели, чем SimpleXML. Поначалу это может показаться немного сложным, но если вы захотите узнать, то обнаружите, что эта библиотека для доступа к XML-документам и манипулирования ими обеспечивает большой контроль над рабочими XML-документами в PHP. Это связано с тем, что DOM различает различные составляющие документа XML, такие как разные типы узлов.

Чтобы изучить некоторые основные функции, связанные с PHP DOM, давайте создадим класс, который может добавлять и удалять книги в библиотеке и запрашивать каталог. Он должен предлагать следующую функциональность:

  • Запрос на книгу, найденную ее ISBN
  • Добавить книгу в библиотеку
  • Удалить книгу из библиотеки
  • Найти все книги определенного жанра

DTD и XML

В этой статье я буду использовать следующие DTD и XML, которые описывают библиотеку и ее книги. Это должно предоставить достаточно материала, чтобы продемонстрировать, как можно использовать расширение:

<!ELEMENT library (book*)> <!ELEMENT book (title, author, genre, chapter*)> <!ATTLIST book isbn ID #REQUIRED> <!ELEMENT title (#PCDATA)> <!ELEMENT author (#PCDATA)> <!ELEMENT genre (#PCDATA)> <!ELEMENT chapter (chaptitle,text)> <!ATTLIST chapter position NMTOKEN #REQUIRED> <!ELEMENT chaptitle (#PCDATA)> <!ELEMENT text (#PCDATA)> 
 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE library SYSTEM "library.dtd"> <library> <book isbn="isbn1234"> <title>A Book</title> <author>An Author</author> <genre>Horror</genre> <chapter position="first"> <chaptitle>chapter one</chaptitle> <text><![CDATA[Lorem Ipsum...]]></text> </chapter> </book> <book isbn="isbn1235"> <title>Another Book</title> <author>Another Author</author> <genre>Science Fiction</genre> <chapter position="first"> <chaptitle>chapter one</chaptitle> <text><![CDATA[<i>Sit Dolor Amet...</i>]]></text> </chapter> </book> </library> 

Одна из самых важных вещей в понимании DOM — это концепция узла . По сути, узел — это любой концептуальный элемент в документе XML. Если это элемент (например, chapter ), то это узел. Если это атрибут (например, isbn ), то он рассматривается как узел DOM. Узлы обеспечивают атомарную структуру документа XML.

PHP DOM подклассы DOMNode для предоставления дочерних классов, которые представляют различные аспекты документа. Итак, DOMDocument фактически наследуется от DOMNode . DOMElement и DOMAttr также наследуются от DOMNode . Наличие общего родительского класса позволяет вам иметь общие методы и свойства, доступные для всех узлов, например, те, которые используются для определения типа узла, значения или даже добавления к нему.

Библиотечный класс

Класс с именем Library предлагает методы для требуемой функциональности, которая была изложена во введении. Он также имеет конструктор и деструктор, а также внутренние свойства для хранения документа DOM и пути к файлу XML. Различные операции выполняются со ссылкой на документ DOM, и путь используется при сохранении дерева обратно в виде XML обратно в файловую систему.

 <?php class Library { private $xmlPath; private $domDocument; public function __construct($xmlPath) { // TODO: instantiate the private variable representing // the DOMDocument } public function __destruct() { // TODO: free memory associated with the DOMDocument } public function getBookByISBN($isbn) { // TODO: return an array with properties of a book } public function addBook($isbn, $title, $author, $genre, $chapters) { // TODO: add a book to the library } public function deleteBook($isbn) { // TODO: Delete a book from the library } public function findBooksByGenre($genre) { // TODO: Return an array of books } } 

Я намеренно сделаю все просто, поскольку пример служит только для демонстрации того, что может сделать DOM. В реальном приложении, возможно, вы бы создали экземпляры объектов книги, чтобы более полно инкапсулировать проблему, и вам, вероятно, также хотелось бы обрабатывать ошибки более изящно. Вам не нужно делать это на этом этапе, хотя. Мы можем просто предположить, что передаваемые и возвращаемые значения являются строками или массивами, а ошибки могут быть обработаны путем генерирования общего исключения.

Обработка объектов Строительство и уничтожение

Конструктор предназначен для указания пути к документу XML, который вы хотите использовать в качестве аргумента. Есть несколько тестов, которые он делает, чтобы убедиться, что документ действителен.

Первый тест состоит в том, чтобы определить, что загружаемый документ использует тип документа «библиотека». Каждый DOMDocument имеет открытое свойство doctype которое возвращает тип документа, используемый XML-документом. Так что для этого примера вы должны увидеть, что свойство doctype установлено в «library», когда вы загрузили документ.

Второй тест состоит в том, чтобы убедиться, что используемое определение определено правильно с использованием общедоступных свойств systemId или publicId . Используемый здесь XML определяется DTD, указанным в системе как library.dtd , поэтому он проверяет это, сравнивая его со свойством systemId .

Третий тест — убедиться, что сам документ действителен в соответствии с DTD. Проверка документа также проверяет, правильно ли сформирован документ (т. Е. Несоответствие тега и т. Д.) И соответствует ли он DTD, на котором он основан.

Как только все эти условия выполнены, он сохраняет ссылку на загруженный документ и путь к файлу XML в качестве внутренних свойств, которые впоследствии будут использоваться другими методами. Но если в какой-то момент один из тестов не пройден, возникает исключение.

 <?php public function __construct($xmlPath) { //loads the document $doc = new DOMDocument(); $doc->load($xmlPath); //is this a library xml file? If ($doc->doctype->name != "library" || $doc->doctype->systemId != "library.dtd") { throw new Exception("Incorrect document type"); } //is the document valid and well-formed? if($doc->validate()) { $this->domDocument = $doc; $this->xmlPath = $xmlPath; } else { throw new Exception("Document did not validate"); } } 

Метод деструктора освобождает любую память, используемую $domDocument . На самом деле это просто простой вызов для сброса свойства.

 <?php public function __destruct() { unset($this->domDocument); } с <?php public function __destruct() { unset($this->domDocument); } 

Вернуть книгу по номеру ISBN

Теперь перейдем к основным методам чтения манипулирования базовым XML-документом.

Первый метод получает детали книги из предоставленного ISBN. Вы можете предоставить ISBN в виде строки, а метод возвращает массив, детализирующий свойства книги.

PHP DOM предоставляет очень простую функцию для возврата определенного элемента на основе его идентификатора — getElementById() который возвращает объект DOMElement . Чтобы это работало, вам нужно будет назначить ID с вашим DTD, как я это сделал:

 <!ATTLIST book isbn ID #REQUIRED> 

Важно знать, что getElementById() работает, только если документ был проверен на соответствие DTD. Если нет, то функция просто не обнаружит тот факт, что элемент имеет идентификатор.

Другой способ получения элементов из документа — использовать getElementsByTagName() . Этот метод возвращает коллекцию узлов, которые были найдены с указанным именем тега. DOMNodeList коллекция — это DOMNodeList , который можно просмотреть.

Элементы в DOMNodeList также могут быть выбраны по их позиции в списке с помощью item() . Поскольку DTD определяет, что книга может иметь только одного автора, мы знаем, что DOMNodeList будет содержать один узел, к которому можно получить доступ с помощью item(0) . DTD применяет этот факт, и если бы он отличался в документе, вы бы получили ошибку проверки при создании объекта библиотеки.

Как только вы нашли нужный вам узел, вы можете найти его значение с помощью открытого свойства nodeValue .

Чтобы получить доступ к атрибутам, вы можете использовать DOMNode которые возвращают DOMNamedNodeMap . Это похоже на DOMNodeList в том DOMNodeList что его можно обойти, но вы также можете выбрать определенный атрибут с помощью getNamedItem() и просто передать имя атрибута в виде строки. Возвращаемое значение является DOMNode .

Таким образом, реализация метода для извлечения книги и ее информации выглядит следующим образом:

 <?php public function getBookByISBN($isbn) { // get a book element from the isbn ID $book = $this->domDocument->getElementById($isbn); // if a book was not returned... if (!$book) { throw new Exception("No book found with ISBN ". $isbn); } $arrBook = array(); $arrBook["isbn"] = $isbn; // get the data from the elements based on their tag names // // we know these DOMNodeLists will only return one // item since the DTD states this $arrBook["author"] = $book->getElementsByTagName("author") ->item(0)->nodeValue; $arrBook["title"] = $book->getElementsByTagName("title") ->item(0)->nodeValue; $arrBook["genre"] = $book->getElementsByTagName("genre") ->item(0)->nodeValue; $chapters = $book->getElementsByTagName("chapter"); $arrChapters = array(); // iterate over the chapter elements foreach($chapters as $chapter) { $chapterId = $chapter->attributes ->getNamedItem("position")->nodeValue; $chapterTitle = $chapter ->getElementsByTagName("chaptitle")->item(0) ->nodeValue; $chapterText = $chapter ->getElementsByTagName("text")->item(0) ->nodeValue; $arrChapter["title"] = $chapterTitle; $arrChapter["text"] = $chapterText; $arrChapters[$chapterId] = $arrChapter; } $arrBook["chapters"] = $arrChapters; return $arrBook; } 

Идентификация и извлечение данных из XML-документа относительно просты. Главное препятствие, которое нужно преодолеть, — это понимание концепции узла; как только вы поймете это, вы обнаружите, что получение нужных вам данных является простым процессом.

Добавление книги в библиотеку

Следующий метод определения добавляет книгу в базу данных XML. Метод принимает свойства и массив глав книги для добавления.

Одним из способов выполнения такой задачи является использование метода createElement() добавление этого нового узла в документ и установка ссылки на него, чтобы вы могли работать с объектом с этого момента. Когда вы создаете элемент, вы также должны добавить его в документ. Использование createElement() не добавляет его автоматически в документ для вас. Он связывает элемент с документом, но это так далеко. Хорошей практикой является добавление элементов, которые вы намереваетесь стать частью документа, как только они будут созданы, чтобы они не были забыты!

Вы можете использовать свойство documentElement для идентификации корневого элемента документа XML. Если бы мы не делали этого и просто добавляли непосредственно в документ, мы фактически добавляли бы дочерний элемент к самому концу документа (то есть вне элемента library ). Это может привести к ошибке проверки. Если вы думаете об этом, такое поведение DOM абсолютно разумно; обработка документа как корневого элемента и добавление к нему дочернего элемента приведет к тому, что он будет помещен после элемента library поскольку это первый дочерний элемент документа.

Конечно, элемент book должен содержать ISBN, поэтому атрибут должен быть добавлен во вновь созданный элемент. Есть два способа сделать это. Простейшим является использование setAttribute() который принимает имя атрибута и значение атрибута в качестве аргументов. Второй способ — создать объект DOMAttr а затем добавить его к элементу. DOMAttr является подклассом DOMNode , поэтому он извлекает выгоду из всех унаследованных методов и свойств, предлагаемых его родителями.

setAttribute() и setAttributeNode() отвечают за добавление и обновление атрибутов, связанных с элементом. Если атрибут не существует, он будет создан. Если он существует, он будет обновлен.

Чтобы предоставить значение для текстового элемента, рекомендуется использовать DOMCdataSection() . Главы книг даны как PCDATA, а не CDATA в DTD. Это потому, что элемент не может быть описан как содержащий непосредственно CDATA; мы должны объявить его как PCDATA, а затем обернуть содержимое в <![CDATA[...]]> . Это звучит нелогично, поскольку нам нужно иметь возможность помещать непарсированные символьные данные в текстовый элемент для последующего использования, но именно поэтому мы должны создать конкретный DOMCdataSection ; это безопасно обернет наш текст в <![CDATA[...]]> . Если вы добавите HTML-код непосредственно к узлу, вы обнаружите, что недопустимые символы, такие как <или &, будут преобразованы в соответствующие им объекты (например, & lt; и & amp;). Это потому, что эти символы имеют особое значение XML. Амперсанд для сущностей и символ «больше» начинают тег. DOM заменяет их, чтобы не вызывать проблем с синтаксическим анализом при загрузке или проверке документа.

Последний шаг в добавлении книги — сохранить новый документ обратно в файл, что делается с помощью метода save() .

Метод в целом выглядит так:

 <?php public function addBook($isbn, $title, $author, $genre, $chapters) { // create a new element represeting the new book $newbook = $this->domDocument->createElement("book"); // append the newly created element $this->domDocument->documentElement ->appendChild($newbook); // setting the attribute can be done in one of two ways // Method One: // $newbook->setAttribute("isbn", $isbn); // Method Two: $idAttribute = new DOMAttr("isbn", $isbn); $newbook->setAttributeNode($idAttribute); $title = $this->domDocument ->createElement("title", $title); $newbook->appendChild($title); $author = $this->domDocument ->createElement("author", $author); $newbook->appendChild($author); $genre = $this->domDocument ->createElement("genre", $genre); $newbook->appendChild($genre); foreach($chapters as $position => $chapter) { $newchapter = $this->domDocument ->createElement("chapter"); $newbook->appendChild($newchapter); $newchapter->setAttribute("position", $position); $newchaptitle = $this->domDocument ->createElement("chaptitle", $chapter["title"]); $newchapter->appendChild($newchaptitle); $newtext = $this->domDocument->createElement("text"); $newchapter->appendChild($newtext); // Rather than creating a new element, create a // DOMCdataSection which ensures our text is // wrapped in <![CDATA[ and ]]> $cdata = new DOMCdataSection($chapter["text"]); $newtext->appendChild($cdata); } // save the document $this->domDocument->save($this->xmlPath); } 

Удаление книги из библиотеки

Следующий метод решения — удаление книги. Это всего лишь случай определения того, какой элемент в документе XML вы хотите удалить, а затем используйте метод removeChild() для его удаления. Однако есть две важные вещи для понимания.

Во-первых, вы не можете удалить дочерний DOMDocument непосредственно из экземпляра DOMDocument . Вы должны получить доступ к элементу documentElement и удалить оттуда дочерний элемент. Это по тем же причинам, по которым вам пришлось ссылаться на documentElement при добавлении книги в библиотеку.

Во-вторых, удаление элемента из документа просто удаляет его из памяти. Если вы хотите сохранить данные, вы должны сохранить их обратно в файл.

Вот как выглядит метод deleteBook() :

 <?php public function deleteBook($isbn) { // get the book element based on its ID $book = $this->domDocument->getElementById($isbn); // simply remove the child from the documents // documentElement $this->domDocument->documentElement->removeChild($book); // save back to disk $this->domDocument->save($this->xmlPath); } 

Найти книги по жанру

Метод поиска конкретных книг по жанру использует XPath для получения нужных нам результатов. getElementById() , как вы видели ранее, является удобным способом выбора элементов из DOM, когда мы объявили ID в DTD. Но что мы можем сделать, если нам нужно запросить некоторые другие данные в XML? Мы можем использовать объект DOMXPath . Сам XPath выходит за рамки этой статьи, но я советую вам взглянуть на некоторые ресурсы, объясняющие синтаксис. Запрос XPath для поиска любого элемента книги в XML, который имеет жанр определенного типа:

  // библиотека / книга / жанр [text () = "некоторый жанр"] / .. 

Этот запрос сначала сообщает, что мы хотим получить доступ к элементу genre в пути //library/book . Две косые черты указывают, что library является корневым элементом, а одиночные косые черты указывают, что book является дочерним элементом library а genre — дочерним элементом book . [text() = "some genre"] указывает, что мы ищем, где текст внутри него — это «некоторый жанр». Сам по себе, результатом будет просто элемент жанра, поэтому /.. в конце помечается, чтобы указать, что нам действительно нужен родитель genre .

XPath — отличный способ найти узлы в структуре. Если вы обнаружите, что перебираете несколько DOMNodeLists и тестируете значения nodeValues для определенных значений, вам, вероятно, будет лучше взглянуть на эквивалентный запрос XPath, который, безусловно, будет намного короче, быстрее и проще для чтения.

Вот как выглядит метод поиска:

 <?php public function findBooksByGenre($genre) { // use XPath to find the book we"re looking for $query = '//library/book/genre[text() = "' . $genre . '"]/..'; // create a new XPath object and associate it with the document we want to query against $xpath = new DOMXPath($this->domDocument); $result = $xpath->query($query); $arrBooks = array(); // iterate of the results foreach($result as $book) { // add the title of the book to an array $arrBooks[] = $book->getElementsByTagName("title")->item(0)->nodeValue; } return $arrBooks; } 

Резюме

Эта статья была просто тестером, чтобы показать вам, как вы можете использовать DOM для манипулирования и получения отчетов из данных XML. PHP DOM не так страшен, как кажется, и вы можете обнаружить, что предпочитаете его над SimpleXML при определенных обстоятельствах.

Одной из самых важных вещей, которые вы узнали, была концепция узла, базового строительного блока XML-документа в отношении DOM. Вы увидели, как загрузить XML-документ в память и проверить его, извлекли данные из XML-документа с помощью getElementById() и getElementsByTagName() , добавили и удалили элементы, работали с атрибутами и посмотрели коллекции DOMNodeList и DOMNamedNodeMap чтобы DOMNamedNodeMap коллекции данных.

Хотя многие вещи, которые вы видели сегодня, — это вещи, которые вы, вероятно, уже легко можете сделать в SimpleXML, я надеюсь, что эта статья продемонстрировала вам, как можно достичь того же с помощью DOM и каковы некоторые из преимуществ DOM.

Изображение через Fotolia