[PHP]xml_parser_freeとは?XMLパーサーインスタンスを正しく解放する方法を徹底解説

PHP

はじめに

これまでの記事で、xml_parser_create()xml_parser_create_ns() を使ってXMLパーサーインスタンスを生成する方法を解説してきました。生成したものを最後まで責任を持って後片付けするのが xml_parser_free() の役割です。

xml_parser_free() は、xml_parser_create() などで確保したXMLパーサーインスタンスの内部リソースを解放するための関数です。現代のPHPはガベージコレクションが優秀なため、「解放を忘れても大きな問題にはならないのでは?」と思われがちですが、大量のXMLを処理するバッチ処理やループ内で解放を怠ると、メモリ使用量が徐々に増加していく原因になります。本記事では基本的な使い方から、実践的な解放パターンまで詳しく解説します。


関数概要

項目内容
関数名xml_parser_free()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_parser_free(XMLParser $parser): true
引数$parserxml_parser_create()等で生成したパーサーインスタンス
戻り値常に true(PHP 8.0以降。それ以前は成功時にtrue、失敗時にfalse
対応バージョンPHP 4以降(PHP 8.0以降は引数の型がXMLParserオブジェクトに変更)
対になる関数xml_parser_create(), xml_parser_create_ns()

リソース管理の全体像(イメージ図)

  XMLパーサーのライフサイクル

  xml_parser_create()
        │  パーサーインスタンスを生成
        │  (内部的にメモリ・リソースを確保)
        ▼
  xml_set_element_handler() などでハンドラ登録
        ▼
  xml_parse() / xml_parse_into_struct()
        │  実際の解析処理を実行
        ▼
  xml_parser_free()        ← ★この記事の対象
        │  確保していたリソースを解放
        ▼
  パーサーインスタンスは使用不可になる

ポイントは、xml_parser_create() で確保されるリソースがPHPのガベージコレクションだけに任せきりにするのではなく、明示的に解放するのが推奨される作法であるという点です。特に、多数のXML文書を順番に処理するループやバッチ処理では、この解放を徹底することがメモリ管理上重要になります。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicParserFreeDemo
{
    public function parseAndCleanup(string $xml): bool
    {
        $parser = xml_parser_create();
        $result = xml_parse($parser, $xml, true);

        // 使い終わったパーサーを明示的に解放する
        xml_parser_free($parser);

        return (bool) $result;
    }
}

$demo = new BasicParserFreeDemo();
var_dump($demo->parseAndCleanup('<root><item/></root>'));

例2:try-finallyで確実に解放を保証するクラス

<?php

class GuaranteedCleanupParser
{
    /**
     * 解析処理中に例外が発生した場合でも、
     * finallyブロックで確実にパーサーを解放する
     */
    public function parseSafely(string $xml, callable $onElement): array
    {
        $parser = xml_parser_create();
        $result = [];

        try {
            xml_set_element_handler(
                $parser,
                function ($p, $name, $attrs) use (&$result, $onElement) {
                    $result[] = $onElement($name, $attrs);
                },
                fn () => null
            );

            if (!xml_parse($parser, $xml, true)) {
                throw new RuntimeException(xml_error_string(xml_get_error_code($parser)));
            }

            return $result;
        } finally {
            // 例外の有無にかかわらず必ず解放される
            xml_parser_free($parser);
        }
    }
}

$parser = new GuaranteedCleanupParser();
print_r($parser->parseSafely('<root><a/><b/></root>', fn ($name) => $name));

例3:デストラクタでの自動解放を実装するラッパークラス

<?php

class ManagedXmlParser
{
    private ?XMLParser $parser;

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

    public function parse(string $xml): bool
    {
        return (bool) xml_parse($this->parser, $xml, true);
    }

    /**
     * オブジェクトが破棄されるタイミングで
     * 自動的にパーサーを解放する
     */
    public function __destruct()
    {
        if ($this->parser !== null) {
            xml_parser_free($this->parser);
            $this->parser = null;
        }
    }
}

$managed = new ManagedXmlParser();
var_dump($managed->parse('<root><item/></root>'));
// $managedがスコープを抜けると自動的に解放される

例4:大量のXMLファイルを処理するバッチ処理での解放パターン

<?php

class BatchXmlProcessor
{
    /**
     * 多数のファイルをループ処理する際、
     * 各イテレーションで確実にパーサーを解放してメモリを解放する
     */
    public function processFiles(array $filePaths): array
    {
        $results = [];

        foreach ($filePaths as $path) {
            $content = file_get_contents($path);
            $parser = xml_parser_create();

            $success = xml_parse($parser, $content, true);
            $results[$path] = $success ? '成功' : 'エラー';

            // ループの各回で確実に解放し、リソースの蓄積を防ぐ
            xml_parser_free($parser);
        }

        return $results;
    }
}

// $processor = new BatchXmlProcessor();
// print_r($processor->processFiles(['/tmp/a.xml', '/tmp/b.xml', '/tmp/c.xml']));

例5:解放後の誤用を防ぐガード付きラッパー

<?php

class GuardedXmlParser
{
    private ?XMLParser $parser;
    private bool $freed = false;

    public function __construct()
    {
        $this->parser = xml_parser_create();
    }

    public function parse(string $xml): bool
    {
        if ($this->freed) {
            throw new LogicException('既に解放済みのパーサーは使用できません');
        }

        return (bool) xml_parse($this->parser, $xml, true);
    }

    public function free(): void
    {
        if (!$this->freed) {
            xml_parser_free($this->parser);
            $this->freed = true;
        }
    }
}

$guarded = new GuardedXmlParser();
$guarded->parse('<root/>');
$guarded->free();

try {
    $guarded->parse('<root/>'); // 解放済みのため例外がスローされる
} catch (LogicException $e) {
    echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}

例6:メモリ使用量の変化を計測して解放の効果を確認するデモ

<?php

class MemoryUsageDemonstrator
{
    /**
     * 大量のパーサー生成・解放を行い、
     * メモリ使用量の推移を確認するデモ(教育目的)
     */
    public function demonstrate(int $iterations = 1000): array
    {
        $before = memory_get_usage();

        for ($i = 0; $i < $iterations; $i++) {
            $parser = xml_parser_create();
            xml_parse($parser, '<root><item/></root>', true);
            xml_parser_free($parser); // 解放を徹底する
        }

        $after = memory_get_usage();

        return [
            'before_bytes' => $before,
            'after_bytes'  => $after,
            'diff_bytes'   => $after - $before,
        ];
    }
}

$demonstrator = new MemoryUsageDemonstrator();
print_r($demonstrator->demonstrate());

例7:複数パーサーを管理し、一括で解放するマネージャークラス

<?php

class MultiParserManager
{
    /** @var array<int, XMLParser> */
    private array $activeParsers = [];

    public function createParser(?string $encoding = null): XMLParser
    {
        $parser = xml_parser_create($encoding);
        $this->activeParsers[] = $parser;
        return $parser;
    }

    /**
     * 管理下にあるすべてのパーサーを一括で解放する
     */
    public function freeAll(): int
    {
        $count = 0;
        foreach ($this->activeParsers as $parser) {
            xml_parser_free($parser);
            $count++;
        }
        $this->activeParsers = [];

        return $count;
    }
}

$manager = new MultiParserManager();
$manager->createParser();
$manager->createParser('UTF-8');
$freedCount = $manager->freeAll();
echo "{$freedCount}個のパーサーを解放しました" . PHP_EOL;

関連関数との比較

関数役割xml_parser_freeとの違い
xml_parser_free()パーサーインスタンスを解放本記事の対象。xml_parser_create()系関数と対になる
xml_parser_create()名前空間非対応のパーサーを生成解放対象となるパーサーを生成する側の関数
xml_parser_create_ns()名前空間対応のパーサーを生成同じく解放対象となるパーサーを生成する側の関数
unset()変数をアンセットするパーサーインスタンス自体の内部リソース解放を保証するものではない
fclose()ファイルストリームを閉じる対象こそ異なるが、「確保したリソースを明示的に解放する」という設計思想は共通

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

  1. 解放を忘れてもすぐにはエラーにならないため見落としやすい PHPのガベージコレクションによって、明示的に解放しなくてもスクリプト終了時には最終的にメモリが回収されます。そのため、解放忘れによる問題は「大量のXMLをループ処理するバッチ処理」など、特定の状況下でのみ顕在化しやすく、見落とされがちです(例4・例6を参照)。
  2. 解放後のパーサーを誤って再利用しようとする xml_parser_free() を呼び出した後のパーサーインスタンスは、それ以上解析処理に使うことはできません。解放済みかどうかを管理するフラグを持たせるなど、誤用を防ぐ設計が有効です(例5を参照)。
  3. 例外発生時に解放処理がスキップされる ハンドラ内で例外が発生した場合、通常の処理フローのまま xml_parser_free() の呼び出しが飛ばされてしまう可能性があります。try...finally を使って、例外の有無にかかわらず確実に解放を行う設計が重要です(例2を参照)。
  4. 二重解放によるエラー 既に解放済みのパーサーに対して再度 xml_parser_free() を呼び出すと、警告やエラーが発生する可能性があります。解放処理を一元管理するクラスなどで、二重解放を防ぐ工夫をしておくと安全です(例5・例7を参照)。
  5. PHP 8.0以降での戻り値の変更 PHP 8.0以降、xml_parser_free() の戻り値は常に true になりました。それ以前のバージョンで書かれた「戻り値をチェックして失敗を検知する」というコードは、意味を成さなくなっている可能性があるため、バージョンアップ時に確認しておくとよいでしょう。

まとめ

観点まとめ
何をする関数かxml_parser_create()系関数で生成したパーサーインスタンスの内部リソースを解放する
主な用途解析処理完了後の後始末、バッチ処理でのメモリ管理
対になる関数xml_parser_create(), xml_parser_create_ns()
解放を怠った場合即座にエラーにはならないが、大量処理時にメモリ使用量が増大する可能性がある
注意点例外発生時の解放漏れ、解放後の誤用、二重解放、PHP 8.0での戻り値の変更

xml_parser_free() は、地味ながらXML処理における「良い作法」を体現する重要な関数です。特に大量のXML文書を処理するバッチ処理においては、try...finally やデストラクタを活用した確実な解放パターンを取り入れることで、メモリ効率の良い堅牢なコードを実装できます。

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