はじめに
これまでの記事で、名前空間対応のパーサーを生成する xml_parser_create_ns() を解説し、その中で名前空間宣言の開始を検知する xml_set_start_namespace_decl_handler() に触れました。今回は、その対となる xml_set_end_namespace_decl_handler() を取り上げます。
XML文書における名前空間宣言(xmlns:prefix="...")には、実は**有効範囲(スコープ)**という概念があります。ある要素で宣言された名前空間プレフィックスは、その要素とその子孫要素の中でのみ有効であり、該当する要素が閉じられると、そのプレフィックスの有効範囲も終了します。この「名前空間の有効範囲が終わるタイミング」を検知するのが xml_set_end_namespace_decl_handler() の役割です。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。
関数概要
| 項目 | 内容 |
|---|---|
| 関数名 | xml_set_end_namespace_decl_handler() |
| 所属拡張 | XML Parser拡張(Expatベース、標準で有効) |
| シグネチャ | xml_set_end_namespace_decl_handler(XMLParser $parser, ?callable $handler): true |
| 引数1 | $parser — xml_parser_create_ns() で生成した名前空間対応のパーサーインスタンス |
| 引数2 | $handler — 名前空間宣言のスコープ終了時に呼び出すコールバック(nullで解除) |
| コールバックの引数 | (XMLParser $parser, string $prefix) |
| 戻り値 | 常に true |
| 対応バージョン | PHP 4.0.5以降 |
| 前提条件 | xml_parser_create_ns() で生成したパーサーであること |
名前空間宣言のスコープの仕組み(イメージ図)
XML文書
<root>
<ns:section xmlns:ns="http://example.com/ns">
<ns:item>value</ns:item>
</ns:section> ← ★ここで"ns"プレフィックスの有効範囲が終了
<!-- この位置では"ns"プレフィックスはもう使えない -->
</root>
│
▼
┌─────────────────────────────────┐
│ <ns:section ...> を検知 │
│ → start_namespace_decl_handlerが発火 │
│ (prefix="ns", uri="http://...") │
├─────────────────────────────────┤
│ </ns:section> を検知(要素の終了と同時) │
│ → end_namespace_decl_handlerが発火 │ ★この記事の対象
│ (prefix="ns") │
└─────────────────────────────────┘
ポイントは、xml_set_end_namespace_decl_handler() のコールバックが該当するプレフィックス名のみを受け取り、URIは渡されないという点です。スコープが終了する時点では「どのURIだったか」よりも「どのプレフィックスが無効になったか」の方が重要な情報だからです。
実践サンプル7選
例1:基本的な使い方
<?php
class BasicEndNamespaceDemo
{
public function trace(string $xml): array
{
$events = [];
$parser = xml_parser_create_ns();
xml_set_end_namespace_decl_handler($parser, function ($p, $prefix) use (&$events) {
$events[] = "スコープ終了: {$prefix}";
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $events;
}
}
$demo = new BasicEndNamespaceDemo();
print_r($demo->trace('<root><ns:section xmlns:ns="http://example.com/ns"><ns:item/></ns:section></root>'));
例2:開始・終了ハンドラをセットで登録し、宣言の有効期間を記録するクラス
<?php
class NamespaceLifecycleTracker
{
private array $log = [];
/**
* start/endの両ハンドラを組み合わせて、
* 各名前空間宣言の"開始"と"終了"を時系列で記録する
*/
public function track(string $xml): array
{
$this->log = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) {
$this->log[] = "開始: {$prefix} => {$uri}";
});
xml_set_end_namespace_decl_handler($parser, function ($p, $prefix) {
$this->log[] = "終了: {$prefix}";
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $this->log;
}
}
$tracker = new NamespaceLifecycleTracker();
$xml = '<root xmlns:a="http://example.com/a"><a:item/></root>';
print_r($tracker->track($xml));
例3:名前空間の入れ子(複数プレフィックス)を正しく管理するクラス
<?php
class NestedNamespaceStackTracker
{
private array $activeNamespaces = [];
private array $snapshots = [];
/**
* 現在アクティブな名前空間プレフィックスの集合を
* スタック的に管理し、各要素処理時点でのスナップショットを記録する
*/
public function trackNesting(string $xml): array
{
$this->activeNamespaces = [];
$this->snapshots = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) {
$this->activeNamespaces[$prefix] = $uri;
});
xml_set_end_namespace_decl_handler($parser, function ($p, $prefix) {
unset($this->activeNamespaces[$prefix]);
});
xml_set_element_handler(
$parser,
function ($p, $name) {
$this->snapshots[] = ['element' => $name, 'active_ns' => $this->activeNamespaces];
},
fn () => null
);
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $this->snapshots;
}
}
$tracker = new NestedNamespaceStackTracker();
$xml = '<root xmlns:a="http://a.example.com"><a:x xmlns:b="http://b.example.com"><a:y/></a:x></root>';
print_r($tracker->trackNesting($xml));
例4:名前空間宣言のバランス(開始と終了の対応)を検証するツール
<?php
class NamespaceBalanceValidator
{
/**
* すべてのstartイベントに対応するendイベントが
* 正しく発生しているかを検証する
*/
public function validate(string $xml): bool
{
$startCount = 0;
$endCount = 0;
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function () use (&$startCount) {
$startCount++;
});
xml_set_end_namespace_decl_handler($parser, function () use (&$endCount) {
$endCount++;
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $startCount === $endCount;
}
}
$validator = new NamespaceBalanceValidator();
var_dump($validator->validate('<root xmlns:a="http://a.example.com"><a:item/></root>'));
// true(well-formedなXMLであれば、start/endの数は必ず一致する)
例5:複数の名前空間を扱うSOAPレスポンスの解析ログ
<?php
class SoapNamespaceLifecycleLogger
{
/**
* SOAPレスポンスに含まれる複数の名前空間宣言について、
* その有効範囲をログとして記録する
*/
public function logLifecycle(string $soapXml): array
{
$log = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$log) {
$log[] = sprintf('[開始] prefix=%s uri=%s', $prefix ?: '(default)', $uri);
});
xml_set_end_namespace_decl_handler($parser, function ($p, $prefix) use (&$log) {
$log[] = sprintf('[終了] prefix=%s', $prefix ?: '(default)');
});
xml_parse($parser, $soapXml, true);
xml_parser_free($parser);
return $log;
}
}
$logger = new SoapNamespaceLifecycleLogger();
$soap = '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body/></soap:Envelope>';
print_r($logger->logLifecycle($soap));
例6:デフォルト名前空間(プレフィックスなし)のスコープ終了を扱う
<?php
class DefaultNamespaceScopeHandler
{
/**
* プレフィックスを持たないデフォルト名前空間(xmlns="...")の場合、
* $prefix引数が空文字列またはnullとして渡される点を確認する
*/
public function inspect(string $xml): array
{
$events = [];
$parser = xml_parser_create_ns();
xml_set_end_namespace_decl_handler($parser, function ($p, $prefix) use (&$events) {
$label = ($prefix === null || $prefix === '') ? '(デフォルト名前空間)' : $prefix;
$events[] = "終了: {$label}";
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $events;
}
}
$handler = new DefaultNamespaceScopeHandler();
print_r($handler->inspect('<feed xmlns="http://www.w3.org/2005/Atom"><entry/></feed>'));
例7:特定要素の内側だけ名前空間の追跡を有効化する
<?php
class ConditionalNamespaceTracking
{
private array $log = [];
/**
* 大きなXML文書の一部分だけ名前空間の追跡が必要な場合、
* 該当区間に入ってからハンドラを登録することで対象を絞り込む
*/
public function trackWithinTag(string $xml, string $targetTag): array
{
$this->log = [];
$parser = xml_parser_create_ns();
xml_set_element_handler(
$parser,
function ($p, $name) use ($targetTag, $parser) {
if ($name === strtoupper($targetTag)) {
xml_set_end_namespace_decl_handler($parser, [$this, 'onEnd']);
}
},
fn () => null
);
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $this->log;
}
public function onEnd(XMLParser $parser, string $prefix): void
{
$this->log[] = "追跡区間内で終了: {$prefix}";
}
}
$tracker = new ConditionalNamespaceTracking();
print_r($tracker->trackWithinTag(
'<root><target><ns:item xmlns:ns="http://example.com"/></target></root>',
'target'
));
関連関数との比較
| 関数 | 役割 | xml_set_end_namespace_decl_handlerとの違い |
|---|---|---|
xml_set_end_namespace_decl_handler() | 名前空間宣言のスコープ終了を検知 | 本記事の対象。プレフィックス名のみを受け取る |
xml_set_start_namespace_decl_handler() | 名前空間宣言のスコープ開始を検知 | プレフィックスとURIの両方を受け取る、対になる関数 |
xml_set_element_handler() | 要素の開始・終了を検知 | 要素そのものを対象とし、xmlns宣言の発生とは別のタイミング |
xml_parser_create_ns() | 名前空間対応のパーサーを生成 | このハンドラを使うための前提条件となる関数 |
DOMXPath::registerNamespace() | XPathクエリ用に名前空間を登録 | SAX方式ではなくDOM方式でのアプローチ |
よくある落とし穴(注意点)
xml_parser_create_ns()で生成したパーサーでなければ機能しない 通常のxml_parser_create()で生成したパーサーに対してこのハンドラを登録しても、名前空間宣言のイベント自体が発生しないため意味を持ちません。- URIは渡されず、プレフィックス名のみが渡される スコープ終了時のコールバックには、開始時とは異なりURI情報が含まれていません。終了時にもURIの情報が必要な場合は、開始ハンドラ側で記録しておいたプレフィックスとURIの対応関係を参照する必要があります(例3を参照)。
- スコープ終了は要素の終了タグと同じタイミングで発生する 名前空間宣言のスコープ終了イベントは、その宣言が行われた要素の終了タグ(
</ns:section>など)が検知されたタイミングと連動して発火します。要素の終了ハンドラとの実行順序を意識しておくとよいでしょう。 - デフォルト名前空間の場合、プレフィックスが空文字列または
nullになるxmlns="..."のようなプレフィックスを持たない宣言の場合、コールバックに渡される$prefixの値が空になります。この場合の表示処理などでは、明示的なハンドリングが必要です(例6を参照)。 - 単体では名前空間URIとの対応が分からない このハンドラ単体で得られる情報は「どのプレフィックスの有効範囲が終わったか」だけです。実務では
xml_set_start_namespace_decl_handler()と組み合わせて、開始時に記録したURI情報と紐付けて使うのが一般的です。
まとめ
| 観点 | まとめ |
|---|---|
| 何をする関数か | 名前空間宣言(xmlns:prefix)の有効範囲(スコープ)が終了したタイミングを検知する |
| 主な用途 | 複数の名前空間が入れ子になった複雑なXMLの追跡、名前空間宣言のバランス検証 |
| 前提条件 | xml_parser_create_ns() で生成した名前空間対応パーサーであること |
| セットで使う関数 | xml_set_start_namespace_decl_handler()(開始側との組み合わせが基本) |
| 注意点 | URIが渡されないこと、要素終了タイミングとの連動、デフォルト名前空間の空プレフィックス |
xml_set_end_namespace_decl_handler() は、複雑な名前空間構造を持つXML文書(SOAPレスポンスや複数スキーマを組み合わせたフィードなど)を正確に解析する際に、名前空間の有効範囲を正しく追跡するための専門的な関数です。開始ハンドラと組み合わせることで、名前空間のライフサイクル全体を把握できます。
