[PHP]xml_set_end_namespace_decl_handlerとは?名前空間宣言のスコープ終了を検知する方法を徹底解説

PHP

はじめに

これまでの記事で、名前空間対応のパーサーを生成する 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$parserxml_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方式でのアプローチ

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

  1. xml_parser_create_ns() で生成したパーサーでなければ機能しない 通常の xml_parser_create() で生成したパーサーに対してこのハンドラを登録しても、名前空間宣言のイベント自体が発生しないため意味を持ちません。
  2. URIは渡されず、プレフィックス名のみが渡される スコープ終了時のコールバックには、開始時とは異なりURI情報が含まれていません。終了時にもURIの情報が必要な場合は、開始ハンドラ側で記録しておいたプレフィックスとURIの対応関係を参照する必要があります(例3を参照)。
  3. スコープ終了は要素の終了タグと同じタイミングで発生する 名前空間宣言のスコープ終了イベントは、その宣言が行われた要素の終了タグ(</ns:section>など)が検知されたタイミングと連動して発火します。要素の終了ハンドラとの実行順序を意識しておくとよいでしょう。
  4. デフォルト名前空間の場合、プレフィックスが空文字列またはnullになる xmlns="..." のようなプレフィックスを持たない宣言の場合、コールバックに渡される $prefix の値が空になります。この場合の表示処理などでは、明示的なハンドリングが必要です(例6を参照)。
  5. 単体では名前空間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レスポンスや複数スキーマを組み合わせたフィードなど)を正確に解析する際に、名前空間の有効範囲を正しく追跡するための専門的な関数です。開始ハンドラと組み合わせることで、名前空間のライフサイクル全体を把握できます。

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