Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【PHP8.x】DOMNode::compareDocumentPosition()メソッドの使い方

compareDocumentPositionメソッドの使い方について、初心者にもわかりやすく解説します。

作成日: 更新日:

基本的な使い方

compareDocumentPositionメソッドは、現在のDOMノードと引数として指定された別のDOMノードが、HTMLやXMLドキュメントのツリー構造内でどのような位置関係にあるかを比較し、その結果を示す整数値を返すメソッドです。このメソッドは、2つのノードの相対的な位置を示すために、複数の状態を組み合わせたビットマスクとして結果を返します。

戻り値は、DOM_DOCUMENT_POSITION_DISCONNECTEDDOM_DOCUMENT_POSITION_PRECEDINGDOM_DOCUMENT_POSITION_FOLLOWINGDOM_DOCUMENT_POSITION_CONTAINSDOM_DOCUMENT_POSITION_CONTAINED_BYといった定数、またはこれらの組み合わせになります。例えば、DOM_DOCUMENT_POSITION_DISCONNECTEDは、2つのノードが異なるドキュメントに属しているか、ツリー構造上で関連がない状態を示します。DOM_DOCUMENT_POSITION_PRECEDINGは、現在のノードが指定されたノードの前に位置することを、DOM_DOCUMENT_POSITION_FOLLOWINGは、現在のノードが指定されたノードの後に位置することを示します。また、DOM_DOCUMENT_POSITION_CONTAINSは、現在のノードが指定されたノードの親または祖先であることを、DOM_DOCUMENT_POSITION_CONTAINED_BYは、現在のノードが指定されたノードの子または子孫であることを意味します。

このメソッドを使用することで、ドキュメントの構造を動的に解析したり、特定のノードが他のノードに対してどこに配置されているかをプログラムで確認したりすることができます。これにより、DOM操作を行う際に、ノード間の正確な位置関係に基づいたロジックを実装することが可能になります。引数には比較対象となるDOMNodeオブジェクトを一つ指定します。

構文(syntax)

1<?php
2
3$doc = new DOMDocument();
4$parentElement = $doc->createElement('parent');
5$doc->appendChild($parentElement);
6
7$firstNode = $doc->createElement('first');
8$parentElement->appendChild($firstNode);
9
10$secondNode = $doc->createElement('second');
11$parentElement->appendChild($secondNode);
12
13$firstNode->compareDocumentPosition($secondNode);
14
15?>

引数(parameters)

DOMNode $other

  • DOMNode $other: 比較対象となる別のDOMNodeオブジェクト

戻り値(return)

int

このメソッドは、2つのDOMNodeオブジェクトを比較し、それらの相対的な位置関係を示す整数値を返します。返される値はビットフラグとして解釈され、具体的な意味はPHPマニュアルで確認できます。

サンプルコード

PHP DOMNode 比較処理

