Completed
Push — master ( d8ec9e...ff7676 )
by smiley
02:32
created

Document   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.06%

Importance

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

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
A magic_get_title() 0 3 1
A magic_set_title() 0 17 3
A _loadDocument() 0 9 4
A _loadDocumentString() 0 9 2
B _toNodeList() 0 13 6
A _HTMLFragmentToNodeList() 0 6 1
A _arrayToNodeList() 0 9 2
A selector2xpath() 0 3 1
A query() 0 3 1
A querySelectorAll() 0 3 1
A removeElementsBySelector() 0 13 3
A insertNodeList() 0 9 2
A inspect() 0 5 2
C select() 0 26 7
B recursivelyCollect() 0 21 5
B _recursivelyFind() 0 18 7
A match() 0 12 3
A newElement() 0 10 2

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 DOMDocument, DOMNode, DOMNodeList, DOMXPath;
16
use chillerlan\PrototypeDOM\Node\{Element, PrototypeNode};
17
use chillerlan\PrototypeDOM\Traits\Magic;
18
use Symfony\Component\CssSelector\CssSelectorConverter;
19
20
/**
21
 * @property string $title
22
 */
23
class Document extends DOMDocument{
24
	use Magic;
25
26
	const NODE_CLASSES = [
27
		'Attr',
28
		'CharacterData',
29
		'Comment',
30
		'DocumentFragment',
31
		'DocumentType',
32
		'Element',
33
		'Text',
34
	];
35
36
	/**
37
	 * Document constructor.
38
	 *
39
	 * @param string      $content
40
	 * @param bool        $xml
41
	 * @param string|null $version
42
	 * @param string|null $encoding
43
	 */
44 42
	public function __construct($content = null, $xml = false, $version = '1.0', $encoding = 'UTF-8'){
45 42
		parent::__construct($version, $encoding);
46
47 42
		foreach(self::NODE_CLASSES as $nodeClass){
48 42
			$this->registerNodeClass('DOM'.$nodeClass, __NAMESPACE__.'\\Node\\'.$nodeClass);
49
		}
50
51 42
		if(!is_null($content)){
52 2
			$this->_loadDocument($content, $xml);
53
		}
54
55 42
	}
56
57
58
	/*********
59
	 * magic *
60
	 *********/
61
62 1
	public function magic_get_title(){
63 1
		return $this->select('head > title')->item(0)->nodeValue ?? null;
64
	}
65
66 1
	public function magic_set_title(string $title){
67 1
		$currentTitle = $this->select('head > title')->item(0);
68
69 1
		if($currentTitle instanceof Element){
70 1
			$currentTitle->update($title);
71
		}
72
		else{
73 1
			$head         = $this->select('head')->item(0);
74 1
			$currentTitle = $this->newElement('title')->update($title);
75
76 1
			if(!$head){
77
				$head = $this->appendChild($this->newElement('head'));
78
			}
79
80 1
			$head->insert($currentTitle);
0 ignored issues
show
Bug introduced by
The method insert() does not exist on DOMNode. Did you maybe mean insertBefore()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
81
		}
82 1
	}
83
84
85
86
	/********
87
	 * ugly *
88
	 ********/
89
90 2
	public function _loadDocument($content, $xml = false){
91
92
		switch(true){
93 2
			case $content instanceof NodeList   : return $this->insertNodeList($content);
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
94
			case $content instanceof DOMNodeList: return $this->insertNodeList(new NodeList($content));
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
95 1
			case is_string($content)            : return $this->_loadDocumentString($content, $xml);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
96
			default: return $this;
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
97
		}
98
	}
99
100 1
	public function _loadDocumentString(string $documentSource, bool $xml = false){
101 1
		$options = LIBXML_COMPACT|LIBXML_NONET;
102
103 1
		$xml
104 1
			? $this->loadXML($documentSource, $options)
105 1
			: $this->loadHTML($documentSource, $options);
106
107 1
		return $this;
108
	}
109
110
	/**
111
	 * @param mixed $content
112
	 *
113
	 * @return \chillerlan\PrototypeDOM\NodeList
114
	 * @throws \Exception
115
	 */
116 8
	public function _toNodeList($content):NodeList{
117
118
		switch(true){
119 8
			case $content instanceof NodeList   : return $content;
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
120 8
			case $content instanceof DOMNodeList: return new NodeList($content);
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
121 3
			case $content instanceof DOMNode    : return $this->_arrayToNodeList([$content]);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
122 7
			case is_array($content)             : return $this->_arrayToNodeList($content);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
123 7
			case is_string($content)            : return $this->_HTMLFragmentToNodeList($content);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
124
			default:
125
				throw new \Exception('invalid content'); // @codeCoverageIgnore
126
		}
127
128
	}
129
130
	/**
131
	 * @param string $content
132
	 *
133
	 * @return \chillerlan\PrototypeDOM\NodeList
134
	 */
135 8
	public function _HTMLFragmentToNodeList(string $content):NodeList{
136 8
		$document = new Document;
137 8
		$document->loadHTML('<html><body id="-import-content">'.$content.'</body></html>');
138
139 8
		return $document->_toNodeList($document->getElementById('-import-content')->childNodes);
140
	}
141
142
	/**
143
	 * @param array $array
144
	 *
145
	 * @return \chillerlan\PrototypeDOM\NodeList
146
	 */
147 3
	public function _arrayToNodeList(array $array):NodeList{
148 3
		$nodelist = new NodeList;
149
150 3
		foreach($array as $node){
151 3
			$nodelist[] = $node;
152
		}
153
154 3
		return $nodelist;
155
	}
156
157
158
	/***********
159
	 * generic *
160
	 ***********/
161
162
	/**
163
	 * @param string $selector
164
	 * @param string $axis
165
	 *
166
	 * @return string
167
	 */
168 41
	public function selector2xpath(string $selector, string $axis = '//'):string{
169 41
		return (new CssSelectorConverter)->toXPath($selector, $axis);
170
	}
171
172
	/**
173
	 * @param string        $xpath
174
	 * @param \DOMNode|null $contextNode
175
	 *
176
	 * @return \chillerlan\PrototypeDOM\NodeList
177
	 */
178 41
	public function query(string $xpath, DOMNode $contextNode = null):NodeList{
179 41
		return new NodeList((new DOMXPath($this))->query($xpath, $contextNode));
180
	}
181
182
	/**
183
	 * @param string        $selector
184
	 * @param \DOMNode|null $contextNode
185
	 * @param string        $axis
186
	 *
187
	 * @return \chillerlan\PrototypeDOM\NodeList
188
	 */
189 41
	public function querySelectorAll(string $selector, DOMNode $contextNode = null, string $axis = 'descendant-or-self::'):NodeList{
190 41
		return $this->query($this->selector2xpath($selector, $axis), $contextNode);
191
	}
192
193
	/**
194
	 * @param string|array  $selectors
195
	 * @param \DOMNode|null $contextNode
196
	 * @param string        $axis
197
	 *
198
	 * @return \chillerlan\PrototypeDOM\Document
199
	 */
200 1
	public function removeElementsBySelector($selectors, DOMNode $contextNode = null, string $axis = 'descendant-or-self::'):Document{
201 1
		$nodes = $this->select($selectors, $contextNode, $axis);
202
203 1
		if(count($nodes) > 0){
204
			/** @var \chillerlan\PrototypeDOM\Node\Element $node */
205 1
			foreach($nodes as $node){
206 1
				$node->remove();
207
			}
208
209
		}
210
211 1
		return $this;
212
	}
213
214
	/**
215
	 * @param \chillerlan\PrototypeDOM\NodeList $nodeList
216
	 *
217
	 * @return \chillerlan\PrototypeDOM\Document
218
	 */
219 2
	public function insertNodeList(NodeList $nodeList):Document{
220
221
		/** @var \DOMNode $node */
222 2
		foreach($nodeList as $node){
223 2
			$this->appendChild($this->importNode($node->cloneNode(true), true));
224
		}
225
226 2
		return $this;
227
	}
228
229
	/*************
230
	 * prototype *
231
	 *************/
232
233
	/**
234
	 * @link http://api.prototypejs.org/dom/Element/inspect/
235
	 *
236
	 * @param \DOMNode|null $context
237
	 * @param bool          $xml
238
	 *
239
	 * @return string
240
	 */
241 8
	public function inspect(DOMNode $context = null, $xml = false):string{
242 8
		return $xml
243 2
			? $this->saveXML($context)
244 8
			: $this->saveHTML($context);
245
	}
246
247
	/**
248
	 * @link http://api.prototypejs.org/dom/Element/select/
249
	 *
250
	 * @param string|array  $selectors
251
	 * @param \DOMNode|null $contextNode
252
	 * @param string        $axis
253
	 * @param int           $nodeType
254
	 *
255
	 * @return \chillerlan\PrototypeDOM\NodeList
256
	 */
257 41
	public function select($selectors = null, DOMNode $contextNode = null, string $axis = 'descendant-or-self::', int $nodeType = XML_ELEMENT_NODE):NodeList{
258
259 41
		if(is_string($selectors)){
260 41
			$selectors = [trim($selectors)];
261
		}
262
263 41
		if(!is_array($selectors) || empty($selectors)){
264 2
			$selectors = ['*'];
265
		}
266
267 41
		$elements = new NodeList;
268
269 41
		foreach($selectors as $selector){
270
271 41
			foreach($this->querySelectorAll($selector, $contextNode, $axis) as $element){
272
273 41
				if($element->nodeType === $nodeType){
274 41
					$elements[] = $element;
275
				}
276
277
			}
278
279
		}
280
281 41
		return $elements;
282
	}
283
284
	/**
285
	 * @link http://api.prototypejs.org/dom/Element/recursivelyCollect/
286
	 *
287
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
288
	 * @param string                                      $property
289
	 * @param int                                         $maxLength
290
	 * @param int                                         $nodeType
291
	 *
292
	 * @return \chillerlan\PrototypeDOM\NodeList
293
	 */
294 3
	public function recursivelyCollect(PrototypeNode $element, string $property, int $maxLength = -1, int $nodeType = XML_ELEMENT_NODE):NodeList{
295 3
		$nodes = new NodeList;
296
297 3
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
298
299 3
			while($element = $element->{$property}){
300
301 3
				if($element->nodeType === $nodeType){
302 3
					$nodes[] = $element;
303
				}
304
305 3
				if(count($nodes) === $maxLength){
306 2
					break;
307
				}
308
309
			}
310
311
		}
312
313 3
		return $nodes;
314
	}
