Passed
Branch isEqualNode (8a5aae)
by Josh
02:01
created

Document::isEqualNode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 5
rs 10
cc 2
nc 2
nop 1
1
<?php declare(strict_types=1);
2
3
/**
4
* @package   s9e\SweetDOM
5
* @copyright Copyright (c) The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\SweetDOM;
9
10
use DOMDocument;
11
use DOMNode;
12
use DOMNodeList;
13
use DOMXPath;
14
use RuntimeException;
15
use const PHP_VERSION;
16
use function func_get_args, method_exists, libxml_get_last_error, trim, version_compare;
17
18
/**
19
* @method Attr|false createAttribute(string $localName)
20
* @method Attr|false createAttributeNS(?string $namespace, string $qualifiedName)
21
* @method CdataSection|false createCDATASection(string $data)
22
* @method Comment createComment(string $data)
23
* @method DocumentFragment createDocumentFragment()
24
* @method Element|false createElement(string $localName, string $value = '')
25
* @method Element|false createElementNS(?string $namespace, string $qualifiedName, string $value = '')
26
* @method Text createTextNode(string $data)
27
* @method ?Element getElementById(string $elementId)
28
* @property ?DocumentType $doctype
29
* @property ?Element $documentElement
30
* @property ?Element $firstElementChild
31
* @property ?Element $lastElementChild
32
* @property ?Document $ownerDocument
33
* @property ?Element $parentElement
34
*/
35
class Document extends DOMDocument
36
{
37
	public NodeCreator $nodeCreator;
38
39
	/**
40
	* @link https://www.php.net/manual/domdocument.construct.php
41
	*/
42
	public function __construct(string $version = '1.0', string $encoding = '')
43
	{
44
		parent::__construct($version, $encoding);
45
46
		$this->nodeCreator = new NodeCreator($this);
47
		foreach ($this->getExtendedClassMap() as $baseClass => $extendedClass)
48
		{
49
			$this->registerNodeClass($baseClass, $extendedClass);
50
		}
51
	}
52
53
	/**
54
	* Evaluate and return the result of a given XPath expression
55
	*/
56
	public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed
57
	{
58
		return $this->xpath('evaluate', func_get_args());
59
	}
60
61
	/**
62
	* Evaluate and return the first element of a given XPath query
63
	*/
64
	public function firstOf(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): ?DOMNode
65
	{
66
		return $this->query(...func_get_args())->item(0);
67
	}
68
69
	public function isEqualNode(?DOMNode $otherNode): bool
70
	{
71
		return method_exists('DOMDocument', 'isEqualNode')
72
		     ? parent::isEqualNode($otherNode)
1 ignored issue
show
Bug introduced by
It seems like $otherNode can also be of type null; however, parameter $arg of DOMNode::isEqualNode() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

72
		     ? parent::isEqualNode(/** @scrutinizer ignore-type */ $otherNode)
Loading history...
73
		     : NodeComparator::isEqualNode($this, $otherNode);
74
	}
75
76
	/**
77
	* Evaluate and return the result of a given XPath query
78
	*/
79
	public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): DOMNodeList
80
	{
81
		$result = $this->xpath('query', func_get_args());
82
		if ($result === false)
83
		{
84
			$errorMessage = libxml_get_last_error()?->message ?? 'No error message';
85
86
			throw new RuntimeException('Invalid XPath query: ' . trim($errorMessage));
87
		}
88
89
		return $result;
90
	}
91
92
	protected function getExtendedClassMap(): array
93
	{
94
		$baseNames = ['Attr', 'CdataSection', 'Comment', 'DocumentFragment', 'Element', 'Text'];
95
		$classMap  = [];
96
		$namespace = $this->getExtendedNamespace(PHP_VERSION);
97
		foreach ($baseNames as $baseName)
98
		{
99
			$classMap['DOM' . $baseName] = $namespace . '\\' . $baseName;
100
		}
101
102
		return $classMap;
103
	}
104
105
	protected function getExtendedNamespace(string $phpVersion): string
106
	{
107
		$namespace = __NAMESPACE__;
108
		if ($this->needsWorkarounds($phpVersion))
109
		{
110
			$namespace .= '\\PatchedNodes';
111
		}
112
		elseif (version_compare($phpVersion, '8.3.0', '<'))
113
		{
114
			$namespace .= '\\ForwardCompatibleNodes';
115
		}
116
117
		return $namespace;
118
	}
119
120
	protected function needsWorkarounds(string $phpVersion): bool
121
	{
122
		if (version_compare($phpVersion, '8.2.10', '>='))
123
		{
124
			// PHP ^8.2.10 needs no workarounds
125
			return false;
126
		}
127
		if (version_compare($phpVersion, '8.1.23', '<'))
128
		{
129
			// Anything older than 8.1.23 does
130
			return true;
131
		}
132
133
		// ~8.1.23 is okay, anything between 8.2.0 and 8.2.9 needs workarounds
134
		return version_compare($phpVersion, '8.2.0-dev', '>=');
1 ignored issue
show
Bug Best Practice introduced by
The expression return version_compare($...ion, '8.2.0-dev', '>=') could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
135
	}
136
137
	/**
138
	* Execute a DOMXPath method and return the result
139
	*/
140
	protected function xpath(string $methodName, array $args): mixed
141
	{
142
		$xpath = new DOMXPath($this);
143
		$xpath->registerNamespace('xsl', 'http://www.w3.org/1999/XSL/Transform');
144
		$xpath->registerNodeNamespaces = true;
145
146
		return $xpath->$methodName(...$args);
147
	}
148
}