[PHP]xml_parser_createとは?Expat XMLパーサーインスタンスを生成する方法を徹底解説

PHP

はじめに

これまでの記事で、xml_parse()xml_parse_into_struct() といったXML解析の中心的な関数を数多く解説してきましたが、そのすべての出発点となっているのが、今回取り上げる xml_parser_create() です。この関数を呼び出すことで初めて、一連のExpatベースのXML処理関数群が使える「パーサーインスタンス」が手に入ります。

xml_parser_create() は、名前の通りXMLパーサーを生成するための関数です。一見単純な関数に見えますが、引数で指定する文字エンコーディングの扱いや、似た名前を持つ xml_parser_create_ns()(名前空間対応版)との違いなど、正しく理解しておくべきポイントがいくつかあります。本記事では、この関数の基本的な役割と、実践的な活用パターンを詳しく解説します。


関数概要

項目内容
関数名xml_parser_create()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_parser_create(?string $encoding = null): XMLParser
引数$encoding — 入力データのエンコーディング(省略時はXML宣言から自動判定)
戻り値XMLパーサーインスタンス(XMLParserオブジェクト)
対応バージョンPHP 4以降(PHP 8.0以降は戻り値がリソース型からXMLParserオブジェクトに変更)
対応エンコーディングUTF-8, ISO-8859-1, US-ASCII
関連関数xml_parser_create_ns()(名前空間対応版)、xml_parser_free()(解放)

全体像(イメージ図)

  一連のXML処理の流れ

  xml_parser_create()          ← ★この記事の対象
  (パーサーインスタンスを生成)
        │
        ▼
  xml_set_element_handler()    ← ハンドラを登録(xml_parse()を使う場合)
        │
        ▼
  xml_parse() / xml_parse_into_struct()
  (実際の解析処理を実行)
        │
        ▼
  xml_parser_free()            ← パーサーを解放

ポイントは、xml_parser_create() が生成するのはあくまで「空の」パーサーインスタンスであるという点です。この時点ではまだXMLの中身は一切解析されておらず、この後に続く xml_set_element_handler() などでのハンドラ登録、そして xml_parse() の呼び出しがあって初めて、実際の解析処理が始まります。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicParserFactory
{
    public function createDefault(): XMLParser
    {
        // エンコーディングを省略すると、XML宣言から自動判定される
        return xml_parser_create();
    }

    public function createWithEncoding(string $encoding): XMLParser
    {
        return xml_parser_create($encoding);
    }
}

$factory = new BasicParserFactory();
$parser = $factory->createDefault();
var_dump($parser instanceof XMLParser); // true (PHP 8.0以降)
xml_parser_free($parser);

例2:生成から解放までをカプセル化した安全なラッパークラス

<?php

class SafeXmlParserWrapper
{
    private XMLParser $parser;

    public function __construct(?string $encoding = null)
    {
        $this->parser = xml_parser_create($encoding);
    }

    public function getParser(): XMLParser
    {
        return $this->parser;
    }

    /**
     * デストラクタでパーサーの解放を保証する
     */
    public function __destruct()
    {
        xml_parser_free($this->parser);
    }
}

$wrapper = new SafeXmlParserWrapper('UTF-8');
xml_set_element_handler(
    $wrapper->getParser(),
    fn ($p, $name) => print("開始: {$name}\n"),
    fn ($p, $name) => print("終了: {$name}\n")
);
xml_parse($wrapper->getParser(), '<root><item/></root>', true);

例3:エンコーディングを明示的に指定して非UTF-8データを扱う

<?php

class LegacyEncodingXmlParser
{
    /**
     * 古いシステムから受け取るISO-8859-1形式のXMLデータを
     * 明示的にエンコーディング指定して解析する
     */
    public function parseLegacyXml(string $xml): array
    {
        $parser = xml_parser_create('ISO-8859-1');
        $result = [];

        xml_set_character_data_handler($parser, function ($p, $data) use (&$result) {
            $trimmed = trim($data);
            if ($trimmed !== '') {
                $result[] = $trimmed;
            }
        });

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

        return $result;
    }
}

$parser = new LegacyEncodingXmlParser();
print_r($parser->parseLegacyXml("<root><name>caf\xe9</name></root>"));

例4:複数のXML文書を並行して処理するためのパーサープール

<?php

class XmlParserPool
{
    /** @var array<string, XMLParser> */
    private array $parsers = [];

    /**
     * 複数の独立したXML文書を扱う際、
     * それぞれに専用のパーサーインスタンスを割り当てる
     */
    public function createParser(string $key, ?string $encoding = null): XMLParser
    {
        $parser = xml_parser_create($encoding);
        $this->parsers[$key] = $parser;
        return $parser;
    }

    public function freeAll(): void
    {
        foreach ($this->parsers as $parser) {
            xml_parser_free($parser);
        }
        $this->parsers = [];
    }
}

$pool = new XmlParserPool();
$parserA = $pool->createParser('feedA');
$parserB = $pool->createParser('feedB');
xml_parse($parserA, '<a><item/></a>', true);
xml_parse($parserB, '<b><item/></b>', true);
$pool->freeAll();

例5:xml_parser_createとxml_parser_create_nsの違いを比較する

<?php

