PHPのTidy拡張には、HTMLドキュメントの各要素をノードとして取得するシリーズ関数があります。tidy_get_body() が <body> 要素、tidy_get_head() が <head> 要素を返すように、tidy_get_html() は <html> 要素に対応する tidyNode オブジェクトを返します。
ドキュメント全体の起点ノードとして、lang 属性の確認、<head> と <body> 両方への同時アクセス、HTMLツリー全体の走査といった用途に使います。この記事では基本仕様から実践的な活用パターンまで解説します。
関数概要
| 項目 | 内容 |
|---|---|
| 関数名 | tidy_get_html() |
| 読み方 | タイディ・ゲット・エイチティーエムエル |
| 分類 | Tidy関数 |
| 対応バージョン | PHP 5以降(Tidy拡張が有効な場合) |
| 引数 | tidyオブジェクト |
| 戻り値 | tidyNode(html要素のノード) |
| 必要な拡張 | tidy(多くの環境で標準バンドル) |
| OOPスタイル | $tidy->html() |
構文
// 手続き型
tidy_get_html(tidy $tidy): tidyNode
// オブジェクト指向型(推奨)
$tidy->html(): tidyNode
tidy_get_html() と tidy_get_root() の違い
混同しやすい2つの関数の違いを最初に整理します。
ドキュメントの構造:
root(ルートノード) ← tidy_get_root() が返す
└── DOCTYPE
└── <html> ← tidy_get_html() が返す
├── <head> ← tidy_get_head() が返す
└── <body> ← tidy_get_body() が返す
tidy_get_root() はDOCTYPE宣言を含むドキュメント全体の根(ルート)を返すのに対し、tidy_get_html() は <html> タグそのものを表すノードを返します。HTMLの構造を <head> と <body> に分けてアクセスする起点としては tidy_get_html() が適しています。
基本的な使い方
<?php
$html = <<<HTML
<!DOCTYPE html>
<html lang="ja">
<head><title>テストページ</title></head>
<body><p>本文</p></body>
</html>
HTML;
$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);
$htmlNode = tidy_get_html($tidy);
echo "ノード名 : " . $htmlNode->name . PHP_EOL;
echo "lang 属性 : " . ($htmlNode->attribute['lang'] ?? '未設定') . PHP_EOL;
echo PHP_EOL;
echo $htmlNode->value . PHP_EOL;
実行結果:
ノード名 : html
lang 属性 : ja
<html lang="ja">
<head>
<title>テストページ</title>
</head>
<body>
<p>本文</p>
</body>
</html>
$htmlNode->value は <html>...</html> 全体を含む文字列を返します。<!DOCTYPE> は含まれません。
実践的なコード例
例1:lang 属性の確認と検証
<?php
class HtmlLangChecker
{
public function check(string $html): array
{
$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);
$htmlNode = tidy_get_html($tidy);
$attrs = $htmlNode->attribute ?? [];
$lang = $attrs['lang'] ?? null;
return [
'has_lang' => isset($lang),
'lang' => $lang,
'is_valid' => isset($lang) && preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $lang),
];
}
}
$checker = new HtmlLangChecker();
$pages = [
'正しいlang属性' => '<html lang="ja"><head><title>T</title></head><body></body></html>',
'lang属性なし' => '<html><head><title>T</title></head><body></body></html>',
'地域コードあり' => '<html lang="en-US"><head><title>T</title></head><body></body></html>',
];
foreach ($pages as $label => $pageHtml) {
$result = $checker->check($pageHtml);
printf(
"%-18s has_lang: %-5s lang: %-8s valid: %s\n",
$label,
$result['has_lang'] ? 'true' : 'false',
$result['lang'] ?? '(なし)',
$result['is_valid'] ? 'true' : 'false'
);
}
実行結果:
正しいlang属性 has_lang: true lang: ja valid: true
lang属性なし has_lang: false lang: (なし) valid: false
地域コードあり has_lang: true lang: en-US valid: true
例2:<html> の子ノード(head・body)に同時アクセスする
<?php
class HtmlStructureAnalyzer
{
public function analyze(string $html): array
{
$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);
$htmlNode = tidy_get_html($tidy);
$result = ['head' => null, 'body' => null];
if ($htmlNode->hasChildren()) {
foreach ($htmlNode->child as $child) {
$name = strtolower($child->name ?? '');
if ($name === 'head') {
$result['head'] = $child;
} elseif ($name === 'body') {
$result['body'] = $child;
}
}
}
return $result;
}
public function report(array $nodes): void
{
foreach (['head', 'body'] as $section) {
$node = $nodes[$section];
if ($node === null) {
echo "{$section}: 存在しません" . PHP_EOL;
continue;
}
$childCount = $node->hasChildren() ? count($node->child) : 0;
printf("%-6s: 子ノード数 %d 件\n", $section, $childCount);
}
}
}
$analyzer = new HtmlStructureAnalyzer();
$nodes = $analyzer->analyze(<<<HTML
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>テスト</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>ヘッダー</header>
<main><p>本文</p></main>
<footer>フッター</footer>
</body>
</html>
HTML);
$analyzer->report($nodes);
実行結果:
head : 子ノード数 3 件
body : 子ノード数 3 件
例3:HTMLツリー全体を再帰的に走査してタグ一覧を収集する
<?php
class FullTreeTagCollector
{
private array $tagCounts = [];
public function collect(string $html): array
{
$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);
$this->tagCounts = [];
$this->traverse(tidy_get_html($tidy));
arsort($this->tagCounts);
return $this->tagCounts;
}
private function traverse(tidyNode $node): void
{
$name = strtolower($node->name ?? '');
if ($name !== '' && $node->type !== TIDY_NODETYPE_TEXT) {
$this->tagCounts[$name] = ($this->tagCounts[$name] ?? 0) + 1;
}
if ($node->hasChildren()) {
foreach ($node->child as $child) {
$this->traverse($child);
}
}
}
}
$collector = new FullTreeTagCollector();
$counts = $collector->collect(<<<HTML
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タイトル</title>
</head>
<body>
<h1>見出し</h1>
<p>段落1</p>
<p>段落2</p>
<ul>
<li>項目1</li>
<li>項目2</li>
<li>項目3</li>
</ul>
</body>
</html>
HTML);
foreach ($counts as $tag => $count) {
printf("%-10s: %d 件\n", "<{$tag}>", $count);
}
実行結果:
<li> : 3 件
<p> : 2 件
<html> : 1 件
<head> : 1 件
<meta> : 1 件
<title> : 1 件
<body> : 1 件
<h1> : 1 件
<ul> : 1 件
例4:OOPスタイル(html())での記述
<?php
class OopHtmlNodeDemo
{
private tidy $tidy;
public function parse(string $html): self
{
$this->tidy = new tidy();
$this->tidy->parseString($html, [
'output-xhtml' => true,
'wrap' => 0,
], 'UTF8');
$this->tidy->cleanRepair();
return $this;
}
public function getHtmlInfo(): array
{
// OOPスタイルの html() は tidy_get_html() と同等
$htmlNode = $this->tidy->html();
return [
'name' => $htmlNode->name,
'attributes' => $htmlNode->attribute ?? [],
'child_count' => $htmlNode->hasChildren() ? count($htmlNode->child) : 0,
];
}
}
$demo = new OopHtmlNodeDemo();
$info = $demo->parse('<html lang="en" dir="ltr"><head><title>T</title></head><body></body></html>')
->getHtmlInfo();
print_r($info);
実行結果:
Array
(
[name] => html
[attributes] => Array
(
[lang] => en
[dir] => ltr
)
[child_count] => 2
)
例5:アクセシビリティ観点での lang 属性チェックをCIに組み込む
<?php
class LangAttributeCiGate
{
public function __construct(
private readonly bool $required = true
) {}
public function evaluate(string $label, string $html): bool
{
$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);
$htmlNode = tidy_get_html($tidy);
$lang = $htmlNode->attribute['lang'] ?? null;
$pass = !$this->required || ($lang !== null && $lang !== '');
printf(
"[%s] %s — lang属性: %s\n",
$pass ? '✓ PASS' : '✗ FAIL',
$label,
$lang ?? '(なし)'
);
return $pass;
}
}
$gate = new LangAttributeCiGate(required: true);
$allOk = true;
$pages = [
'トップ' => '<html lang="ja"><head><title>T</title></head><body></body></html>',
'商品ページ' => '<html><head><title>T</title></head><body></body></html>',
'ブログ' => '<html lang="ja-JP"><head><title>T</title></head><body></body></html>',
];
foreach ($pages as $label => $html) {
if (!$gate->evaluate($label, $html)) {
$allOk = false;
}
}
echo PHP_EOL . ($allOk ? "✓ すべてのページが合格しました。" : "✗ lang属性が不足しているページがあります。") . PHP_EOL;
実行結果:
[✓ PASS] トップ — lang属性: ja
[✗ FAIL] 商品ページ — lang属性: (なし)
[✓ PASS] ブログ — lang属性: ja-JP
✗ lang属性が不足しているページがあります。
例6:<html> ノードから <head> と <body> の情報を一括レポート
<?php
class HtmlNodeSummaryReporter
{
public function report(string $html): void
{
$tidy = tidy_parse_string($html, [
'output-xhtml' => true,
'accessibility-check' => 1,
], 'UTF8');
tidy_clean_repair($tidy);
$htmlNode = tidy_get_html($tidy);
echo "=== <html> 要素の情報 ===" . PHP_EOL;
printf("ノード名 : %s\n", $htmlNode->name);
printf("lang属性 : %s\n", $htmlNode->attribute['lang'] ?? '(未設定)');
printf("子ノード数 : %d\n", $htmlNode->hasChildren() ? count($htmlNode->child) : 0);
echo PHP_EOL . "=== 品質サマリー ===" . PHP_EOL;
printf("エラー : %d 件\n", tidy_error_count($tidy));
printf("警告 : %d 件\n", tidy_warning_count($tidy));
printf("A11y警告 : %d 件\n", tidy_access_count($tidy));
if ($tidy->errorBuffer) {
echo PHP_EOL . "=== 詳細ログ ===" . PHP_EOL;
echo $tidy->errorBuffer . PHP_EOL;
}
}
}
$reporter = new HtmlNodeSummaryReporter();
$reporter->report(<<<HTML
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>レポートテスト</title>
</head>
<body>
<img src="logo.png">
<p>本文</p>
</body>
</html>
HTML);
実行結果:
=== <html> 要素の情報 ===
ノード名 : html
lang属性 : ja
子ノード数 : 2
=== 品質サマリー ===
エラー : 0 件
警告 : 0 件
A11y警告 : 1 件
=== 詳細ログ ===
line 6 column 3 - Access: [1.1.1]: <img> missing 'alt' text.
例7:tidy_get_html() と tidy_get_root() の違いを確認する
<?php
class HtmlVsRootComparison
{
public function compare(string $html): void
{
$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);
$root = tidy_get_root($tidy);
$htmlNode = tidy_get_html($tidy);
echo "=== tidy_get_root() ===" . PHP_EOL;
printf("ノード名 : %s\n", $root->name ?? '(名前なし)');
printf("type : %d\n", $root->type);
printf("子数 : %d\n", $root->hasChildren() ? count($root->child) : 0);
echo PHP_EOL . "=== tidy_get_html() ===" . PHP_EOL;
printf("ノード名 : %s\n", $htmlNode->name);
printf("type : %d\n", $htmlNode->type);
printf("子数 : %d\n", $htmlNode->hasChildren() ? count($htmlNode->child) : 0);
}
}
$comp = new HtmlVsRootComparison();
$comp->compare('<!DOCTYPE html><html lang="ja"><head><title>T</title></head><body><p>本文</p></body></html>');
実行結果:
=== tidy_get_root() ===
ノード名 : (名前なし)
type : 0
子数 : 2
=== tidy_get_html() ===
ノード名 : html
type : 1
子数 : 2
tidy_get_root() のノードは名前を持たない最上位コンテナ(DOCTYPEと<html>の親)であるのに対し、tidy_get_html() はhtmlという名前を持つ具体的な要素ノードであることがわかります。
関連関数との比較
| 関数 | OOPスタイル | 返却するノード | 特徴 |
|---|---|---|---|
tidy_get_root() | root() | ドキュメント最上位ルート | DOCTYPEを含む、名前なしノード |
tidy_get_html() | html() | <html> 要素 | lang属性やhead/bodyへのアクセス起点 |
tidy_get_head() | head() | <head> 要素 | メタ情報・スタイル・スクリプト |
tidy_get_body() | body() | <body> 要素 | ページのコンテンツ部分 |
tidy_get_output() | value プロパティ | ドキュメント全体(文字列) | ノードではなく文字列で返す |
よくある落とし穴・注意点
$node->valueに<!DOCTYPE>は含まれないtidy_get_html()は<html>...</html>要素のみを返します。<!DOCTYPE html>宣言はルートノードに属し、<html>ノードのvalueには含まれません。DOCTYPE も含めた全体が必要なら(string) $tidy(またはtidy_get_output())を使いましょう。$node->childはnullの場合がある<html>要素が空の場合や特殊なHTMLではchildがnullになることがあります。hasChildren()で確認してからアクセスしてください。tidy_get_root()との混同 「HTMLドキュメントの根」という意味でtidy_get_root()を使いたくなりますが、ツリーを<head>/<body>に分けてアクセスする起点としてはtidy_get_html()のほうが適しています。用途に応じて使い分けましょう。tidy_clean_repair()を呼ばないとノードが不完全 他のノード取得関数と同様に、cleanRepair()を呼んだ後にtidy_get_html()を使うのが正しい手順です。- Tidy が
<html>タグを自動補完する<html>タグを省略したHTMLフラグメントを渡しても、Tidy が自動補完するためtidy_get_html()は常に有効なノードを返します。ただし補完された<html>にはlang属性がないことが多いため、langチェックを行う際は注意してください。
まとめ
| ポイント | 内容 |
|---|---|
| 役割 | HTMLの <html> 要素を tidyNode として取得する |
| 引数 | tidyオブジェクト |
| 戻り値 | tidyNode(html要素のノード) |
| OOPスタイル | $tidy->html() |
$node->value | <html>...</html> 全体の文字列(DOCTYPE は含まない) |
tidy_get_root() との違い | html要素そのものを返す(rootはDOCTYPEも含む最上位コンテナ) |
| よく組み合わせる関数 | tidy_clean_repair(), tidy_get_head(), tidy_get_body() |
| 主な用途 | lang属性の確認・CI検証、head/bodyへの同時アクセス、ツリー全体の走査 |
| 注意点 | value にDOCTYPEは含まれない、child は null チェックが必要 |
tidy_get_html() は Tidy の4つのノード取得関数(root / html / head / body)の中で、「HTMLドキュメントの構造全体を扱う起点」として最も活用場面が多い関数です。特に lang 属性の検証やアクセシビリティ要件のCI組み込み、<head> と <body> をまたぐ統合分析に適しています。
