[PHP]xml_set_default_handlerとは?他のハンドラで処理されないXMLデータを捕捉する方法を徹底解説

PHP

はじめに

前回の記事では、要素内のテキストを取得する xml_set_character_data_handler() を解説しました。SAX方式のXML解析では、要素の開始・終了、テキストデータ、コメント、処理命令など、それぞれ専用のハンドラが用意されていますが、XML文書にはこれらのどの専用ハンドラでも捕捉されない部分が存在します。DTD宣言や、明示的にハンドラを登録していないイベント種別などがその例です。

そうした「その他すべて」を受け取るための、いわば安全網のような役割を果たすのが xml_set_default_handler() です。この関数を使うことで、他のハンドラで処理されなかった生のXMLデータを捕捉し、デバッグや特殊な用途(元のXML構造をできるだけ忠実に再現するなど)に活用できます。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


関数概要

項目内容
関数名xml_set_default_handler()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_set_default_handler(XMLParser $parser, ?callable $handler): true
引数1$parserxml_parser_create()等で生成したパーサーインスタンス
引数2$handler — デフォルトイベント検知時に呼び出すコールバック(nullで解除)
コールバックの引数(XMLParser $parser, string $data)
戻り値常に true
対応バージョンPHP 4以降
捕捉する内容他の専用ハンドラで処理されないXMLの部分(DTD宣言、未処理のマークアップなど)

「その他すべて」を捕捉する仕組み(イメージ図)

  XML文書の各部分
  ┌─────────────────────────────┐
  │ <!DOCTYPE root SYSTEM "..."> │ ← 専用ハンドラが登録されていなければ
  │ <root>                       │    default_handlerに渡される
  │   <item>value</item>         │ ← xml_set_element_handler等が
  │   <!-- comment -->           │    登録されていればそちらが優先される
  │ </root>                      │
  └─────────────────────────────┘
              │
              ▼
   ┌─────────────────────────────────┐
   │ 各部分に対応する専用ハンドラが         │
   │ 登録されていれば、そちらが呼び出される  │
   │ 登録されていなければ default_handler   │
   │ ★この記事の対象 が呼び出される         │
   └─────────────────────────────────┘

