Completed
Push — master ( e607fe...66e77b )
by Daniele
02:36
created

FluidContext::specialContentHandler()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 20
Code Lines 5

Duplication

Lines 3
Ratio 15 %

Code Coverage

Tests 5
CRAP Score 4
Metric Value
dl 3
loc 20
ccs 5
cts 5
cp 1
rs 9.2
cc 4
eloc 5
nc 2
nop 5
crap 4
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);
1 ignored issue
show
Bug introduced by
The call to FluidNamespace::__construct() misses a required argument $uri.

This check looks for function calls that miss required arguments.

Loading history...
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);
1 ignored issue
show
Unused Code introduced by
The call to NewableTrait::__construct() has too many arguments starting with $arguments.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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);
1 ignored issue
show
Bug introduced by
The call to FluidNamespace::__construct() misses a required argument $uri.

This check looks for function calls that miss required arguments.

Loading history...
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 insertElement($element, array $optionals, callable $fn)
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
                $new_context = [];
963
964 1
                foreach ($this->nodes as $n) {
965 1
                        foreach ($element as $k => $v) {
966
                                // I give up, it's a too complex job for only one method like me.
967 1
                                $cx = $this->handleInsertion($n, $k, $v, $optionals, $fn);
968
969 1
                                $new_context = \array_merge($new_context, $cx);
970
                        }
971
                }
972
973 1
                $context = $this->newContext($new_context);
974
975
                // Setting the attributes is an help that the appendChild method
976
                // offers to the user and is the same of:
977
                // 1. appending a child switching the context
978
                // 2. setting the attributes over the new context.
979 1
                if (! empty($attributes)) {
980 1
                        $context->setAttribute($attributes);
981
                }
982
983 1
                if ($switch_context) {
984 1
                        return $context;
985
                }
986
987 1
                return $this;
988
        }
989
990 1
        protected function handleInsertion(...$arguments)
991
        {
992 1
                $check_sequence = [ 'specialContentHandler',
993
                                    'specialAttributeHandler',
994
                                    'stringStringHandler',
995
                                    'stringMixedHandler',
996
                                    'integerArrayHandler',
997
                                    'integerStringNotXmlHandler',
998
                                    'integerXmlHandler',
999
                                    'integerDomdocumentHandler',
1000
                                    'integerDomnodelistHandler',
1001
                                    'integerDomnodeHandler',
1002
                                    'integerSimplexmlHandler',
1003
                                    'integerFluidxmlHandler',
1004
                                    'integerFluidcontextHandler' ];
1005
1006 1
                foreach ($check_sequence as $check) {
1007 1
                        $ret = $this->$check(...$arguments);
1008
1009 1
                        if ($ret !== false) {
1010 1
                                return $ret;
1011
                        }
1012
                }
1013
1014 1
                throw new \Exception('XML document not supported.');
1015
        }
1016
1017 1
        protected function createElement($name, $value = null)
1018
        {
1019
                // The DOMElement instance must be different for every node,
1020
                // otherwise only one element is attached to the DOM.
1021
1022 1
                $uri = null;
1023
1024
                // The node name can contain the namespace id prefix.
1025
                // Example: xsl:template
1026 1
                $name_parts = \explode(':', $name, 2);
1027
1028 1
                $name = \array_pop($name_parts);
1029 1
                $id   = \array_pop($name_parts);
1030
1031 1
                if ($id) {
1032 1
                        $ns  = $this->namespaces[$id];
1033 1
                        $uri = $ns->uri();
1034
1035 1
                        if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) {
1036 1
                                $name = "{$id}:{$name}";
1037
                        }
1038
                }
1039
1040
                // Algorithm 1:
1041 1
                $el = new \DOMElement($name, $value, $uri);
1042
1043
                // Algorithm 2:
1044
                // $el = $dom->createElement($name, $value);
1045
1046 1
                return $el;
1047
        }
1048
1049 1
        protected function attachNodes($parent, $nodes, $fn)
1050
        {
1051 1
                if (! \is_array($nodes) && ! $nodes instanceof \Traversable) {
1052 1
                        $nodes = [ $nodes ];
1053
                }
1054
1055 1
                $context = [];
1056
1057 1
                foreach ($nodes as $el) {
1058 1
                        $el        = $this->dom->importNode($el, true);
1059 1
                        $context[] = \call_user_func($fn, $parent, $el);
1060
                }
1061
1062 1
                return $context;
1063
        }
1064
1065 1
        protected function specialContentHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $fn is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1066
        {
1067 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...
1068 1
                        return false;
1069
                }
1070
1071
                // The user has passed an element text content:
1072
                // [ '@' => 'Element content.' ]
