XML - morze możliwości - XPath, XPointer, XInclude

Często odbywają się dyskusje dotyczące formatu w jakim przechowywać konfiguracja aplikacji. Najczęściej wybierane sposoby to pliki INI, YAML oraz XML. Za najlepsze rozwiązanie uważam wykorzystanie XML’a.

Główną zaletą XML jest jego popularność oraz szereg standartów które znacznie usprawniają prace. Struktury XML są bardzo łatwo rozszerzalne poprzez XInclude. Walidację takiego pliku można bardzo szybko rozwiązać tworząc plik XSD, natomiast jeżeli potrzebujemy ładnej prezentacji zawartości, wystarczy stworzyć plik XSLT oraz napisać kilkadziesiąt linijek.

Przykładowy plik XML

  1. < ?xml version="1.0" encoding="UTF-8"?>
  2. <adress -book>
  3.         <presons>
  4.                 <person id="1">
  5.                         <firstname>Jan</firstname>
  6.                         <lastname>Kowlaski</lastname>
  7.                         <email>jan-kowalski@gmail.com</email>
  8.                         <phone>000 123 456</phone>
  9.                 </person>
  10.                 <person id="2">
  11.                         <firstname>Piotr</firstname>
  12.                         <lastname>Nowak</lastname>
  13.                         <email>piotr-nowak@gmail.com</email>
  14.                         <phone>000 123 456</phone>
  15.                 </person>
  16.                 <person id="1">
  17.                         <firstname>Paweł</firstname>
  18.                         <lastname>Kwiatkowski</lastname>
  19.                         <email>jan-kowalski@gmail.com</email>
  20.                         <phone>000 123 456</phone>
  21.                 </person>
  22.         </presons>
  23. </adress>

XPath jest językiem używanym do lokalizacji oraz pozyskiwania informacji z drzewa XML. Język ten jest również potrzebny podczas używania XPointer oraz XSLT

Do przetwarzania XML’a będziemy używać rozszerzenia DOM dla PHP.

Zacznijmy od podstawowego wyrażenia xpath. Pobierzemy wszystkie nazwiska z naszej książki adresowej:

  1. < ?php
  2.  
  3. $dom = new DOMDocument();
  4. $dom->load(‘address-book.xml’);
  5.  
  6. $xpath = new DOMXPath($dom);
  7. $persons = $xpath->query("/address-book/persons/person/lastname");
  8.  
  9. foreach ($persons as $person) {
  10.         echo $person->nodeValue."<br />";
  11. }
  12.  
  13. ?>
  1. Kowlaski
  2. Nowak
  3. Kwiatkowski
  4. Jankowski

Do wykonywania operacji korzystając z XPath służy klasa DOMXPath. Tworząc egzemplarz tej klasy przekazujemy obiekt DOM na którym chcemy operować, następnie używając metody query która zwraca to o co prosiliśmy.

A o co prosiliśmy i jak prosić? W przykładzie powyżej napisaliśmy bardzo proste wyrażenie, które zwróciło nam tylko nazwiska (lastname) osób w książce. Poszczególne poziomy w dokumencie XML oddzielane są znakiem /. Jezeli zapytanie zaczyna się od slasha oznacza to że wyszukiwanie ma zacząć się od najwyższego elementu.

Używając XPath możemy pisać bardziej szczególowe zapytania. Wybrać nazwisko osoby o numerze id równym 1:

  1. /address-book/persons/person[@id='1']/lastname

Dodający przy person nawias kwadratowy ograniczyliśmy wyszukiwanie tylko do elementów person które posiadają atrybut id o wartości równej 1. Znak @ przed id oznacza że uwzględniamy atrybuty nie elementy.

  1. /address-book/persons/person[firstname='Paweł' and city='Warszawa']/lastname

Powyższe zapytanie zwróci nam nazwiska osób których imię to Paweł oraz miejsce zamieszkania to Warszawa. W tym zapytanie nie używaliśmy znaku @ ponieważ interesowały nas elementy a nie atyrbuy.

Przedstawiłem tutaj podstawowe sposoby które najczęściej są używane podczas korzystanie z XPath. Po więcej informacji zapraszam do dokumentacji XPath.

XInclude

XPointer

XPointer jest rozszerzeniem XPath i używa jego składni. Jest to język do adresowania fragmentów dokumentów XML. Został przewidziany do stosowania w adresach URI.

