DOMDocument - Manipulando documentos XML em PHP

A classe DOMDocument é muito conveniente quando precisamos lidar com documentos XML ou HTML em PHP. Ela pode ser utilizada para converter ou criar documentos com bastante facilidade. Neste tutorial vamos ver como carregar um documento XML, pegar seus valores, percorrer seus nós, acessar os atributos etc.

Então, primeiramente vamos ao XML que iremos utilizar no nosso exemplo, um pedaço feed do podcast Escriba Café.

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
   <channel>
      <title>Escriba Cafe - A história da humanidade</title>
      <link>http://escribacafe.com</link>
      <description><![CDATA[A história do homem, seu mundo, o universo e seus mistérios. Nesse podcast criado e apresentado por Christian Gurtner, a cada episódio você conhece um pouco mais sobre o mundo e o universo.]]></description>
      <atom:link href="https://www.spreaker.com/show/1652479/episodes/feed" rel="self" type="application/rss+xml" />
      <language>pt</language>
      <category>History</category>
      <copyright>Copyright Löwenttur</copyright>
      <image>
         <url>https://d3wo5wojvuv7l.cloudfront.net/t_rss_itunes_square_1400/images.spreaker.com/original/d3c026e72b01479e5d9ada5998348a2c.jpg</url>
         <title>Escriba Cafe - A história da humanidade</title>
         <link>http://escribacafe.com</link>
      </image>
      <lastBuildDate>Wed, 30 Jun 2021 22:48:54 +0000</lastBuildDate>
      <itunes:author>Superplayer &amp; Co e Christian Gurtner</itunes:author>
      <itunes:owner>
         <itunes:name>Löwenttur</itunes:name>
         <itunes:email>condado@lowenttur.com</itunes:email>
      </itunes:owner>
      <itunes:image href="https://d3wo5wojvuv7l.cloudfront.net/t_rss_itunes_square_1400/images.spreaker.com/original/d3c026e72b01479e5d9ada5998348a2c.jpg" />
      <itunes:subtitle>A história do homem, seu mundo, o universo e seus mistérios. Nesse podcast criado e apresentado por Christian Gurtner, a cada episódio você conhece um pouco mais sobre o mundo e o universo.</itunes:subtitle>
      <itunes:summary><![CDATA[A história do homem, seu mundo, o universo e seus mistérios. Nesse podcast criado e apresentado por Christian Gurtner, a cada episódio você conhece um pouco mais sobre o mundo e o universo.]]></itunes:summary>
      <itunes:category text="History" />
      <itunes:explicit>clean</itunes:explicit>
      <itunes:type>episodic</itunes:type>

      <item>
         <title>Jack, o Estripador</title>
         <link>https://www.spreaker.com/user/lowenttur/jackoestripador</link>
         <description><![CDATA[Conheça a história do assassino que aterrorizou Londres no século XIX e ganhou fama por todo o mundo.]]></description>
         <guid isPermaLink="false">https://api.spreaker.com/episode/45517459</guid>
         <pubDate>Wed, 30 Jun 2021 22:48:54 +0000</pubDate>
         <enclosure url="https://chtbl.com/track/AG8F54/api.spreaker.com/download/episode/45517459/jackoestripador.mp3" length="39981386" type="audio/mpeg" />
         <itunes:author>Superplayer &amp; Co e Christian Gurtner</itunes:author>
         <itunes:subtitle>Conheça a história do assassino que aterrorizou Londres no século XIX e ganhou fama por todo o mundo.</itunes:subtitle>
         <itunes:summary><![CDATA[Conheça a história do assassino que aterrorizou Londres no século XIX e ganhou fama por todo o mundo.]]></itunes:summary>
         <itunes:duration>1931</itunes:duration>
         <itunes:explicit>clean</itunes:explicit>
         <itunes:image href="https://d3wo5wojvuv7l.cloudfront.net/t_rss_itunes_square_1400/images.spreaker.com/original/57bc5eed100e5f6f6e1b3460bea78a50.jpg" />
         <itunes:episodeType>full</itunes:episodeType>
      </item>
      <item>
         <title>William Shakespeare</title>
         <link>https://www.spreaker.com/user/lowenttur/escriba-shakespeare</link>
         <description><![CDATA[A enigmática história de um dos maiores autores de todos os tempos.]]></description>
         <guid isPermaLink="false">https://api.spreaker.com/episode/44068944</guid>
         <pubDate>Fri, 26 Mar 2021 23:36:31 +0000</pubDate>
         <enclosure url="https://chtbl.com/track/AG8F54/api.spreaker.com/download/episode/44068944/escriba_shakespeare.mp3" length="31037920" type="audio/mpeg" />
         <itunes:author>Superplayer &amp; Co e Christian Gurtner</itunes:author>
         <itunes:subtitle>A enigmática história de um dos maiores autores de todos os tempos.</itunes:subtitle>
         <itunes:summary><![CDATA[A enigmática história de um dos maiores autores de todos os tempos.]]></itunes:summary>
         <itunes:duration>1484</itunes:duration>
         <itunes:keywords>teatro,literatura,inglaterra,história</itunes:keywords>
         <itunes:explicit>clean</itunes:explicit>
         <itunes:image href="https://d3wo5wojvuv7l.cloudfront.net/t_rss_itunes_square_1400/images.spreaker.com/original/993f3603bf68b096e03b7dd9adfdba5f.jpg" />
         <itunes:episodeType>full</itunes:episodeType>
      </item>
      <item>
         <title>Pompeia</title>
         <link>https://www.spreaker.com/user/lowenttur/escribapompeia</link>
         <description><![CDATA[Breve história de Pompeia e Herculano e o trágico fim que as fez desaparecer por séculos e a filosofia soterrada com elas.]]></description>
         <guid isPermaLink="false">https://api.spreaker.com/episode/42729556</guid>
         <pubDate>Thu, 31 Dec 2020 02:06:17 +0000</pubDate>
         <enclosure url="https://chtbl.com/track/AG8F54/api.spreaker.com/download/episode/42729556/escribapompeia.mp3" length="30016618" type="audio/mpeg" />
         <itunes:author>Superplayer &amp; Co e Christian Gurtner</itunes:author>
         <itunes:subtitle>Breve história de Pompeia e Herculano e o trágico fim que as fez desaparecer por séculos e a filosofia soterrada com elas.</itunes:subtitle>
         <itunes:summary><![CDATA[Breve história de Pompeia e Herculano e o trágico fim que as fez desaparecer por séculos e a filosofia soterrada com elas.]]></itunes:summary>
         <itunes:duration>1423</itunes:duration>
         <itunes:explicit>clean</itunes:explicit>
         <itunes:image href="https://d3wo5wojvuv7l.cloudfront.net/t_rss_itunes_square_1400/images.spreaker.com/original/e95d5c6bdf1d8f82d830fbb44bafc750.jpg" />
         <itunes:episodeType>full</itunes:episodeType>
      </item>
   </channel>
