Completed
Push — master ( 66e77b...24d3ae )
by Daniele
02:35
created

FluidContext::doAppendChild()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
3
// Copyright (c) 2016, Daniele Orlando <fluidxml(at)danieleorlando.com>
4
// All rights reserved.
5
//
6
// Redistribution and use in source and binary forms, with or without modification,
7
// are permitted provided that the following conditions are met:
8
//
9
// 1. Redistributions of source code must retain the above copyright notice, this
10
//    list of conditions and the following disclaimer.
11
//
12
// 2. Redistributions in binary form must reproduce the above copyright notice,
13
//    this list of conditions and the following disclaimer in the documentation
14
//    and/or other materials provided with the distribution.
15
//
16
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19
// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
20
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
24
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
25
// OF THE POSSIBILITY OF SUCH DAMAGE.
26
27
/**
28
 * FluidXML is a PHP library, under the Servo PHP framework umbrella,
29
 * specifically designed to manipulate XML documents with a concise
30
 * and fluent interface.
31
 *
32
 * It leverages XPath and the fluent programming technique to be fun
33
 * and effective.
34
 *
35
 * @author Daniele Orlando <fluidxml(at)danieleorlando.com>
36
 *
37
 * @license BSD-2-Clause
38
 * @license https://opensource.org/licenses/BSD-2-Clause
39
 */
40
41
namespace FluidXml;
42
43
/**
44
 * Constructs a new FluidXml instance.
45
 *
46
 * ```php
47
 * $xml = fluidxml();
48
 * // is the same of
49
 * $xml = new FluidXml();
50
 *
51
 * $xml = fluidxml([
52
 *
53
 *   'root'       => 'doc',
54
 *
55
 *   'version'    => '1.0',
56
 *
57
 *   'encoding'   => 'UTF-8',
58
 *
59
 *   'stylesheet' => null ]);
60
 * ```
61
 *
62
 * @param array $arguments Options that influence the construction of the XML document.
63
 *
64
 * @return FluidXml A new FluidXml instance.
65
 */