Przykładowe wyrażenia XPath używając XPointer wyglądają następująco:

  1. xpointer(/address-book/persons/person[@id='1']/lastname)

Wspomniałem o używaniu w adresach URI. Tak przykładowo by wyglądało wyrażenie jako fragment URI:

  1. http://www.example.com/address-book.xml#xpointer(/address-book/persons/person[@id='1']/lastname)

XInclude

XInclude jest standartem który pozwala nam dołączać zewnętrzne dokumenty lub ich fragmnety do pliku XML. XInclude zawiera się w przestrzeni nazw http://www.w3.org/2001/XInclude. W tym namespace mamy dwa elementy zakładając że zadeklarujemy prefix xi będzie to xi:include oraz xi:fallback. Pierwszy z nich określa lokalizacje jednostki którą chcemy dołączyć - może być to mały zakres XML’a wybrany XPath’em lub cały plik. Może się zdarzyć że XInclude zwróci błąd. Plik lub wybrany zakres który chcemy dołączyć może nie istnieć. Aby osłużyć błąd XInclude oferuje element xi:fallback.

Rozszerzmy naszą książkę adresową o grupy. Mając wiele konkatków w pewnym momencie dobrze by było je jakoś uporządkować.

  1. <address -book>
  2.         <persons>
  3.                 <person id="1">
  4.                         <firstname>Jan</firstname>
  5.                         <lastname>Kowlaski</lastname>
  6.                         <email>jan-kowalski@gmail.com</email>
  7.                         <phone>000 123 456</phone>
  8.                         <city>Warszawa</city>
  9.                 </person>
  10.                 <person id="2">
  11.                         <firstname>Piotr</firstname>
  12.                         <lastname>Nowak</lastname>
  13.                         <email>piotr-nowak@gmail.com</email>
  14.                         <phone>000 123 456</phone>
  15.                         <city>Kraków</city>
  16.                 </person>
  17.                 <person id="3">
  18.                         <firstname>Paweł</firstname>
  19.                         <lastname>Kwiatkowski</lastname>
  20.                         <email>pawel-kwiatkowski@gmail.com</email>
  21.                         <phone>000 123 456</phone>
  22.                         <city>Warszawa</city>
  23.                 </person>
  24.                 <person id="4">
  25.                         <firstname>Paweł</firstname>
  26.                         <lastname>Jankowski</lastname>
  27.                         <email>robert-jankowski@gmail.com</email>
  28.                         <phone>000 123 456</phone>
  29.                         <city>Poznań</city>
  30.                 </person>
  31.         </persons>
  32.  
  33.         <groups>
  34.                 <group name="Znajomi">
  35.                         <persons>
  36.                                 <person id="2">
  37.                                         <firstname>Piotr</firstname>
  38.                                         <lastname>Nowak</lastname>
  39.                                         <email>piotr-nowak@gmail.com</email>
  40.                                         <phone>000 123 456</phone>
  41.                                         <city>Kraków</city>
  42.                                 </person>
  43.                                 <person id="3">
  44.                                         <firstname>Paweł</firstname>
  45.                                         <lastname>Kwiatkowski</lastname>
  46.                                         <email>pawel-kwiatkowski@gmail.com</email>
  47.                                         <phone>000 123 456</phone>
  48.                                         <city>Warszawa</city>
  49.                                 </person>
  50.                         </persons>     
  51.                 </group>
  52.  
  53.                 <group name="Praca">
  54.                         <persons>
  55.                                 <person id="1">
  56.                                         <firstname>Jan</firstname>
  57.                                         <lastname>Kowlaski</lastname>
  58.                                         <email>jan-kowalski@gmail.com</email>
  59.                                         <phone>000 123 456</phone>
  60.                                         <city>Warszawa</city>
  61.                                 </person>
  62.                                 <person id="4">
  63.                                         <firstname>Paweł</firstname>
  64.                                         <lastname>Jankowski</lastname>
  65.                                         <email>robert-jankowski@gmail.com</email>
  66.                                         <phone>000 123 456</phone>
  67.                                         <city>Poznań</city>
  68.                                 </person>
  69.                         </persons>     
  70.                 </group>               
  71.         </groups>
  72.  
  73. </address>

Nie trudno zauważyć że takie rozwiązanie jest kłopotliwe. Gdy ktoś zmieni numer będziemy musieli zmieniać ten numer w kilku miejscach.

