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

PHP

はじめに

これまでの記事で、XML解析中の現在位置をバイトオフセットで取得する xml_get_current_byte_index() を解説してきました。今回紹介する xml_get_current_column_number() は、同じ「現在位置」を**行内の列番号(何行目の何文字目か、のうちの「何文字目」の部分)**として取得するための関数です。

エディタでコードを書いていて「12行目の5列目に構文エラーがあります」というメッセージを見たことがある方は多いでしょう。それと同じように、XMLパーサーもエラー発生箇所を「行番号+列番号」で特定できます。この関数は、テキストエディタのようなユーザーフレンドリーなエラー表示を実装したい場合に、xml_get_current_line_number() とセットで使われる重要な関数です。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


関数概要

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

位置情報の関係性(イメージ図)

  XML文書(2行目に注目)
  1: <root>
  2:   <item>value</item>
              ▲
              │
  この "<item>" タグの開始位置に注目すると...

  xml_get_current_line_number() → 2   (2行目)
  xml_get_current_column_number() → 2 (その行の2文字目、インデントのスペース2つ分の後)

ポイントは、xml_get_current_column_number() が**「その行の先頭から何文字目か」**を表しているという点です。行番号だけでは「その行のどのあたりでエラーが起きたか」までは分かりませんが、列番号と組み合わせることで、テキストエディタのカーソル位置のように正確な地点を特定できます。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicColumnNumberDemo
{
    public function reportPosition(string $xml): void
    {
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name, $attrs) {
                $line = xml_get_current_line_number($parser);
                $column = xml_get_current_column_number($parser);
                echo "要素 <{$name}> は {$line}行目 {$column}列目で見つかりました" . PHP_EOL;
            },
            fn () => null
        );

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

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

例2:エディタ風のエラーメッセージを生成するクラス

<?php

class EditorStyleErrorFormatter
{
    /**
     * "12:5: エラー内容" のような、
     * テキストエディタでおなじみの形式でエラーを表示する
     */
    public function parseAndFormat(string $xml, string $fileName = 'input.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);
        $column = xml_get_current_column_number($parser);
        $message = xml_error_string(xml_get_error_code($parser));
        xml_parser_free($parser);

        return "{$fileName}:{$line}:{$column}: {$message}";
    }
}

$formatter = new EditorStyleErrorFormatter();
echo $formatter->parseAndFormat("<root>\n  <item>value</root>", 'feed.xml') . PHP_EOL;

例3:エラー行にカーソル位置を示す視覚的なエラー表示ツール

<?php

class VisualErrorPointer
{
    /**
     * エラーが発生した行の内容を表示し、
     * 列番号の位置に "^" 記号でカーソル位置を示す
     */
    public function displayError(string $xml): void
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            echo '解析に成功しました' . 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);

        $lines = explode("\n", $xml);
        $targetLine = $lines[$line - 1] ?? '';

        echo "エラー: {$message}\n";
        echo $targetLine . "\n";
        echo str_repeat(' ', $column) . "^\n";
    }
}

$pointer = new VisualErrorPointer();
$pointer->displayError("<root>\n  <item>value</unknown>\n</root>");

例4:行・列番号を組み合わせた構造化エラーオブジェクトの生成

<?php

class XmlParseError
{
    public function __construct(
        public readonly string $message,
        public readonly int $line,
        public readonly int $column
    ) {
    }

    public function __toString(): string
    {
        return "[{$this->line}:{$this->column}] {$this->message}";
    }
}

class StructuredXmlParser
{
    public function parse(string $xml): array|XmlParseError
    {
        $parser = xml_parser_create();
        $result = [];

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

        if (!xml_parse($parser, $xml, true)) {
            $error = new XmlParseError(
                xml_error_string(xml_get_error_code($parser)),
                xml_get_current_line_number($parser),
                xml_get_current_column_number($parser)
            );
            xml_parser_free($parser);
            return $error;
        }

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

$parser = new StructuredXmlParser();
$result = $parser->parse('<root><item>value</root>');
echo $result instanceof XmlParseError ? (string) $result : print_r($result, true);

例5:複数のXMLファイルを検証し、位置情報付きレポートを生成するツール

<?php

class MultiFileXmlValidator
{
    /**
     * 複数ファイルを検証し、それぞれのエラー位置(行:列)を
     * 一覧レポートとしてまとめる
     */
    public function validate(array $filePaths): array
    {
        $report = [];

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

            if (xml_parse($parser, $content, true)) {
                $report[$path] = '問題なし';
            } else {
                $line = xml_get_current_line_number($parser);
                $column = xml_get_current_column_number($parser);
                $message = xml_error_string(xml_get_error_code($parser));
                $report[$path] = "{$line}行{$column}列: {$message}";
            }

            xml_parser_free($parser);
        }

        return $report;
    }
}

// $validator = new MultiFileXmlValidator();
// print_r($validator->validate(['/tmp/a.xml', '/tmp/b.xml']));

例6:インデントの深さを列番号から推測するデバッグ補助クラス

<?php

class IndentationAnalyzer
{
    /**
     * 各要素の開始列番号を記録することで、
     * XML文書のインデント構造をおおまかに可視化する
     * (厳密な階層検出ではなく、あくまで参考情報としての利用を想定)
     */
    public function analyzeIndentation(string $xml): array
    {
        $records = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name) use (&$records) {
                $records[] = [
                    'tag'    => $name,
                    'line'   => xml_get_current_line_number($parser),
                    'column' => xml_get_current_column_number($parser),
                ];
            },
            fn () => null
        );

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

