[PHP]xml_get_current_line_numberとは?XML解析中の現在位置を行番号で取得する方法を徹底解説

PHP

はじめに

これまでの記事で、XML解析中の現在位置を取得する xml_get_current_byte_index()(バイトオフセット)と xml_get_current_column_number()(列番号)を解説してきました。今回はその中でも最も直感的で使用頻度の高い xml_get_current_line_number() を取り上げます。

xml_get_current_line_number() は、XMLパーサーが現在処理している位置が文書の何行目にあたるかを取得するための関数です。「XMLの12行目に問題があります」というエラーメッセージは、開発者にとって最も理解しやすい形式の一つであり、この関数はXML解析エラーのデバッグにおいて最も基本的かつ重要な役割を果たします。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


関数概要

項目内容
関数名xml_get_current_line_number()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_get_current_line_number(XMLParser $parser): int
引数$parserxml_parser_create() で生成したパーサーインスタンス
戻り値現在の行番号(1始まり)。取得できない場合は 0
対応バージョンPHP 4以降(PHP 8.0以降は引数の型がXMLParserオブジェクトに変更)
関連関数xml_get_current_column_number()xml_get_current_byte_index()

位置情報の全体像(イメージ図)

  XML文書
  1: <catalog>
  2:   <item>
  3:     <name>ノート</name>
  4:   </item>
  5: </catalog>
              │
              │ <name>要素のハンドラ内で呼び出すと...
              ▼
  xml_get_current_line_number($parser) → 3
        ▲
        └─ 「3行目」という、人間にとって最も分かりやすい形式の位置情報

ポイントは、xml_get_current_line_number() が返す行番号が1始まり(ほとんどのテキストエディタと同じ数え方)であるという点です。前回解説した列番号(0始まり)とは異なる基準になっているため、混同しないよう注意が必要です。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicLineNumberDemo
{
    public function reportLines(string $xml): void
    {
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name, $attrs) {
                $line = xml_get_current_line_number($parser);
                echo "{$line}行目: <{$name}>が開始されました" . PHP_EOL;
            },
            fn () => null
        );

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

$demo = new BasicLineNumberDemo();
$demo->reportLines("<root>\n  <item>value</item>\n</root>");

例2:シンプルなエラー行報告ツール

<?php

class SimpleErrorLineReporter
{
    /**
     * エラー発生時、最も基本的な"何行目でエラーが起きたか"を報告する
     */
    public function report(string $xml): ?string
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            xml_parser_free($parser);
            return null;
        }

        $line = xml_get_current_line_number($parser);
        $message = xml_error_string(xml_get_error_code($parser));
        xml_parser_free($parser);

        return "{$line}行目でエラーが発生しました: {$message}";
    }
}

$reporter = new SimpleErrorLineReporter();
echo $reporter->report("<config>\n  <setting>\n  <value>on</setting>\n</config>") . PHP_EOL;

例3:問題のある行のテキストを抜き出して表示するクラス

<?php

class ErrorLineExtractor
{
    /**
     * xml_get_current_line_number()の結果を使って、
     * 元のXML文字列から該当行のテキストそのものを取得する
     */
    public function extractErrorLine(string $xml): ?array
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            xml_parser_free($parser);
            return null;
        }

        $line = xml_get_current_line_number($parser);
        $message = xml_error_string(xml_get_error_code($parser));
        xml_parser_free($parser);

        $lines = explode("\n", $xml);
        // 行番号は1始まりのため、配列インデックスに変換する際は-1する
        $lineContent = $lines[$line - 1] ?? '(該当行を取得できません)';

        return [
            'line'    => $line,
            'content' => $lineContent,
            'message' => $message,
        ];
    }
}

$extractor = new ErrorLineExtractor();
print_r($extractor->extractErrorLine("<root>\n  <item>value</root>"));

例4:大量の要素を含むXMLで特定タグの出現行を一覧化するツール

<?php

class TagLineIndexer
{
    /**
     * 特定のタグ名が何行目に出現するかを
     * 一覧としてまとめる(大規模XMLの目視確認などに便利)
     */
    public function indexTagLines(string $xml, string $targetTag): array
    {
        $lines = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name) use (&$lines, $targetTag) {
                if ($name === $targetTag) {
                    $lines[] = xml_get_current_line_number($parser);
                }
            },
            fn () => null
        );

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

        return $lines;
    }
}

$indexer = new TagLineIndexer();
$xml = "<catalog>\n  <item>A</item>\n  <item>B</item>\n  <item>C</item>\n</catalog>";
print_r($indexer->indexTagLines($xml, 'item'));
// [2, 3, 4]

例5:CSV形式でエラーサマリーをエクスポートするバッチツール

<?php

class XmlErrorSummaryExporter
{
    /**
     * 複数のXMLファイルを検証し、
     * "ファイル名,行番号,エラー内容" のCSV形式でまとめる
     */
    public function exportSummary(array $filePaths, string $outputCsvPath): void
    {
        $handle = fopen($outputCsvPath, 'w');
        fputcsv($handle, ['ファイル名', '行番号', 'エラー内容']);

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

            if (!xml_parse($parser, $content, true)) {
                fputcsv($handle, [
                    basename($path),
                    xml_get_current_line_number($parser),
                    xml_error_string(xml_get_error_code($parser)),
                ]);
            }

            xml_parser_free($parser);
        }

