PHP/Tutorials/DOMDocument
Die DOM-Klassensammlung in PHP für das Document Object Model (DOM) stellt einen vollständigen Parser für HTML- und XML-Elemente zur Verfügung. Mit Hilfe ihrer Methoden kann man Elemente und Teilbäume aus dem Elementenbaum extrahieren, kopieren, manipulieren und auch in ihn einfügen. So ist es ist z. B. mit wenigen Zeilen möglich, alle Hyperlinks eines Dokumente auszulesen, alle Source-Referenzen auf Bilder auszulesen oder einen kompletten Bereich (=>Fragment, z. B. DIV mit Unterelementen) aus einem Dokument zu extrahieren und in ein anderes wieder einzufügen.
Inhaltsverzeichnis
Sinn und Zweck
Wenn man mit HTML-Code auf der Basis von Stringfunktionen hantiert, muss man immer wieder darauf achten, dass die Daten kontextgerecht kodiert werden und dass öffnende und schließende Tags zusammenpassen. Diese Sorgen hat man nicht, wenn man mit dem DOM ein Datenmodell hat, das am Ende korrekt zu HTML-Code serialisiert an den Browser geht.
$html = '<h1>' . 'Warum 2 < als 1 ist' . '<h1>';
echo $html;
Verwendung
Die diversen DOM-Klassen in PHP folgen dem DOM-Standard und entsprechen damit weitestgehend den Möglichkeiten, die auch z. B. JavaScript im Browser bietet. Daher leistet die DOMDocument-Klasse ähnliches wie der Browser, wenn er ein HTML-Dokument parst.
Laden eines HTML-Dokuments
$dom = new DOMDocument;
$dom->loadHTMLFile( 'index.html' );
echo $dom->saveHTML();
// kürzer:
$dom2 = DOMDocument::loadHTMLFile( 'index.html' );
echo $dom2->saveHTML();
In der ersten Anweisung wird eine Instanz der Klasse DOMDocument erzeugt. Dann wird mittels loadHTMLFile()
der Inhalt der Datei index.html geladen und geparst. Ab jetzt hat $dom
ein HTML-Dokument in seiner Datenstruktur.
Anschließend wird die Datenstruktur mit saveHTML()
wieder zu einem String mit HTML-Code serialisiert und per echo
an den Browser ausgegeben.
DOM-Methoden
Die Datenstruktur in einem DOMDocument können Sie gemäß des DOM-Standards mit DOM-Methoden nutzen:
Diese beiden Methoden seien nur beispielhaft aufgeführt. Die so erhaltenen Element-Knoten sind wiederum Objekte, aber nicht vom Typ DOMDocument, sondern DOMElement, womit sie wieder andere Möglichkeiten gemäß des DOM-Standards bieten:
Das DOM traversieren und Elemente finden
Es gibt verschiedene Möglichkeiten sich durch das DOM zu bewegen, um Elemente zu finden. Die Funktion getElementsByTagName()
z. B. sucht alle Elemente, die den übergebenen Namen besitzen:
$Dom = DOMDocument::loadHTMLFile( 'index.html' );
$DomNodeList = $Dom->getElementsByTagName( 'p' );
Rückgabewert ist eine DOMNodeList-Klasse, über die Sie dann mit foreach iterieren und so auf jedes einzelne Element zugreifen können:
$xml = '<nav>
<ul>
<li><a href="link_1.html">Wiki</a></li>
<li><a href="link_2.html">Blog</a></li>
<li><a href="link_3.html">Forum</a></li>
</ul>
</nav>';
$dom = DOMDocument::loadXML($xml);
$links = $dom->getElementsByTagName('a');
foreach ($links as $book) {
echo $book->nodeValue, PHP_EOL;
}
echo
-Anweisungen ergeben diese Ausgabe: Wiki Blog Forum
Hauptartikel: PHP/Tutorials/Link-Checker
Die Browser-Hersteller haben die Entwicklungen im Bereich JavaScript mitgetragen und den erweiterten Bedarf an DOM-Methoden umgesetzt, sodass es nun in JavaScript Dinge wie getElementsByClassName
gibt, die über den eigentlichen DOM-Standard hinausgehen. Insbesondere das sehr nützliche querySelector
, welches CSS-Selektoren versteht und so passende Elemente im Dokument findet, ist in den DOM-Klassen von PHP nicht verfügbar und wird es auch nie sein. Dafür aber gibt es die DOMXPath-Klasse, mit der man etwas ähnliches erreichen kann:
DOMXPath
Die XML Path Language (XPath) dient dem Adressieren von Knoten in XML-Dokumenten (und so auch HTML-Dokumenten) und benutzt hierzu Pfaden ähnliche Ausdrücke. Die PHP DOMXPath-Klasse benutzt jedoch eine andere Notation.
$html = '<!DOCTYPE html><html>
<head><meta charset="utf-8"><title>test</title></head>
<body>
<div>test1</div>
<div>test2</div>
<div class="blog">
test3
<a href="1">a1-test3</a>
<p><a href="2">a2-test3</a></p>
</div>
<div>test4</div>
<div class="blog">
test5
<a href="3">a1-test5</a>
<a href="4">a2-test5</a>
</div>
<div>test2</div>
<div>test2</div>
</body></html>
';
$doc = DOMDocument::loadHTML($html);
$xpath = new DOMXpath($doc);
// alle <a href="..."> innerhalb von <div class="blog">
// CSS-Selektor: div.blog a[href]
// z.B. JavaScript: document.querySelector('div.blog a[href]')
$links = $xpath->query('//div[@class="blog"]//a[@href]');
foreach ($links as $a) {
echo $a->getAttribute('href'),' - ',$a->textContent,PHP_EOL;
}
echo
-Anweisung in der Schleife führt zu diesem Ergebnis: 1 - a1-test3 2 - a2-test3 3 - a1-test5 4 - a2-test5
Praxisbeispiele
Metavariablen in Templates ersetzen
Wer für seine Anwendung HTML-Dokumente als Vorlagendateien (Templates) bereit hält, verwendet vielleicht in diesen Dateien bestimmte Metavariablen als Platzhalter für die einzusetzenden Werte.
{$var}
). Das ist eine willkürliche Festlegung und kann selbstverständlich vollkommen anders gelöst werden.Das Template
Das Template ist ein vollwertiges HTML-Dokument, welches prinzipiell alle Anforderungen der Spezifikation erfüllt. Je nach dem wie die Applikation mit Templates umgeht, können prinzipiell alle Teile eines Templates für die Applikation von Bedeutung sein. Wir sehen hier, dass im <head>
und im <body>
Metavariablen zum Einsatz kommen:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>{$title}</title>
</head>
<body>
<form action="{$path}" method="post">
<fieldset id="data">
<legend>vorhandene Datensätze</legend>
<ul></ul>
</fieldset>
<fieldset id="settings">
<legend>Einstellungen</legend>
<ul>
<li>
<label>
Titel des Webangebots
<input name="title" value="{$title}">
</label>
</li>
<li>
<label>
<input name="mod" type="checkbox">
Nachricht des Tages anzeigen
</label>
</li>
</ul>
<p>
<button name="save-settings">
Einstellungen speichern
</button>
</p>
</fieldset>
</form>
</body>
</html>
Das Ersetzen
Wie kann man nun das in ein DOMDocument-Objekt geladene Template bearbeiten und die Metavariablen darin ersetzen? Die folgende Funktion kann das leisten:
/**
* Metavariablen {$var} in einem DOM-Knoten ersetzen
*
* Diese Funktion arbeitet rekursiv!
*
* @param DOMNode
* @param array
*/
function replace_metavars ($node, $data) {
// finde {$irgend-was} in Attributwerten
if ($node->hasAttributes()) {
foreach ($node->attributes as $a) {
$replace = $a->value;
foreach ($data as $key => $value) {
$replace = str_replace(
sprintf('{$%s}', $key), // search
$value, // replace
$replace
);
}
$a->childNodes->item(0)->replaceData(
0, // ab erstem Zeichen
strlen($a->value), // bis letztes Zeichen
$replace
);
}
}
// finde {$irgend-was} in Textknoten
if ($node->firstChild) {
foreach ($node->childNodes as $child) {
// Textknoten?
if ($child->nodeType == XML_TEXT_NODE) {
$replace = $child->textContent;
foreach ($data as $key => $value) {
$replace = str_replace(
sprintf('{$%s}', $key), // search
$value, // replace
$replace
);
}
$child->replaceData(
0, // ab erstem Zeichen
strlen($child->textContent), // bis letztes Zeichen
$replace
);
} else {
// Nein, alle Kindknoten abarbeiten
$t->replace_metavars($child, $data);
}
}
}
}
// Metavariablen sollen diese Werte haben
$replace = array(
'path' => './irgend/wo/script.php',
'title' => 'Toller Seitentitel'
);
// Template laden
$tmp = new DOMDocument('1.0', 'utf-8');
$tmp->loadHTMLFile($pfad_zum_template);
// alle <html>-Elemente (es gibt nur eines) durchforsten
foreach ($tmp->getElementsByTagName('html') as $html) {
replace_metavars($html, $replace);
}
Der Vorteil bei der Verwendung der DOMDocument-Klasse ist, dass man sich an dieser Stelle keine Gedanken um kontextgerechte Maskierung der Daten machen muss. Die Funktion replace_metavars
verwendet deshalb auch nirgendwo eine Maskierungsfunktion wie z. B. htmlspecialchars
(für Textknoten) oder rawurlencode
(für Attributwerte).
Template und Ausgabe-HTML
Jetzt haben wir ein ausgefülltes Template. Wie kommen nun dessen Bestandteile in das Gesamtdokument, welches letzten Endes an den Browser versandt werden soll? Hier müssen zwei DOMDocument-Instanzen miteinander zusammengeführt werden. Dabei werden Elementknoten aus einem Dokument in das andere importiert.
Nehmen wir an, dass der Inhalt des <body>
-Elements aus unserem Template in das <main>
-Element des Ausgabedokuments eingefügt werden soll:
/**
* alle Kindknoten eines DOM-Knotens in einen anderen überführen
*
* @param DOMNode
* @param DOMNode
* @param bool
*/
function move_childnodes ($from, $to, $backwards=false) {
if ($backwards) {
while ($from->lastChild) {
$n = $from->removeChild($from->lastChild);
// verschiedene Dokumente?
if ($n->ownerDocument !== $to->ownerDocument) {
$n = $to->ownerDocument(
$n, // DOM-Knoten
true // mit komplettem Inhalt
);
}
$to->insertBefore($n, $to->firstChild);
}
} else {
while ($from->firstChild) {
$n = $from->removeChild($from->firstChild);
// verschiedene Dokumente?
if ($n->ownerDocument !== $to->ownerDocument) {
$n = $to->ownerDocument->importNode(
$n, // DOM-Knoten
true // mit komplettem Inhalt
);
}
$to->appendChild($n);
}
}
}
// Ausgabedokument laden
$doc = new DOMDocument('1.0', 'utf-8');
$doc->loadHTMLFile($pfad_zum_haupttemplate);
// Template laden
$tmp = new DOMDocument('1.0', 'utf-8');
$tmp->loadHTMLFile($pfad_zum_template);
// <body> des Templates in <main> des Ausgabedokuments einfügen
move_childnodes(
$tmp->getElementsByTagName('body')->item(0),
$doc->getElementsByTagName('main')->item(0)
);
move_childnodes
schneidet jeweils den ersten Kindknoten des übergebenen Dokumentknotens aus und importiert ihn (falls nötig - hier ist es nötig) in das einzufügende Dokument, um ihn im übergebenen Dokumentknoten anzufügen.Diese Funktion kennt auch einen Rückwärtsmodus, falls man die Inhalte am Anfang eines Dokumentknotens einfügen möchte. Dafür kann man den (optionalen) dritten Parameter mit
true
übergeben.Prinzipiell kann man nun auch Inhalte wie oben im Template das <title>
-Element oder andere Bestandteile des <head>
ins Ausgabedokument überführen. Das mag besonders bei ergänzenden CSS- oder JavaScript-Dokumenten sinnvoll sein, die bei Bedarf ergänzt werden sollen.
Formularinhalte vorbelegen
Mit den Möglichkeiten der DOM-Klassen von PHP kann man Formularelemente bequem vorbelegen. Schauen wir uns das folgende Formular an:
<form method="post">
<h1>Einstellungen</h1>
<ul>
<li>
<label>
Titel des Webangebots
<input name="title">
</label>
</li>
<li>
<label>
<input name="mod" type="checkbox">
Nachricht des Tages anzeigen
</label>
</li>
<li>
<label for="mod-text">Wortlaut der Nachricht</label>
<textarea id="mod-text" name="mod-text"></textarea>
</li>
<li>
<label>
Darkmode
<select name="mode">
<option value="light">hell</option>
<option value="dark">dunkel</option>
</select>
</label>
</li>
</ul>
<p><button name="save">speichern</button></p>
</form>
Die folgende PHP-Funktion kann die im obigen Beispiel aufgeführten Eingabefelder sinnvoll vorbelegen:
/**
* Formularelemente mit Inhalten befüllen
*
* Folgende Elemente werden unterstützt:
* - <input (keine Radio-Buttons)
* - <textarea>
* - <option> (mit value-Attribut)
*/
function preselect_form_values ($node, $data) {
foreach ($node->getElementsByTagName('*') as $n) {
if ($n->hasAttribute('name')) {
foreach (array_keys($data) as $key) {
if ($n->getAttribute('name') === $key) {
switch ($n->tagName) {
case 'input':
if ($n->hasAttribute('type')
&& 'checkbox' == $n->getAttribute('type')
) {
// check?
if ($data[$key]) {
$n->setAttribute('checked', 'checked');
}
} else {
// wie type="text" behandeln
$value = $data[$key];
if (is_array($value)) {
$value = implode(', ', $value);
}
$n->setAttribute('value', $value);
}
break;
case 'select':
// finde auszuwählendes <option>-Element
foreach ($n->getElementsByTagName('option') as $o) {
if ($o->hasAttribute('value')
&& $o->getAttribute('value') == $data[$key]
) {
$o->setAttribute('selected', 'selected');
}
}
break;
case 'textarea':
while ($n->firstChild) {
$n->removeChild($n->firstChild);
}
$value = $data[$key];
if (is_array($value)) {
$value = implode("\r\n", $value);
}
$n->appendChild(
$n->ownerDocument->createTextNode($value)
);
break;
}
}
}
}
}
}
// einzutragende Formularwerte
$data = array(
'title' => 'Seitentitel',
'mod' => true,
'mod-text' => "Nachricht des Tages: Alles im grünen Bereich.\r\n\r\nWir wünschen frohes Schaffen!",
'mode' => 'dark'
);
// Template laden
$tmp = new DOMDocument('1.0', 'utf-8');
$tmp->loadHTMLFile($pfad_zum_template);
preselect_form_values(
$tmp->getElementsByTagName('form')->item(0),
$data
);
preselect_form_values
sammelt diejenigen Kindknoten des im ersten Parameter übergebenen Dokumentknotens, die vom Typ input
, select
oder textarea
sind. Danach prüft sie anhand deren name
-Attribute in der als zweiten Parameter übergebenen Liste, ob sie einen passenden Wert in das value
-Attribut schreiben, das Attribut selected
vergeben, oder den Wert aus der Liste als Textknoten-Kindknoten anhängen soll.preselect_form_values
noch nicht mit Radio-Buttons umgehen.Weblinks
- php.net: The DOMDocument class
- binarytides.com: Php tutorial – Parse html DOM with DOMDocument
- the-art-of-web.com: PHP: Parsing HTML files with DOMDocument and DOMXpath
- php-rocks.com: Einführung DOMDocument / DOMXPath
<
) notiert werden und zweitens ist das schließendeh1
-Tag kein schließendes, sondern erneut ein öffnendes (korrekt wäre</h1>
gewesen).