Problem ten możemy rozwiązać właśnie dzięki XInclude. Zamiast posiadać te same wpisy kilka razy, będzie je dołączać na podstawie numeru ID.

  1. < ?xml version="1.0" encoding="UTF-8"?>
  2. <address -book xmlns:xi="http://www.w3.org/2001/XInclude">
  3.         <persons>
  4.                 <person id="1">
  5.                         <firstname>Jan</firstname>
  6.                         <lastname>Kowlaski</lastname>
  7.                         <email>jan-kowalski@gmail.com</email>
  8.                         <phone>000 123 456</phone>
  9.                         <city>Warszawa</city>
  10.                 </person>
  11.                 <person id="2">
  12.                         <firstname>Piotr</firstname>
  13.                         <lastname>Nowak</lastname>
  14.                         <email>piotr-nowak@gmail.com</email>
  15.                         <phone>000 123 456</phone>
  16.                         <city>Kraków</city>
  17.                 </person>
  18.                 <person id="3">
  19.                         <firstname>Paweł</firstname>
  20.                         <lastname>Kwiatkowski</lastname>
  21.                         <email>pawel-kwiatkowski@gmail.com</email>
  22.                         <phone>000 123 456</phone>
  23.                         <city>Warszawa</city>
  24.                 </person>
  25.                 <person id="4">
  26.                         <firstname>Paweł</firstname>
  27.                         <lastname>Jankowski</lastname>
  28.                         <email>robert-jankowski@gmail.com</email>
  29.                         <phone>000 123 456</phone>
  30.                         <city>Poznań</city>
  31.                 </person>
  32.         </persons>
  33.  
  34.         <groups>
  35.                 <group name="Znajomi">
  36.                         <persons>
  37.                                 <xi :include xpointer="xpointer(/address-book/persons/person[@id='1'])" />
  38.                                 <xi :include xpointer="xpointer(/address-book/persons/person[@id='4'])" />
  39.                         </persons>     
  40.                 </group>
  41.  
  42.                 <group name="Praca">
  43.                         <persons>
  44.                                 <xi :include xpointer="xpointer(/address-book/persons/person[@id='3'])" />
  45.                                 <xi :include xpointer="xpointer(/address-book/persons/person[@id='4'])" />
  46.                         </persons>     
  47.                 </group>               
  48.         </groups>
  49.  
  50. </address>

Przetwórzmy teraz ten plik używając PHP i DOM:

  1. < ?php
  2.  
  3. $dom = new DOMDocument();
  4. $dom->preserverWhiteSpace = true;
  5. $dom->formatOutput = true;
  6.  
  7. $dom->load(‘address-book.xml’);
  8. $dom->xinclude();
  9. $dom->save("adress-book-xinclude.xml");
  10.  
  11. ?>

Za XInclude w klasie odpowiedzialna jest metoda xinclude(). Na końcu zapisaliśmy zawartość w nowym pliku żeby móc sprawdzić czy to rzeczywiście działa. Otwierając plik powinniśmy zobaczyć zamiast wpisów xi:include, dołączone właściwe watrości.

Nasza książka może się rozrosnąć a trzymanie wszystkiego w jednym pliku może stać się nie wygodne. A co gdybyśmy chcieli trzymać grupy kontaktów w odzielnym pliku, np.: address-book-groups.xml?

Zawartość pliku wyglądała by wtedy następująco:

  1. < ?xml version="1.0" encoding="UTF-8"?>
  2. <address -book xmlns:xi="http://www.w3.org/2001/XInclude">
  3.         <groups>
  4.                 <group name="Znajomi">
  5.                         <persons>
  6.                                 <xi :include href="address-book.xml" xpointer="xpointer(/address-book/persons/person[@id='1'])"/>
  7.                                 <xi :include href="address-book.xml" xpointer="xpointer(/address-book/persons/person[@id='4'])"/>
  8.                         </persons>     
  9.                 </group>
  10.  
  11.                 <group name="Praca">
  12.                         <persons>
  13.                                 <xi :include href="address-book.xml" xpointer="xpointer(/address-book/persons/person[@id='3'])"/>
  14.                                 <xi :include href="address-book.xml" xpointer="xpointer(/address-book/persons/person[@id='4'])"/>
  15.                         </persons>     
  16.                 </group>               
  17.         </groups>
  18. </address>