1<?php
2
3declare(strict_types=1);
4
5namespace App\Dom;
6
7/**
8 * HTML ドキュメント内の2つの DOM ノードを比較するためのユーティリティクラス。
9 *
10 * `DOMNode::compareDocumentPosition` メソッドの利用例を提供します。
11 * システムエンジニアを目指す初心者にも分かりやすいよう、
12 * PHPの推奨コーディングスタイルとPHPDocコメントに沿って記述されています。
13 * Composerを使ったプロジェクトでオートロードされることを想定した名前空間を使用しています。
14 */
15class DomNodeComparer
16{
17    /**
18     * 指定された HTML 文字列から DOM ノードを抽出し、それらの位置を比較します。
19     *
20     * @param string $htmlString 比較するノードを含む HTML 文字列。
21     * @param string $xpath1     最初のノードを選択するための XPath クエリ。例: `//div[@id="container"]`
22     * @param string $xpath2     2番目のノードを選択するための XPath クエリ。例: `//p[contains(., "最初の段落")]`
23     * @return string 比較結果を説明する文字列。ノードが見つからない場合はエラーメッセージを返します。
24     */
25    public function compareNodes(string $htmlString, string $xpath1, string $xpath2): string
26    {
27        $dom = new \DOMDocument();
28        // HTMLのロード時の警告を抑制します(例: 不完全なHTMLでもパースを試みるため)。
29        @$dom->loadHTML($htmlString);
30        $xpath = new \DOMXPath($dom);
31
32        // XPath クエリを使用してノードを取得します。
33        // queryメソッドはDOMNodeListオブジェクトを返すか、エラー時にはfalseを返します。
34        $nodeList1 = $xpath->query($xpath1);
35        $nodeList2 = $xpath->query($xpath2);
36
37        // ノードが見つからなかった場合のエラーハンドリング
38        if ($nodeList1 === false || $nodeList1->length === 0) {
39            return "エラー: 最初のノード ({$xpath1}) が見つかりませんでした。";
40        }
41        if ($nodeList2 === false || $nodeList2->length === 0) {
42            return "エラー: 2番目のノード ({$xpath2}) が見つかりませんでした。";
43        }
44
45        // DOMNodeList の item(0) はDOMNodeまたはnullを返します。
46        // ここでは最初のマッチングノードを使用します。
47        $node1 = $nodeList1->item(0);
48        $node2 = $nodeList2->item(0);
49
50        // item(0) が null を返す可能性に備えた追加のチェック
51        if ($node1 === null || $node2 === null) {
52            return "エラー: ノードの取得中に予期せぬ問題が発生しました。";
53        }
54        
55        // 2つのノード間の位置関係を比較します。
56        // 戻り値はビットマスクであり、複数の情報を含むことがあります。
57        $position = $node1->compareDocumentPosition($node2);
58
59        $resultMessages = [];
60
61        // 比較結果のビットマスクを解釈します。
62        // 各DOM_DOCUMENT_POSITION_xxx定数を論理積(&)でチェックし、
63        // 該当する場合に説明メッセージを追加します。
64        if ($position === 0) {
65            $resultMessages[] = "ノードは同じです(同じノードへの参照)。";
66        } else {
67            // ノードが異なるドキュメントツリーに属している、またはツリーから切り離されている場合
68            if (($position & \DOM_DOCUMENT_POSITION_DISCONNECTED) === \DOM_DOCUMENT_POSITION_DISCONNECTED) {
69                $resultMessages[] = "ノードは互いに接続されていません(例: 異なるDOMDocumentインスタンスに属している)。";
70            }
71            // `other`ノードがレシーバーノード($node1)の前に現れる場合
72            if (($position & \DOM_DOCUMENT_POSITION_PRECEDING) === \DOM_DOCUMENT_POSITION_PRECEDING) {
73                $resultMessages[] = "ノード1 ({$xpath1}) はノード2 ({$xpath2}) の前にあります。";
74            }
75            // `other`ノードがレシーバーノード($node1)の後に現れる場合
76            if (($position & \DOM_DOCUMENT_POSITION_FOLLOWING) === \DOM_DOCUMENT_POSITION_FOLLOWING) {
77                $resultMessages[] = "ノード1 ({$xpath1}) はノード2 ({$xpath2}) の後にあります。";
78            }
79            // レシーバーノード($node1)が `other`ノード($node2)を含む場合
80            if (($position & \DOM_DOCUMENT_POSITION_CONTAINS) === \DOM_DOCUMENT_POSITION_CONTAINS) {
81                $resultMessages[] = "ノード1 ({$xpath1}) はノード2 ({$xpath2}) を含んでいます。";
82            }
83            // レシーバーノード($node1)が `other`ノード($node2)に含まれる場合
84            if (($position & \DOM_DOCUMENT_POSITION_CONTAINED_BY) === \DOM_DOCUMENT_POSITION_CONTAINED_BY) {
85                $resultMessages[] = "ノード1 ({$xpath1}) はノード2 ({$xpath2}) に含まれています。";
86            }
87            // 実装固有の比較結果の場合
88            if (($position & \DOM_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) === \DOM_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) {
89                $resultMessages[] = "比較結果は実装固有です。";
90            }
91        }
92
93        return "比較結果:\n- " . implode("\n- ", $resultMessages);
94    }
95}
96
97// --- サンプルコードの実行部分 ---
98// この部分は、上記の DomNodeComparer クラスがどのように使用されるかを示します。
99// Composer を利用する場合、通常は `vendor/autoload.php` を読み込み、`use` ステートメントでクラスをインポートします。
100// この単一ファイルでの動作例では、名前空間を考慮したフル修飾名 (`\App\Dom\DomNodeComparer`) を使用しています。
101
102$htmlContent = <<<HTML
103<!DOCTYPE html>
104<html>
105<head>
106    <title>DOM比較の例</title>
107</head>
108<body>
109    <div id="container">
110        <h1>ドキュメントのタイトル</h1>
111        <p id="first-paragraph">これは最初の段落です。</p>
112        <span id="target-span">これはターゲットのスパンです。</span>
113        <p id="second-paragraph">これは2番目の段落です。</p>
114        <ul>
115            <li>リストアイテム1</li>
116            <li id="list-item-2">リストアイテム2</li>
117        </ul>
118    </div>
119    <div id="another-container">
120        <p>異なるコンテナ内の段落。</p>
121    </div>
122</body>
123</html>
124HTML;
125
126$comparer = new \App\Dom\DomNodeComparer();
127
128echo "--- サンプル1: 親子関係 (コンテナが段落を含む) ---\n";
129// ノード1 (#container) はノード2 (#first-paragraph) を含んでいます。
130// 結果: ノード1はノード2を含んでいます。ノード1はノード2の後にあります。
131$result1 = $comparer->compareNodes(
132    $htmlContent,
133    '//div[@id="container"]',
134    '//p[@id="first-paragraph"]'
135);
136echo $result1 . "\n\n";
137
138echo "--- サンプル2: 兄弟関係 (先行ノード) ---\n";
139// ノード1 (#first-paragraph) はノード2 (#target-span) の前にあります。
140$result2 = $comparer->compareNodes(
141    $htmlContent,
142    '//p[@id="first-paragraph"]',
143    '//span[@id="target-span"]'
144);
145echo $result2 . "\n\n";
146
147echo "--- サンプル3: 兄弟関係 (後続ノード) ---\n";
148// ノード1 (#target-span) はノード2 (#first-paragraph) の後にあります。
149$result3 = $comparer->compareNodes(
150    $htmlContent,
151    '//span[@id="target-span"]',
152    '//p[@id="first-paragraph"]'
153);
154echo $result3 . "\n\n";
155
156echo "--- サンプル4: 同じノードの比較 ---\n";
157// ノードは同じです(同じノードへの参照)。
158$result4 = $comparer->compareNodes(
159    $htmlContent,
160    '//p[@id="first-paragraph"]',
161    '//p[@id="first-paragraph"]'
162);
163echo $result4 . "\n\n";
164
165echo "--- サンプル5: 異なる親を持つノードの比較 (DOM順序) ---\n";
166// ノード1 (#first-paragraph) はノード2 (#list-item-2) の前にあります。
167// 2つのノードは異なる親を持っていますが、DOMツリー上での文書順序は定義されます。
168$result5 = $comparer->compareNodes(
169    $htmlContent,
170    '//p[@id="first-paragraph"]',
171    '//li[@id="list-item-2"]'
172);
173echo $result5 . "\n\n";
174
175echo "--- サンプル6: 存在しないノードの指定 ---\n";
176// エラーメッセージが出力されます。
177$result6 = $comparer->compareNodes(
178    $htmlContent,
179    '//p[@id="non-existent-paragraph"]', // 存在しないノードのXPath
180    '//span[@id="target-span"]'
181);
182echo $result6 . "\n\n";
183

