Completed
Push — master ( 4f6354...e74d94 )
by Thomas
02:57
created

Element::blockReadOnlyProperties()   B

Complexity

Conditions 5
Paths 5

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 14
Code Lines 10

Code Coverage

Tests 10
CRAP Score 5

Importance

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