66
function fluidify(...$arguments)
67
{
68 1
        return FluidXml::load(...$arguments);
69
}
70
71
function fluidxml(...$arguments)
72
{
73 1
        return new FluidXml(...$arguments);
74
}
75
76
function fluidns(...$arguments)
77
{
78 1
        return new FluidNamespace(...$arguments);
79
}
80
81
function is_an_xml_string($string)
82
{
83 1
        if (! \is_string($string)) {
84 1
                return false;
85
        }
86
87
        // Removes any empty new line at the beginning,
88
        // otherwise the first character check may fail.
89 1
        $string = \ltrim($string);
90
91 1
        return $string[0] === '<';
92
}
93
94
function domdocument_to_string_without_headers(\DOMDocument $dom)
95
{
96 1
        return $dom->saveXML($dom->documentElement);
97
}
98
99
function domnodelist_to_string(\DOMNodeList $nodelist)
100
{
101 1
        $nodes = [];
102
103 1
        foreach ($nodelist as $n) {
104 1
                $nodes[] = $n;
105
        }
106
107 1
        return domnodes_to_string($nodes);
108
}
109
110
function domnodes_to_string(array $nodes)
111
{
112 1
        $dom = $nodes[0]->ownerDocument;
113 1
        $xml = '';
114
115 1
        foreach ($nodes as $n) {
116 1
                $xml .= $dom->saveXML($n) . PHP_EOL;
117
        }
118
119 1
        return \rtrim($xml);
120
}
121
122
function simplexml_to_string_without_headers(\SimpleXMLElement $element)
123
{
124 1
        $dom = \dom_import_simplexml($element);
125
126 1
        return $dom->ownerDocument->saveXML($dom);
127
}
128
129
interface FluidInterface
130
{
131
        /**
132
         * Executes an XPath query.
133
         *
134
         * ```php
135
         * $xml = fluidxml();
136
137
         * $xml->query("/doc/book[@id='123']");
138
         *
139
         * // Relative queries are valid.
140
         * $xml->query("/doc")->query("book[@id='123']");
141
         * ```
142
         *
143
         * @param string $xpath The XPath to execute.
144
         *
145
         * @return FluidContext The context associated to the DOMNodeList.
146
         */
147
        public function query(...$xpath);
148
        public function times($times, callable $fn = null);
149
        public function each(callable $fn);
150
151
        /**
152
         * Append a new node as child of the current context.
153
         *
154
         * ```php
155
         * $xml = fluidxml();
156
157
         * $xml->appendChild('title', 'The Theory Of Everything');
158
         * $xml->appendChild([ 'author' => 'S. Hawking' ]);
159
         *
160
         * $xml->appendChild('chapters', true)->appendChild('chapter', ['id'=> 1]);
161
         *
162
         * ```
163
         *
164
         * @param string|array $child The child/children to add.
165
         * @param string $value The child text content.
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
166
         * @param bool $switchContext Whether to return the current context
0 ignored issues
show
Bug introduced by
There is no parameter named $switchContext. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
167
         *                            or the context of the created node.
168
         *
169
         * @return FluidContext The context associated to the DOMNodeList.
170
         */
171
        public function appendChild($child, ...$optionals);
172
        public function prependSibling($sibling, ...$optionals);
173
        public function appendSibling($sibling, ...$optionals);
174
        public function setAttribute(...$arguments);
175
        public function setText($text);
176
        public function appendText($text);
177
        public function setCdata($text);
178
        public function appendCdata($text);
179
        public function remove(...$xpath);
180
        public function xml($strip = false);
181
        // Aliases:
182
        public function add($child, ...$optionals);
183
        public function prepend($sibling, ...$optionals);
184
        public function insertSiblingBefore($sibling, ...$optionals);
185
        public function append($sibling, ...$optionals);
186
        public function insertSiblingAfter($sibling, ...$optionals);
187
        public function attr(...$arguments);
188
        public function text($text);
189
}
190
191
trait ReservedCallTrait
192
{
193 1
        public function __call($method, $arguments)
194
        {
195 1
                $m = "{$method}_";
196
197 1
                if (\method_exists($this, $m)) {
198 1
                        return $this->$m(...$arguments);
199
                }
200
201
                throw new \Exception("Method '$method' not found.");
202
        }
203
}
204
205
trait ReservedCallStaticTrait
206
{
207 1
        public static function __callStatic($method, $arguments)
208
        {
209 1
                $m = "{$method}_";
210
211 1
                if (\method_exists(static::class, $m)) {
212 1
                        return static::$m(...$arguments);
213
                }
214
215
                throw new \Exception("Method '$method' not found.");
216
        }
217
}
218
219
trait NewableTrait
220
{
221
        // This method should be called 'new',
222
        // but for compatibility with PHP 5.6
223
        // it is shadowed by the __callStatic() method.
224 1
        public static function new_(...$arguments)
225
        {
226 1
                return new static(...$arguments);
227
        }
228
}
229
230
trait FluidNamespaceTrait
231
{
232
        private $namespaces = [];
233
234 1
        public function namespaces()
235
        {
236 1
                return $this->namespaces;
237
        }
238
239
        // This method should be called 'namespace',
240
        // but for compatibility with PHP 5.6
241
        // it is shadowed by the __call() method.
242 1
        protected function namespace_(...$arguments)
243
        {
244 1
                $namespaces = [];
245
246 1
                if (\is_string($arguments[0])) {
247 1
                        $args = [ $arguments[0], $arguments[1] ];
248
249 1
                        if (isset($arguments[2])) {
250 1
                                $args[] = $arguments[2];
251
                        }
252
253 1
                        $namespaces[] = new FluidNamespace(...$args);
254 1
                } else if (\is_array($arguments[0])) {
255 1
                        $namespaces = $arguments[0];
256
                } else {
257 1
                        $namespaces = $arguments;
258
                }
259
260 1
                foreach ($namespaces as $n) {
261 1
                        $this->namespaces[$n->id()] = $n;
262
                }
263
264 1
                return $this;
265
        }
266
}
267
268
class FluidXml implements FluidInterface
269
{
270
        use FluidNamespaceTrait,
271
            NewableTrait,
272
            ReservedCallTrait,          // For compatibility with PHP 5.6.
273
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
274
275
        const ROOT_NODE = 'doc';
276
277
        private $dom;
278
279 1
        public static function load($document)
280
        {
281 1
                if (\is_string($document) && ! is_an_xml_string($document)) {
282
                        // Removes any empty new line at the beginning,
283
                        // otherwise the first character check fails.
284
285 1
                        $file        = $document;
286 1
                        $is_file     = \is_file($file);
287 1
                        $is_readable = \is_readable($file);
288
289 1
                        if ($is_file && $is_readable) {
290 1
                                $document = \file_get_contents($file);
291
                        }
292
293 1
                        if (! $is_file || ! $is_readable || ! $document) {
294 1
                                throw new \Exception("File '$file' not accessible.");
295
                        }
296
                }
297
298 1
                return (new FluidXml(['root' => null]))->appendChild($document);
299
        }
300
301 1
        public function __construct($root = null, $options = [])
302
        {
303 1
                $defaults = [ 'root'       => self::ROOT_NODE,
304 1
                              'version'    => '1.0',
305 1
                              'encoding'   => 'UTF-8',
306
                              'stylesheet' => null ];
307
308 1
                if (\is_string($root)) {
309
                        // The root option can be specified as first argument
310
                        // because it is the most common.
311 1
                        $defaults['root'] = $root;
312 1
                } else if (\is_array($root)) {
313
                        // If the first argument is an array, the user has skipped
314
                        // the root option and is passing a bunch of options all together.
315 1
                        $options = $root;
316
                }
317
318 1
                $opts = \array_merge($defaults, $options);
319
320 1
                $this->dom = new \DOMDocument($opts['version'], $opts['encoding']);
321 1
                $this->dom->formatOutput       = true;
322 1
                $this->dom->preserveWhiteSpace = false;
323
324 1
                if ($opts['root']) {
325 1
                        $this->appendSibling($opts['root']);
326
                }
327
328 1
                if ($opts['stylesheet']) {
329
                        $attrs = 'type="text/xsl" '
330 1
                               . "encoding=\"{$opts['encoding']}\" "
331 1
                               . 'indent="yes" '
332 1
                               . "href=\"{$opts['stylesheet']}\"";
333 1
                        $stylesheet = new \DOMProcessingInstruction('xml-stylesheet', $attrs);
334
335 1
                        $this->dom->insertBefore($stylesheet, $this->dom->documentElement);
336
                }
337 1
        }
338
339 1
        public function xml($strip = false)
340
        {
341 1
                if ($strip) {
342 1
                        return domdocument_to_string_without_headers($this->dom);
343
                }
344
345 1
                return $this->dom->saveXML();
346
        }
347
348 1
        public function dom()
349
        {
350 1
                return $this->dom;
351
        }
352
353 1
        public function query(...$xpath)
354
        {
355 1
                return $this->newContext()->query(...$xpath);
356
        }
357
358 1
        public function times($times, callable $fn = null)
359
        {
360 1
                return $this->newContext()->times($times, $fn);
361
        }
362
363 1
        public function each(callable $fn)
364
        {
365 1
                return $this->newContext()->each($fn);
366
        }
367
368 1
        public function appendChild($child, ...$optionals)
369
        {
370
                // If the user has requested ['root' => null] at construction time
371
                // 'newContext()' promotes DOMDocument as root node.
372 1
                $context     = $this->newContext();
373 1
                $new_context = $context->appendChild($child, ...$optionals);
374
375 1
                return $this->chooseContext($context, $new_context);
376
        }
377
378
        // Alias of appendChild().
379 1
        public function add($child, ...$optionals)
380
        {
381 1
                return $this->appendChild($child, ...$optionals);
382
        }
383
384 1 View Code Duplication
        public function prependSibling($sibling, ...$optionals)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
385
        {
386 1
                if ($this->dom->documentElement === null) {
387
                        // If the document doesn't have at least one root node,
388
                        // the sibling creation fails. In this case we replace
389
                        // the sibling creation with the creation of a generic node.
390 1
                        return $this->appendChild($sibling, ...$optionals);
391
                }
392
393 1
                $context     = $this->newContext();
394 1
                $new_context = $context->prependSibling($sibling, ...$optionals);
395
396 1
                return $this->chooseContext($context, $new_context);
397
        }
398
399
        // Alias of prependSibling().
400 1
        public function prepend($sibling, ...$optionals)
401
        {
402 1
                return $this->prependSibling($sibling, ...$optionals);
403
        }
404
405
        // Alias of prependSibling().
406 1
        public function insertSiblingBefore($sibling, ...$optionals)
407
        {
408 1
                return $this->prependSibling($sibling, ...$optionals);
409
        }
410
411 1 View Code Duplication
        public function appendSibling($sibling, ...$optionals)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
412
        {
413 1
                if ($this->dom->documentElement === null) {
414
                        // If the document doesn't have at least one root node,
415
                        // the sibling creation fails. In this case we replace
416
                        // the sibling creation with the creation of a generic node.
417 1
                        return $this->appendChild($sibling, ...$optionals);
418
                }
419
420 1
                $context     = $this->newContext();
421 1
                $new_context = $context->appendSibling($sibling, ...$optionals);
422
423 1
                return $this->chooseContext($context, $new_context);
424
        }
425
426
        // Alias of appendSibling().
427 1
        public function append($sibling, ...$optionals)
428
        {
429 1
                return $this->appendSibling($sibling, ...$optionals);
430
        }
431
432
        // Alias of appendSibling().
433 1
        public function insertSiblingAfter($sibling, ...$optionals)
434
        {
435 1
                return $this->appendSibling($sibling, ...$optionals);
436
        }
437
438 1
        public function setAttribute(...$arguments)
439
        {
440 1
                $this->newContext()->setAttribute(...$arguments);
441
442 1
                return $this;
443
        }
444
445
        // Alias of setAttribute().
446 1
        public function attr(...$arguments)
447
        {
448 1
                return $this->setAttribute(...$arguments);
449
        }
450
451 1
        public function appendText($text)
452
        {
453 1
                $this->newContext()->appendText($text);
454
455 1
                return $this;
456
        }
457
458 1
        public function appendCdata($text)
459
        {
460 1
                $this->newContext()->appendCdata($text);
461
462 1
                return $this;
463
        }
464
465 1
        public function setText($text)
466
        {
467 1
                $this->newContext()->setText($text);
468
469 1
                return $this;
470
        }
471
472
        // Alias of setText().
473 1
        public function text($text)
474
        {
475 1
                return $this->setText($text);
476
        }
477
478 1
        public function setCdata($text)
479
        {
480 1
                $this->newContext()->setCdata($text);
481
482 1
                return $this;
483
        }
484
485
        // Alias of setCdata().
486 1
        public function cdata($text)
487
        {
488 1
                return $this->setCdata($text);
489
        }
490
491 1
        public function remove(...$xpath)
492
        {
493 1
                $this->newContext()->remove(...$xpath);
494
495 1
                return $this;
496
        }
497
498 1
        protected function newContext($context = null)
499
        {
500 1
                if (! $context) {
501 1
                        $context = $this->dom->documentElement;
502
                }
503
504
                // If the user has requested ['root' => null] at construction time
505
                // the 'documentElement' property is null because we have not created
506
                // a root node yet.
507 1
                if (! $context) {
508
                        // Whether there is not a root node, the DOMDocument is
509
                        // promoted as root node.
510 1
                        $context = $this->dom;
511
                }
512
513 1
                return new FluidContext($context, $this->namespaces);
514
        }
515
516 1
        protected function chooseContext($help_context, $new_context)
517
        {
518
                // If the two contextes are diffent, the user has requested
519
                // a switch of the context and we have to return it.
520 1
                if ($help_context !== $new_context) {
521 1
                        return $new_context;
522
                }
523
524 1
                return $this;
525
        }
526
}
527
528
class FluidContext implements FluidInterface, \ArrayAccess, \Iterator
529
{
530
        use FluidNamespaceTrait,
531
            NewableTrait,
532
            ReservedCallTrait,          // For compatibility with PHP 5.6.
533
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
534
535
        private $dom;
536
        private $nodes = [];
537
        private $seek = 0;
538
539 1
        public function __construct($context, array $namespaces = [])
540
        {
541 1
                if (! \is_array($context)) {
542 1
                        $context = [ $context ];
543
                }
544
545 1
                foreach ($context as $n) {
546 1
                        if ($n instanceof \DOMDocument) {
547 1
                                $this->dom     = $n;
548 1
                                $this->nodes[] = $n;
549 1
                        } else if ($n instanceof \DOMNode) {
550 1
                                $this->dom     = $n->ownerDocument;
551 1
                                $this->nodes[] = $n;
552 1
                        } else if ($n instanceof \DOMNodeList) {
553 1
                                $this->dom   = $n[0]->ownerDocument;
554 1
                                $this->nodes = \iterator_to_array($n);
555 1
                        } else if ($n instanceof FluidContext) {
556 1
                                $this->dom   = $n[0]->ownerDocument;
557 1
                                $this->nodes = \array_merge($this->nodes, $n->asArray());
558
                        } else {
559 1
                                throw new \Exception('Node type not recognized.');
560
                        }
561
                }
562
563 1
                if (! empty($namespaces)) {
564 1
                        $this->namespace(...\array_values($namespaces));
0 ignored issues
show
Bug introduced by
The method namespace() does not exist on FluidXml\FluidContext. Did you maybe mean namespaces()?

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...
565
                }
566 1
        }
567
568 1
        public function asArray()
569
        {
570 1
                return $this->nodes;
571
        }
572
573
        // \ArrayAccess interface.
574 1
        public function offsetSet($offset, $value)
575
        {
576
                // if (\is_null($offset)) {
577
                //         $this->nodes[] = $value;
578
                // } else {
579
                //         $this->nodes[$offset] = $value;
580
                // }
581 1
                throw new \Exception('Setting a context element is not allowed.');
582
        }
583
584
        // \ArrayAccess interface.
585 1
        public function offsetExists($offset)
586
        {
587 1
                return isset($this->nodes[$offset]);
588
        }
589
590
        // \ArrayAccess interface.
591 1
        public function offsetUnset($offset)
592
        {
593
                // unset($this->nodes[$offset]);
594 1
                \array_splice($this->nodes, $offset, 1);
595 1
        }
596
597
        // \ArrayAccess interface.
598 1
        public function offsetGet($offset)
599
        {
600 1
                if (isset($this->nodes[$offset])) {
601 1
                        return $this->nodes[$offset];
602
                }
603
604 1
                return null;
605
        }
606
607
        // \Iterator interface.
608 1
        public function rewind()
609
        {
610 1
                $this->seek = 0;
611 1
        }
612
613
        // \Iterator interface.
614 1
        public function current()
615
        {
616 1
                return $this->nodes[$this->seek];
617
        }
618
619
        // \Iterator interface.
620 1
        public function key()
621
        {
622 1
                return $this->seek;
623
        }
624
625
        // \Iterator interface.
626 1
        public function next()
627
        {
628 1
                ++$this->seek;
629 1
        }
630
631
        // \Iterator interface.
632 1
        public function valid()
633
        {
634 1
                return isset($this->nodes[$this->seek]);
635
        }
636
637 1
        public function length()
638
        {
639 1
                return \count($this->nodes);
640
        }
641
642 1
        public function query(...$xpath)
643
        {
644 1
                $xpaths = $xpath;
645
646 1
                if (\is_array($xpath[0])) {
647 1
                        $xpaths = $xpath[0];
648
                }
649
650 1
                $domxp = new \DOMXPath($this->dom);
651
652 1
                foreach ($this->namespaces as $n) {
653 1
                        $domxp->registerNamespace($n->id(), $n->uri());
654
                }
655
656 1
                $results = [];
657
658 1
                foreach ($this->nodes as $n) {
659 1
                        foreach ($xpaths as $x) {
660
                                // Returns a DOMNodeList.
661 1
                                $res = $domxp->query($x, $n);
662
663
                                // Algorithm 1:
664 1
                                $results = \array_merge($results, \iterator_to_array($res));
665
666
                                // Algorithm 2:
667
                                // foreach ($res as $r) {
668
                                //         $results[] = $r;
669
                                // }
670
671
                                // Algorithm 3:
672
                                // for ($i = 0, $l = $res->length; $i < $l; ++$i) {
673
                                //         $results[] = $res->item($i);
674
                                // }
675
                        }
676
                }
677
678
                // Performing over multiple sibling nodes a query that ascends
679
                // the xpath, relative (../..) or absolute (//), returns identical
680
                // matching results that must be collapsed in an unique result
681
                // otherwise a subsequent operation is performed multiple times.
682 1
                $unique_results = [];
683 1
                foreach ($results as $r) {
684 1
                        $found = false;
685
686 1
                        foreach ($unique_results as $u) {
687 1
                                if ($r === $u) {
688 1
                                        $found = true;
689
                                }
690
                        }
691
692 1
                        if (! $found) {
693 1
                                $unique_results[] = $r;
694
                        }
695
                }
696
697 1
                return $this->newContext($unique_results);
698
        }
699
700 1
        public function times($times, callable $fn = null)
701
        {
702 1
                if ($fn === null) {
703 1
                        return new FluidRepeater($this, $times);
704
                }
705
706 1
                for ($i = 0; $i < $times; ++$i) {
707 1
                        $args = [$this, $i];
708
709 1
                        if ($fn instanceof \Closure) {
710 1
                                $fn = $fn->bindTo($this);
711
712 1
                                \array_shift($args);
713
                        }
714
715 1
                        \call_user_func($fn, ...$args);
716
                }
717
718 1
                return $this;
719
        }
720
721 1
        public function each(callable $fn)
722
        {
723 1
                foreach ($this->nodes as $i => $n) {
724 1
                        $cx   = $this->newContext($n);
725 1
                        $args = [$cx, $i, $n];
726
727 1
                        if ($fn instanceof \Closure) {
728 1
                                $fn = $fn->bindTo($cx);
729
730 1
                                \array_shift($args);
731
                        }
732
733 1
                        \call_user_func($fn, ...$args);
734
                }
735
736 1
                return $this;
737
        }
738
739 1
        public static function doAppendChild($parent, $element)
740
        {
741 1
                return $parent->appendChild($element);
742
        }
743
744
        // appendChild($child, $value?, $attributes? = [], $switchContext? = false)
745 1
        public function appendChild($child, ...$optionals)
746
        {
747 1
                return $this->insertElement($child, $optionals, [ static::class, 'doAppendChild' ]);
748
        }
749
750
        // Alias of appendChild().
751 1
        public function add($child, ...$optionals)
752
        {
753 1
                return $this->appendChild($child, ...$optionals);
754
        }
755
756 1
        public static function doPrependSibling($sibling, $element)
757
        {
758 1
                return $sibling->parentNode->insertBefore($element, $sibling);
759
        }
760
761 1
        public function prependSibling($sibling, ...$optionals)
762
        {
763 1
                return $this->insertElement($sibling, $optionals, [ static::class, 'doPrependSibling' ]);
764
        }
765
766
        // Alias of prependSibling().
767 1
        public function prepend($sibling, ...$optionals)
768
        {
769 1
                return $this->prependSibling($sibling, ...$optionals);
770
        }
771
772
        // Alias of prependSibling().
773 1
        public function insertSiblingBefore($sibling, ...$optionals)
774
        {
775 1
                return $this->prependSibling($sibling, ...$optionals);
776
        }
777
778 1
        public static function doAppendSibling($sibling, $element)
779
        {
780
                // If ->nextSibling is null, $element is simply appended as last sibling.
781 1
                return $sibling->parentNode->insertBefore($element, $sibling->nextSibling);
782
        }
783
784 1
        public function appendSibling($sibling, ...$optionals)
785
        {
786 1
                return $this->insertElement($sibling, $optionals, [ static::class, 'doAppendSibling' ]);
787
        }
788
789
        // Alias of appendSibling().
790 1
        public function append($sibling, ...$optionals)
791
        {
792 1
                return $this->appendSibling($sibling, ...$optionals);
793
        }
794
795
        // Alias of appendSibling().
796 1
        public function insertSiblingAfter($sibling, ...$optionals)
797
        {
798 1
                return $this->appendSibling($sibling, ...$optionals);
799
        }
800
801
        // Arguments can be in the form of:
802
        // setAttribute($name, $value)
803
        // setAttribute(['name' => 'value', ...])
804 1
        public function setAttribute(...$arguments)
805
        {
806
                // Default case is:
807
                // [ 'name' => 'value', ... ]
808 1
                $attrs = $arguments[0];
809
810
                // If the first argument is not an array,
811
                // the user has passed two arguments:
812
                // 1. is the attribute name
813
                // 2. is the attribute value
814 1
                if (! \is_array($arguments[0])) {
815 1
                        $attrs = [$arguments[0] => $arguments[1]];
816
                }
817
818 1
                foreach ($this->nodes as $n) {
819 1
                        foreach ($attrs as $k => $v) {
820
                                // Algorithm 1:
821 1
                                $n->setAttribute($k, $v);
822
823
                                // Algorithm 2:
824
                                // $n->setAttributeNode(new \DOMAttr($k, $v));
825
826
                                // Algorithm 3:
827
                                // $n->appendChild(new \DOMAttr($k, $v));
828
829
                                // Algorithm 2 and 3 have a different behaviour
830
                                // from Algorithm 1.
831
                                // The attribute is still created or setted, but
832
                                // changing the value of an existing attribute
833
                                // changes even the order of that attribute
834
                                // in the attribute list.
835
                        }
836
                }
837
838 1
                return $this;
839
        }
840
841
        // Alias of setAttribute().
842 1
        public function attr(...$arguments)
843
        {
844 1
                return $this->setAttribute(...$arguments);
845
        }
846
847 1
        public function appendText($text)
848
        {
849 1
                foreach ($this->nodes as $n) {
850 1
                        $n->appendChild(new \DOMText($text));
851
                }
852
853 1
                return $this;
854
        }
855
856 1
        public function appendCdata($text)
857
        {
858 1
                foreach ($this->nodes as $n) {
859 1
                        $n->appendChild(new \DOMCDATASection($text));
860
                }
861
862 1
                return $this;
863
        }
864
865 1
        public function setText($text)
866
        {
867 1
                foreach ($this->nodes as $n) {
868
                        // Algorithm 1:
869 1
                        $n->nodeValue = $text;
870
871
                        // Algorithm 2:
872
                        // foreach ($n->childNodes as $c) {
873
                        //         $n->removeChild($c);
874
                        // }
875
                        // $n->appendChild(new \DOMText($text));
876
877
                        // Algorithm 3:
878
                        // foreach ($n->childNodes as $c) {
879
                        //         $n->replaceChild(new \DOMText($text), $c);
880
                        // }
881
                }
882
883 1
                return $this;
884
        }
885
886
        // Alias of setText().
887 1
        public function text($text)
888
        {
889 1
                return $this->setText($text);
890
        }
891
892 1
        public function setCdata($text)
893
        {
894 1
                foreach ($this->nodes as $n) {
895 1
                        $n->nodeValue = '';
896 1
                        $n->appendChild(new \DOMCDATASection($text));
897
                }
898
899 1
                return $this;
900
        }
901
902
        // Alias of setCdata().
903 1
        public function cdata($text)
904
        {
905 1
                return $this->setCdata($text);
906
        }
907
908 1
        public function remove(...$xpath)
909
        {
910
                // Arguments can be empty, a string or an array of strings.
911
912 1
                if (empty($xpath)) {
913
                        // The user has requested to remove the nodes of this context.
914 1
                        $targets = $this->nodes;
915
                } else {
916 1
                        $targets = $this->query(...$xpath);
917
                }
918
919 1
                foreach ($targets as $t) {
920 1
                        $t->parentNode->removeChild($t);
921
                }
922
923 1
                return $this;
924
        }
925
926 1
        public function xml($strip = false)
927
        {
928 1
                return domnodes_to_string($this->nodes);
929
        }
930
931 1
        protected function newContext($context)
932
        {
933 1
                return new FluidContext($context, $this->namespaces);
934
        }
935
936 1
        protected function handleOptionals($element, array $optionals)
937
        {
938 1
                if (! \is_array($element)) {
939 1
                        $element = [ $element ];
940
                }
941
942 1
                $switch_context = false;
943 1
                $attributes     = [];
944
945 1
                foreach ($optionals as $opt) {
946 1
                        if (\is_array($opt)) {
947 1
                                $attributes = $opt;
948
949 1
                        } else if (\is_bool($opt)) {
950 1
                                $switch_context = $opt;
951
952 1
                        } else if (\is_string($opt)) {
953 1
                                $e = \array_pop($element);
954
955 1
                                $element[$e] = $opt;
956
957
                        } else {
958 1
                                throw new \Exception("Optional argument '$opt' not recognized.");
959
                        }
960
                }
961
962 1
                return [ $element, $attributes, $switch_context ];
963
        }
964
965 1
        protected function insertElement($element, array $optionals, callable $fn)
966
        {
967 1
                list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals);
968
969 1
                $new_context = [];
970
971 1
                foreach ($this->nodes as $n) {
972 1
                        foreach ($element as $k => $v) {
973
                                // I give up, it's a too complex job for only one method like me.
974 1
                                $cx = $this->handleInsertion($n, $k, $v, $fn, $optionals);
975
976 1
                                $new_context = \array_merge($new_context, $cx);
977
                        }
978
                }
979
980 1
                $context = $this->newContext($new_context);
981
982
                // Setting the attributes is an help that the appendChild method
983
                // offers to the user and is the same of:
984
                // 1. appending a child switching the context
985
                // 2. setting the attributes over the new context.
986 1
                if (! empty($attributes)) {
987 1
                        $context->setAttribute($attributes);
988
                }
989
990 1
                if ($switch_context) {
991 1
                        return $context;
992
                }
993
994 1
                return $this;
995
        }