1073
1074
                // Algorithm 1:
1075 1
                $this->newContext($parent)->appendText($v);
1076
1077
                // Algorithm 2:
1078
                // $this->setText($v);
1079
1080
                // The user can specify multiple '@' special elements
1081
                // so Algorithm 1 is the right choice.
1082
1083 1
                return [];
1084
        }
1085
1086 1
        protected function specialAttributeHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $fn is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1087
        {
1088 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...
1089 1
                        return false;
1090
                }
1091
1092
                // The user has passed an attribute name and an attribute value:
1093
                // [ '@attribute' => 'Attribute content' ]
1094
1095 1
                $attr = \substr($k, 1);
1096 1
                $this->newContext($parent)->setAttribute($attr, $v);
1097
1098 1
                return [];
1099
        }
1100
1101 1 View Code Duplication
        protected function stringStringHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
1157
        {
1158 1
                if (! \is_int($k) || ! \is_string($v) || is_an_xml_string($v)) {
1159 1
                        return false;
1160
                }
1161
1162
                // The user has passed a node name without a node value:
1163
                // [ 'element', ... ]
1164
1165 1
                $el = $this->createElement($v);
1166 1
                $el = \call_user_func($fn, $parent, $el);
1167
1168 1
                return [ $el ];
1169
        }
1170
1171 1
        protected function integerXmlHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1172
        {
1173 1
                if (! \is_int($k) || ! is_an_xml_string($v)) {
1174 1
                        return false;
1175
                }
1176
1177
                // The user has passed an XML document instance:
1178
                // [ '<tag></tag>', DOMNode, SimpleXMLElement, FluidXml ]
1179
1180 1
                $nodes = [];
0 ignored issues
show
Unused Code introduced by
$nodes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1181
1182 1
                $wrapper = new \DOMDocument();
1183 1
                $wrapper->formatOutput       = true;
1184 1
                $wrapper->preserveWhiteSpace = false;
1185
1186 1
                $vv = \ltrim($v);
1187 1
                if ($vv[1] === '?') {
1188 1
                        $wrapper->loadXML($vv);
1189 1
                        $nodes = $wrapper->childNodes;
1190
                } else {
1191
                        // A way to import strings with multiple root nodes.
1192 1
                        $wrapper->loadXML("<root>$vv</root>");
1193
1194
                        // Algorithm 1:
1195 1
                        $nodes = $wrapper->documentElement->childNodes;
1196
1197
                        // Algorithm 2:
1198
                        // $dom_xp = new \DOMXPath($dom);
1199
                        // $nodes = $dom_xp->query('/root/*');
1200
                }
1201
1202 1
                return $this->attachNodes($parent, $nodes, $fn);
1203
        }
1204
1205 1
        protected function integerDomdocumentHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1206
        {
1207 1
                if (! \is_int($k) || ! $v instanceof \DOMDocument) {
1208 1
                        return false;
1209
                }
1210
1211
                // A DOMDocument can have multiple root nodes.
1212
1213
                // Algorithm 1:
1214 1
                return $this->attachNodes($parent, $v->childNodes, $fn);
1215
1216
                // Algorithm 2:
1217
                // return $this->attachNodes($parent, $v->documentElement, $fn);
1218
        }
1219
1220 1 View Code Duplication
        protected function integerDomnodelistHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
1221
        {
1222 1
                if (! \is_int($k) || ! $v instanceof \DOMNodeList) {
1223 1
                        return false;
1224
                }
1225
1226 1
                return $this->attachNodes($parent, $v, $fn);
1227
        }
1228
1229 1 View Code Duplication
        protected function integerDomnodeHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
1230
        {
1231 1
                if (! \is_int($k) || ! $v instanceof \DOMNode) {
1232 1
                        return false;
1233
                }
1234
1235 1
                return $this->attachNodes($parent, $v, $fn);
1236
        }
1237
1238 1
        protected function integerSimplexmlHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1239
        {
1240 1
                if (! \is_int($k) || ! $v instanceof \SimpleXMLElement) {
1241 1
                        return false;
1242
                }
1243
1244 1
                return $this->attachNodes($parent, \dom_import_simplexml($v), $fn);
1245
        }
1246
1247 1
        protected function integerFluidxmlHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1248
        {
1249 1
                if (! \is_int($k) || ! $v instanceof FluidXml) {
1250 1
                        return false;
1251
                }
1252
1253 1
                return $this->attachNodes($parent, $v->dom()->documentElement, $fn);
1254
        }
1255
1256 1 View Code Duplication
        protected function integerFluidcontextHandler($parent, $k, $v, $optionals, $fn)
0 ignored issues
show
Unused Code introduced by
The parameter $optionals is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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