</rss>

Carregando um documento para um DOMDocument

Para criar um documento devemos instanciar um objeto DOMDocument e depois carregar o conteúdo de um XML ou HTML para ele.

$doc = new \DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->load('feed.xml');

Para carregar um XML podemos utilizar o método load que recebe o caminho do arquivo XML que queremos carregar, ou podemos utilizar o método loadXML que carrega o documento através de uma string passada como parâmetro.

$xml = file_get_contents('feed.xml');

$doc = new \DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->loadXML( $xml ); 

Caso esteja trabalhando com HTML, temos os métodos loadHTML para carregar HTML de uma string e loadHTMLFile para carregar HTML de um arquivo.

Lendo os valores dos elementos

Para ler o valor de um elemento(nó ou nodo), primeiro devemos recuperá-lo. Podemos fazer isso através dos métodos getElementById que retorna um objeto DOMElement representando o elemento que possui o id passado por parâmetro para o método, ele é mais útil quando estamos lidando com documento HTML. Também temos método getElementsByTagName que retorna um DOMNodeList, um objeto que basicamente é uma lista de DOMNode contendo todos os elementos que possuem aquele nome de tag.

$channel = $doc->getElementsByTagName('channel')->item(0);
foreach( $channel->childNodes as $node){
    if($node->nodeName === 'title'){
        echo $node->nodeValue;
    }
}