996
997 1
        protected function handleInsertion(...$arguments)
998
        {
999 1
                $check_sequence = [ 'specialContentHandler',
1000
                                    'specialAttributeHandler',
1001
                                    'stringStringHandler',
1002
                                    'stringMixedHandler',
1003
                                    'integerArrayHandler',
1004
                                    'integerStringNotXmlHandler',
1005
                                    'integerXmlHandler',
1006
                                    'integerDomdocumentHandler',
1007
                                    'integerDomnodelistHandler',
1008
                                    'integerDomnodeHandler',
1009
                                    'integerSimplexmlHandler',
1010
                                    'integerFluidxmlHandler',
1011
                                    'integerFluidcontextHandler' ];
1012
1013 1
                foreach ($check_sequence as $check) {
1014 1
                        $ret = $this->$check(...$arguments);
1015
1016 1
                        if ($ret !== false) {
1017 1
                                return $ret;
1018
                        }
1019
                }
1020
1021 1
                throw new \Exception('XML document not supported.');
1022
        }
1023
1024 1
        protected function createElement($name, $value = null)
1025
        {
1026
                // The DOMElement instance must be different for every node,
1027
                // otherwise only one element is attached to the DOM.
1028
1029 1
                $uri = null;
1030
1031
                // The node name can contain the namespace id prefix.
1032
                // Example: xsl:template
1033 1
                $name_parts = \explode(':', $name, 2);
1034
1035 1
                $name = \array_pop($name_parts);
1036 1
                $id   = \array_pop($name_parts);
1037
1038 1
                if ($id) {
1039 1
                        $ns  = $this->namespaces[$id];
1040 1
                        $uri = $ns->uri();
1041
1042 1
                        if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) {
1043 1
                                $name = "{$id}:{$name}";
1044
                        }
1045
                }