315
316
	/**
317
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
318
	 * @param string                                      $property
319
	 * @param string|null                                 $selector
320
	 * @param int                                         $index
321
	 * @param int                                         $nodeType
322
	 *
323
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeElement|null
324
	 */
325 4
	public function _recursivelyFind(PrototypeNode $element, string $property, string $selector = null, int $index = 0, int $nodeType = XML_ELEMENT_NODE){
326
327 4
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
328
329
			/** @var \chillerlan\PrototypeDOM\Node\Element $element */
330 4
			while($element = $element->{$property}){
331
332 4
				if($element->nodeType !== $nodeType || !is_null($selector) && !$element->match($selector) || --$index >= 0){
333 4
					continue;
334
				}
335
336 4
				return $element;
337
			}
338
339
		}
340
341 4
		return null;
342
	}
343
344
	/**
345
	 * @link http://api.prototypejs.org/dom/Element/match/
346
	 *
347
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
348
	 * @param string                                      $selector
349
	 *
350
	 * @return bool
351
	 */
352 4
	public function match(PrototypeNode $element, string $selector):bool{
353
354 4
		foreach($this->select($selector) as $match){
355
356 4
			if($element->isSameNode($match)){
357 4
				return true;
358
			}
359
360
		}
361
362 4
		return false;
363
	}
364
365
	/**
366
	 * @link http://api.prototypejs.org/dom/Element/new/
367
	 *
368
	 * @param string     $tag
369
	 * @param array|null $attributes
370
	 *
371
	 * @return \chillerlan\PrototypeDOM\Node\Element
372
	 */
373 6
	public function newElement(string $tag, array $attributes = null):Element{
374
		/** @var \chillerlan\PrototypeDOM\Node\Element $element */
375 6
		$element = $this->createElement($tag);
376
377 6
		if(!is_null($attributes)){
378 4
			$element->setAttributes($attributes);
379
		}
380
381 6
		return $element;
382
	}
383
384
}
385