Skip to content

Extraindo o Conteúdo do Body de Documentos HTML Completos em PHP com DOMDocument

Introdução

Editores de conteúdo, page builders e ferramentas de importação costumam receber dois formatos diferentes de HTML:

  • um fragmento, como <p>Fale conosco</p>
  • um documento completo, como <!doctype html><html><head>...</head><body>...</body></html>

Se o seu componente espera um fragmento, renderizar um documento completo dentro dele pode criar markup inválido, metadados duplicados ou bugs de layout. A solução não é um sanitizador completo. É um pequeno passo de normalização: quando a entrada é um documento completo, extrair apenas o conteúdo interno de <body>. Quando a entrada já é um fragmento, mantê-la igual.

Este artigo mostra um helper prático em PHP usando DOMDocument.

O Objetivo

O helper deve seguir três regras:

  • entrada vazia retorna uma string vazia
  • fragmentos HTML normais retornam sem alterações
  • documentos HTML completos retornam apenas os filhos do body

Por exemplo:

php
$html = '<!doctype html>
<html>
    <head><title>Ignorado</title></head>
    <body>
        <section>
            <h2>Formulário de reserva</h2>
            <p>Escolha um horário.</p>
        </section>
    </body>
</html>';

echo bodyContents($html);

Saída:

html
<section>
    <h2>Formulário de reserva</h2>
    <p>Escolha um horário.</p>
</section>

Mas este fragmento permanece exatamente igual:

php
echo bodyContents('<p>Já é um fragmento</p>');
// <p>Já é um fragmento</p>

O Helper

Esta é a implementação completa:

php
function bodyContents(string $html): string
{
    $trimmed = trim($html);

    if ($trimmed === '') {
        return '';
    }

    if (! preg_match('/<html\b|<body\b|<!doctype/i', $trimmed)) {
        return $html;
    }

    $dom = new DOMDocument('1.0', 'UTF-8');

    libxml_use_internal_errors(true);
    $loaded = $dom->loadHTML(
        '<?xml encoding="UTF-8">' . $trimmed,
        LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED
    );
    libxml_clear_errors();

    if (! $loaded) {
        return $html;
    }

    $body = $dom->getElementsByTagName('body')->item(0);

    if (! $body) {
        return $html;
    }

    $inner = '';

    foreach ($body->childNodes as $child) {
        $inner .= $dom->saveHTML($child);
    }

    return $inner;
}

A função é intencionalmente conservadora. Ela só faz parse quando a entrada parece ser um documento completo. Isso evita passar cada pequeno fragmento pelo DOMDocument, que pode normalizar espaços, reparar tags ou alterar levemente a formatação de saída.

Por Que Detectar Antes de Fazer Parse?

DOMDocument::loadHTML() é útil, mas não é um formatador neutro. Ele interpreta e repara markup. Para HTML com formato de documento, isso é exatamente o que queremos. Para fragmentos simples, pode ser desnecessário e surpreendente.

Este guard mantém o caminho comum simples:

php
if (! preg_match('/<html\b|<body\b|<!doctype/i', $trimmed)) {
    return $html;
}

O padrão detecta os sinais mais comuns de um documento completo:

  • uma tag <html>
  • uma tag <body>
  • uma declaração <!doctype>

Se nenhum desses marcadores existir, o helper assume que a entrada já está pronta para renderização inline.

Fazendo Parse Sem Warnings

HTML real vindo de editores pode incluir tags HTML5, documentos incompletos ou markup que gera avisos no libxml. O manual do PHP para DOMDocument::loadHTML observa que o comportamento do parser depende do libxml e que HTML moderno pode gerar warnings.

Para um helper de normalização, esses warnings não devem vazar para logs ou respostas, então o parser usa tratamento interno de erros:

php
libxml_use_internal_errors(true);
$loaded = $dom->loadHTML($source, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
libxml_clear_errors();

As flags reduzem wrappers extras de documento na saída serializada. Mesmo assim, a função trata falha de parse como não fatal:

php
if (! $loaded) {
    return $html;
}

Esse fallback é importante. Se o helper não conseguir extrair um body com confiança, ele deve devolver a entrada original em vez de descartar conteúdo silenciosamente.

Preservando UTF-8

A implementação adiciona uma dica de encoding XML antes de fazer parse:

php
'<?xml encoding="UTF-8">' . $trimmed

Esse é um workaround comum de DOMDocument para conteúdo UTF-8. Sem um sinal explícito de encoding, caracteres não ASCII podem ser interpretados incorretamente dependendo da entrada e do comportamento do libxml.

Se seus documentos de origem já têm metadados confiáveis de charset, talvez você não precise exatamente dessa abordagem. Para snippets colados e conteúdo de CMS, a dica explícita é um bom padrão defensivo.

Extraindo Apenas os Filhos do Body

Depois que o documento carrega, o passo importante é serializar cada filho de <body>:

php
$body = $dom->getElementsByTagName('body')->item(0);

$inner = '';

foreach ($body->childNodes as $child) {
    $inner .= $dom->saveHTML($child);
}

DOMDocument::saveHTML() pode serializar o documento completo ou um nó específico. O manual do PHP documenta esse parâmetro opcional em DOMDocument::saveHTML. Passar cada nó filho nos dá o equivalente a innerHTML no navegador.

Assim evitamos retornar o wrapper <body>.

Usando em um Pipeline de Views

Um caso comum é normalizar o formato do HTML salvo antes de entregá-lo a um componente:

php
$template = bodyContents((string) $storedTemplate);

return view('components.dynamic-form', [
    'template' => $template,
]);

Em Laravel Blade, isso pode ficar assim:

blade
@php
    $template = \App\Support\Html::bodyContents((string) $template);
@endphp

<livewire:dynamic-form :template="$template" />

Agora os editores podem colar um fragmento ou um documento HTML completo, e o componente ainda recebe o formato esperado.

Limite de Segurança Importante

Importante

Este helper não sanitiza HTML. Ele apenas extrai o conteúdo do body quando a entrada tem formato de documento.

Se usuários puderem enviar HTML não confiável, execute um sanitizador real depois da extração. Por exemplo, use um sanitizador baseado em allowlist, como HTML Purifier, ou o sanitizador já aprovado no seu stack.

O pipeline seguro é:

  1. normalizar o formato do documento com bodyContents()
  2. sanitizar tags e atributos não confiáveis
  3. renderizar com as regras de escaping adequadas ao seu framework

Não trate parsing DOM como filtro de segurança.

Nota Sobre PHP 8.4

Para código novo em PHP 8.4+, veja também Dom\HTMLDocument, que o PHP recomenda para parsing compatível com HTML5. DOMDocument::loadHTML() continua amplamente disponível e funciona bem para esta tarefa estreita de extração, mas parsing de HTML moderno continua evoluindo.

Se você precisa de construção precisa de árvores HTML5, prefira o parser mais novo. Se você só precisa de um pequeno helper de compatibilidade em uma aplicação PHP 8.1/8.2/8.3, DOMDocument ainda é uma escolha prática.

Conclusão

bodyContents() é pequeno, mas evita um problema comum de renderização: documentos HTML completos vazando para componentes que esperam fragmentos.

Os padrões úteis são:

  • detectar entrada com formato de documento antes de fazer parse
  • manter fragmentos sem alterações
  • fazer parse com tratamento silencioso de erros do libxml
  • extrair os filhos do body com saveHTML($child)
  • voltar para a entrada original quando o parse falhar

Isso dá ao seu pipeline de conteúdo um formato HTML estável sem fingir que resolve sanitização, validação ou limpeza completa de HTML.