このサンプルコードは、PHPのDOMNode::compareDocumentPositionメソッドを使用し、HTMLドキュメント内の二つのDOMノードが互いにどのような位置関係にあるかを比較する方法を示しています。このメソッドは、引数として比較対象のDOMNodeを受け取り、二つのノード間の位置関係を示す整数値(ビットマスク)を返します。

コードでは、まず与えられたHTML文字列を解析し、XPathクエリを使って目的のDOMノードを抽出しています。その後、取得した二つのノードに対してcompareDocumentPositionメソッドを実行し、その戻り値を詳細に解釈しています。戻り値はビットマスクであるため、DOM_DOCUMENT_POSITION_xxxといった定数と論理積演算子(&)を用いて、ノードが同じであるか、先行する、後続する、含む、含まれる、接続されていない、といった複数の位置関係を識別し、それぞれの状態に応じた説明メッセージを生成しています。

また、ノードが見つからなかった場合のエラーハンドリングや、PHPDocによるドキュメント化、Composerを用いたオートロードを想定した名前空間の使用など、PHPの標準的な開発において推奨されるコーディングスタイルが取り入れられています。これにより、堅牢性と可読性の高いコードの記述方法を学ぶことができます。

compareDocumentPositionメソッドの戻り値はビットマスクであり、複数の状態を示す可能性があります。そのため、単なる比較ではなく、論理積(&)演算子を使って各定数と照合し、結果を慎重に解釈してください。HTMLの読み込みやXPathクエリによるノードの取得は失敗する可能性があり、その場合はnullfalseが返されるため、必ず適切なエラーハンドリングを行うことが重要です。サンプルコードでは警告を抑制していますが、実運用ではエラーログの確認や入力値のバリデーションを強化することをお勧めします。また、Composerを利用する際は、名前空間とオートロードの仕組みを理解すると良いでしょう。PHPの組み込み定数はグローバル名前空間に存在することが多いため、\プレフィックスをつけて使用するのが一般的です。