No exemplo acima estamos pegando o titulo do podcast, para isso buscamos o elemento channel do documento através do método getElementsByTagName. Como o método getElementsByTagName retorna um DOMNodeList, ou seja, uma lista de elementos encontrados, devemos informar qual deles queremos pegar, no nosso caso o primeiro elemento(e no caso único) então utilizamos o método item() passando a posição 0 por parâmetro, este método irá retornar um objeto DOMNode.

Como o elemento channel tem vários filhos e queremos apenas o elemento title, então devemos percorrer todos os filhos até encontrar o elemento com o nome title. Fazemos isso percorrendo o atributo childNodes do channel que é um DOMNodeList com todos os elemento filhos. Assim comparamos o nome de cada elemento(nodeName) e quando encontrarmos exibimos o valor do elemento através do atributo nodeValue.

Também podemos pegar o valor de um atributo.

$item = $doc->getElementsByTagName('item')->item(0);
foreach( $item->childNodes as $node){
    if($node->nodeName === 'enclosure'){
        echo $node->attributes->getNamedItem("url")->nodeValue;
    }
}

Neste exemplo pegamos o primeiro elemento item percorremos seus elementos filhos até encontrar o o elemento de nome enclousure, e dele acessamos seus atributos pegando o item com o nome url e depois seu valor através de nodeValue.

Uma maneira mais fácil

Como você pode ter notado pegar informações utilizando somente a classe DOMDocument é possível mas não prático, tendo que ir percorrendo os elementos, recuperando seus filhos e comparando nomes até encontrar o qual desejamos. Para facilitar podemos utilizar outra classe, a DOMXPath que permite realizar consultas utilizando XPath, uma linguagem de consulta própria do XML.

Por exemplo para pegarmos o titulo do podcast poderíamos utilizar o seguinte código.

$xpath = new DOMXPath($doc);
$title = $xpath->query('/rss/channel/title')->item(0)->nodeValue;
echo $title;

Primeiro criamos um objeto DOMXPath, depois chamamos o método query passando uma consulta XPath, no caso queremos selecionar a partir do nó raiz, então iniciamos com barra(/) depois o caminhos dos elementos dentro do XML, depois selecionamos o elemento rss, e dentro dele selecionamos o elemento channel, e ai sim dentro dele selecionamos o elemento title. De certo modo é como estivéssemos montando um caminho em um sistema de diretórios onde cada elemento é uma pasta.

O método query irá retornar um DOMNodeList com todos os elementos encontrados, no nosso caso ele irá encontrar somente um, então utilizamos o método item para retornar o primeiro elemento e depois acessamos o atributo nodeValue do objeto para pegar o valor do elemento.

Uma consulta XPath é formada de algumas expressões como:

ExpressãoDescrição
nodeNameSeleciona todos os elementos com o nome “nodeName
/Seleciona a partir do elemento raiz
.Seleciona a partir do elemento atual
..Seleciona o elemento pai do elemento atual
@Seleciona atributos
nodeName[2]Seleciona o segundo elemento “nodeName
nodeName[last()]Seleciona o último elemento nodeName
nodeName[last()-1] Seleciona o penúltimo elemento nodeName
nodeName[@lang=’pt’]Seleciona o elemento nodeName que possua o atributo lang igual a ‘pt’

Vamos a um outro exemplo pegando todos os arquivos de áudio dos episódio do podcast.

$items = $xpath->query('/rss/channel/item');
foreach($items as $item){
    $mp3 = $xpath->query('./enclosure/@url', $item)->item(0)->nodeValue;
    echo $mp3 . PHP_EOL;
}

