[PHP]xml_set_element_handlerとは?XML要素の開始・終了を検知する最重要ハンドラを徹底解説

PHP

はじめに

これまでの記事で xml_set_character_data_handler()xml_set_default_handler() を解説する中で、必ずと言っていいほど一緒に登場してきたのが xml_set_element_handler() です。今回はこの関数を正面から取り上げます。SAX方式のXML解析において、これは間違いなく最も基本的で、最も使用頻度の高い関数です。

xml_set_element_handler() は、XML文書中の要素の開始タグと終了タグを検知したときに呼び出される、2つのコールバック関数(ハンドラ)を一度に登録するための関数です。<item> という開始タグに遭遇したとき、そして </item> という終了タグに遭遇したとき、それぞれ別々のコールバックが呼び出されます。この2つのイベントを軸に、これまでの記事で紹介してきたテキスト抽出や階層構造の構築といった処理が組み立てられています。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


関数概要

項目内容
関数名xml_set_element_handler()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_set_element_handler(XMLParser $parser, callable $start_handler, callable $end_handler): true
引数1$parserxml_parser_create()等で生成したパーサーインスタンス
引数2$start_handler — 開始タグ検知時に呼び出すコールバック
引数3$end_handler — 終了タグ検知時に呼び出すコールバック
start側の引数(XMLParser $parser, string $name, array $attributes)
end側の引数(XMLParser $parser, string $name)
戻り値常に true
対応バージョンPHP 4以降

開始・終了イベントの流れ(イメージ図)

  XML文書
  '<item id="101">ノート</item>'
              │
              ▼
        xml_parse()による解析
              │
   ┌──────────┼──────────────┬──────────┐
   ▼          ▼              ▼          ▼
  開始タグ検知  文字データ検知   終了タグ検知
  <item id="101">  "ノート"    </item>
   │            (character_data  │
   ▼             handlerの領域)  ▼
  start_handler                 end_handler
  ($name="item",                 ($name="item")
   $attrs=["id"=>"101"])
   ★この記事の対象               ★この記事の対象

ポイントは、xml_set_element_handler()第2引数(開始)が要素名と属性配列の両方を受け取るのに対し、第3引数(終了)は要素名のみを受け取るという、非対称な設計になっている点です。これは、終了タグには属性情報が存在しない(XMLの終了タグには属性を書けない)というXML自体の仕様に対応した、理にかなった設計です。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicElementHandlerDemo
{
    public function traceElements(string $xml): void
    {
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name, $attrs) {
                echo "開始: <{$name}>";
                if (!empty($attrs)) {
                    echo ' 属性: ' . json_encode($attrs, JSON_UNESCAPED_UNICODE);
                }
                echo PHP_EOL;
            },
            function ($p, $name) {
                echo "終了: </{$name}>" . PHP_EOL;
            }
        );

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

$demo = new BasicElementHandlerDemo();
$demo->traceElements('<item id="101">ノート</item>');

例2:ネストの深さをインデントで可視化するツリー表示ツール

<?php

class IndentedTreeDisplay
{
    private int $depth = 0;

    /**
     * 開始・終了ハンドラで深さのカウンターを増減させ、
     * インデント付きでXMLのツリー構造を表示する
     */
    public function display(string $xml): void
    {
        $this->depth = 0;
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) {
                echo str_repeat('  ', $this->depth) . "<{$name}>" . PHP_EOL;
                $this->depth++;
            },
            function ($p, $name) {
                $this->depth--;
                echo str_repeat('  ', $this->depth) . "</{$name}>" . PHP_EOL;
            }
        );

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

$display = new IndentedTreeDisplay();
$display->display('<catalog><item><name>ノート</name></item></catalog>');

例3:スタックを使って親子関係を追跡するクラス

<?php

class ParentTrackingParser
{
    private array $stack = [];
    private array $relationships = [];

    /**
     * 開始・終了のたびにスタックを操作することで、
     * 各要素の親要素が何であるかを記録する
     */
    public function trackParents(string $xml): array
    {
        $this->stack = [];
        $this->relationships = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) {
                $parent = end($this->stack) ?: 'ROOT';
                $this->relationships[] = "{$name} の親: {$parent}";
                $this->stack[] = $name;
            },
            function ($p, $name) {
                array_pop($this->stack);
            }
        );

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

        return $this->relationships;
    }
}

$tracker = new ParentTrackingParser();
print_r($tracker->trackParents('<catalog><category><item/></category></catalog>'));

例4:特定タグの属性値を条件にフィルタリングして抽出するクラス

<?php

class AttributeFilteringExtractor
{
    /**
     * 開始タグの属性配列を利用して、
     * 特定の条件に合致する要素だけを抽出する
     */
    public function extractByAttribute(string $xml, string $attrName, string $attrValue): array
    {
        $matched = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name, $attrs) use (&$matched, $attrName, $attrValue) {
                if (isset($attrs[$attrName]) && $attrs[$attrName] === $attrValue) {
                    $matched[] = ['tag' => $name, 'attributes' => $attrs];
                }
            },
            fn () => null
        );

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

        return $matched;
    }
}

$extractor = new AttributeFilteringExtractor();
$xml = '<items><item type="book" id="1"/><item type="dvd" id="2"/></items>';
print_r($extractor->extractByAttribute($xml, 'type', 'book'));

例5:要素の出現回数をカウントする統計収集クラス

<?php

