SimpleXML og dine XML data

Det kan til tider være nødvendigt at gennemlæse eller danne XML dynamisk til brug på sit website. Det kunne eksempelvis være at indhente et RSS feed og vise nyheder på dit site fra dette.
Visse webservices og API'er anvender også XML til kommunikation og dataudveksling. Og det ville jo være meget rart, hvis de var noget man kunne tilgå nemt, ikke?

PHP har en udvidelse kaldet SimpleXML som i bund og grund er et meget simpelt, men stadig utroligt effektivt, værktøj til at danne og læse XML data med.
Artiklen her vil gennemgå nogle essentielle og simple metoder og begreber, som gør at du kan få en let forståelse for SimpleXML og anvendelsen af dette.
Forudsætningen for artiklens forståelse er et grundlæggende kendskab til PHP og XML.

Artiklen er opdelt således:
  1. SimpleXML og simpelt XML
    1. Den simple XML
    2. Instantiering af dit XML
    3. Hvad så nu?
  2. Når SimpleXML ikke længere er så simpelt
    1. Den simple XML
  3. Særlige karakterer i XML elementer
    1. Hyphen i mine XML elementer
    2. Kolon i mine XML elementer
  4. Attributter i XML
  5. Namespaces og SimpleXML
    1. Funktionen children()
    2. Funktionen attributes()

SimpleXML og simpelt XML

Langt de fleste udbydere idag har SimpleXML slået til idet at det aktivt skal fravælges for ikke at være en del af PHP. Det giver dig den fordel at du til hver en tid har et simpelt værktøj ved hånden som du kan anvende til din XML.

Den simple XML

Nedenstående XML er et simpelt eksempel som jeg vil anvende til de første gennemgange til artiklen.
Den vil skabe rammen for de efterfølgende kodeeksempler, så du også kan følge eksemplerne og prøve dem af i praksis.
Følgende XML stump er et minimalistik eksempel på en lille ordre købt igennem en webshop.

<?xml version="1.0"?>
<!-- order.xml -->
<purchase-order>
    <date>2005-10-31</date>
    <number>12345</number>
    <purchasedby>
        <name>John Doe</name>
        <address>My address</address>
    </purchasedby>
    <order-items>
        <item quantity="3">
            <code>687</code>
            <type>DVD Labels</type>
            <label>Empty DVD labels ready for print</label>
        </item>
        <item quantity="1">
            <code>129851</code>
            <type>DVD Spinnel</type>
            <label>30 DVD+R</label>
        </item>
    </order-items>
</purchase-order>
Instantiering af dit XML

For at kunne anvende din XML gennem PHP, skal vi have instantieret SimpleXML objektet med indholdet af din XML.
Det kan gøre på flere forskellige måder, men jeg vel gennemgående benytte mig af den samme metode for at lette forståelsen og brugen.

SimpleXML har en __construct() metode som anvendes når du forsøger at instantiere din XML.

<?php

    
/* Her hentes XML filens indhold ind i en variabel */
    
$xmlContent file_get_contents("sti/til/order.xml");

    
/* Her instancieres XML indholdet */
    
$xmlObject = new SimpleXMLElement$xmlContent );

?>

Ovenstående vil typisk typisk fungere og du vil ikke få nogle fejl. Såfremt dit XML ikke virker efter hensigten vil du får en stak advarsler fra PHP og slutteligt en fatal error fordi at der smides en undtagelse som du ikke fanger. Det bliver ikke noget der dækkes i denne artikel - udgangspunktet er at din XML er korrekt formateret.

Hvad så nu?

Alle dine data fra XML dokumentet er nu tilgængeligt i objektet $xmlObject.
Set ud fra et hierarkisk synspunkt, vil $xmlObject faktisk svare til at du ser på XML indholdet fra elementet purchase-order som er rodelementet (root).
Så for at vise date og number elementerne (child elementer til root), tilgår vi dem som almindelige variabler gennem et objekt:

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

?>

Det var egentlig ret simpelt ikke? Altså, med et objekts selector ( -> ) kan vi tilgå child-elementer fra vores XML objekt.

Når SimpleXML ikke længere er så simpelt

Det er jo tilsyneladende ret nemt at fange et child-element - så længe det ligger i roden af XML dokumentet, altså.
Men hvad så når du skal have fat i disse child-elementers elementer (subchilds)?

Få fat i et subchild

Nu vil det være relevant for vores yderligere visning, hvis vi kunne vise informationer om vores kunde.
Kundens oplysninger ligger i child-elementet purchased-by.
Vi ved jo allerede nu, hvordan vi får fat i et child-element til root - så nu skal vi blot have fat i subchild-elementerne name og address

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
echo "Navn: ".$xmlObject->purchasedby->name//Vis kundens navn
    
echo "Adresse: ".$xmlObject->purchasedby->address// Vis kundens adresse


