Extraer el Contenido del Body de Documentos HTML Completos en PHP con DOMDocument
Introducción
Editores de contenido, page builders e importadores suelen recibir dos formas distintas de HTML:
- un fragmento, como
<p>Contáctanos</p> - un documento completo, como
<!doctype html><html><head>...</head><body>...</body></html>
Si tu componente espera un fragmento, renderizar un documento completo dentro de él puede crear markup inválido, metadata duplicada o bugs de layout. La solución no es un sanitizador completo. Es un paso pequeño de normalización: cuando el input es un documento completo, extraer solo el contenido interno de <body>. Cuando el input ya es un fragmento, dejarlo igual.
Este artículo muestra un helper práctico en PHP usando DOMDocument.
El Objetivo
El helper debe seguir tres reglas:
- input vacío devuelve una cadena vacía
- fragmentos HTML normales se devuelven sin cambios
- documentos HTML completos devuelven solo los hijos del body
Por ejemplo:
$html = '<!doctype html>
<html>
<head><title>Ignorado</title></head>
<body>
<section>
<h2>Formulario de reserva</h2>
<p>Elige una hora.</p>
</section>
</body>
</html>';
echo bodyContents($html);Salida:
<section>
<h2>Formulario de reserva</h2>
<p>Elige una hora.</p>
</section>Pero este fragmento queda exactamente igual:
echo bodyContents('<p>Ya es un fragmento</p>');
// <p>Ya es un fragmento</p>El Helper
Esta es la implementación completa:
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;
}La función es conservadora a propósito. Solo parsea el input que parece un documento completo. Así evita pasar cada fragmento pequeño por DOMDocument, que puede normalizar espacios, reparar tags o cambiar levemente el formato de salida.
Por Qué Detectar Antes de Parsear
DOMDocument::loadHTML() es útil, pero no es un formateador neutral. Parsea y repara markup. Para HTML con forma de documento, eso es justo lo que queremos. Para fragmentos simples, puede ser innecesario y sorpresivo.
Este guard mantiene simple el camino común:
if (! preg_match('/<html\b|<body\b|<!doctype/i', $trimmed)) {
return $html;
}El patrón detecta las señales habituales de un documento completo:
- un tag
<html> - un tag
<body> - una declaración
<!doctype>
Si no existe ninguna de esas señales, el helper asume que el input ya sirve para renderizarse inline.
Parsear Sin Warnings
El HTML real de editores puede incluir tags HTML5, documentos incompletos o markup que genera quejas en libxml. El manual de PHP para DOMDocument::loadHTML indica que el comportamiento del parser depende de libxml y que el HTML moderno puede producir warnings.
Para un helper de normalización, esos warnings no deberían aparecer en logs o respuestas, así que el parser usa manejo interno de errores:
libxml_use_internal_errors(true);
$loaded = $dom->loadHTML($source, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
libxml_clear_errors();Las flags reducen wrappers extra de documento en la salida serializada. Aun así, la función trata un fallo de parseo como algo no fatal:
if (! $loaded) {
return $html;
}Ese fallback importa. Si el helper no puede extraer un body con confianza, debe devolver el input original en vez de descartar contenido silenciosamente.
Preservar UTF-8
La implementación antepone una pista de encoding XML antes de parsear:
'<?xml encoding="UTF-8">' . $trimmedEste es un workaround común de DOMDocument para contenido UTF-8. Sin una señal explícita de encoding, los caracteres no ASCII pueden interpretarse mal dependiendo del input y del comportamiento de libxml.
Si tus documentos fuente ya tienen metadata de charset confiable, puede que no necesites este enfoque exacto. Para snippets pegados y contenido de CMS, la pista explícita es un buen default defensivo.
Extraer Solo los Hijos del Body
Cuando el documento ya cargó, el paso importante es serializar cada hijo de <body>:
$body = $dom->getElementsByTagName('body')->item(0);
$inner = '';
foreach ($body->childNodes as $child) {
$inner .= $dom->saveHTML($child);
}DOMDocument::saveHTML() puede serializar el documento completo o un nodo específico. El manual de PHP documenta ese parámetro opcional en DOMDocument::saveHTML. Pasar cada nodo hijo nos da el equivalente a innerHTML en el navegador.
Así evitamos devolver el wrapper <body>.
Usarlo en un Pipeline de Views
Un caso común es normalizar la forma del HTML guardado antes de entregarlo a un componente:
$template = bodyContents((string) $storedTemplate);
return view('components.dynamic-form', [
'template' => $template,
]);En Laravel Blade, podría verse así:
@php
$template = \App\Support\Html::bodyContents((string) $template);
@endphp
<livewire:dynamic-form :template="$template" />Ahora los editores pueden pegar un fragmento o un documento HTML completo, y el componente igual recibe la forma que espera.
Límite de Seguridad Importante
Importante
Este helper no sanitiza HTML. Solo extrae el contenido del body cuando el input tiene forma de documento.
Si los usuarios pueden enviar HTML no confiable, ejecuta un sanitizador real después de la extracción. Por ejemplo, usa un sanitizador basado en allowlist como HTML Purifier o el sanitizador ya aprobado en tu stack.
El pipeline seguro es:
- normalizar la forma del documento con
bodyContents() - sanitizar tags y atributos no confiables
- renderizar con las reglas de escaping correctas para tu framework
No trates el parseo DOM como un filtro de seguridad.
Nota Sobre PHP 8.4
Para código nuevo en PHP 8.4+, revisa también Dom\HTMLDocument, que PHP recomienda para parseo compatible con HTML5. DOMDocument::loadHTML() sigue estando ampliamente disponible y funciona bien para esta tarea estrecha de extracción, pero el parseo de HTML moderno sigue evolucionando.
Si necesitas construcción precisa de árboles HTML5, prefiere el parser nuevo. Si solo necesitas un helper pequeño de compatibilidad en una app PHP 8.1/8.2/8.3, DOMDocument sigue siendo una opción práctica.
Conclusión
bodyContents() es pequeño, pero evita un problema común de render: documentos HTML completos filtrándose dentro de componentes que esperan fragmentos.
Los patrones útiles son:
- detectar input con forma de documento antes de parsear
- dejar los fragmentos sin cambios
- parsear con manejo silencioso de errores de libxml
- extraer los hijos del body con
saveHTML($child) - volver al input original cuando el parseo falle
Así tu pipeline de contenido recibe una forma HTML estable sin fingir que resuelve sanitización, validación o limpieza completa de HTML.
