Passed
Push — master ( fa8b55...986d5c )
by smiley
01:43
created

Document   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Test Coverage

Coverage 97.94%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 58
eloc 111
c 3
b 0
f 0
dl 0
loc 335
ccs 95
cts 97
cp 0.9794
rs 4.5599

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
A loadDocumentFile() 0 12 3
B recursivelyFind() 0 18 7
A getElementById() 0 7 2
A inspect() 0 7 2
A recursivelyCollect() 0 22 5
A loadDocumentString() 0 12 3
A loadDocument() 0 19 6
A insertNodeList() 0 7 2
A newElement() 0 9 2
A removeElementsBySelector() 0 12 3
A match() 0 11 3
A selector2xpath() 0 2 1
A query() 0 4 2
A select() 0 21 5
B toNodeList() 0 22 7
A getElementsByClassName() 0 2 1
A querySelectorAll() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Document often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Document, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Class Document
4
 *
5
 * @filesource   Document.php
6
 * @created      05.05.2017
7
 * @package      chillerlan\PrototypeDOM
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2017 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\PrototypeDOM;
14
15
use chillerlan\PrototypeDOM\Node\{
16
	Attr, CdataSection, CharacterData, Comment, DocumentFragment, DocumentType, Element, Entity,
17
	EntityReference, Node, Notation, ProcessingInstruction, PrototypeHTMLElement, PrototypeNode, Text,
18
};
19
use DOMDocument, DOMException, DOMNode, DOMNodeList, DOMXPath;
20
use Symfony\Component\CssSelector\CssSelectorConverter;
21
22
use function count, in_array, is_file, is_iterable, is_readable, is_string;
23
24
use const LIBXML_COMPACT, LIBXML_HTML_NODEFDTD, LIBXML_HTML_NOIMPLIED, LIBXML_NOERROR, LIBXML_NONET, XML_ELEMENT_NODE;
25
26
/**
27
 *
28
 */