1046
1047
                // Algorithm 1:
1048 1
                $el = new \DOMElement($name, $value, $uri);
1049
1050
                // Algorithm 2:
1051
                // $el = $dom->createElement($name, $value);
1052
1053 1
                return $el;
1054
        }
1055
1056 1
        protected function attachNodes($parent, $nodes, $fn)
1057
        {
1058 1
                if (! \is_array($nodes) && ! $nodes instanceof \Traversable) {
1059 1
                        $nodes = [ $nodes ];
1060
                }
1061
1062 1
                $context = [];
1063
1064 1
                foreach ($nodes as $el) {
1065 1
                        $el        = $this->dom->importNode($el, true);
1066 1
                        $context[] = \call_user_func($fn, $parent, $el);
1067
                }
1068
1069 1
                return $context;
1070
        }
1071
1072 1
        protected function specialContentHandler($parent, $k, $v)
1073
        {
1074 1 View Code Duplication
                if (! \is_string($k) || $k !== '@'|| ! \is_string($v)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1075 1
                        return false;
1076
                }
1077
1078
                // The user has passed an element text content:
1079
                // [ '@' => 'Element content.' ]
1080
1081
                // Algorithm 1:
1082 1
                $this->newContext($parent)->appendText($v);
1083
1084
                // Algorithm 2:
1085
                // $this->setText($v);
1086
1087
                // The user can specify multiple '@' special elements
1088
                // so Algorithm 1 is the right choice.
1089
1090 1
                return [];
1091
        }
