Element   D
last analyzed

Complexity

Total Complexity 80

Size/Duplication

Total Lines 495
Duplicated Lines 7.47 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 37
loc 495
ccs 171
cts 171
cp 1
rs 4.0645
c 0
b 0
f 0
wmc 80
lcom 1
cbo 12

30 Methods

Rating   Name   Duplication   Size   Complexity  
A __set() 0 4 1
B __isset() 0 13 5
B __get() 0 13 5
A __unset() 0 4 1
B blockReadOnlyProperties() 0 14 5
A hasAttribute() 0 7 2
A getAttribute() 7 7 2
A getAttributeNode() 0 7 2
A setAttribute() 8 8 2
A removeAttribute() 7 7 2
A setIdAttribute() 8 8 2
D append() 0 27 9
A appendElement() 0 6 1
A appendXml() 0 5 1
A saveXml() 0 3 1
A saveXmlFragment() 0 7 2
A saveHtml() 0 3 1
A getElementsByTagName() 7 7 2
A offsetExists() 0 6 2
A offsetGet() 0 6 2
B offsetSet() 0 18 5
A offsetUnset() 0 7 2
A isNodeOffset() 0 11 4
A isAttributeOffset() 0 3 2
A getIterator() 0 3 1
A count() 0 4 2
A resolveTagName() 0 8 2
A getDocument() 0 3 1
B applyNamespaces() 0 16 8
A isCurrentNamespace() 0 6 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Element 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 Element, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * FluentDOM\DOM\Element extends PHPs DOMDocument class. It adds some generic namespace handling on
4
 * the document level and registers extended Node classes for convenience.
5
 *
6
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7
 * @copyright Copyright (c) 2009-2017 FluentDOM Contributors
8
 */