class ElementCountCollector
{
    /**
     * 開始タグのイベントごとにカウンターをインクリメントし、
     * タグ名ごとの出現回数を集計する
     */
    public function countTags(string $xml): array
    {
        $counts = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) use (&$counts) {
                $counts[$name] = ($counts[$name] ?? 0) + 1;
            },
            fn () => null
        );

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

        return $counts;
    }
}

$collector = new ElementCountCollector();
$xml = '<catalog><item/><item/><category><item/></category></catalog>';
print_r($collector->countTags($xml));
// ['CATALOG' => 1, 'ITEM' => 3, 'CATEGORY' => 1]

例6:クラスメソッドを配列コールバック構文で登録する

<?php

class MethodBasedHandler
{
    private array $log = [];

    public function parse(string $xml): array
    {
        $this->log = [];
        $parser = xml_parser_create();

        // クラスのメソッドをそれぞれコールバックとして渡す
        xml_set_element_handler(
            $parser,
            [$this, 'onStart'],
            [$this, 'onEnd']
        );

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

        return $this->log;
    }

    public function onStart(XMLParser $parser, string $name, array $attrs): void
    {
        $this->log[] = "START: {$name}";
    }

    public function onEnd(XMLParser $parser, string $name): void
    {
        $this->log[] = "END: {$name}";
    }
}

$handler = new MethodBasedHandler();
print_r($handler->parse('<root><item/></root>'));

例7:特定要素をスキップして子孫要素を無視する制御フロー

<?php

class SkippableSubtreeParser
{
    private int $skipDepth = 0;
    private array $processedTags = [];

    /**
     * 特定のタグに入ったら、その中身をすべて無視し、
     * 対応する終了タグが来るまでスキップする
     */
    public function parseIgnoringTag(string $xml, string $ignoreTag): array
    {
        $this->skipDepth = 0;
        $this->processedTags = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) use ($ignoreTag) {
                if ($this->skipDepth > 0) {
                    $this->skipDepth++;
                    return;
                }
                if ($name === $ignoreTag) {
                    $this->skipDepth = 1;
                    return;
                }
                $this->processedTags[] = $name;
            },
            function ($p, $name) {
                if ($this->skipDepth > 0) {
                    $this->skipDepth--;
                }
            }
        );

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

        return $this->processedTags;
    }
}

$parser = new SkippableSubtreeParser();
$xml = '<root><keep/><ignore><nested/><deep/></ignore><keep2/></root>';
print_r($parser->parseIgnoringTag($xml, 'IGNORE'));
// ['ROOT', 'KEEP', 'KEEP2'] (IGNOREの中身はスキップされる)

関連関数との比較

関数役割xml_set_element_handlerとの違い
xml_set_element_handler()要素の開始・終了を検知するハンドラを登録本記事の対象。XML解析における最も基本的なハンドラ
xml_set_character_data_handler()要素内のテキストデータを検知要素の「構造」ではなく「中身のテキスト」を扱う
xml_set_default_handler()他のハンドラで処理されないデータを捕捉専用ハンドラが登録されていない場合の受け皿
xml_set_start_namespace_decl_handler()名前空間宣言の開始を検知要素そのものではなくxmlns宣言に特化
SimpleXMLElementXML全体をオブジェクトツリーとして読み込むイベント駆動ではなく、階層構造を直接操作できるDOM方式の代替

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

  1. 開始ハンドラと終了ハンドラでは受け取る引数の数が異なる 開始ハンドラは (parser, name, attributes) の3引数、終了ハンドラは (parser, name) の2引数です。コールバック関数の定義時に引数の数を間違えるとエラーになります。
  2. 属性値はすべて文字列として渡される <item count="5"> のような数値らしき属性値も、$attrs['count'] は文字列 "5" として渡されます。数値として扱いたい場合は明示的にキャストしましょう。
  3. 要素名はデフォルトで大文字に変換される xml_parser_create() のデフォルト設定(case folding)により、<item>"ITEM" として渡されます。元の大文字小文字を保持したい場合は XML_OPTION_CASE_FOLDING を無効化する必要があります。
  4. 階層構造を扱うには自前でスタック管理が必要 xml_set_element_handler() 単体では「今どの要素の子要素か」という情報は直接渡されません。親子関係を追跡したい場合は、例3のようにスタック構造を自分で実装する必要があります。
  5. ハンドラ内で例外が発生すると、パーサーの状態管理が複雑になる コールバック内で例外をスローする場合、xml_parser_free() による解放処理が確実に行われるよう、try...finally などで囲む設計が重要です(本記事のシリーズの他の記事でも触れているXML解析全体の設計指針です)。

まとめ

観点まとめ
何をする関数かXML要素の開始タグ・終了タグを検知する2つのコールバック関数を一度に登録する
主な用途ツリー構造の可視化、親子関係の追跡、属性ベースのフィルタリング、要素の出現回数集計
引数の非対称性開始ハンドラは属性情報も受け取るが、終了ハンドラは要素名のみ
組み合わせる関数xml_set_character_data_handler()(要素の中身のテキスト取得と組み合わせるのが基本)
注意点引数の数の違い、属性値が常に文字列であること、要素名の大文字化、階層管理の自前実装

xml_set_element_handler() は、SAX方式のXML解析における文字通りの中心的存在です。要素の開始・終了という2つのイベントを正確に捉えることが、これまでの記事で紹介してきた様々な高度なXML処理テクニックすべての土台となっています。

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