1092
1093 1
        protected function specialAttributeHandler($parent, $k, $v)
1094
        {
1095 1 View Code Duplication
                if (! \is_string($k) || $k[0] !== '@' || ! \is_string($v)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1096 1
                        return false;
1097
                }
1098
1099
                // The user has passed an attribute name and an attribute value:
1100
                // [ '@attribute' => 'Attribute content' ]
1101
1102 1
                $attr = \substr($k, 1);
1103 1
                $this->newContext($parent)->setAttribute($attr, $v);
1104
1105 1
                return [];
1106
        }
1107
1108 1 View Code Duplication
        protected function stringStringHandler($parent, $k, $v, $fn)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1109
        {
1110 1
                if (! \is_string($k) || ! \is_string($v)) {
1111 1
                        return false;
1112
                }
1113
1114
                // The user has passed an element name and an element value:
1115
                // [ 'element' => 'Element content' ]
1116
1117 1
                $el = $this->createElement($k, $v);
1118 1
                $el = \call_user_func($fn, $parent, $el);
1119
1120 1
                return [ $el ];
1121
        }
1122
1123 1
        protected function stringMixedHandler($parent, $k, $v, $fn, $optionals)
1124
        {
1125 1
                if (! \is_string($k) || \is_string($v)) {
1126 1
                        return false;
1127
                }
1128
1129
                // The user has passed one of these two cases:
1130
                // - [ 'element' => [...] ]
1131
                // - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ]
1132
1133 1
                $el = $this->createElement($k);
1134 1
                $el = \call_user_func($fn, $parent, $el);
1135
1136
                // The new children elements must be created in the order
1137
                // they are supplied, so 'appendChild' is the perfect operation.
1138 1
                $this->newContext($el)->appendChild($v, ...$optionals);
1139
1140 1
                return [ $el ];
1141
        }