        return $records;
    }
}

$analyzer = new IndentationAnalyzer();
print_r($analyzer->analyzeIndentation("<root>\n  <child>\n    <grandchild/>\n  </child>\n</root>"));

例7:CI/CDパイプライン向けのGitHub Actions形式エラー出力

<?php

class GithubActionsAnnotationFormatter
{
    /**
     * GitHub Actionsのアノテーション形式
     * ("::error file=...,line=...,col=...::message") で
     * エラーを出力し、CI上でエラー箇所を直接ハイライトできるようにする
     */
    public function annotate(string $xml, string $fileName): void
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            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(
            "::error file=%s,line=%d,col=%d::%s\n",
            $fileName,
            $line,
            $column,
            $message
        );
    }
}

$formatter = new GithubActionsAnnotationFormatter();
$formatter->annotate("<config>\n  <invalid></config>", 'config.xml');

関連関数との比較

関数役割xml_get_current_column_numberとの違い
xml_get_current_column_number()現在位置を行内の列番号で取得本記事の対象。「行の何文字目か」を表す
xml_get_current_line_number()現在位置を行番号で取得「何行目か」を表し、列番号とセットで使われることが多い
xml_get_current_byte_index()現在位置を文書全体のバイトオフセットで取得行・列に分解せず、文書全体を通したバイト位置を表す
xml_error_string()エラーコードを説明文字列に変換位置情報ではなく、エラーの「内容」を扱う
preg_matchPREG_OFFSET_CAPTURE正規表現マッチ位置のオフセットを取得XMLパーサーとは異なる文脈だが、同様に「テキスト中の位置」を扱う機能

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

  1. 列番号は0始まりであることに注意する 人間の感覚では「1文字目」を1と数えたくなりますが、xml_get_current_column_number() は0始まりの値を返します。エディタ風の表示にする場合、+1 するかどうかは要件に応じて検討しましょう(多くのエディタは1始まりで表示するため、変換が必要な場合があります)。
  2. マルチバイト文字を含む行での列番号の解釈 バイトオフセットと同様、列番号もバイト単位で計算される場合があるため、日本語などのマルチバイト文字が混在する行では、見た目の文字位置とずれることがあります。正確な文字単位の位置が必要な場合は追加の変換処理を検討しましょう。
  3. ハンドラの外側で呼び出しても意味のある値が得られない xml_get_current_byte_index() と同様、この関数も要素ハンドラなどの内部、またはエラー発生直後に呼び出すことで意味を持ちます。
  4. タブ文字の扱いに一貫性がない場合がある タブ文字(\t)を1文字としてカウントするか、複数文字分の幅として扱うかは環境によって解釈が分かれることがあります。厳密な桁揃えが必要な表示を行う場合は、事前に文書内のタブをスペースに統一しておくと安全です。
  5. 戻り値-1のケースを見落とす 位置情報が取得できない場合、-1 が返ることがあります。表示処理でこの値をそのまま使うと str_repeat(' ', -1) のようなエラーの原因になるため、事前のチェックが重要です(例3のような視覚的表示を行う場合は特に注意してください)。

まとめ

観点まとめ
何をする関数かXMLパーサーの現在の解析位置を、行内の列番号として取得する
主な用途エディタ風のエラーメッセージ生成、視覚的なエラー箇所表示、CI/CDのアノテーション出力
セットで使う関数xml_get_current_line_number()(行番号との組み合わせが基本)
列番号の基準0始まり。表示用途では+1変換が必要な場合がある
注意点マルチバイト文字での位置ずれ、タブ文字の扱い、-1が返るケースの考慮

xml_get_current_column_number() は、xml_get_current_line_number() と組み合わせることで、エラー発生箇所を「行:列」という馴染み深い形式で特定できる便利な関数です。開発者向けのわかりやすいエラー表示や、CI/CDパイプラインでの自動アノテーションなど、XML解析エラーへの対応を一段階洗練させたい場面で活用してみてください。

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