Wycieliśmy element groups wraz z jego dziećmi z pliku address-book.xml, dodając przy wartościach dołączanych atrybut href, wskazujący na plik w którym znajduje się nasz XML z osobami.

XSLT

Format XML jest dość czytelnym formatem jednak nie nadaje się on do prezentacji. Zdecydowanie lepszym pomysłem jest wyświetlić dane osadzając je ładnie np.: w HTMLu. Mamy do wyboru dwie drogi: pierwsza użycie DOM w PHP, parsując dokument i wyświetlając go wg. naszych oczekiwań. Drugim, lepszym sposobem, jest stworzenie pliku XSL oraz wykonać transformację przy użyciu XSLT.

Pliki XSL są dokumentami opisującymi sposób prezentacji i przekształceń dokumentów XML. Spróbujemy teraz pisząc kilka prostych podstawowych instrukcji, przekształcić XML naszej książki aby była ona prezentowana w formie tabeli HTML.

Jako że nasz plik jest dość prosty to w również prosty sposób zaprezentujemy go w tabeli html, przy okazji sortując

  1. < ?xml version="1.0" encoding="utf-8"?>
  2. <xsl :stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  3. </xsl><xsl :template match="/">
  4. <html>
  5. <head>
  6.         <title>XML - morze możliwości</title>
  7.  
  8.          <meta http-equiv="content-type" content="text/html; charset=utf-8;" />
  9.          <meta http-equiv="content-language" content="pl" />
  10.  </head>
  11.  <body>
  12.    <h2>Książka adresowa</h2>
  13.    <table>
  14.      <tr>
  15.        <th>Imię</th>
  16.        <th>Nazwisko</th>
  17.        <th>E-Mail</th>
  18.      </tr>
  19.         <xsl :for-each select="address-book/persons/person">
  20.         <xsl :sort select="lastname" />
  21.       <tr>
  22.         <td><xsl :value-of select="firstname"/></td>
  23.         <td><xsl :value-of select="lastname"/></td>
  24.         <td><xsl :value-of select="email"/></td>
  25.       </tr>
  26.         </xsl>
  27.    </table>
  28.  </body>
  29.  </html>
  30. </xsl>

Plik zaczynami od przypisania prefixu dla przestrzeni nazw XSL. Następie posługujemy się elementem xsl:template który definiuje szablon. Nasz szablon zaczyna się od podstawowych elementów html. Aby przetworzyć każdą osobę posłużymy się elementem xsl:for-each, w atrybucie select zaznaczając zbiór węzów do przetworznia używając wyrażeń XPath. Następnie zaznaczamy że chceby aby osoby były posortowane alfabetycznie wg. nazwiska i umieszczamy szablon który ma być używany przy każdym elemencie który ma być przetworzony. Zawiera on html wiersza tabeli. W komórkach tabeli do wyświetlenia imienia i nazwiska osoby użwywamy elementu xsl:value-of który zwraca wartość węzła podanego w atrybucie select. Trzeba zwrócić uwagę na to że podajemy tam wyrażenie XPath węzła który jest dostarczany przez xsl:for-each. W naszym przypadku będzie to np.:

  1. <firstname>Jan</firstname>
  2. <lastname>Kowlaski</lastname>
  3. <email>jan-kowalski@gmail.com</email>
  4. <phone>000 123 456</phone>
  5. <city>Warszawa</city>

Dlatego też atrybut select będzie zawierał wyrażenie firstname/ a nie jak niektóry mogli mylnie sądzić address-book/persons/person/firstname. Na końcu dodajmy w pliku naszej książki linijkę podpinającą plik XSL:

  1. < ?xml version="1.0" encoding="UTF-8"?>
  2. < ?xml-stylesheet type="text/xsl" href="address-book.xsl"?>
  3. <address -book xmlns:xi="http://www.w3.org/2001/XInclude">
  4.         <persons>
  5.                 [...]
  6.         </persons>
  7. </address>

Gdy otworzymy teraz plik XML naszej książki w przeglądarce naszym oczom ukaże się tabelka z osobami a nie drzewo z XML’em.

W tym przypadku processorem XSLT był silnik przeglądarki. Jednak możemy zrobić to samo używając PHP i DOM’a.

  1. < ?php
  2.  
  3. $dom = new DomDocument();
  4. $dom->load("address-book.xml");
  5.  
  6. $xsl = new DomDocument();
  7. $xsl->load("address-book.xsl");
  8.