29
class Document extends DOMDocument{
30
31
	protected const NODE_CLASSES = [
32
		'DOMAttr'                  => Attr::class,
33
		'DOMCdataSection'          => CdataSection::class,
34
		'DOMCharacterData'         => CharacterData::class,
35
		'DOMComment'               => Comment::class,
36
		'DOMDocumentFragment'      => DocumentFragment::class,
37
		'DOMDocumentType'          => DocumentType::class,
38
		'DOMElement'               => Element::class,
39
		'DOMEntity'                => Entity::class,
40
		'DOMEntityReference'       => EntityReference::class,
41
		'DOMNode'                  => Node::class,
42
		'DOMNotation'              => Notation::class,
43
		'DOMProcessingInstruction' => ProcessingInstruction::class,
44 42
		'DOMText'                  => Text::class,
45 42
	];
46
47 42
	protected const LOAD_OPTIONS = LIBXML_COMPACT | LIBXML_NONET | LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR;
48 42
49
	protected CssSelectorConverter $cssSelectorConverter;
50
51 42
	/**
52 2
	 * Document constructor.
53
	 *
54
	 * @param string|\DOMNodeList|\chillerlan\PrototypeDOM\NodeList $content
55 42
	 * @param bool|null                                             $xml
56
	 * @param string|null                                           $version
57
	 * @param string|null                                           $encoding
58
	 */
59
	public function __construct($content = null, bool $xml = null, string $version = null, string $encoding = null){
60
		parent::__construct($version ?? '1.0', $encoding ?? 'UTF-8');
61
62 1
		foreach($this::NODE_CLASSES as $baseClass => $extendedClass){
63 1
			$this->registerNodeClass($baseClass, $extendedClass);
64
		}
65
66 1
		if($content !== null){
67 1
			$this->loadDocument($content, $xml);
68
		}
69 1
70 1
		$this->cssSelectorConverter = new CssSelectorConverter;
71
	}
72
73 1
	/**
74 1
	 * @param string|\DOMNodeList|\chillerlan\PrototypeDOM\NodeList $content
75
	 * @param bool|null $xml
76 1
	 *
77
	 * @return \chillerlan\PrototypeDOM\Document
78
	 * @throws \DOMException
79
	 */
80 1
	public function loadDocument($content, bool $xml = null):Document{
81
82 1
		if($content instanceof NodeList){
83
			return $this->insertNodeList($content);
84
		}
85
86
		if($content instanceof DOMNodeList){
87
			return $this->insertNodeList(new NodeList($content));
88
		}
89
90 2
		if(!is_string($content)){
0 ignored issues
show
introduced by
The condition is_string($content) is always true.
Loading history...
91
			throw new DOMException('invalid document content');
92
		}
93 2
94 1
		if(is_file($content) && is_readable($content)){
95 1
			return $this->loadDocumentFile($content, $xml);
96
		}
97
98
		return $this->loadDocumentString($content, $xml);
99
	}
100 1
101 1
	/**
102
	 * @throws \DOMException
103 1
	 */
104 1
	public function loadDocumentFile(string $file, bool $xml = null, int $options = null):Document{
105 1
		$options = $options ?? $this::LOAD_OPTIONS;
106
107 1
		$result = $xml === true
108
			? $this->load($file, $options)
109
			: $this->loadHTMLFile($file, $options);
110
111
		if($result === false){
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
112
			throw new DOMException('failed to load document from file: '.$file); // @codeCoverageIgnore
113
		}
114
115
		return $this;
116 8
	}
117
118
	/**
119 8
	 * @throws \DOMException
120 8
	 */
121 7
	public function loadDocumentString(string $documentSource, bool $xml = null, int $options = null):Document{
122 7
		$options = $options ?? $this::LOAD_OPTIONS;
123 7
124
		$result = $xml === true
125
			? $this->loadXML($documentSource, $options)
126
			: $this->loadHTML($documentSource, $options);
127
128
		if($result === false){
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
129
			throw new DOMException('failed to load document from string'); // @codeCoverageIgnore
130
		}
131
132
		return $this;
133
	}
134
135 8
	/**
136 8
	 * @param \chillerlan\PrototypeDOM\NodeList|\chillerlan\PrototypeDOM\Node\PrototypeNode|DOMNodeList|DOMNode|string $content
137 8
	 *
138
	 * @throws \DOMException
139 8
	 */
140
	public function toNodeList($content):NodeList{
141
142
		if($content instanceof NodeList){
143
			return $content;
144
		}
145
146
		if($content instanceof DOMNode || $content instanceof PrototypeNode){
147 3
			return new NodeList([$content]);
148 3
		}
149
150 3
		if($content instanceof DOMNodeList || is_iterable($content)){
151 3
			return new NodeList($content);
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type string; however, parameter $nodes of chillerlan\PrototypeDOM\NodeList::__construct() does only seem to accept iterable|null, 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

151
			return new NodeList(/** @scrutinizer ignore-type */ $content);
Loading history...
152
		}
153
154 3
		if(is_string($content)){
0 ignored issues
show
introduced by
The condition is_string($content) is always true.
Loading history...
155
			$document = new self;
156
			$document->loadHTML('<html lang="en"><body id="-import-content">'.$content.'</body></html>');
157
158
			return $document->toNodeList($document->getElementById('-import-content')->childNodes);
159
		}
160
161
		throw new DOMException('invalid content');
162
	}
163
164
	/***********
165
	 * generic *
166
	 ***********/
167
168 41
	/**
169 41
	 *
170
	 */
171
	public function selector2xpath(string $selector, string $axis = null):string{
172
		return $this->cssSelectorConverter->toXPath($selector, $axis ?? '//');
173
	}
174
175
	/**
176
	 *
177
	 */
178 41
	public function query(string $xpath, DOMNode $contextNode = null):?NodeList{
179 41
		$q = (new DOMXPath($this))->query($xpath, $contextNode);
180
181
		return $q !== false ? new NodeList($q) : null;
182
	}
183
184
	/**
185
	 *
186
	 */
187
	public function querySelectorAll(string $selector, DOMNode $contextNode = null, string $axis = null):?NodeList{
188
		return $this->query($this->cssSelectorConverter->toXPath($selector, $axis ?? 'descendant-or-self::'), $contextNode);
189 41
	}
190 41
191
	/**
192
	 *
193
	 */
194
	public function removeElementsBySelector(array $selectors, DOMNode $contextNode = null, string $axis = null):Document{
195
		$nodes = $this->select($selectors, $contextNode, $axis ?? 'descendant-or-self::');
196
197
		if(count($nodes) > 0){
198
199
			foreach($nodes as $node){
200 1
				$node->removeNode();
0 ignored issues
show
Bug introduced by
The method removeNode() does not exist on null. ( Ignorable by Annotation )

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

200
				$node->/** @scrutinizer ignore-call */ 
201
           removeNode();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
201 1
			}
202
203 1
		}
204
205 1
		return $this;
206 1
	}
207
208
	/**
209
	 *
210
	 */
211 1
	public function insertNodeList(NodeList $nodeList):Document{
212
213
		foreach($nodeList as $node){
214
			$this->appendChild($this->importNode($node->cloneNode(true), true));
215
		}
216
217
		return $this;
218
	}
219 2
220
	/**
221
	 * @inheritDoc
222 2
	 *
223 2
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeHTMLElement|\DOMNode|null
224
	 * @throws \DOMException
225
	 */
226 2
	public function getElementById($elementId):?DOMNode{
227
228
		if(!is_string($elementId)){
229
			throw new DOMException('invalid element id');
230
		}
231
232
		return $this->select(['#'.$elementId])[0] ?? null;
233
	}
234
235
	/**
236
	 *
237
	 */
238
	public function getElementsByClassName(string $className):NodeList{
239
		return $this->select(['.'.$className]);
240
	}
241 8
242 8
	/*************
243 2
	 * prototype *
244 8
	 *************/
245
246
	/**
247
	 * @link http://api.prototypejs.org/dom/Element/inspect/
248
	 */
249
	public function inspect(DOMNode $context = null, bool $xml = null):string{
250
251
		if($xml === true){
252
			return $this->saveXML($context);
253
		}
254
255
		return $this->saveHTML($context);
256
	}
257 41
258
	/**
259 41
	 * @link http://api.prototypejs.org/dom/Element/select/
260 41
	 * @see https://secure.php.net/manual/dom.constants.php
261
	 */
262
	public function select(array $selectors = null, DOMNode $contextNode = null, string $axis = null, int $nodeType = null):NodeList{
263 41
		$nodeType = $nodeType ?? XML_ELEMENT_NODE;
264 2
		$elements = new NodeList;
265
266
		foreach($selectors ?? ['*'] as $selector){
267 41
268
			if(!is_string($selector)){
269 41
				continue;
270
			}
271 41
272
			foreach($this->querySelectorAll($selector, $contextNode, $axis ?? 'descendant-or-self::') as $element){
273 41
274 41
				if($element->nodeType === $nodeType){
275
					$elements[] = $element;
276
				}
277
278
			}
279
280
		}
281 41
282
		return $elements;
283
	}
284
285
	/**
286
	 * @see http://api.prototypejs.org/dom/Element/recursivelyCollect/
287
	 * @see https://secure.php.net/manual/dom.constants.php
288
	 */
289
	public function recursivelyCollect(DOMNode $element, string $property, int $maxLength = null, int $nodeType = null):NodeList{
290
		$nodeType  = $nodeType ?? XML_ELEMENT_NODE;
291
		$maxLength = $maxLength ?? -1;
292
		$nodes     = new NodeList;
293
294 3
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
295 3
296
			while($element = $element->{$property}){
297 3
298
				if($element->nodeType === $nodeType){
299 3
					$nodes[] = $element;
300
				}
301 3
302 3
				if(count($nodes) === $maxLength){
303
					break;
304
				}
305 3
306 2
			}
307
308
		}
309
310
		return $nodes;
311
	}