?>

Altså, vi kan nu konstatere at vi kan gå kan tilgå subchild-elementer ved hjælp af objekt selectoren. Vi kan faktisk gå alle de niveauer ned vi vil.
Det kan dog hurtigt blive uoverskueligt at skulle gå hele vejen ned gennem subchilds, for hver linie man har. Der er så en mulighed for at kunne tildele en variabel et specifikt child.

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
$purchasedby $xmlObject->purchasedby// Tildel child-element purchasedby til en variabel

    
echo "Navn: ".$purchasedby->name//Vis kundens navn med den nye variabel
    
echo "Adresse: ".$purchasedby->address// Vis kundens adresse med den nye variabel


?>

Lige netop tildelingen at et specifikt child- eller subchild-element kan være behjælpeligt idet det vil lette forståelsen og anvendelsen af de muligheder SimpleXML giver.

Særlige karakterer i XML elementer

Du vil til tider støde på at din XML virker utilgængelig, grundet den måde det er skrevet på. Særligt er det for elementers navngivning der er delt med eksempelvis hyphens (-) eller kolon(:).
Det er dog vigtigt at bemærke at opdelingen med de to førnævnte tegn har to forskellige betydninger!

Hyphen i mine XML elementer

Indtil videre har vi med stor succes opnået at udskrive elementers indhold ud for os.
Det fortsætter vi med, eftersom det nu kunne være interessant at vide hvad kunden har købt.

Nu er det sådan at child-elementet order-items indeholder nogle subchilds som er navnangivet identisk - de kaldes item.
Men med en ganske simpel foreach-løkke kan man faktisk iterere sig igennem disse.
Det man skal gøre er at vælge sig ind til de elementer man gerne vil iterere over - i dette tilfælde er det child-elementerne til order-items.
Derefter kan vi, ligesom vi gjorde med kundeoplysningerne, tilgå subchild-elementer i den variabel der indeholder det oprindelige child:

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
$purchasedby $xmlObject->purchasedby// Tildel child-element purchasedby til en variabel

    
echo "Navn: ".$purchasedby->name//Vis kundens navn med den nye variabel
    
echo "Adresse: ".$purchasedby->address// Vis kundens adresse med den nye variabel

    // Vi itererer nu over <item> elementerne i XML'en og lægger indholdet af hver <item> i  variablen
    
foreach($xmlObject->order-items->item as $item)
    {
        echo 
$item->code;
        echo 
$item->type;
        echo 
$item->label;
    }


?>

Super simpelt - bortset fra at det ikke virker...

Du skulle nu gerne få en fejl der lyder noget i retning af Parse error: syntax error, unexpected T_OBJECT_OPERATOR in....
Årsagen er at du har en hyphen i order-items elementet.

For at komme udover det, må vi bruge en lidt speciel konstruktion til at angive vores elementer til SimpleXML.
Vi skal anvende syntaksen {"order-items"} udelukkende fordi vi har et tegn med som vi ikke kan arbejde med i PHP i den sammenhæng som vi forsøger at gøre det i.
For at få det til at fungere, skal der altså stå:

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
$purchasedby $xmlObject->purchasedby// Tildel child-element purchasedby til en variabel

    
echo "Navn: ".$purchasedby->name//Vis kundens navn med den nye variabel
    
echo "Adresse: ".$purchasedby->address// Vis kundens adresse med den nye variabel

    // Vi itererer nu over <item> elementerne i XML'en og lægger indholdet af hver <item> i  variablen
    