1142
1143 1
        protected function integerArrayHandler($parent, $k, $v, $fn, $optionals)
1144
        {
1145 1
                if (! \is_int($k) || ! \is_array($v)) {
1146 1
                        return false;
1147
                }
1148
1149
                // The user has passed a wrapper array:
1150
                // [ [...], ... ]
1151
1152 1
                $context = [];
1153
1154 1
                foreach ($v as $kk => $vv) {
1155 1
                        $cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals);
1156
1157 1
                        $context = \array_merge($context, $cx);
1158
                }
1159
1160 1
                return $context;
1161
        }
1162
1163 1 View Code Duplication
        protected function integerStringNotXmlHandler($parent, $k, $v, $fn)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1164
        {
1165 1
                if (! \is_int($k) || ! \is_string($v) || is_an_xml_string($v)) {
1166 1
                        return false;
1167
                }
1168
1169
                // The user has passed a node name without a node value:
1170
                // [ 'element', ... ]
1171
1172 1
                $el = $this->createElement($v);
1173 1
                $el = \call_user_func($fn, $parent, $el);
1174
1175 1
                return [ $el ];
1176
        }
1177
1178 1
        protected function integerXmlHandler($parent, $k, $v, $fn)
1179
        {
1180 1
                if (! \is_int($k) || ! is_an_xml_string($v)) {
1181 1
                        return false;
1182
                }
1183
1184
                // The user has passed an XML document instance:
1185
                // [ '<tag></tag>', DOMNode, SimpleXMLElement, FluidXml ]
1186
1187 1
                $wrapper = new \DOMDocument();
1188 1
                $wrapper->formatOutput       = true;
1189 1
                $wrapper->preserveWhiteSpace = false;
1190
1191 1
                $v = \ltrim($v);
1192
1193 1
                if ($v[1] === '?') {
1194 1
                        $wrapper->loadXML($v);
1195 1
                        $nodes = $wrapper->childNodes;
1196
                } else {
1197
                        // A way to import strings with multiple root nodes.
1198 1
                        $wrapper->loadXML("<root>$v</root>");
1199
1200
                        // Algorithm 1:
1201 1
                        $nodes = $wrapper->documentElement->childNodes;
1202
1203
                        // Algorithm 2:
1204
                        // $dom_xp = new \DOMXPath($dom);
1205
                        // $nodes = $dom_xp->query('/root/*');
1206
                }
1207
1208 1
                return $this->attachNodes($parent, $nodes, $fn);
1209
        }