Primeiro pegamos todos os elementos item do documento XML, sendo que cada um representa um episódio. Lembrando que o método query retorna um DOMNodeList e implementa a interface Traversable então podemos percorrer ele com um foreach. Na linha 3, utilizamos o método query para selecionar o elemento enclosure dentro do item atual para isso passamos para ele uma string de consulta XPath como primeiro parâmetro e como o segundo o objeto DOMNode que será o elemento atual no qual queremos realizar a consulta.

Na consulta XPath começamos com “.” (ponto) para começar o caminho de seleção do nó atual, e dentro dele acessamos o elemento enclosure, e em seguida acessamos o atributo url dentro dele, note o “@” antes do nome do atributo para sinalizar que queremos acessar o atributo e não um elemento filho.

Criando um documento XML

Podemos criar um documento XML através da classe DOMDocument criando elemento por elemento e adicionando uns dentro dos outros.

$dom = new DOMDocument('1.0', 'utf-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;

$books = $dom->createElement('books');

$book1 = $dom->createElement('book');
$book1->appendChild( $dom->createElement('name', 'PHP - Introdução'));

$image1 = $dom->createElement('image');
$url1 = $dom->createAttribute('url');
$url1->value = 'http://www.example.com/book1.jpg';
$image1->appendChild($url1);
$book1->appendChild($image1);

$book2 = $dom->createElement('book');
$book2->appendChild( $dom->createElement('name', 'PHP - Intermediário'));

$image2 = $dom->createElement('image');
$url2 = $dom->createAttribute('url');
$url2->value = 'http://www.example.com/book2.jpg';
$image2->appendChild($url2);
$book2->appendChild($image2);


$books->appendChild($book1);
$books->appendChild($book2);
$dom->appendChild($books);
echo $dom->saveXML();

Como vimos na linha 1 criamos o documento passando a versão e seu enconding. Na linha 5 criamos um elemento books que será o nó raiz do nosso documento. Adicionamos este elemento ao documento através do método appendChild na linha 28.

Na linha linha 7 criamos um elemento book, este será adicionado ao elemento raiz books na linha 26. Ao elemento book iremos adicionar um elemento name, e um elemento image com um atributo. Criamos o elemento name através do método createElement na linha 8, passando como parâmetros o nome do elemento e seu valor. Em sequencia passamos este objeto criado para o método appendChild do objeto $book1.

Na linha 10 criamos um outro elemento com o nodeName de image, este elemento não terá um nó de texto, mas um atributo url. Para isso criamos um objeto de atributo através do método createAttribute, e na linha 12 adicionamos seu conteúdo para o atributo value. Após isso adicionamos o objeto de atributo url para dentro do elemento image na linha 13 e o objeto de image para dentro do objeto $book1 através do método appendChild na linha 14.

Para criar o segundo livro basicamente fizemos o mesmo processo, que poderia facilmente ser feito em um laço pegando as informações de uma banco ou algo parecido.

Por fim geramos o XML através do método saveXML na linha 29 e exibimos na tela. Também podemos salvar o conteúdo para um arquivo através do método save que recebe o caminho do arquivo.

<?xml version="1.0" encoding="utf-8"?>
<books>
  <book>
    <name>PHP - Introdução</name>
    <image url="http://www.example.com/book1.jpg"/>
  </book>
  <book>
    <name>PHP - Intermediário</name>
    <image url="http://www.example.com/book2.jpg"/>
  </book>
</books>

Validando um documento

Antes manipular um documento XML é interessante se certificar se ele possui todos os elemento que esperamos. Para isso podemos utilizar um documento XSD, que é um documento que descreve as regra de validação de um documento. Normalmente ele é disponibilizado por quem desenvolveu o formato do XML que estamos manipulando. Um exemplo de XSD é o XSD do RSS.

Podemos validar facilmente um documento XML utilizando um arquivo .xsd que o define, para isso utilizamos o método schemaValidate passando o caminho para o arquivo XSD de validação.

$doc = new \DOMDocument();
$doc->load('feed.xml'); 


if( $doc->schemaValidate('rss-2_0.xsd')){
    echo "Válido";
}else{
    echo "inválido";
}

Bom este é o básico para lidar com XML, como sempre mais detalhes, como lidar com namespaces e etc, pode ser encontrado na documentação.