9
10
namespace FluentDOM\DOM {
11
12
  use FluentDOM\Appendable;
13
  use FluentDOM\Utility\Iterators\ElementIterator;
14
  use FluentDOM\Utility\QualifiedName;
15
16
  /**
17
   * FluentDOM\DOM\Element extends PHPs DOMDocument class. It adds some generic namespace handling on
18
   * the document level and registers extended Node classes for convenience.
19
   *
20
   * @property-read Document $ownerDocument
21
   * @property-read Element $nextElementSibling
22
   * @property-read Element $previousElementSibling
23
   * @property-read Element $firstElementChild
24
   * @property-read Element $lastElementChild
25
   * @property-read \DOMNode|Node\ChildNode|Node\NonDocumentTypeChildNode|Node $firstChild
26
   * @property-read \DOMNode|Node\ChildNode|Node\NonDocumentTypeChildNode|Node $lastChild
27
   */
28
  class Element
29
    extends \DOMElement
30
    implements
31
      \ArrayAccess,
32
      \Countable,
33
      \IteratorAggregate,
34
      Node,
35
      Node\ChildNode,
36
      Node\NonDocumentTypeChildNode,
37
      Node\ParentNode {
38
39
    use
40
      Node\ChildNode\Implementation,
41
      Node\NonDocumentTypeChildNode\Implementation,
42
      Node\QuerySelector\Implementation,
43
      Node\StringCast,
44
      Node\Xpath,
45
      Node\ParentNode\Implementation
46
      {
47
        Node\ParentNode\Implementation::append as appendToParentNode;
48
      }
49
50
    /**
51
     * @param string $name
52
     * @return bool
53
     */
54 9
    public function __isset(string $name) {
55
      switch ($name) {
56 9
      case 'nextElementSibling' :
57 2
        return $this->getNextElementSibling() !== NULL;
58 7
      case 'previousElementSibling' :
59 2
        return $this->getPreviousElementSibling() !== NULL;
60 5
      case 'firstElementChild' :
61 2
        return $this->getFirstElementChild() !== NULL;
62 3
      case 'lastElementChild' :
63 2
        return $this->getLastElementChild() !== NULL;
64
      }
65 1
      return FALSE;
66
    }
67
68
    /**
69
     * @param string $name
70
     * @return \DOMNode|Element|NULL
71
     */
72 5
    public function __get(string $name) {
73
      switch ($name) {
74 5
      case 'nextElementSibling' :
75 1
        return $this->getNextElementSibling();
76 4
      case 'previousElementSibling' :
77 1
        return $this->getPreviousElementSibling();
78 3
      case 'firstElementChild' :
79 1
        return $this->getFirstElementChild();
80 2
      case 'lastElementChild' :
81 1
        return $this->getLastElementChild();
82
      }
83 1
      return $this->$name;
84
    }
85
86
    /**
87
     * @param string $name
88
     * @param $value
89
     * @throws \BadMethodCallException
90
     */
91 5
    public function __set(string $name, $value) {
92 5
      $this->blockReadOnlyProperties($name);
93 1
      $this->$name = $value;
94 1
    }
95
96
    /**
97
     * @param string $name
98
     * @throws \BadMethodCallException
99
     */
100 8
    public function __unset(string $name) {
101 8
      $this->blockReadOnlyProperties($name);
102 4
      unset($this->$name);
103 4
    }
104
105
    /**
106
     * @param string $name
107
     * @throws \BadMethodCallException
108
     */
109 13
    private function blockReadOnlyProperties(string $name) {
110
      switch ($name) {
111 13
      case 'nextElementSibling' :
112 11
      case 'previousElementSibling' :
113 9
      case 'firstElementChild' :
114 7
      case 'lastElementChild' :
115 8
        throw new \BadMethodCallException(
116 8
          sprintf(
117 8
            'Can not write readonly property %s::$%s.',
118 8
            get_class($this), $name
119
          )
120
        );
121
      }
122 5
    }
123
124
    /**
125
     * Validate if an attribute exists
126
     *
127
     * @param string $name
128
     * @return bool
129
     * @throws \LogicException
130
     */
131 6
    public function hasAttribute($name): bool {
132 6
      list($namespaceURI, $localName) = $this->resolveTagName($name);
133 6
      if ($namespaceURI !== '') {
134 2
        return parent::hasAttributeNS($namespaceURI, $localName);
135
      }
136 4
      return parent::hasAttribute($name);
137
    }
138
139
    /**
140
     * Get an attribute value
141
     *
142
     * @param string $name
143
     * @return string|NULL
144
     * @throws \LogicException
145
     */
146 4 View Code Duplication
    public function getAttribute($name) {
147 4
      list($namespaceURI, $localName) = $this->resolveTagName($name);
148 4
      if ($namespaceURI !== '') {
149 1
        return parent::getAttributeNS($namespaceURI, $localName);
150
      }
151 3
      return parent::getAttribute($name);
152
    }
153
154
    /**
155
     * Get an attribute value
156
     *
157
     * @param string $name
158
     * @return Attribute|\DOMAttr|NULL
159
     * @throws \LogicException
160
     */
161 2
    public function getAttributeNode($name) {
162 2
      list($namespaceURI, $localName) = $this->resolveTagName($name);
163 2
      if ($namespaceURI !== '') {
164 1
        return parent::getAttributeNodeNS($namespaceURI, $localName);
165
      }
166 1
      return parent::getAttributeNode($name);
167
    }
168
169
    /**
170
     * Set an attribute on an element
171
     *
172
     * @todo validate return value
173
     * @param string $name
174
     * @param string $value
175
     * @return Attribute
176
     * @throws \LogicException
177
     */
178 6 View Code Duplication
    public function setAttribute($name, $value) {
179 6
      list($namespaceURI) = $this->resolveTagName($name);
180 6
      if ($namespaceURI !== '') {
181
        /** @noinspection PhpVoidFunctionResultUsedInspection */
182 3
        return parent::setAttributeNS($namespaceURI, $name, $value);
183
      }
184 3
      return parent::setAttribute($name, $value);
185
    }
186
187
    /**
188
     * Set an attribute on an element
189
     *
190
     * @param string $name
191
     * @return bool
192
     * @throws \LogicException
193
     */
194 3 View Code Duplication
    public function removeAttribute($name): bool {
195 3
      list($namespaceURI, $localName) = $this->resolveTagName($name);
196 3
      if ($namespaceURI !== '') {
197 1
        return (bool)parent::removeAttributeNS($namespaceURI, $localName);
198
      }
199 2
      return (bool)parent::removeAttribute($name);
200
    }
201
202
    /**
203
     * Set an attribute on an element
204
     *
205
     * @param string $name
206
     * @param bool $isId
207
     * @throws \LogicException
208
     */
209 2 View Code Duplication
    public function setIdAttribute($name, $isId) {
210 2
      list($namespaceURI, $localName) = $this->resolveTagName($name);
211 2
      if ($namespaceURI !== '') {
212 1
        parent::setIdAttributeNS($namespaceURI, $localName, $isId);
213
      } else {
214 1
        parent::setIdAttribute($name, $isId);
215
      }
216 2
    }
217
218
    /**
219
     * Append a value to the element node
220
     *
221
     * The value can be:
222
     *
223
     * - a node (automatically imported and cloned)
224
     * - an object implementing FluentDOM\Appendable (method appendTo())
225
     * - a scalar or object castable to string (adds a text node)
226
     * - an array (sets attributes)
227
     *
228
     * @param \Traversable|\DOMNode|Appendable|array|string|callable $value
229
     * @return $this|Element
230
     * @throws \LogicException
231
     */
232 14
    public function append($value): Element {
233 14
      if ($value instanceof \DOMAttr) {
234 2
        $this->setAttributeNode(
235 2
          $value->ownerDocument === $this->ownerDocument
236 2
            ? $value : $this->ownerDocument->importNode($value)
237
        );
238 12
      } elseif ($value instanceof Appendable) {
239 2
        $this->ownerDocument->namespaces()->store();
240 2
        $value->appendTo($this);
241 2
        $this->ownerDocument->namespaces()->restore();
242 11
      } elseif ($value instanceof \Closure && !$value instanceof \DOMNode) {
243 1
        $this->append($value());
244 11
      } elseif (is_array($value)) {
245 2
        $nodes = [];
246 2
        foreach ($value as $name => $data) {
247 2
          if (QualifiedName::validate($name)) {
248 2
            $this->setAttribute($name, (string)$data);
249
          } else {
250 1
            $nodes[] = $data;
251
          }
252
        }
253 2
        $this->appendToParentNode($nodes);
254
      } else {
255 9
        $this->appendToParentNode($value);
256
      }
257 14
      return $this;
258
    }
259
260
    /**
261
     * Append an child element
262
     *
263
     * @param string $name
264
     * @param string $content
265
     * @param array $attributes
266
     * @return Element
267
     * @throws \LogicException
268
     */
269 1
    public function appendElement(string $name, $content = '', array $attributes = NULL): Element {
270 1
      $this->appendChild(
271 1
        $node = $this->getDocument()->createElement($name, $content, $attributes)
272
      );
273 1
      return $node;
274
    }
275
276
    /**
277
     * Append an xml fragment to the element node
278
     *
279
     * @param string $xmlFragment
280
     * @throws \InvalidArgumentException
281
     */
282 2
    public function appendXml(string $xmlFragment) {
283 2
      $fragment = $this->getDocument()->createDocumentFragment();
284 2
      $fragment->appendXml($xmlFragment);
285 2
      $this->appendChild($fragment);
286 2
    }
287
288
    /**
289
     * save the element node as XML
290
     *
291
     * @return string
292
     */
293 1
    public function saveXml(): string {
294 1
      return $this->getDocument()->saveXML($this);
295
    }
296
297
    /**
298
     * Save the child nodes of this element as an XML fragment.
299
     *
300
     * @return string
301
     */
302 1
    public function saveXmlFragment(): string {
303 1
      $result = '';
304 1
      foreach ($this->childNodes as $child) {
305 1
        $result .= $this->getDocument()->saveXML($child);
306
      }
307 1
      return $result;
308
    }
309
310
    /**
311
     * save the element node as HTML
312
     *
313
     * @return string
314
     */
315 2
    public function saveHtml(): string {
316 2
      return $this->getDocument()->saveHTML($this);
317
    }
318
319
    /**
320
     * Allow getElementsByTagName to use the defined namespaces.
321
     *
322
     * @param string $name
323
     * @return \DOMNodeList
324
     * @throws \LogicException
325
     */
326 2 View Code Duplication
    public function getElementsByTagName($name): \DOMNodeList {
327 2
      list($namespaceURI, $localName) = $this->resolveTagName($name);
328 2
      if ($namespaceURI !== '') {
329 1
        return parent::getElementsByTagNameNS($namespaceURI, $localName);
330
      }
331 1
      return parent::getElementsByTagName($localName);
332
    }
333
334
    /***************************
335
     * Array Access Interface
336
     ***************************/
337
338
    /**
339
     * Validate if an offset exists. If a integer is provided
340
     * it will check for a child node, if a string is provided for an attribute.
341
     *
342
     * @param int|string $offset
343
     * @return bool
344
     * @throws \InvalidArgumentException
345
     */
346 5
    public function offsetExists($offset): bool {
347 5
      if ($this->isNodeOffset($offset)) {
348 3
        return $this->count() > $offset;
349
      }
350 2
      return $this->hasAttribute($offset);
351
    }
352
353
    /**
354
     * Get a child node by its numeric index, or an attribute by its name.
355
     *
356
     * @param int|string $offset
357
     * @return \DOMNode|mixed|string
358
     * @throws \InvalidArgumentException
359
     */
360 4
    public function offsetGet($offset) {
361 4
      if ($this->isNodeOffset($offset)) {
362 2
        return $this->childNodes->item((int)$offset);
363
      }
364 2
      return $this->getAttribute($offset);
365
    }
366
367
    /**
368
     * @param int|string $offset
369
     * @param \DOMNode|string $value
370
     * @throws \LogicException
371
     */
372 4
    public function offsetSet($offset, $value) {
373 4
      if (NULL === $offset || $this->isNodeOffset($offset)) {
374 3
        if (!($value instanceOf \DOMNode)) {
375 1
          throw new \InvalidArgumentException(
376 1
            '$value is not a valid \\DOMNode'
377
          );
378
        }
379 2
        if (NULL === $offset) {
380 1
          $this->appendChild($value);
381
        } else {
382 1
          $this->replaceChild(
383 1
            $value, $this->childNodes->item((int)$offset)
384
          );
385
        }
386
      } else {
387 1
        $this->setAttribute($offset, (string)$value);
388
      }
389 3
    }
390
391
    /**
392
     * Remove a child node using its index or an attribute node using its name.
393
     *
394
     * @param int|string $offset
395
     * @throws \InvalidArgumentException
396
     */
397 2
    public function offsetUnset($offset) {
398 2
      if ($this->isNodeOffset($offset)) {
399 1
        $this->removeChild($this->childNodes->item((int)$offset));
400
      } else {
401 1
        $this->removeAttribute($offset);
402
      }
403 2
    }
404
405
    /**
406
     * Node offsets are integers, or strings containing only digits.
407
     *
408
     * @param mixed $offset
409
     * @throws \InvalidArgumentException
410
     * @return bool
411
     */
412 14
    private function isNodeOffset($offset): bool {
413 14
      if (is_int($offset) || ctype_digit((string)$offset)) {
414 8
        return TRUE;
415
      }
416 7
      if ($this->isAttributeOffset($offset)) {
417 6
        return FALSE;
418
      }
419 1
      throw new \InvalidArgumentException(
420 1
        'Invalid offset. Use integer for child nodes and strings for attributes.'
421
      );
422
    }
423
424
    /**
425
     * Attribute offsets are strings that can not only contains digits.
426
     *
427
     * @param mixed $offset
428
     * @return bool
429
     */
430 7
    private function isAttributeOffset($offset): bool {
431 7
      return (is_string($offset) && !ctype_digit((string)$offset));
432
    }
433
434
    /*************************
435
     * Iterator
436
     ************************/
437
438
    /**
439
     * Return Iterator for child nodes.
440
     *
441
     * @return \Iterator
442
     */
443 1
    public function getIterator(): \Iterator {
444 1
      return new ElementIterator($this);
445
    }
446
447
    /*************************
448
     * Countable
449
     ************************/
450
451
    /**
452
     * Return child node count
453
     *
454
     * @return int
455
     */
456 4
    public function count(): int {
457 4
      $nodes = $this->childNodes;
458 4
      return ($nodes instanceOf \DOMNodeList) ? $nodes->length : 0;
459
    }
460
461
    /**
462
     * Resolves a provided tag name into namespace and local name
463
     *
464
     * @param string $name
465
     * @return string[]
466
     * @throws \LogicException
467
     */
468 13
    private function resolveTagName(string $name): array {
469 13
      list($prefix, $localName) = QualifiedName::split($name);
470 13
      if (empty($prefix)) {
471 10
        return ['', (string)$localName];
472
      }
473 3
      $namespaceURI = $this->getDocument()->namespaces()->resolveNamespace($prefix);
474 3
      return [(string)$namespaceURI, (string)$localName];
475
    }
476
477
    /**
478
     * A getter for the owner document
479
     *
480
     * @return Document
481
     */
482 6
    private function getDocument(): Document {
483 6
      return $this->ownerDocument;
484
    }
485
486
    /**
487
     * Sets all namespaces registered on the document as xmlns attributes on the element.
488
     *
489
     * @param NULL|string|array $prefixes
490
     * @throws \LogicException
491
     */
492 4
    public function applyNamespaces($prefixes = NULL) {
493 4
      if ($prefixes !== NULL && !is_array($prefixes)) {
494 1
        $prefixes = [$prefixes];
495
      }
496 4
      foreach ($this->getDocument()->namespaces() as $prefix => $namespaceURI) {
497
        if (
498 4
          !$this->isCurrentNamespace($prefix, $namespaceURI) &&
499 4
          ($prefixes === NULL || in_array($prefix, $prefixes, TRUE))
500
        ) {
501 4
          $this->setAttribute(
502 4
            ($prefix === '#default') ? 'xmlns' : 'xmlns:'.$prefix,
503 4
            $namespaceURI
504
          );
505
        }
506
      }
507 4
    }
508
509
    /**
510
     * Return TRUE if the provided namespace is the same as the one on the element
511
     *
512
     * @param string $prefix
513
     * @param string $namespaceURI
514
     * @return bool
515
     */
516 4
    private function isCurrentNamespace(string $prefix, string $namespaceURI): bool {
517
      return (
518 4
        $namespaceURI === $this->namespaceURI &&
519 4
        $prefix === ($this->prefix ?: '#default')
520
      );
521
    }
522
  }
523
}