PHP DOMNode比較でノード位置を特定する

1<?php
2
3/**
4 * 2つのDOMノードの位置関係を比較し、その結果を分かりやすく表示します。
5 *
6 * この関数は、DOMNode::compareDocumentPosition メソッドを使用して、
7 * DOMツリー内における2つのノードの相対的な位置を判定する方法を示します。
8 * PHPの推奨コーディングスタイル(PSR-12など)とPHPDocコメントの記述例を含みます。
9 * phpdocumentorのようなツールは、これらのコメント構造から自動的にドキュメントを生成できます。
10 * PHPDocコメントの書き方は、ドキュメント生成ツールを利用する際の「オプション」の一つとも言えます。
11 *
12 * @param DOMNode $nodeA 比較する最初のDOMノード。
13 * @param DOMNode $nodeB 比較する2番目のDOMノード。
14 * @return void 結果を標準出力に表示するだけです。
15 */
16function displayNodePositionComparison(DOMNode $nodeA, DOMNode $nodeB): void
17{
18    // ノードの識別を分かりやすくするため、ノード名とテキストの一部を取得
19    $nodeAName = $nodeA->nodeName . ' (ID: ' . ($nodeA->hasAttribute('id') ? $nodeA->getAttribute('id') : 'なし') . ')';
20    $nodeBName = $nodeB->nodeName . ' (ID: ' . ($nodeB->hasAttribute('id') ? $nodeB->getAttribute('id') : 'なし') . ')';
21
22    echo "--- ノード比較: '{$nodeAName}' vs '{$nodeBName}' ---\n";
23
24    // DOMNode::compareDocumentPosition メソッドを呼び出し、結果(ビットマスク)を取得
25    $result = $nodeA->compareDocumentPosition($nodeB);
26
27    // 戻り値のビットマスクをDOM_POSITION定数と比較し、各位置関係を解釈して表示
28    if ($result === DOM_POSITION_DISCONNECTED) {
29        echo "- 2つのノードは異なるドキュメントに属しているか、ツリー内で互いに接続されていません。\n";
30    }
31
32    if ($result & DOM_POSITION_CONTAINS) {
33        echo "- '{$nodeAName}' が '{$nodeBName}' を含んでいます。\n";
34    }
35
36    if ($result & DOM_POSITION_CONTAINED_BY) {
37        echo "- '{$nodeAName}' が '{$nodeBName}' に含まれています。\n";
38    }
39
40    if ($result & DOM_POSITION_APPEARED_PRECEDING) {
41        echo "- '{$nodeAName}' は '{$nodeBName}' の前(ドキュメント順)にあります。\n";
42    }
43
44    if ($result & DOM_POSITION_APPEARED_FOLLOWING) {
45        echo "- '{$nodeAName}' は '{$nodeBName}' の後(ドキュメント順)にあります。\n";
46    }
47
48    // DOM_POSITION_IMPLEMENTATION_SPECIFIC は通常、具体的な意味を持たないため、ここでは割愛
49    // if ($result & DOM_POSITION_IMPLEMENTATION_SPECIFIC) {
50    //     echo "- 実装固有の位置関係が存在します。\n";
51    // }
52
53    echo "\n";
54}
55
56// 単体で動作可能なコード本体
57// DOMDocumentオブジェクトを作成し、HTMLコンテンツを読み込む
58$dom = new DOMDocument();
59
60// libxml_use_internal_errors を使用して、無効なHTMLによる警告を抑制
61// これにより、サンプルコードの出力がクリーンになります。
62libxml_use_internal_errors(true);
63$html = '
64<!DOCTYPE html>
65<html>
66<head>
67    <title>DOMNode Position Example</title>
68</head>
69<body>
70    <div id="container">
71        <p id="first-paragraph">これは最初の段落です。</p>
72        <span id="span-element">これはスパン要素です。</span>
73        <p id="second-paragraph">これは2番目の段落です。</p>
74    </div>
75    <div id="another-container">
76        <a href="#" id="link-element">リンク</a>
77    </div>
78    <div id="empty-div"></div>
79</body>
80</html>
81';
82$dom->loadHTML($html);
83libxml_use_internal_errors(false); // 抑制を解除
84
85// 比較対象となるDOMノードをIDで取得
86$container = $dom->getElementById('container');
87$firstParagraph = $dom->getElementById('first-paragraph');
88$secondParagraph = $dom->getElementById('second-paragraph');
89$spanElement = $dom->getElementById('span-element');
90$linkElement = $dom->getElementById('link-element');
91$body = $dom->getElementsByTagName('body')->item(0); // body要素を取得
92$emptyDiv = $dom->getElementById('empty-div');
93
94// さまざまなノード間の位置関係を比較し、結果を表示
95if ($container && $firstParagraph) {
96    // 例1: 親ノードと子ノード
97    displayNodePositionComparison($container, $firstParagraph);
98}
99
100if ($firstParagraph && $container) {
101    // 例2: 子ノードと親ノード(逆方向)
102    displayNodePositionComparison($firstParagraph, $container);
103}
104
105if ($firstParagraph && $spanElement) {
106    // 例3: 兄弟ノード(ドキュメント順)
107    displayNodePositionComparison($firstParagraph, $spanElement);
108}
109
110if ($spanElement && $firstParagraph) {
111    // 例4: 兄弟ノード(ドキュメント逆順)
112    displayNodePositionComparison($spanElement, $firstParagraph);
113}
114
115if ($firstParagraph && $secondParagraph) {
116    // 例5: 同じ階層だが、間に他のノードがある場合
117    displayNodePositionComparison($firstParagraph, $secondParagraph);
118}
119
120if ($firstParagraph && $linkElement) {
121    // 例6: 異なる親を持つノード
122    displayNodePositionComparison($firstParagraph, $linkElement);
123}
124
125if ($body && $container) {
126    // 例7: 祖先ノードと子孫ノード
127    displayNodePositionComparison($body, $container);
128}
129
130if ($emptyDiv && $firstParagraph) {
131    // 例8: ドキュメント順でずっと後にあるノード
132    displayNodePositionComparison($emptyDiv, $firstParagraph);
133}