class NamespaceSupportComparator
{
    /**
     * 名前空間を含むXMLを扱う場合、
     * xml_parser_create_ns()を使うとプレフィックスとローカル名を分離できる
     */
    public function compareTagHandling(string $xml): array
    {
        $tagsWithoutNs = [];
        $parserA = xml_parser_create();
        xml_set_element_handler(
            $parserA,
            function ($p, $name) use (&$tagsWithoutNs) { $tagsWithoutNs[] = $name; },
            fn () => null
        );
        xml_parse($parserA, $xml, true);
        xml_parser_free($parserA);

        $tagsWithNs = [];
        $parserB = xml_parser_create_ns();
        xml_set_element_handler(
            $parserB,
            function ($p, $name) use (&$tagsWithNs) { $tagsWithNs[] = $name; },
            fn () => null
        );
        xml_parse($parserB, $xml, true);
        xml_parser_free($parserB);

        return ['without_ns' => $tagsWithoutNs, 'with_ns' => $tagsWithNs];
    }
}

$comparator = new NamespaceSupportComparator();
print_r($comparator->compareTagHandling('<ns:root xmlns:ns="http://example.com"><ns:item/></ns:root>'));

例6:パーサー生成時にユーザーデータを紐付けて状態を管理する

<?php

class StatefulXmlParser
{
    /**
     * xml_parser_set_option()と組み合わせ、
     * パーサー生成直後にオプションを設定しておく実践パターン
     */
    public function createConfiguredParser(): XMLParser
    {
        $parser = xml_parser_create('UTF-8');

        // 大文字小文字の変換を無効化
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
        // 出力エンコーディングをUTF-8に統一
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
        // 要素間の空白を保持しない
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, true);

        return $parser;
    }
}

$factory = new StatefulXmlParser();
$parser = $factory->createConfiguredParser();
xml_parse($parser, '<Root>  <Item>value</Item>  </Root>', true);
xml_parser_free($parser);

例7:ファクトリーメソッドパターンで用途別のパーサーを使い分ける

<?php

class XmlParserFactory
{
    /**
     * 用途に応じて異なる設定のパーサーを生成する
     * ファクトリーメソッドパターンの実装例
     */
    public static function forRssFeed(): XMLParser
    {
        $parser = xml_parser_create('UTF-8');
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
        return $parser;
    }

    public static function forConfigFile(): XMLParser
    {
        $parser = xml_parser_create();
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, true);
        return $parser;
    }
}

$rssParser = XmlParserFactory::forRssFeed();
xml_parse($rssParser, '<rss><item><title>News</title></item></rss>', true);
xml_parser_free($rssParser);

関連関数との比較

関数役割xml_parser_createとの違い
xml_parser_create()名前空間非対応のパーサーを生成本記事の対象。シンプルなXML処理向け
xml_parser_create_ns()名前空間対応のパーサーを生成要素名からプレフィックスとローカル名を分離して扱える
xml_parser_free()パーサーインスタンスを解放生成したパーサーの後始末を行う対になる関数
xml_parser_set_option()パーサーの動作オプションを設定生成後に大文字小文字変換や空白除去などの挙動を調整する
simplexml_load_string()XML文字列をオブジェクトとして直接読み込むパーサーの明示的な生成・解放が不要な、より簡易な代替手段

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

  1. パーサーの解放(xml_parser_free())を忘れる xml_parser_create() で生成したパーサーは、使い終わったら明示的に解放するのが望ましい作法です。大量のXMLを処理するループの中で解放を忘れると、メモリ使用量が増大していく可能性があります(例2のようなデストラクタでの解放が有効です)。
  2. 名前空間を持つXMLに対して通常版を使ってしまう 名前空間プレフィックス(ns:item など)を個別に扱いたい場合、通常の xml_parser_create() では要素名がプレフィックス込みの文字列としてそのまま渡されます。プレフィックスとローカル名を分離したい場合は xml_parser_create_ns() を使う必要があります(例5を参照)。
  3. PHP 8.0以降での戻り値の型変更に注意する PHP 8.0より前は xml_parser_create() の戻り値がリソース型でしたが、PHP 8.0以降は XMLParser オブジェクトに変更されています。is_resource() による型チェックを行っている古いコードは、instanceof XMLParser のような形に修正が必要です。
  4. エンコーディング指定は「入力」データの解釈に関わるものだと理解する xml_parser_create() の引数で指定するエンコーディングは、あくまで入力XMLの解釈に使われるものです。出力(コールバックに渡される文字列)のエンコーディングを制御したい場合は、別途 xml_parser_set_option()XML_OPTION_TARGET_ENCODING を設定する必要があります(例6を参照)。
  5. 1つのパーサーインスタンスを複数のXML文書で使い回そうとする 基本的に1つのパーサーインスタンスは1つの解析セッション用です。異なるXML文書を解析したい場合は、それぞれに新しいパーサーインスタンスを生成するのが安全です(例4のようなプール管理が参考になります)。

まとめ

観点まとめ
何をする関数かExpatベースのXMLパーサーインスタンスを新規生成する
主な用途xml_parse()xml_parse_into_struct()を使う前の準備段階
名前空間対応非対応。名前空間を扱いたい場合はxml_parser_create_ns()を使う
対になる関数xml_parser_free()(解放)
注意点解放忘れによるメモリ増大、名前空間対応版との使い分け、PHP 8.0での型変更

xml_parser_create() は、一連のExpatベースXML処理のすべての起点となる、シンプルながら重要な関数です。生成したパーサーを適切に設定し、使い終わったら確実に解放するという基本を押さえることで、堅牢なXML処理機能を構築できます。

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