312
313 3
	/**
314
	 * @see https://secure.php.net/manual/dom.constants.php
315
	 */
316
	public function recursivelyFind(PrototypeNode $element, string $property = null, string $selector = null, int $index = null, int $nodeType = null):?DOMNode{
317
		$nodeType = $nodeType ?? XML_ELEMENT_NODE;
318
		$index    = $index ?? 0;
319
320
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
321
322
			while($element = $element->{$property}){
323
324
				if($element->nodeType !== $nodeType || $selector !== null && !$element->match($selector) || --$index >= 0){
325 4
					continue;
326
				}
327 4
328
				return $element;
329
			}
330 4
331
		}
332 4
333 4
		return null;
334
	}
335
336 4
	/**
337
	 * @link http://api.prototypejs.org/dom/Element/match/
338
	 */
339
	public function match(DOMNode $element, string $selector):bool{
340
341 4
		foreach($this->select([$selector]) as $match){
342
343
			if($element->isSameNode($match)){
0 ignored issues
show
Bug introduced by
It seems like $match can also be of type null; however, parameter $otherNode of DOMNode::isSameNode() 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

343
			if($element->isSameNode(/** @scrutinizer ignore-type */ $match)){
Loading history...
344
				return true;
345
			}
346
347
		}
348
349
		return false;
350
	}
351
352 4
	/**
353
	 * @link http://api.prototypejs.org/dom/Element/new/
354 4
	 */
355
	public function newElement(string $tag, array $attributes = null):PrototypeHTMLElement{
356 4
		/** @var \chillerlan\PrototypeDOM\Node\PrototypeHTMLElement $element */
357 4
		$element = $this->createElement($tag);
358
359
		if($attributes !== null){
360
			$element->setAttributes($attributes);
361
		}
362 4
363
		return $element;
364
	}
365
366
}
367