        fclose($handle);
    }
}

// $exporter = new XmlErrorSummaryExporter();
// $exporter->exportSummary(['/tmp/a.xml', '/tmp/b.xml'], '/tmp/error_summary.csv');

例6:行番号と列番号を組み合わせたIDE風エラー表示

<?php

class IdeStyleErrorDisplay
{
    /**
     * xml_get_current_line_number()とxml_get_current_column_number()を
     * 組み合わせ、IDEの問題パネルのような表示を再現する
     */
    public function display(string $xml, string $fileName): void
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            echo "✓ {$fileName}: 問題は見つかりませんでした" . PHP_EOL;
            xml_parser_free($parser);
            return;
        }

        $line = xml_get_current_line_number($parser);
        $column = xml_get_current_column_number($parser);
        $message = xml_error_string(xml_get_error_code($parser));
        xml_parser_free($parser);

        printf("✗ %s [%d, %d]: %s\n", $fileName, $line, $column, $message);
    }
}

$display = new IdeStyleErrorDisplay();
$display->display("<root>\n  <a></root>", 'sample.xml');

例7:JSON形式で位置情報を含むエラー詳細をAPIレスポンスとして返す

<?php

class XmlValidationApiResponder
{
    /**
     * REST APIのレスポンスとして、
     * 行番号を含む構造化されたエラー情報をJSON形式で返す
     */
    public function validate(string $xml): array
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            xml_parser_free($parser);
            return ['valid' => true];
        }

        $response = [
            'valid' => false,
            'error' => [
                'line'    => xml_get_current_line_number($parser),
                'column'  => xml_get_current_column_number($parser),
                'message' => xml_error_string(xml_get_error_code($parser)),
            ],
        ];

        xml_parser_free($parser);
        return $response;
    }
}

$responder = new XmlValidationApiResponder();
echo json_encode($responder->validate("<root>\n  <item>value</root>"), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

関連関数との比較

関数役割xml_get_current_line_numberとの違い
xml_get_current_line_number()現在位置を行番号で取得本記事の対象。1始まりで、最も直感的な位置情報
xml_get_current_column_number()現在位置を行内の列番号で取得0始まりで、行番号と組み合わせて詳細な位置を特定する
xml_get_current_byte_index()現在位置を文書全体のバイトオフセットで取得行に分解せず、文書全体を通した位置を表す
xml_error_string()エラーコードを説明文字列に変換位置情報ではなく、エラーの「内容」を扱う
debug_backtrace()PHPコード自体の呼び出し履歴を取得XML文書内の位置ではなく、PHPスクリプト実行時の呼び出し元を追跡する

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

  1. 行番号は1始まり、列番号は0始まりという違いに注意する xml_get_current_line_number() は1始まりですが、前回解説した xml_get_current_column_number() は0始まりです。両者を組み合わせて表示する際、この基準の違いを意識しないと、配列インデックスへの変換などでオフバイワンエラーを起こしやすくなります(例3を参照)。
  2. 配列インデックスへの変換時は-1を忘れない explode("\n", $xml) で行の配列を作り、xml_get_current_line_number() の結果でその行の内容を取得したい場合、行番号は1始まりなのに対して配列インデックスは0始まりのため、$lines[$line - 1] のように変換する必要があります。
  3. ハンドラの外側で呼び出しても意味のある値が得られない 他の位置取得系関数と同様、要素ハンドラの内部、またはエラー発生直後に呼び出すのが基本的な使い方です。
  4. 改行コードの違い(\r\n\n)による行カウントのずれ Windows形式の改行コード(\r\n)を含むファイルを扱う場合でも、Expatパーサーは基本的に正しく行を認識しますが、ファイルの読み込み方法によっては予期しない挙動になることもあるため、大量のファイルを処理する際は事前に動作確認をしておくと安心です。
  5. 戻り値0のケースを見落とす 位置情報が取得できない場合、0 が返ることがあります(列番号やバイトインデックスの-1とは異なる基準である点に注意)。この値をそのまま「1行目」と誤認しないよう、必要に応じてチェックを入れましょう。

まとめ

観点まとめ
何をする関数かXMLパーサーの現在の解析位置を、文書内の行番号として取得する
主な用途エラー発生行の報告、該当行テキストの抜粋表示、IDE風のエラー表示、APIレスポンスへの組み込み
行番号の基準1始まり(列番号の0始まりとは異なる点に注意)
セットで使う関数xml_get_current_column_number()(より詳細な位置特定)、xml_error_string()(エラー内容の取得)
注意点配列インデックス変換時の-1忘れ、行番号と列番号の基準の違い、戻り値0のケース

xml_get_current_line_number() は、XML解析エラーへの対応において最も基本的で、かつ実務上最も使用頻度の高い位置情報取得関数です。列番号やバイトオフセットと組み合わせることで、開発者にとって分かりやすく、かつプログラム的にも扱いやすいエラーハンドリングを実装できます。

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