1210
1211 1
        protected function integerDomdocumentHandler($parent, $k, $v, $fn)
1212
        {
1213 1
                if (! \is_int($k) || ! $v instanceof \DOMDocument) {
1214 1
                        return false;
1215
                }
1216
1217
                // A DOMDocument can have multiple root nodes.
1218
1219
                // Algorithm 1:
1220 1
                return $this->attachNodes($parent, $v->childNodes, $fn);
1221
1222
                // Algorithm 2:
1223
                // return $this->attachNodes($parent, $v->documentElement, $fn);
1224
        }
1225
1226 1 View Code Duplication
        protected function integerDomnodelistHandler($parent, $k, $v, $fn)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1227
        {
1228 1
                if (! \is_int($k) || ! $v instanceof \DOMNodeList) {
1229 1
                        return false;
1230
                }
1231
1232 1
                return $this->attachNodes($parent, $v, $fn);
1233
        }
1234
1235 1 View Code Duplication
        protected function integerDomnodeHandler($parent, $k, $v, $fn)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1236
        {
1237 1
                if (! \is_int($k) || ! $v instanceof \DOMNode) {
1238 1
                        return false;
1239
                }
1240
1241 1
                return $this->attachNodes($parent, $v, $fn);
1242
        }
1243
1244 1
        protected function integerSimplexmlHandler($parent, $k, $v, $fn)
