Test Failed
Push — master ( 9cf31c...ad793d )
by smiley
03:41
created

Document   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.04%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 5
dl 0
loc 362
ccs 100
cts 102
cp 0.9804
rs 6.5957
c 0
b 0
f 0

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
};
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '}', expecting T_STRING or T_FUNCTION or T_CONST
Loading history...
19
use chillerlan\Traits\Magic;
20
use DOMDocument, DOMException, DOMNode, DOMNodeList, DOMXPath;
21
use Symfony\Component\CssSelector\CssSelectorConverter;
22
23
/**
24
 * @property string $title
25
 */
26
class Document extends DOMDocument{
27
	use Magic;
28
29
	protected const NODE_CLASSES = [
30
		'DOMAttr'                  => Attr::class,
31
		'DOMCdataSection'          => CdataSection::class,
32
		'DOMCharacterData'         => CharacterData::class,
33
		'DOMComment'               => Comment::class,
34
		'DOMDocumentFragment'      => DocumentFragment::class,
35
		'DOMDocumentType'          => DocumentType::class,
36
		'DOMElement'               => Element::class,
37
		'DOMEntity'                => Entity::class,
38
		'DOMEntityReference'       => EntityReference::class,
39
		'DOMNode'                  => Node::class,
40
		'DOMNotation'              => Notation::class,
41
		'DOMProcessingInstruction' => ProcessingInstruction::class,
42
		'DOMText'                  => Text::class,
43
	];
44 42
45 42
	protected const LOAD_OPTIONS = \LIBXML_COMPACT|\LIBXML_NONET|\LIBXML_HTML_NODEFDTD|\LIBXML_HTML_NOIMPLIED|\LIBXML_NOERROR;
46
47 42
	/**
48 42
	 * @var \Symfony\Component\CssSelector\CssSelectorConverter
49
	 */
50
	protected $cssSelectorConverter;
51 42
52 2
	/**
53
	 * Document constructor.
54
	 *
55 42
	 * @param \chillerlan\PrototypeDOM\NodeList|\DOMNodeList|string|null $content
56
	 * @param bool|null                                                  $xml
57
	 * @param string|null                                                $version
58
	 * @param string|null                                                $encoding
59
	 */
60
	public function __construct($content = null, bool $xml = null, string $version = null, string $encoding = null){
61
		parent::__construct($version ?? '1.0', $encoding ?? 'UTF-8');
62 1
63 1
		foreach($this::NODE_CLASSES as $baseClass => $extendedClass){
64
			$this->registerNodeClass($baseClass, $extendedClass);
65
		}
66 1
67 1
		if($content !== null){
68
			$this->loadDocument($content, $xml);
69 1
		}
70 1
71
		$this->cssSelectorConverter = new CssSelectorConverter;
72
	}
73 1
74 1
75
	/*********
76 1
	 * magic *
77
	 *********/
78
79
	public function magic_get_title():?string{
80 1
		return $this->select(['head > title'])->item(0)->nodeValue ?? null;
81
	}
82 1
83
	/**
84
	 * @param string $title
85
	 *
86
	 * @throws \DOMException
87
	 */
88
	public function magic_set_title(string $title):void{
89
		$currentTitle = $this->select(['head > title'])->item(0);
90 2
91
		if($currentTitle instanceof Element){
92
			$currentTitle->update($title);
93 2
			return;
94 1
		}
95 1
96
		$head         = $this->select(['head'])->item(0);
97
		$currentTitle = $this->newElement('title')->update($title);
98
99
		if(!$head){
100 1
			$html = $this->select(['html'])->first();
101 1
102
			if(!$html instanceof PrototypeHTMLElement){
103 1
				throw new DOMException('<html> header missing');
104 1
			}
105 1
106
			$head = $this->newElement('head');
107 1
			$html->insert_top($head);
108
		}
109
110
		$head->insert($currentTitle);
111
	}
112
113
	/**
114
	 * @param \chillerlan\PrototypeDOM\NodeList|\DOMNodeList|string $content
115
	 * @param bool                                                  $xml
116 8
	 *
117
	 * @return \chillerlan\PrototypeDOM\Document
118
	 * @throws \DOMException
119 8
	 */
120 8
121 7
	public function loadDocument($content, bool $xml = null):Document{
122 7
123 7
		if($content instanceof NodeList){
124
			return $this->insertNodeList($content);
125
		}
126
127
		if($content instanceof DOMNodeList){
128
			return $this->insertNodeList(new NodeList($content));
129
		}
130
131
		if(!\is_string($content)){
132
			throw new DOMException('invalid document content');
133
		}
134
135 8
		if(\is_file($content) && \is_readable($content)){
136 8
			return $this->loadDocumentFile($content, $xml);
137 8
		}
138
139 8
		return $this->loadDocumentString($content, $xml);
140
	}
141
142
	/**
143
	 * @param string    $file
144
	 * @param bool|null $xml
145
	 * @param int|null  $options
146
	 *
147 3
	 * @return \chillerlan\PrototypeDOM\Document
148 3
	 * @throws \DOMException
149
	 */
150 3
	public function loadDocumentFile(string $file, bool $xml = null, int $options = null):Document{
151 3
		$options = $options ?? $this::LOAD_OPTIONS;
152
153
		$result = $xml === true
154 3
			? $this->load($file, $options)
155
			: $this->loadHTMLFile($file, $options);
156
157
		if($result === false){
158
			throw new DOMException('failed to load document from file: '.$file); // @codeCoverageIgnore
159
		}
160
161
		return $this;
162
	}
163
164
	/**
165
	 * @param string   $documentSource
166
	 * @param bool     $xml
167
	 * @param int|null $options
168 41
	 *
169 41
	 * @return \chillerlan\PrototypeDOM\Document
170
	 * @throws \DOMException
171
	 */
172
	public function loadDocumentString(string $documentSource, bool $xml = null, int $options = null):Document{
173
		$options = $options ?? $this::LOAD_OPTIONS;
174
175
		$result = $xml === true
176
			? $this->loadXML($documentSource, $options)
177
			: $this->loadHTML($documentSource, $options);
178 41
179 41
		if($result === false){
180
			throw new DOMException('failed to load document from string'); // @codeCoverageIgnore
181
		}
182
183
		return $this;
184
	}
185
186
	/**
187
	 * @param mixed $content
188
	 *
189 41
	 * @return \chillerlan\PrototypeDOM\NodeList
190 41
	 * @throws \Exception
191
	 */
192
	public function toNodeList($content):NodeList{
193
194
		if($content instanceof NodeList){
195
			return $content;
196
		}
197
198
		if($content instanceof DOMNode || $content instanceof PrototypeNode){
199
			return new NodeList([$content]);
200 1
		}
201 1
202
		if($content instanceof DOMNodeList || \is_iterable($content)){
203 1
			return new NodeList($content);
204
		}
205 1
206 1
		if(\is_string($content)){
207
			$document = new self;
208
			$document->loadHTML('<html><body id="-import-content">'.$content.'</body></html>');
209
210
			return $document->toNodeList($document->getElementById('-import-content')->childNodes);
211 1
		}
212
213
		throw new DOMException('invalid content');
214
	}
215
216
	/***********
217
	 * generic *
218
	 ***********/
219 2
220
	/**
221
	 * @param string $selector
222 2
	 * @param string $axis
223 2
	 *
224
	 * @return string
225
	 */
226 2
	public function selector2xpath(string $selector, string $axis = null):string{
227
		return $this->cssSelectorConverter->toXPath($selector, $axis ?? '//');
228
	}
229
230
	/**
231
	 * @param string        $xpath
232
	 * @param \DOMNode|null $contextNode
233
	 *
234
	 * @return \chillerlan\PrototypeDOM\NodeList|null
235
	 */
236
	public function query(string $xpath, DOMNode $contextNode = null):?NodeList{
237
		$q = (new DOMXPath($this))->query($xpath, $contextNode);
238
239
		return $q !== false ? new NodeList($q) : null;
240
	}
241 8
242 8
	/**
243 2
	 * @param string        $selector
244 8
	 * @param \DOMNode|null $contextNode
245
	 * @param string        $axis
246
	 *
247
	 * @return \chillerlan\PrototypeDOM\NodeList|null
248
	 */
249
	public function querySelectorAll(string $selector, DOMNode $contextNode = null, string $axis = null):?NodeList{
250
		return $this->query($this->cssSelectorConverter->toXPath($selector, $axis ?? 'descendant-or-self::'), $contextNode);
251
	}
252
253
	/**
254
	 * @param string|array  $selectors
255
	 * @param \DOMNode|null $contextNode
256
	 * @param string        $axis
257 41
	 *
258
	 * @return \chillerlan\PrototypeDOM\Document
259 41
	 */
260 41
	public function removeElementsBySelector($selectors, DOMNode $contextNode = null, string $axis = null):Document{
261
		/** @var \chillerlan\PrototypeDOM\NodeList $nodes */
262
		$nodes = $this->select($selectors, $contextNode, $axis ?? 'descendant-or-self::');
263 41
264 2
		if(\count($nodes) > 0){
265
266
			foreach($nodes as $node){
267 41
				$node->remove();
268
			}
269 41
270
		}
271 41
272
		return $this;
273 41
	}
274 41
275
	/**
276
	 * @param \chillerlan\PrototypeDOM\NodeList $nodeList
277
	 *
278
	 * @return \chillerlan\PrototypeDOM\Document
279
	 */
280
	public function insertNodeList(NodeList $nodeList):Document{
281 41
282
		/** @var \DOMNode $node */
283
		foreach($nodeList as $node){
284
			$this->appendChild($this->importNode($node->cloneNode(true), true));
285
		}
286
287
		return $this;
288
	}
289
290
	/*************
291
	 * prototype *
292
	 *************/
293
294 3
	/**
295 3
	 * @link http://api.prototypejs.org/dom/Element/inspect/
296
	 *
297 3
	 * @param \DOMNode|null $context
298
	 * @param bool          $xml
299 3
	 *
300
	 * @return string
301 3
	 */
302 3
	public function inspect(DOMNode $context = null, bool $xml = null):string{
303
304
		if($xml === true){
305 3
			return $this->saveXML($context);
306 2
		}
307
308
		return $this->saveHTML($context);
309
	}
310
311
	/**
312
	 * @link http://api.prototypejs.org/dom/Element/select/
313 3
	 *
314
	 * @param string|array  $selectors
315
	 * @param \DOMNode|null $contextNode
316
	 * @param string        $axis
317
	 * @param int           $nodeType https://secure.php.net/manual/dom.constants.php
318
	 *
319
	 * @return \chillerlan\PrototypeDOM\NodeList
320
	 */
321
	public function select(array $selectors = null, DOMNode $contextNode = null, string $axis = null, int $nodeType = \XML_ELEMENT_NODE):NodeList{
322
		$elements = new NodeList;
323
324
		foreach($selectors ?? ['*'] as $selector){
325 4
326
			if(!\is_string($selector)){
327 4
				continue;
328
			}
329
330 4
			foreach($this->querySelectorAll($selector, $contextNode, $axis ?? 'descendant-or-self::') as $element){
331
332 4
				if($element->nodeType === $nodeType){
333 4
					$elements[] = $element;
334
				}
335
336 4
			}
337
338
		}
339
340
		return $elements;
341 4
	}
342
343
	/**
344
	 * @link http://api.prototypejs.org/dom/Element/recursivelyCollect/
345
	 *
346
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
347
	 * @param string                                      $property
348
	 * @param int                                         $maxLength
349
	 * @param int                                         $nodeType https://secure.php.net/manual/dom.constants.php
350
	 *
351
	 * @return \chillerlan\PrototypeDOM\NodeList
352 4
	 */
353
	public function recursivelyCollect(PrototypeNode $element, string $property, int $maxLength = -1, int $nodeType = \XML_ELEMENT_NODE):NodeList{
354 4
		$nodes = new NodeList;
355
356 4
		if(\in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
357 4
358
			while($element = $element->{$property}){
359
360
				if($element->nodeType === $nodeType){
361
					$nodes[] = $element;
362 4
				}
363
364
				if(\count($nodes) === $maxLength){
365
					break;
366
				}
367
368
			}
369
370
		}
371
372
		return $nodes;
373 6
	}
374
375 6
	/**
376
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
377 6
	 * @param string                                      $property
378 4
	 * @param string|null                                 $selector
379
	 * @param int                                         $index
380
	 * @param int                                         $nodeType https://secure.php.net/manual/dom.constants.php
381 6
	 *
382
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeNode|null
383
	 */
384
	public function recursivelyFind(PrototypeNode $element, string $property = null, string $selector = null, int $index = 0, int $nodeType = \XML_ELEMENT_NODE):?PrototypeNode{
385
386
		if(\in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
387
388
			/** @var \chillerlan\PrototypeDOM\Node\Element $element */
389
			while($element = $element->{$property}){
390
391
				if($element->nodeType !== $nodeType || $selector !== null && !$element->match($selector) || --$index >= 0){
392
					continue;
393
				}
394
395
				return $element;
396
			}
397
398
		}
399
400
		return null;
401
	}
402
403
	/**
404
	 * @link http://api.prototypejs.org/dom/Element/match/
405
	 *
406
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode|\DOMNode $element
407
	 * @param string                                               $selector
408
	 *
409
	 * @return bool
410
	 */
411
	public function match(PrototypeNode $element, string $selector):bool{
412
413
		/** @var \chillerlan\PrototypeDOM\Node\Element $match */
414
		foreach($this->select([$selector]) as $match){
415
416
			if($element->isSameNode($match)){
417
				return true;
418
			}
419
420
		}
421
422
		return false;
423
	}
424
425
	/**
426
	 * @link http://api.prototypejs.org/dom/Element/new/
427
	 *
428
	 * @param string     $tag
429
	 * @param array|null $attributes
430
	 *
431
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeHTMLElement
432
	 */
433
	public function newElement(string $tag, array $attributes = null):PrototypeHTMLElement{
434
		/** @var \chillerlan\PrototypeDOM\Node\Element $element */
435
		$element = $this->createElement($tag);
436
437
		if($attributes !== null){
438
			$element->setAttributes($attributes);
439
		}
440
441
		return $element;
442
	}
443
444
	/**
445
	 * @param string $elementId
446
	 *
447
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeNode|null
448
	 * @throws \DOMException
449
	 */
450
	public function getElementById($elementId):?PrototypeNode{
451
452
		if(!\is_string($elementId)){
453
			throw new DOMException('invalid element id');
454
		}
455
456
		return $this->select(['#'.$elementId])[0] ?? null;
457
	}
458
459
}
460