Document   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 112
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 17
eloc 36
c 2
b 0
f 0
dl 0
loc 112
rs 10
ccs 29
cts 29
cp 1

9 Methods

Rating   Name   Duplication   Size   Complexity  
A evaluate() 0 3 1
A __construct() 0 8 2
A firstOf() 0 3 1
A needsWorkarounds() 0 15 3
A isEqualNode() 0 5 2
A getExtendedClassMap() 0 11 2
A getExtendedNamespace() 0 13 3
A xpath() 0 7 1
A query() 0 11 2
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 26
* @method DocumentFragment createDocumentFragment()
24
* @method Element|false createElement(string $localName, string $value = '')
25 26
* @method Element|false createElementNS(?string $namespace, string $qualifiedName, string $value = '')
26
* @method Text createTextNode(string $data)
27 26
* @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 2
{
37
	public NodeCreator $nodeCreator;
38 2
39 2
	/**
40
	* @link https://www.php.net/manual/domdocument.construct.php
41 1
	*/
42
	public function __construct(string $version = '1.0', string $encoding = '')
43
	{
44 2
		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 2
	* Evaluate and return the result of a given XPath expression
55
	*/
56 2
	public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed
57 2
	{
58
		return $this->xpath('evaluate', func_get_args());
59 2
	}
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 1
	}
68
69 1
	public function isEqualNode(?DOMNode $otherNode): bool
70
	{
71
		return method_exists('DOMDocument', 'isEqualNode')
72
		     ? parent::isEqualNode($otherNode)
73
		     : NodeComparator::isEqualNode($this, $otherNode);
74
	}
75
76
	/**
77
	* Evaluate and return the result of a given XPath query
78 2
	*/
79
	public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): DOMNodeList
80 2
	{
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 1
		return $result;
90
	}
91 1
92 1
	protected function getExtendedClassMap(): array
93
	{
94 1
		$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 2
105
	protected function getExtendedNamespace(string $phpVersion): string
106 2
	{
107 2
		$namespace = __NAMESPACE__;
108
		if ($this->needsWorkarounds($phpVersion))
109 2
		{
110
			$namespace .= '\\PatchedNodes';
111
		}
112
		elseif (version_compare($phpVersion, '8.3.0', '<'))
113
		{
114
			$namespace .= '\\ForwardCompatibleNodes';
115
		}
116
117
		return $namespace;
118 2
	}
119
120 2
	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 2
			// Anything older than 8.1.23 does
130
			return true;
131 2
		}
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', '>=');
135
	}
136
137
	/**
138
	* Execute a DOMXPath method and return the result
139
	*/
140 1
	protected function xpath(string $methodName, array $args): mixed
141
	{
142 1
		$xpath = new DOMXPath($this);
143 1
		$xpath->registerNamespace('xsl', 'http://www.w3.org/1999/XSL/Transform');
144
		$xpath->registerNodeNamespaces = true;
145 1
146
		return $xpath->$methodName(...$args);
147
	}
148
}