はじめに
前々回の記事では、名前空間宣言のスコープ終了を検知する xml_set_end_namespace_decl_handler() を解説しました。今回はその対となる、名前空間宣言の開始を検知する xml_set_start_namespace_decl_handler() を正面から取り上げます。
XML文書中で xmlns:prefix="URI" のような名前空間宣言に遭遇した瞬間を捉えるのが、この関数の役割です。SOAP APIのレスポンス解析、複数のスキーマが混在するフィードの処理など、名前空間を扱うXML処理において、xml_set_element_handler() と並んで欠かせない重要な関数です。本記事では基本的な使い方から、実践的な活用パターンまで詳しく解説します。
関数概要
| 項目 | 内容 |
|---|---|
| 関数名 | xml_set_start_namespace_decl_handler() |
| 所属拡張 | XML Parser拡張(Expatベース、標準で有効) |
| シグネチャ | xml_set_start_namespace_decl_handler(XMLParser $parser, ?callable $handler): true |
| 引数1 | $parser — xml_parser_create_ns() で生成した名前空間対応のパーサーインスタンス |
| 引数2 | $handler — 名前空間宣言の開始検知時に呼び出すコールバック(nullで解除) |
| コールバックの引数 | (XMLParser $parser, string $prefix, ?string $uri) |
| 戻り値 | 常に true |
| 対応バージョン | PHP 4.0.5以降 |
| 前提条件 | xml_parser_create_ns() で生成したパーサーであること |
検知のタイミング(イメージ図)
XML文書
<root>
<ns:section xmlns:ns="http://example.com/ns" xmlns:other="http://example.com/other">
<ns:item>value</ns:item>
</ns:section>
</root>
│
▼
<ns:section ...> の開始タグを検知する際、
属性として書かれた複数のxmlns宣言それぞれについて
start_namespace_decl_handlerが個別に呼び出される
│
┌──────────┴──────────────────────┐
▼ ▼
1回目の呼び出し 2回目の呼び出し
prefix="ns" prefix="other"
uri="http://example.com/ns" uri="http://example.com/other"
★この記事の対象 ★この記事の対象
│
▼(この後)
<ns:section>要素そのものの開始イベントが発火する
(xml_set_element_handlerで検知)
ポイントは、xml_set_start_namespace_decl_handler() のイベントが要素そのものの開始イベントより先に発火するという点です。1つの要素に複数の名前空間宣言が含まれる場合、それぞれについて個別にこのハンドラが呼び出されてから、ようやく要素の開始ハンドラ(xml_set_element_handler())が呼び出されます。
実践サンプル7選
例1:基本的な使い方
<?php
class BasicStartNamespaceDemo
{
public function trace(string $xml): array
{
$events = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$events) {
$events[] = "{$prefix} => {$uri}";
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $events;
}
}
$demo = new BasicStartNamespaceDemo();
print_r($demo->trace('<root xmlns:ns="http://example.com/ns"><ns:item/></root>'));
例2:文書内で使われているすべての名前空間URIを収集するツール
<?php
class NamespaceUriCollector
{
/**
* XML文書中で宣言されているすべての名前空間URIを
* 重複なく収集する
*/
public function collectUris(string $xml): array
{
$uris = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$uris) {
if ($uri !== null) {
$uris[$uri] = true;
}
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return array_keys($uris);
}
}
$collector = new NamespaceUriCollector();
$xml = '<root xmlns:a="http://a.example.com" xmlns:b="http://b.example.com"><a:x/><b:y/></root>';
print_r($collector->collectUris($xml));
例3:プレフィックスとURIの対応表を作成するクラス
<?php
class PrefixUriMapBuilder
{
/**
* 文書内で宣言されているプレフィックスとURIの
* 対応関係をマップとして構築する
*/
public function buildMap(string $xml): array
{
$map = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$map) {
$key = $prefix ?? '(default)';
$map[$key] = $uri;
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $map;
}
}
$builder = new PrefixUriMapBuilder();
print_r($builder->buildMap('<root xmlns="http://default.example.com" xmlns:custom="http://custom.example.com"><item/></root>'));
例4:特定の名前空間URIが使われているかを検証するバリデーター
<?php
class RequiredNamespaceValidator
{
/**
* SOAPレスポンスなど、特定の名前空間URIが
* 必ず含まれていることを期待する文書の検証に使う
*/
public function requiresNamespace(string $xml, string $requiredUri): bool
{
$found = false;
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$found, $requiredUri) {
if ($uri === $requiredUri) {
$found = true;
}
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $found;
}
}
$validator = new RequiredNamespaceValidator();
$soap = '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body/></soap:Envelope>';
var_dump($validator->requiresNamespace($soap, 'http://schemas.xmlsoap.org/soap/envelope/'));
例5:名前空間宣言と要素の開始イベントの発火順序を確認するデモ
<?php
class EventOrderDemonstrator
{
/**
* start_namespace_decl_handlerが
* element_handlerより先に発火することを実際に確認する
*/
public function demonstrateOrder(string $xml): array
{
$order = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix) use (&$order) {
$order[] = "namespace開始: {$prefix}";
});
xml_set_element_handler(
$parser,
function ($p, $name) use (&$order) {
$order[] = "要素開始: {$name}";
},
fn () => null
);
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $order;
}
}
$demonstrator = new EventOrderDemonstrator();
print_r($demonstrator->demonstrateOrder('<ns:root xmlns:ns="http://example.com"/>'));
// ["namespace開始: ns", "要素開始: http://example.com:ROOT"] の順になる
例6:デフォルト名前空間(プレフィックスなし)を区別して扱う
<?php
class DefaultNamespaceAwareCollector
{
/**
* プレフィックスを持たないデフォルト名前空間(xmlns="...")の場合、
* $prefix引数がnullになる点を明示的に処理する
*/
public function collect(string $xml): array
{
$result = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$result) {
$result[] = [
'is_default' => $prefix === null,
'prefix' => $prefix,
'uri' => $uri,
];
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return $result;
}
}
$collector = new DefaultNamespaceAwareCollector();
print_r($collector->collect('<feed xmlns="http://www.w3.org/2005/Atom"><entry/></feed>'));
例7:外部XMLフィードの名前空間互換性を事前チェックするツール
<?php
class FeedNamespaceCompatibilityChecker
{
private const KNOWN_NAMESPACES = [
'http://www.w3.org/2005/Atom' => 'Atom',
'http://purl.org/rss/1.0/' => 'RSS 1.0',
'http://purl.org/dc/elements/1.1/' => 'Dublin Core',
];
/**
* 外部から取得したフィードXMLの名前空間を確認し、
* 既知の形式かどうかを判定する
*/
public function identifyFormat(string $xml): array
{
$identified = [];
$parser = xml_parser_create_ns();
xml_set_start_namespace_decl_handler($parser, function ($p, $prefix, $uri) use (&$identified) {
if (isset(self::KNOWN_NAMESPACES[$uri])) {
$identified[] = self::KNOWN_NAMESPACES[$uri];
}
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
return array_unique($identified);
}
}
$checker = new FeedNamespaceCompatibilityChecker();
print_r($checker->identifyFormat('<feed xmlns="http://www.w3.org/2005/Atom"><entry/></feed>'));
関連関数との比較
| 関数 | 役割 | xml_set_start_namespace_decl_handlerとの違い |
|---|---|---|
xml_set_start_namespace_decl_handler() | 名前空間宣言のスコープ開始を検知 | 本記事の対象。プレフィックスとURIの両方を受け取る |
xml_set_end_namespace_decl_handler() | 名前空間宣言のスコープ終了を検知 | プレフィックス名のみを受け取る、対になる関数 |
xml_set_element_handler() | 要素の開始・終了を検知 | 名前空間宣言のイベントより後に発火する、要素そのものの検知 |
xml_parser_create_ns() | 名前空間対応のパーサーを生成 | このハンドラを使うための前提条件となる関数 |
DOMXPath::registerNamespace() | XPathクエリ用に名前空間を登録 | SAX方式ではなくDOM方式でのアプローチ |
よくある落とし穴(注意点)
xml_parser_create_ns()で生成したパーサーでなければ機能しない 通常のxml_parser_create()で生成したパーサーにこのハンドラを登録しても、名前空間宣言のイベント自体が発生しないため意味を持ちません。- デフォルト名前空間の場合、
$prefixがnullになるxmlns="..."のようにプレフィックスを持たない宣言の場合、コールバックの第2引数がnullになります。この値を文字列として扱おうとすると警告が発生する可能性があるため、明示的なnullチェックが必要です(例6を参照)。 - 要素の開始イベントより先に発火する順序を理解しておく 1つの要素に複数の名前空間宣言が含まれる場合、それぞれの宣言について個別にこのハンドラが呼ばれた「後」に、ようやく要素自体の開始イベントが発火します。この順序を前提とした状態管理(例えば「今の要素にどの名前空間が有効か」という情報の記録)を行う際は、この発火順序を正しく理解しておく必要があります(例5を参照)。
- 1つの要素に複数の名前空間宣言があると複数回呼び出される
<root xmlns:a="..." xmlns:b="...">のように1つの要素に複数のxmlns属性がある場合、このハンドラはその数だけ呼び出されます。単一の名前空間のみを想定したロジックだと、意図しない動作になる可能性があります。 - URIそのものの妥当性(実在するURLかどうか)はチェックされない 名前空間URIはあくまで「識別子」として機能するものであり、実際にアクセス可能なURLである必要はありません。このハンドラはURIの文字列を渡すだけで、その先の内容を検証するものではないことを理解しておきましょう。
まとめ
| 観点 | まとめ |
|---|---|
| 何をする関数か | 名前空間宣言(xmlns:prefix="URI")の開始(スコープの始まり)を検知する |
| 主な用途 | 文書内で使われる名前空間URIの収集、SOAP/Atomなど特定形式の判定、名前空間の互換性チェック |
| 前提条件 | xml_parser_create_ns() で生成した名前空間対応パーサーであること |
| 発火タイミング | 該当要素自体の開始イベント(xml_set_element_handler())より前に発火する |
| 注意点 | デフォルト名前空間でのnullプレフィックス、複数宣言時の複数回呼び出し、発火順序の理解 |
xml_set_start_namespace_decl_handler() は、名前空間対応のXML解析において、プレフィックスとURIの対応関係を正確に把握するための出発点となる関数です。xml_set_end_namespace_decl_handler() と組み合わせることで、複雑な名前空間構造を持つXML文書のライフサイクル全体を追跡できます。