ポイントは、xml_set_default_handler() が**「専用ハンドラが登録されていない場合の受け皿」**として機能するという点です。例えば xml_set_comment_handler() を登録していない状態でコメントに遭遇すると、そのコメントの生データが xml_set_default_handler() に渡されます。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicDefaultHandlerDemo
{
    public function captureUnhandled(string $xml): array
    {
        $captured = [];
        $parser = xml_parser_create();

        // 他のハンドラで処理されないデータをすべて捕捉する
        xml_set_default_handler($parser, function ($p, $data) use (&$captured) {
            $captured[] = $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $captured;
    }
}

$demo = new BasicDefaultHandlerDemo();
print_r($demo->captureUnhandled('<!DOCTYPE root><root><item/></root>'));

例2:DTD宣言を検出してログに記録するクラス

<?php

class DtdDeclarationLogger
{
    /**
     * 他のハンドラを一切登録しない状態で、
     * default_handlerだけを使ってDOCTYPE宣言部分を捕捉する
     */
    public function extractDoctype(string $xml): ?string
    {
        $buffer = '';
        $parser = xml_parser_create();

        xml_set_default_handler($parser, function ($p, $data) use (&$buffer) {
            $buffer .= $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        if (preg_match('/<!DOCTYPE[^>]*>/', $buffer, $matches)) {
            return $matches[0];
        }

        return null;
    }
}

$logger = new DtdDeclarationLogger();
echo $logger->extractDoctype('<!DOCTYPE html><root><item/></root>') . PHP_EOL;

例3:元のXML構造をできるだけ忠実に再構築するツール

<?php

class XmlReconstructor
{
    private string $reconstructed = '';

    /**
     * 要素ハンドラとdefault_handlerを組み合わせることで、
     * 解析しつつ元に近いXML文字列を再構築する(簡易実装)
     */
    public function reconstruct(string $xml): string
    {
        $this->reconstructed = '';
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name, $attrs) {
                $attrString = '';
                foreach ($attrs as $key => $value) {
                    $attrString .= " {$key}=\"{$value}\"";
                }
                $this->reconstructed .= "<{$name}{$attrString}>";
            },
            function ($p, $name) {
                $this->reconstructed .= "</{$name}>";
            }
        );

        xml_set_character_data_handler($parser, function ($p, $data) {
            $this->reconstructed .= $data;
        });

        // コメントなど他のハンドラで扱われない部分もそのまま含める
        xml_set_default_handler($parser, function ($p, $data) {
            $this->reconstructed .= $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $this->reconstructed;
    }
}

$reconstructor = new XmlReconstructor();
echo $reconstructor->reconstruct('<root attr="1"><item>text</item></root>') . PHP_EOL;

例4:デバッグ用に「どの部分が未処理か」を可視化するツール

<?php

class UnhandledPartVisualizer
{
    /**
     * 意図的に一部のハンドラのみを登録し、
     * default_handlerでどの部分が捕捉されずに残っているかを確認する
     */
    public function visualize(string $xml): array
    {
        $unhandledParts = [];
        $parser = xml_parser_create();

        // element_handlerのみ登録し、character_dataやcommentハンドラは登録しない
        xml_set_element_handler($parser, fn () => null, fn () => null);

        xml_set_default_handler($parser, function ($p, $data) use (&$unhandledParts) {
            $unhandledParts[] = $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $unhandledParts;
    }
}

$visualizer = new UnhandledPartVisualizer();
print_r($visualizer->visualize('<root>text<!-- comment --></root>'));
// character_dataとcommentのハンドラが未登録のため、それらがdefault_handlerに集約される

例5:実体参照(エンティティ)の生データを確認するツール

<?php

class RawEntityInspector
{
    /**
     * 外部実体参照など、特殊なマークアップに遭遇した際の
     * 生データをdefault_handlerで確認する
     */
    public function inspect(string $xml): array
    {
        $rawEvents = [];
        $parser = xml_parser_create();

        xml_set_default_handler($parser, function ($p, $data) use (&$rawEvents) {
            $rawEvents[] = $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $rawEvents;
    }
}

$inspector = new RawEntityInspector();
print_r($inspector->inspect('<root>&lt;escaped&gt;</root>'));

例6:ハンドラを動的に切り替えて特定範囲だけ捕捉するクラス

<?php

class ToggleableDefaultCapture
{
    private array $captured = [];
    private bool $capturing = false;

    /**
     * 特定の要素の内側にいる間だけdefault_handlerを有効化する
     */
    public function captureWithin(string $xml, string $targetTag): array
    {
        $this->captured = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) use ($targetTag, $parser) {
                if ($name === $targetTag) {
                    $this->capturing = true;
                    xml_set_default_handler($parser, [$this, 'onDefault']);
                }
            },
            function ($p, $name) use ($targetTag, $parser) {
                if ($name === $targetTag) {
                    $this->capturing = false;
                    xml_set_default_handler($parser, null);
                }
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $this->captured;
    }

    public function onDefault(XMLParser $parser, string $data): void
    {
        if ($this->capturing) {
            $this->captured[] = $data;
        }
    }
}

$capture = new ToggleableDefaultCapture();
print_r($capture->captureWithin('<root><raw><!-- inside --></raw></root>', 'raw'));

例7:ハンドラの優先順位を確認する比較実験クラス

<?php

class HandlerPriorityExperiment
{
    /**
     * 専用ハンドラ(character_data)を登録した場合と
     * しなかった場合とで、default_handlerの呼び出され方の違いを確認する
     */
    public function compareWithAndWithoutCharacterHandler(string $xml): array
    {
        // ケース1: character_dataハンドラを登録する
        $parser1 = xml_parser_create();
        $defaultCalls1 = [];
        xml_set_character_data_handler($parser1, fn () => null);
        xml_set_default_handler($parser1, function ($p, $data) use (&$defaultCalls1) {
            $defaultCalls1[] = $data;
        });
        xml_parse($parser1, $xml, true);
        xml_parser_free($parser1);

        // ケース2: character_dataハンドラを登録しない
        $parser2 = xml_parser_create();
        $defaultCalls2 = [];
        xml_set_default_handler($parser2, function ($p, $data) use (&$defaultCalls2) {
            $defaultCalls2[] = $data;
        });
        xml_parse($parser2, $xml, true);
        xml_parser_free($parser2);

        return ['with_handler' => $defaultCalls1, 'without_handler' => $defaultCalls2];
    }
}

$experiment = new HandlerPriorityExperiment();
print_r($experiment->compareWithAndWithoutCharacterHandler('<root>text content</root>'));

関連関数との比較

関数役割xml_set_default_handlerとの違い
xml_set_default_handler()他のハンドラで処理されないデータを捕捉本記事の対象。専用ハンドラの「受け皿」として機能する
xml_set_character_data_handler()要素内のテキストデータを検知登録されていればテキストデータはこちらが優先される
xml_set_element_handler()要素の開始・終了を検知登録されていれば要素の境界はこちらが優先される
xml_set_comment_handler()XMLコメントを検知登録されていればコメントはこちらが優先される
xml_set_notation_decl_handler()記法宣言(notation declaration)を検知より専門的なDTD関連の宣言に特化したハンドラ

よくある落とし穴(注意点)

  1. 専用ハンドラを登録すると、そちらが優先されてdefault_handlerには渡されない xml_set_character_data_handler() などの専用ハンドラを登録している場合、該当するイベントは専用ハンドラの方に渡され、xml_set_default_handler() には渡されません。「両方から同じデータを取得したい」という誤解をしないよう注意しましょう(例7を参照)。
  2. 捕捉される内容が実装依存で予測しにくい どのような文字列がどのタイミングで xml_set_default_handler() に渡されるかは、Expatパーサーの内部実装に依存する部分が大きく、必ずしも直感的ではありません。デバッグ的な用途であれば有用ですが、本番のロジックをこの挙動に強く依存させるのはリスクがあります。
  3. DTDの詳細な解析には別の専用ハンドラの方が適している DOCTYPE宣言やDTD関連の情報を体系的に扱いたい場合、xml_set_notation_decl_handler()xml_set_external_entity_ref_handler() など、より専門的なハンドラが用意されています。xml_set_default_handler() はあくまで簡易的な捕捉手段と捉えましょう。
  4. 大量のデータが捕捉され、パフォーマンスに影響する可能性がある 専用ハンドラを一切登録しない状態で xml_set_default_handler() だけを使うと、文書のほぼ全体がこのハンドラに流れ込むことになり、処理量が増大します。用途を明確にした上で使用しましょう(例4を参照)。
  5. ハンドラの登録・解除タイミングの管理が複雑になりがち 例6のように動的にハンドラを切り替える実装は柔軟性が高い一方、状態管理が複雑になりやすく、バグの温床にもなり得ます。シンプルな用途であれば、常時登録しておいてハンドラ内部でフラグ判定する方が見通しが良い場合もあります。

まとめ

観点まとめ
何をする関数か他の専用ハンドラで処理されないXMLデータを捕捉するハンドラを登録する
主な用途DTD宣言の検出、元のXML構造の再構築、デバッグ時の未処理部分の可視化
優先順位のルール専用ハンドラ(xml_set_character_data_handler()など)が登録されていれば、そちらが優先される
挙動の予測可能性実装依存の部分があり、本番ロジックの中心に据えるのはリスクがある
注意点専用ハンドラとの併用時の優先順位、パフォーマンスへの影響、状態管理の複雑さ

xml_set_default_handler() は、SAX方式のXML解析における「取りこぼし」を防ぐための安全網として機能する、やや特殊な関数です。日常的なXML処理で頻繁に使う機会は少ないかもしれませんが、DTD情報の検出やデバッグ、元のXML構造をできるだけ忠実に扱いたい特殊なユースケースにおいて、その真価を発揮します。

タイトルとURLをコピーしました