PHP/Tutorials/DOMDocument

Aus SELFHTML-Wiki
< PHP‎ | Tutorials
Wechseln zu: Navigation, Suche

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.

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.

Hantieren mit Strings
$html = '<h1>' . 'Warum 2 < als 1 ist' . '<h1>';
echo $html;
Dieses Beispiel produziert gleich zwei Fehler: Erstens muss das kleiner-als-Zeichen als HTML-Entität (&lt;) notiert werden und zweitens ist das schließende h1-Tag kein schließendes, sondern erneut ein öffnendes (korrekt wäre </h1> gewesen).

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

Öffnen und Auslesen von HTML-Dokumenten
$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:

alle Textabsätze finden
$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:

Alle Links finden
$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;
}
Die 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.

Überprüfung auf vorhandene Verweise
$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;
}
Die 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.

Beachten Sie: In diesem Kapitel sehen Metavariablen wie in geschweifte Klammern eingeschlossene PHP-Variablen aus ({$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:

Metavariablen in einem Template
<!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>
Die Metavariablen werden hier sowohl in Attributwerten, als auch in den Textknoten von HTML-Elementen eingesetzt.

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 ersetzen
/**
 * 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:

Template-Inhalt ins Ausgabedokument einfügen
/**
 * 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)
);
Die Funktion 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:

Formularwerte vorbelegen - das Formular
<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>
Es kommen verschiedene (aber nicht alle möglichen!) Typen von Eingabemöglichkeiten zum Einsatz.

Die folgende PHP-Funktion kann die im obigen Beispiel aufgeführten Eingabefelder sinnvoll vorbelegen:

Formularwerte vorbelegen - die PHP-Funktion
/**
 * 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
);
Die Funktion 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.
Beachten Sie: In dieser Form kann die Funktion preselect_form_values noch nicht mit Radio-Buttons umgehen.

Weblinks