外部サービスのAPIレスポンス、ユーザー投稿コンテンツ、レガシーシステムからエクスポートされたHTML——こうした「出所が不明で品質が保証されていないHTML」を安全に扱うことは、Webアプリケーション開発でよく直面する課題です。
tidy_clean_repair() は、PHPのTidy拡張が提供する関数で、壊れた・不完全なHTMLを自動的に修正・整形してくれます。タグの閉じ忘れ補完、属性の修正、不正なネストの是正など、人手では手間のかかる作業を一括で処理できます。
関数概要
| 項目 | 内容 |
|---|---|
| 関数名 | tidy_clean_repair() |
| 読み方 | タイディ・クリーン・リペア |
| 分類 | Tidy関数 |
| 対応バージョン | PHP 5以降(Tidy拡張が有効な場合) |
| 引数 | tidyオブジェクト |
| 戻り値 | bool(成功時 true、失敗時 false) |
| 必要な拡張 | tidy(多くの環境で標準バンドル) |
| OOPスタイル | $tidy->cleanRepair() |
構文
// 手続き型
tidy_clean_repair(tidy $tidy): bool
// オブジェクト指向型(推奨)
$tidy->cleanRepair(): bool
tidy_clean_repair() は tidy_parse_string() や tidy_parse_file() でHTMLを読み込んだ後に呼び出し、解析済みのHTMLを実際に修正・整形します。この関数を呼ばなければ、修正は適用されません。 tidy_access_count() や tidy_error_count() などの結果も、この関数を呼んだ後に取得するのが正しい手順です。
処理の流れ
不正・不完全なHTML
│
│ tidy_parse_string() / tidy_parse_file()
▼
解析済みtidyオブジェクト(まだ未修正)
│
│ tidy_clean_repair() ← ここで修正を適用
▼
修正済みtidyオブジェクト
│
├─→ (string) $tidy または $tidy->value で修正後のHTMLを取得
├─→ tidy_access_count() でアクセシビリティ警告数を確認
├─→ tidy_error_count() でエラー数を確認
└─→ tidy_warning_count() で警告数を確認
parse → cleanRepair → 出力 or カウント取得 という3ステップが基本パターンです。
基本的な使い方
<?php
$brokenHtml = '<html><body><p>段落が<b>閉じていません<p>次の段落</body></html>';
$tidy = tidy_parse_string($brokenHtml, [], 'UTF8');
tidy_clean_repair($tidy);
echo $tidy . PHP_EOL;
実行結果(整形後のHTML):
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content="HTML Tidy for HTML5 for Linux version 5.x.x">
<title></title>
</head>
<body>
<p>段落が<b>閉じていません</b></p>
<p>次の段落</p>
</body>
</html>
<b> タグの閉じ忘れ補完、<p> タグの適切な分割など、壊れていた構造が自動修正されています。
実践的なコード例
例1:HTMLを修正して取得する基本クラス
<?php
class HtmlRepairer
{
private array $config;
public function __construct(array $config = [])
{
$this->config = array_merge([
'output-xhtml' => true,
'char-encoding' => 'utf8',
'wrap' => 0, // 行の折り返しを無効化
'indent' => true,
], $config);
}
public function repair(string $html): string
{
$tidy = tidy_parse_string($html, $this->config, 'UTF8');
tidy_clean_repair($tidy);
return (string) $tidy;
}
public function repairBodyOnly(string $fragment): string
{
$this->config['show-body-only'] = true;
$tidy = tidy_parse_string($fragment, $this->config, 'UTF8');
tidy_clean_repair($tidy);
return (string) $tidy;
}
}
$repairer = new HtmlRepairer();
$broken = '<div><p>未閉じの<strong>タグ<ul><li>項目1<li>項目2</ul></div>';
echo $repairer->repairBodyOnly($broken);
実行結果:
<div>
<p>未閉じの<strong>タグ</strong></p>
<ul>
<li>項目1</li>
<li>項目2</li>
</ul>
</div>
例2:修正前後の差分をレポートする
<?php
class HtmlRepairReporter
{
public function report(string $html): void
{
$config = [
'accessibility-check' => 1,
'show-body-only' => true,
'wrap' => 0,
];
$tidy = tidy_parse_string($html, $config, 'UTF8');
tidy_clean_repair($tidy);
$repaired = (string) $tidy;
echo "=== 修正前 ===" . PHP_EOL;
echo $html . PHP_EOL . PHP_EOL;
echo "=== 修正後 ===" . PHP_EOL;
echo $repaired . PHP_EOL . PHP_EOL;
echo "=== 検出サマリー ===" . PHP_EOL;
printf("エラー : %d 件\n", tidy_error_count($tidy));
printf("警告 : %d 件\n", tidy_warning_count($tidy));
printf("アクセシビリティ警告: %d 件\n", tidy_access_count($tidy));
if ($tidy->errorBuffer) {
echo PHP_EOL . "=== 詳細ログ ===" . PHP_EOL;
echo $tidy->errorBuffer . PHP_EOL;
}
}
}
$reporter = new HtmlRepairReporter();
$reporter->report('<p>テスト<b>未閉じ<p>2段落目<img src="img.jpg">');
実行結果:
=== 修正前 ===
<p>テスト<b>未閉じ<p>2段落目<img src="img.jpg">
=== 修正後 ===
<p>テスト<b>未閉じ</b></p>
<p>2段落目<img src="img.jpg"></p>
=== 検出サマリー ===
エラー : 0 件
警告 : 1 件
アクセシビリティ警告: 1 件
=== 詳細ログ ===
line 1 column 22 - Warning: <p> anchor "img.jpg" already defined
line 1 column 22 - Access: [1.1.1]: <img> missing 'alt' text.
例3:ユーザー投稿コンテンツの安全な整形
<?php
class UserContentSanitizer
{
private array $config;
public function __construct()
{
$this->config = [
'show-body-only' => true,
'output-xhtml' => true,
'char-encoding' => 'utf8',
'drop-proprietary-attributes' => true,
'wrap' => 0,
];
}
public function sanitize(string $userInput): string
{
// まずXSS対策として危険なタグを取り除く(strip_tagsと組み合わせる例)
$allowed = '<p><br><strong><em><ul><ol><li><a><blockquote>';
$stripped = strip_tags($userInput, $allowed);
// Tidyで構造を修正・整形
$tidy = tidy_parse_string($stripped, $this->config, 'UTF8');
$success = tidy_clean_repair($tidy);
if (!$success) {
return htmlspecialchars($userInput);
}
return trim((string) $tidy);
}
}
$sanitizer = new UserContentSanitizer();
$userInput = '<p>こんにちは!<script>alert("XSS")</script><strong>強調<em>ネスト</strong></em>';
echo $sanitizer->sanitize($userInput);
実行結果:
<p>こんにちは!<strong>強調<em>ネスト</em></strong></p>
<script> タグは strip_tags() で除去し、不正なネスト(<strong> が <em> を閉じる前に閉じている)は Tidy が自動修正しています。
注意:Tidy単体はXSS対策のツールではありません。セキュリティのためのサニタイズには
strip_tags()やHTMLPurifierなどを必ず併用してください。
例4:OOPスタイルでの記述
<?php
class TidyRepairService
{
private tidy $tidy;
public function parse(string $html, array $config = []): self
{
$this->tidy = new tidy();
$this->tidy->parseString($html, array_merge([
'output-xhtml' => true,
'show-body-only' => true,
'wrap' => 0,
], $config), 'UTF8');
// OOPスタイルの cleanRepair() は tidy_clean_repair() と同等
$this->tidy->cleanRepair();
return $this;
}
public function getHtml(): string
{
return (string) $this->tidy;
}
public function hasErrors(): bool
{
return $this->tidy->getErrorCount() > 0;
}
}
$service = new TidyRepairService();
$service->parse('<ul><li>A<li>B<li>C</ul>');
echo $service->getHtml();
echo "エラーあり: " . ($service->hasErrors() ? 'はい' : 'いいえ') . PHP_EOL;
実行結果:
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
エラーあり: いいえ
例5:戻り値(bool)を使った失敗ハンドリング
<?php
class SafeHtmlRepairer
{
public function repair(string $html): ?string
{
$tidy = tidy_parse_string($html, [
'show-body-only' => true,
'wrap' => 0,
], 'UTF8');
$result = tidy_clean_repair($tidy);
if ($result === false) {
trigger_error('tidy_clean_repair() が失敗しました。', E_USER_WARNING);
return null;
}
return (string) $tidy;
}
}
$repairer = new SafeHtmlRepairer();
$output = $repairer->repair('<p>テスト</p>');
if ($output !== null) {
echo $output;
} else {
echo "修正に失敗しました。" . PHP_EOL;
}
実行結果:
<p>テスト</p>
戻り値の false チェックを入れておくと、Tidy拡張の内部エラーが発生した際にも安全にフォールバックできます。
例6:設定オプションで出力形式をカスタマイズする
<?php
class ConfigurableHtmlFormatter
{
public function format(string $html, string $mode = 'xhtml'): string
{
$configs = [
'xhtml' => [
'output-xhtml' => true,
'indent' => true,
'wrap' => 80,
],
'html5' => [
'output-html' => true,
'indent' => true,
'wrap' => 0,
],
'compact' => [
'output-xhtml' => true,
'indent' => false,
'wrap' => 0,
],
];
$config = $configs[$mode] ?? $configs['xhtml'];
$config['char-encoding'] = 'utf8';
$tidy = tidy_parse_string($html, $config, 'UTF8');
tidy_clean_repair($tidy);
return (string) $tidy;
}
}
$formatter = new ConfigurableHtmlFormatter();
$html = '<div><p>テスト<strong>太字</strong></p></div>';
echo "--- compact モード ---" . PHP_EOL;
echo $formatter->format($html, 'compact') . PHP_EOL;
実行結果:
--- compact モード ---
<!DOCTYPE html>
<html><head><title></title></head><body><div><p>テスト<strong>太字</strong></p></div></body></html>
例7:CIパイプラインでHTML品質をチェックしてから修正する
<?php
class HtmlQualityPipeline
{
public function __construct(
private readonly int $maxErrors = 0,
private readonly int $maxAccessibilityWarnings = 0
) {}
public function run(string $label, string $html): bool
{
$config = [
'accessibility-check' => 1,
'show-body-only' => true,
'wrap' => 0,
];
$tidy = tidy_parse_string($html, $config, 'UTF8');
tidy_clean_repair($tidy);
$errors = tidy_error_count($tidy);
$accessCount = tidy_access_count($tidy);
$pass = $errors <= $this->maxErrors
&& $accessCount <= $this->maxAccessibilityWarnings;
printf(
"[%s] %s | エラー: %d件 | A11y警告: %d件\n",
$pass ? '✓ PASS' : '✗ FAIL',
$label,
$errors,
$accessCount
);
return $pass;
}
}
$pipeline = new HtmlQualityPipeline(maxErrors: 0, maxAccessibilityWarnings: 0);
$pipeline->run('トップ', '<html><body><h1>ホーム</h1><p>本文</p></body></html>');
$pipeline->run('商品ページ', '<html><body><img src="item.jpg"><a href="#"></a></body></html>');
実行結果:
[✓ PASS] トップ | エラー: 0件 | A11y警告: 0件
[✗ FAIL] 商品ページ | エラー: 0件 | A11y警告: 2件
関連関数との比較
| 関数 | OOPスタイル | 役割 |
|---|---|---|
tidy_parse_string() | parseString() | HTML文字列を解析してtidyオブジェクトを生成 |
tidy_parse_file() | parseFile() | HTMLファイルを解析してtidyオブジェクトを生成 |
tidy_clean_repair() | cleanRepair() | 解析済みHTMLを修正・整形する(本関数) |
tidy_get_output() | value プロパティ | 修正後のHTML文字列を取得する |
tidy_error_count() | getErrorCount() | 構文エラーの件数を取得 |
tidy_warning_count() | getWarningCount() | 警告の件数を取得 |
tidy_access_count() | getAccessCount() | アクセシビリティ警告の件数を取得 |
よくある落とし穴・注意点
cleanRepair()を呼ばないと修正が適用されないtidy_parse_string()でHTMLを読み込んだだけでは、内部でHTMLが解析されるのみで修正は行われません。tidy_clean_repair()を呼んで初めて修正が tidy オブジェクトに反映されます。「修正後のHTMLを取得したのに元のHTMLのままだった」というトラブルのほとんどはこの呼び忘れが原因です。show-body-onlyを忘れると<html>〜<body>が補完される Tidyはデフォルトで完全なHTMLドキュメント構造(<!DOCTYPE>、<html>、<head>、<body>)を補完して出力します。HTMLのフラグメント(部品)だけを処理したい場合は、設定で'show-body-only' => trueを指定してください。- XSSサニタイズとして過信しない Tidy の主目的はHTMLの構造修正であり、セキュリティフィルタではありません。
<script>タグやjavascript:URLなどの危険なコンテンツは除去されないことがあります。ユーザー入力を処理する際はstrip_tags()やHTMLPurifierなどのセキュリティ特化ライブラリを前段に組み合わせてください。 - 戻り値の
falseは稀だが無視しないtidy_clean_repair()は通常trueを返しますが、内部エラーが発生した場合はfalseを返します。この戻り値を無視して後続処理を進めると、未修正のHTMLを扱うことになります。重要な処理では戻り値を確認する習慣をつけましょう。 - Tidy拡張が有効か事前に確認する 多くの環境でTidy拡張はバンドルされていますが、有効化されているとは限りません。
extension_loaded('tidy')で確認してから使いましょう。また、共有ホスティング環境では無効になっていることもあります。
まとめ
| ポイント | 内容 |
|---|---|
| 役割 | 解析済みtidyオブジェクトのHTMLを修正・整形する |
| 引数 | tidyオブジェクト |
| 戻り値 | bool(成功: true、失敗: false) |
| 呼び出しタイミング | parse の直後、出力やカウント取得の前 |
| OOPスタイル | $tidy->cleanRepair() |
| よく組み合わせる関数 | tidy_parse_string(), tidy_error_count(), tidy_access_count() |
| 主な用途 | 不正HTMLの自動修正、ユーザー投稿コンテンツの整形、CI品質チェック |
| 注意点 | 呼び忘れに注意・XSS対策にはならない・show-body-only の設定忘れ |
tidy_clean_repair() は Tidy 拡張を使う際の「修正を確定させるスイッチ」であり、この関数なしでは解析しても何も変わりません。parse → cleanRepair → 取得 の3ステップを身体に染み込ませることで、壊れたHTMLの自動修正ワークフローをスムーズに組み立てられるようになります。