foreach($xmlObject->{"order-items"}->item as $item// <-- Bemærk forskellen
    
{
        echo 
$item->code;
        echo 
$item->type;
        echo 
$item->label;
    }


?>

Perfekt - nu virker det som det skal.
Det er så også muligt for os at fastslå at vi også manuelt kan tilgå de enkelte subchild-elementer som vi ville tilgå et array.
Forestil dig dette:

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
$purchasedby $xmlObject->purchasedby// Tildel child-element purchasedby til en variabel

    
echo "Navn: ".$purchasedby->name//Vis kundens navn med den nye variabel
    
echo "Adresse: ".$purchasedby->address// Vis kundens adresse med den nye variabel

    // Nu tilgår vi <item> elementerne, ligesom vi kan tilgå et almindeligt array (et array index starter altid med 0).

    // Det første <item> element
    
echo $xmlObject->{"order-items"}->item[0]->code;
    echo 
$xmlObject->{"order-items"}->item[0]->type;
    echo 
$xmlObject->{"order-items"}->item[0]->label;

    
// Det andet <item> element.
    
echo $xmlObject->{"order-items"}->item[1]->code;
    echo 
$xmlObject->{"order-items"}->item[1]->type;
    echo 
$xmlObject->{"order-items"}->item[1]->label;

?>

Det er dog lidt mere bøvlet at arbejde med, end man på almindelig vis normalt vil gennemgå et array, men det giver dig en forståelse for, hvor simpelt det er at arbejde med.

Et nyt problem er dog så dukket op - vi skal jo også have have antallet af bestilte produkter frem
Det er angivet som attributten quantity i subchild-elementet item.
Men det tager vi lige om lidt...

Kolon i mine XML elementer

Du kan støde på at der i din XML er angivet elementer med et kolon i navnet. Disse er namespaces.
De fortjener et kapitel for sig selv, da det er lidt omfangsrigt at få til at virke, så for vi fortsætter lige med den XML vi har og går videre til behandling af attributter.

Atributter i XML

Når vi nu har hentet kundens bestilte varer ud af vores XML, men vi skal også have mængen af de bestilte varer.
I vores XML er det angivet som attributten quantity i subchild-elementet item.

Når XML'en er indlæst i SimpleXML vil et elements attributter være tilgængelige som et associated array.
Det betyder at vi tilgår en attribut således:

<?php

    
echo $xmlObject->child->subchild["attribute"];

?>

Det virker ret ligetil, ikke? Vi anvender så samme princip på vores XML for at få mængden af bestillinger ud:

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
$purchasedby $xmlObject->purchasedby// Tildel child-element purchasedby til en variabel

    
echo "Navn: ".$purchasedby->name//Vis kundens navn med den nye variabel
    
echo "Adresse: ".$purchasedby->address// Vis kundens adresse med den nye variabel

    // Vi itererer nu over <item> elementerne i XML'en og lægger indholdet af hver <item> i  variablen
    
foreach($xmlObject->{"order-items"}->item as $item)
    {
        echo 
$item["quantity"]; // Her udskrives 'quantity' attributten fra <item> elementet

        
echo $item->code;
        echo 
$item->type;
        echo 
$item->label;
    }

?>

Og princippet er fuldstændig det samme, hvis vi ville tilgå elementer manuelt:

<?php

    $xmlContent 
file_get_contents("sti/til/order.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"Dato: ".$xmlObject->date//Vis datoen
    
echo "Ordrenummer: ".$xmlObject->number// Vis ordrenummeret

    
$purchasedby $xmlObject->purchasedby// Tildel child-element purchasedby til en variabel

    
echo "Navn: ".$purchasedby->name//Vis kundens navn med den nye variabel
    
echo "Adresse: ".$purchasedby->address// Vis kundens adresse med den nye variabel

    // Nu tilgår vi <item> elementerne, ligesom vi kan tilgå et almindeligt array (et array index starter altid med 0).

    // Det første <item> element
    
echo $xmlObject->{"order-items"}->item[0]["quantity"]; // Her udskrives 'quantity' attributten fra <item> elementet

    
echo $xmlObject->{"order-items"}->item[0]->code;
    echo 
$xmlObject->{"order-items"}->item[0]->type;
    echo 
$xmlObject->{"order-items"}->item[0]->label;

    
// Det andet <item> element.
    
echo $xmlObject->{"order-items"}->item[1]["quantity"]; // Her udskrives 'quantity' attributten fra <item> elementet

    
echo $xmlObject->{"order-items"}->item[1]->code;
    echo 
$xmlObject->{"order-items"}->item[1]->type;
    echo 
$xmlObject->{"order-items"}->item[1]->label;

?>

Namespaces og SimpleXML

En mere kompleks historie kommer på banen, når vi snakker om namespaces i din XML.
Men vi skal først bruge et eksempel at gå ud fra.
Herunder laver vi et eksempel der angiver et billede galleri:

<?xml version="1.0" encoding="UTF-8"?>
<!-- gallery.xml -->
<images xmlns:media="http://example.org/images-namespace/">
    <gallery name="Julen 2007">
        <image>
            <media:url type="private">http://example.org/images/DSC00053.JPG</media:url>
            <media:label>Juletræet</media:label>
            <media:caption>Her danser Josefine og Emil rundt om juletræet</media:caption>
            <media:info width="800" height="600" type="image/jpeg" />
        </image>
        <image>
            <media:url type="private">http://example.org/images/DSC00056.JPG</media:url>
            <media:label>Flæskesteg a la mor</media:label>
            <media:caption>Mors fine flæskesteg med de lækre og sprøde svær ;)</media:caption>
            <media:info width="800" height="600" type="image/jpeg" />
        </image>
    </gallery>
</images>

Som det kan ses i root-elementet images er der angivet et namespace kaldet media.
Selve namespacet er angivet unikt - i langt de fleste tilfælde - som en URL. I ovenstående tilfælde har jeg valgt at navngive mit namespace med http://example.org/images-namespace/. I virkeligheden kunne der stå praktisk talt hvad som helst - det kunne være en URI eller URN eller bare et tilfældigt ord.

Indledningvis instantierer vi blot vores nye XML ligesom før. Der er ikke noget særligt ved det blot fordi der nu er et namespace i.

<?php

    $xmlContent 
file_get_contents("sti/til/gallery.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

?>

Tja, ingen problemer der. Men lad os nu få udskrevet navnet på vores galleri - igen, ved hjælp af de samme teknikker som før.

<?php

    $xmlContent 
file_get_contents("sti/til/gallery.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"<h1>".$xmlObject->gallery[0]["name"]."</h1>";

?>

Ja, heller ingen ben i det - det har vi jo også set før.
Og nu skal vi have fat i billederne, vise det og udskrive de relevanter oplysninger for hvert billede - dem itererer vi blot over ligesom vi gjorde tidligere:

<?php

    $xmlContent 
file_get_contents("sti/til/gallery.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"<h1>".$xmlObject->gallery[0]["name"]."</h1>";

    foreach(
$xmlObject->gallery[0]->image as $image)
    {
        
// Vi benytter den specielle konstruktion fordi der er et særligt tegn i.
        
echo $image->{"media:url"};
        echo 
$image->{"media:label"};
        echo 
$image->{"media:caption"};
    }

?>

Igen, lige så simpelt som tidligere - bortset fra at det ikke virker...

Problemet er at media subchild-elementet er tilknyttet et namespace. For at kunne få dem frem, må vi have de tunge skyts frem fra SimpleXML.

Når du tilgår et element i din XML med SimpleXML, er alle SimpleXML elementerne faktisk PHP objekter. Det betyder at der også er tilknyttet nogle funktioner til dine SimpleXML elementer; blandt andet children() og attributes().
Navngivningen af de funktioner bør være selvforklarende, men vi tager lige et par praktiske eksempler.

Funktionen children()

Det er en ret simpel funktion som returnerer child-elementer til et valgt elementer. Men da SimpleXML i forvejen tilbyder lette og nemme metoder, som beskrevet tidligere, at tilgå elementerne i forvejen er det nemmest at anvende dem når det kan lade sig gøre.

For at kunne anvende funktionen på elementer der er omfattet af mulighederne med namespaces, skal du rent faktisk kende navnet på det namespace du skal anvende.
Til det formål findes der også nogle andre funktioner, i SimpleXML, som kan finde dem frem for dig og du kan behandle disse dynamisk. Det er dog et emne som ikke bliver dækket her.

Nu er vi til gengæld så heldige at vi netop før konstaterede at namespacet var navngivet http://example.org/images-namespace/. Det skal bruges som en parameter i children() funktionen.
Men nok snak, lad os se et brugbart eksempel...

<?php

    $xmlContent 
file_get_contents("sti/til/gallery.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"<h1>".$xmlObject->gallery[0]["name"]."</h1>";

    foreach(
$xmlObject->gallery[0]->image as $image)
    {
        
// her får vi alle media namespacets elementer ind i variablen 
        
$media $image->children("http://example.org/images-namespace/");

        
// herfra kan vi så gøre præcis som tidligere
        
echo $media->url;
        echo 
$media->label;
        echo 
$media->caption;
    }

?>

Ved at hente namespacets elementer ind i en variabel, kan vi nu gøre som vi tidligere har gjort - med en enkelt undtagelse; attributterne er stadig ikke tilgængelig for namespacets elementer. Det er derfor vi har funktion attributes() som vi vil gennemgå med et hurtigt eksempel.

Funktionen attributes()

For at hente attributter på et element som er en del af et namespace, bliver vi nød til at anvende denne funktion. Ligesom med children() kan denne naturligvis også benyttes på et almindeligt SimpleXML element, men det er trods alt stadig nemmere at anvende den simple tilgang som vi dækkede tidligere i artiklen.

Men igen, lad os tege et eksempel.
Vi skal have fat i attributten type fra namespace-elementet media:url:

<?php

    $xmlContent 
file_get_contents("sti/til/gallery.xml");
    
$xmlObject = new SimpleXMLElement$xmlContent );

    echo 
"<h1>".$xmlObject->gallery[0]["name"]."</h1>";

    foreach(
$xmlObject->gallery[0]->image as $image)
    {
        
// her får vi alle media namespacets elementer ind i variablen 
        
$media $image->children("http://example.org/images-namespace/");
        
$urlAttributes $media->url->attributes(); // hent <media:url> attributter ind i 

        
echo $urlAttributes["type"]; //Udskriv 'type' attributten fra <media:url> som vi hentede på linen før denne

        
echo $media->url;
        echo 
$media->label;
        echo 
$media->caption;
    }

?>

Selvom det godt kan virke lidt langhåret, så er det faktisk - med lidt øvelse - et rigtig godt værktøj at tilgå dine XML filer og data med.