このPHPサンプルコードは、DOMツリー内における2つのノードの相対的な位置を比較するDOMNode::compareDocumentPositionメソッドの使い方を示しています。このメソッドは、HTMLなどの構造化された文書で、ある要素が別の要素の親であるか、子であるか、前にあるか、後ろにあるか、あるいは全く関係ない位置にあるかなどを判定する際に利用されます。

引数$otherには、比較したいもう一方のDOMNodeオブジェクトを渡します。戻り値は整数値で、これは複数の位置関係を示す定数(例: DOM_POSITION_CONTAINSDOM_POSITION_APPEARED_PRECEDINGなど)を組み合わせたビットマスク形式で返されます。サンプルコードでは、この戻り値を各定数と比較することで、具体的な位置関係を分かりやすく表示しています。

コードにはPHPDocコメントが記述されており、これはphpdocumentorのようなツールを用いて、コードから自動的にドキュメントを生成する際の標準的な記述方法です。これらのコメントは、引数や戻り値の意味、関数の概要などを効率的に伝えるための「オプション」として機能します。このメソッドを活用することで、DOM操作において要素間の複雑な位置関係を正確に把握し、より高度な処理を実装できるようになります。

DOMNode::compareDocumentPositionは、二つのDOMノード間の位置関係をビットマスク形式の整数で返します。複数の位置関係が同時に成立しうるため、戻り値を各定数と比較する際は、必ず&(ビットAND演算子)を使ってビットが立っているか確認してください。DOM_POSITION_DISCONNECTEDは他の状態と排他的な特別な値です。サンプルコードのようにノードを取得する際、getElementByIdなどで要素が見つからない場合はnullが返されるため、比較処理の前に必ずノードの存在チェックを行ってください。libxml_use_internal_errorsによるエラー抑制は、開発時の利便性向上に役立ちますが、本番運用では潜在的な問題を早期に発見し、適切にエラー処理を行う設計を検討することが重要です。

関連コンテンツ

関連IT用語

関連プログラミング言語