はじめに
これまでの記事で 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 | $parser — xml_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宣言に特化 |
SimpleXMLElement | XML全体をオブジェクトツリーとして読み込む | イベント駆動ではなく、階層構造を直接操作できるDOM方式の代替 |
よくある落とし穴(注意点)
- 開始ハンドラと終了ハンドラでは受け取る引数の数が異なる 開始ハンドラは
(parser, name, attributes)の3引数、終了ハンドラは(parser, name)の2引数です。コールバック関数の定義時に引数の数を間違えるとエラーになります。 - 属性値はすべて文字列として渡される
<item count="5">のような数値らしき属性値も、$attrs['count']は文字列"5"として渡されます。数値として扱いたい場合は明示的にキャストしましょう。 - 要素名はデフォルトで大文字に変換される
xml_parser_create()のデフォルト設定(case folding)により、<item>は"ITEM"として渡されます。元の大文字小文字を保持したい場合はXML_OPTION_CASE_FOLDINGを無効化する必要があります。 - 階層構造を扱うには自前でスタック管理が必要
xml_set_element_handler()単体では「今どの要素の子要素か」という情報は直接渡されません。親子関係を追跡したい場合は、例3のようにスタック構造を自分で実装する必要があります。 - ハンドラ内で例外が発生すると、パーサーの状態管理が複雑になる コールバック内で例外をスローする場合、
xml_parser_free()による解放処理が確実に行われるよう、try...finallyなどで囲む設計が重要です(本記事のシリーズの他の記事でも触れているXML解析全体の設計指針です)。
まとめ
| 観点 | まとめ |
|---|---|
| 何をする関数か | XML要素の開始タグ・終了タグを検知する2つのコールバック関数を一度に登録する |
| 主な用途 | ツリー構造の可視化、親子関係の追跡、属性ベースのフィルタリング、要素の出現回数集計 |
| 引数の非対称性 | 開始ハンドラは属性情報も受け取るが、終了ハンドラは要素名のみ |
| 組み合わせる関数 | xml_set_character_data_handler()(要素の中身のテキスト取得と組み合わせるのが基本) |
| 注意点 | 引数の数の違い、属性値が常に文字列であること、要素名の大文字化、階層管理の自前実装 |
xml_set_element_handler() は、SAX方式のXML解析における文字通りの中心的存在です。要素の開始・終了という2つのイベントを正確に捉えることが、これまでの記事で紹介してきた様々な高度なXML処理テクニックすべての土台となっています。