1245
        {
1246 1
                if (! \is_int($k) || ! $v instanceof \SimpleXMLElement) {
1247 1
                        return false;
1248
                }
1249
1250 1
                return $this->attachNodes($parent, \dom_import_simplexml($v), $fn);
1251
        }
1252
1253 1
        protected function integerFluidxmlHandler($parent, $k, $v, $fn)
1254
        {
1255 1
                if (! \is_int($k) || ! $v instanceof FluidXml) {
1256 1
                        return false;
1257
                }
1258
1259 1
                return $this->attachNodes($parent, $v->dom()->documentElement, $fn);
1260
        }
1261
1262 1 View Code Duplication
        protected function integerFluidcontextHandler($parent, $k, $v, $fn)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1263
        {
1264 1
                if (! \is_int($k) || ! $v instanceof FluidContext) {
1265 1
                        return false;
1266
                }
1267
1268 1
                return $this->attachNodes($parent, $v->asArray(), $fn);
1269
        }
1270
}
1271
1272
class FluidNamespace
1273
{
1274
        const ID   = 'id'  ;
1275
        const URI  = 'uri' ;
1276
        const MODE = 'mode';
1277
1278
        const MODE_IMPLICIT = 0;
1279
        const MODE_EXPLICIT = 1;
1280
1281
        private $config = [ self::ID   => '',
1282
                            self::URI  => '',
1283
                            self::MODE => self::MODE_EXPLICIT ];
1284
1285 1
        public function __construct($id, $uri, $mode = 1)
1286
        {
1287 1
                if (\is_array($id)) {
1288 1
                        $args = $id;
1289 1
                        $id   = $args[self::ID];
1290 1
                        $uri  = $args[self::URI];
1291
1292 1
                        if (isset($args[self::MODE])) {
1293 1
                                $mode = $args[self::MODE];
1294
                        }
1295
                }
1296
1297 1
                $this->config[self::ID]   = $id;
1298 1
                $this->config[self::URI]  = $uri;
1299 1
                $this->config[self::MODE] = $mode;
1300 1
        }
1301
1302 1
        public function id()
1303
        {
1304 1
                return $this->config[self::ID];
1305
        }
1306
1307 1
        public function uri()
1308
        {
1309 1
                return $this->config[self::URI];
1310
        }
1311
1312 1
        public function mode()
1313
        {
1314 1
                return $this->config[self::MODE];
1315
        }
1316
1317 1
        public function querify($xpath)
1318
        {
1319 1
                $id = $this->id();
1320
1321 1
                if ($id) {
1322 1
                        $id .= ':';
1323
                }
1324
1325
                // An XPath query may not start with a slash ('/').
1326
                // Relative queries are an example '../target".
1327 1
                $new_xpath = '';
1328
1329 1
                $nodes = \explode('/', $xpath);
1330
1331 1
                foreach ($nodes as $node) {
1332
                        // An XPath query may have multiple slashes ('/')
1333
                        // example: //target
1334 1
                        if ($node) {
1335 1
                                $new_xpath .= "{$id}{$node}";
1336
                        }
1337
1338 1
                        $new_xpath .= '/';
1339
                }
1340
1341
                // Removes the last appended slash.
1342 1
                return \substr($new_xpath, 0, -1);
1343
        }
1344
}
1345
1346
class FluidRepeater
1347
{
1348
        private $context;
1349
        private $times;
1350
1351 1
        public function __construct($context, $times)
1352
        {
1353 1
                $this->context = $context;
1354 1
                $this->times   = $times;
1355 1
        }
1356
1357 1
        public function __call($method, $arguments)
1358
        {
1359 1
                $new_context = [];
1360
1361 1
                for ($i = 0, $l = $this->times; $i < $l; ++$i) {
1362 1
                        $new_context[] = $this->context->$method(...$arguments);
1363
                }
1364
1365 1
                if ($new_context[0] !== $this->context) {
1366 1
                        return new FluidContext($new_context, $this->context->namespaces());
1367
                }
1368
1369 1
                return $this->context;
1370
        }
1371
}
1372