Completed
Push — master ( 3ca83b...f04eb9 )
by Daniele
03:06
created

FluidContext::length()   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 0
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
use \FluidXml\Core\FluidInterface;
45
use \FluidXml\Core\FluidDocument;
46
use \FluidXml\Core\FluidInsertionHandler;
47
use \FluidXml\Core\FluidContext;
48
use \FluidXml\Core\NewableTrait;
49
use \FluidXml\Core\ReservedCallTrait;
50
use \FluidXml\Core\ReservedCallStaticTrait;
51
52
/**
53
 * Constructs a new FluidXml instance.
54
 *
55
 * ```php
56
 * $xml = fluidxml();
57
 * // is the same of
58
 * $xml = new FluidXml();
59
 *
60
 * $xml = fluidxml([
61
 *
62
 *   'root'       => 'doc',
63
 *
64
 *   'version'    => '1.0',
65
 *
66
 *   'encoding'   => 'UTF-8',
67
 *
68
 *   'stylesheet' => null ]);
69
 * ```
70
 *
71
 * @param array $arguments Options that influence the construction of the XML document.
72
 *
73
 * @return FluidXml A new FluidXml instance.
74
 */
75
function fluidify(...$arguments)
76
{
77 1
        return FluidXml::load(...$arguments);
78
}
79
80
function fluidxml(...$arguments)
81
{
82 1
        return new FluidXml(...$arguments);
83
}
84
85
function fluidns(...$arguments)
86
{
87 1
        return new FluidNamespace(...$arguments);
88
}
89
90
function is_an_xml_string($string)
91
{
92
        // Removes any empty new line at the beginning,
93
        // otherwise the first character check may fail.
94 1
        $string = \ltrim($string);
95
96 1
        return $string[0] === '<';
97
}
98
99
function domdocument_to_string_without_headers(\DOMDocument $dom)
100
{
101 1
        return $dom->saveXML($dom->documentElement);
102
}
103
104
function domnodelist_to_string(\DOMNodeList $nodelist)
105
{
106 1
        $nodes = [];
107
108 1
        foreach ($nodelist as $n) {
109 1
                $nodes[] = $n;
110
        }
111
112 1
        return domnodes_to_string($nodes);
113
}
114
115
function domnodes_to_string(array $nodes)
116
{
117 1
        $dom = $nodes[0]->ownerDocument;
118 1
        $xml = '';
119
120 1
        foreach ($nodes as $n) {
121 1
                $xml .= $dom->saveXML($n) . PHP_EOL;
122
        }
123
124 1
        return \rtrim($xml);
125
}
126
127
function simplexml_to_string_without_headers(\SimpleXMLElement $element)
128
{
129 1
        $dom = \dom_import_simplexml($element);
130
131 1
        return $dom->ownerDocument->saveXML($dom);
132
}
133
134
/**
135
 * @method FluidXml namespace(...$arguments)
136
 */
137
class FluidXml implements FluidInterface
138
{
139
        use NewableTrait,
140
            ReservedCallTrait,          // For compatibility with PHP 5.6.
141
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
142
143
        const ROOT_NODE = 'doc';
144
145
        private $document;
146
        private $handler;
147
148 1
        public static function load($document)
149
        {
150 1
                if (\is_string($document) && ! is_an_xml_string($document)) {
151
                        // Removes any empty new line at the beginning,
152
                        // otherwise the first character check fails.
153
154 1
                        $file        = $document;
155 1
                        $is_file     = \is_file($file);
156 1
                        $is_readable = \is_readable($file);
157
158 1
                        if ($is_file && $is_readable) {
159 1
                                $document = \file_get_contents($file);
160
                        }
161
162 1
                        if (! $is_file || ! $is_readable || ! $document) {
163 1
                                throw new \Exception("File '$file' not accessible.");
164
                        }
165
                }
166
167 1
                return (new FluidXml(['root' => null]))->appendChild($document);
168
        }
169
170 1
        public function __construct($root = null, $options = [])
171
        {
172 1
                $defaults = [ 'root'       => self::ROOT_NODE,
173 1
                              'version'    => '1.0',
174 1
                              'encoding'   => 'UTF-8',
175
                              'stylesheet' => null ];
176
177 1
                if (\is_string($root)) {
178
                        // The root option can be specified as first argument
179
                        // because it is the most common.
180 1
                        $defaults['root'] = $root;
181 1
                } else if (\is_array($root)) {
182
                        // If the first argument is an array, the user has skipped
183
                        // the root option and is passing a bunch of options all together.
184 1
                        $options = $root;
185
                }
186
187 1
                $opts = \array_merge($defaults, $options);
188
189 1
                $this->document = new FluidDocument();
190 1
                $doc            = $this->document;
191
192 1
                $doc->dom = new \DOMDocument($opts['version'], $opts['encoding']);
193 1
                $doc->dom->formatOutput       = true;
194 1
                $doc->dom->preserveWhiteSpace = false;
195
196 1
                $doc->xpath    = new \DOMXPath($doc->dom);
197
198 1
                $this->handler = new FluidInsertionHandler($doc);
199
200 1
                if (! empty($opts['root'])) {
201 1
                        $this->appendSibling($opts['root']);
202
                }
203
204 1
                if (! empty($opts['stylesheet'])) {
205
                        $attrs = 'type="text/xsl" '
206 1
                               . "encoding=\"{$opts['encoding']}\" "
207 1
                               . 'indent="yes" '
208 1
                               . "href=\"{$opts['stylesheet']}\"";
209 1
                        $stylesheet = new \DOMProcessingInstruction('xml-stylesheet', $attrs);
210
211 1
                        $doc->dom->insertBefore($stylesheet, $doc->dom->documentElement);
212
                }
213 1
        }
214
215 1
        public function xml($strip = false)
216
        {
217 1
                if ($strip) {
218 1
                        return domdocument_to_string_without_headers($this->document->dom);
219
                }
220
221 1
                return $this->document->dom->saveXML();
222
        }
223
224 1
        public function dom()
225
        {
226 1
                return $this->document->dom;
227
        }
228
229 1
        public function namespaces()
230
        {
231 1
                return $this->document->namespaces;
232
        }
233
234
        // This method should be called 'namespace',
235
        // but for compatibility with PHP 5.6
236
        // it is shadowed by the __call() method.
237 1
        protected function namespace_(...$arguments)
238
        {
239 1
                $namespaces = [];
240
241 1
                if (\is_string($arguments[0])) {
242 1
                        $args = [ $arguments[0], $arguments[1] ];
243
244 1
                        if (isset($arguments[2])) {
245 1
                                $args[] = $arguments[2];
246
                        }
247
248 1
                        $namespaces[] = new FluidNamespace(...$args);
249 1
                } else if (\is_array($arguments[0])) {
250 1
                        $namespaces = $arguments[0];
251
                } else {
252 1
                        $namespaces = $arguments;
253
                }
254
255 1
                foreach ($namespaces as $n) {
256 1
                        $this->document->namespaces[$n->id()] = $n;
257 1
                        $this->document->xpath->registerNamespace($n->id(), $n->uri());
258
                }
259
260 1
                return $this;
261
        }
262
263 1
        public function query(...$xpath)
264
        {
265 1
                return $this->context()->query(...$xpath);
266
        }
267
268 1
        public function times($times, callable $fn = null)
269
        {
270 1
                return $this->context()->times($times, $fn);
271
        }
272
273 1
        public function each(callable $fn)
274
        {
275 1
                return $this->context()->each($fn);
276
        }
277
278 1
        public function appendChild($child, ...$optionals)
279
        {
280
                // If the user has requested ['root' => null] at construction time
281
                // 'context()' promotes DOMDocument as root node.
282 1
                $context     = $this->context();
283 1
                $new_context = $context->appendChild($child, ...$optionals);
284
285 1
                return $this->chooseContext($context, $new_context);
286
        }
287
288
        // Alias of appendChild().
289 1
        public function add($child, ...$optionals)
290
        {
291 1
                return $this->appendChild($child, ...$optionals);
292
        }
293
294 1 View Code Duplication
        public function prependSibling($sibling, ...$optionals)
295
        {
296 1
                if ($this->document->dom->documentElement === null) {
297
                        // If the document doesn't have at least one root node,
298
                        // the sibling creation fails. In this case we replace
299
                        // the sibling creation with the creation of a generic node.
300 1
                        return $this->appendChild($sibling, ...$optionals);
301
                }
302
303 1
                $context     = $this->context();
304 1
                $new_context = $context->prependSibling($sibling, ...$optionals);
305
306 1
                return $this->chooseContext($context, $new_context);
307
        }
308
309
        // Alias of prependSibling().
310 1
        public function prepend($sibling, ...$optionals)
311
        {
312 1
                return $this->prependSibling($sibling, ...$optionals);
313
        }
314
315
        // Alias of prependSibling().
316 1
        public function insertSiblingBefore($sibling, ...$optionals)
317
        {
318 1
                return $this->prependSibling($sibling, ...$optionals);
319
        }
320
321 1 View Code Duplication
        public function appendSibling($sibling, ...$optionals)
322
        {
323 1
                if ($this->document->dom->documentElement === null) {
324
                        // If the document doesn't have at least one root node,
325
                        // the sibling creation fails. In this case we replace
326
                        // the sibling creation with the creation of a generic node.
327 1
                        return $this->appendChild($sibling, ...$optionals);
328
                }
329
330 1
                $context     = $this->context();
331 1
                $new_context = $context->appendSibling($sibling, ...$optionals);
332
333 1
                return $this->chooseContext($context, $new_context);
334
        }
335
336
        // Alias of appendSibling().
337 1
        public function append($sibling, ...$optionals)
338
        {
339 1
                return $this->appendSibling($sibling, ...$optionals);
340
        }
341
342
        // Alias of appendSibling().
343 1
        public function insertSiblingAfter($sibling, ...$optionals)
344
        {
345 1
                return $this->appendSibling($sibling, ...$optionals);
346
        }
347
348 1
        public function setAttribute(...$arguments)
349
        {
350 1
                $this->context()->setAttribute(...$arguments);
351
352 1
                return $this;
353
        }
354
355
        // Alias of setAttribute().
356 1
        public function attr(...$arguments)
357
        {
358 1
                return $this->setAttribute(...$arguments);
359
        }
360
361 1
        public function appendText($text)
362
        {
363 1
                $this->context()->appendText($text);
364
365 1
                return $this;
366
        }
367
368 1
        public function appendCdata($text)
369
        {
370 1
                $this->context()->appendCdata($text);
371
372 1
                return $this;
373
        }
374
375 1
        public function setText($text)
376
        {
377 1
                $this->context()->setText($text);
378
379 1
                return $this;
380
        }
381
382
        // Alias of setText().
383 1
        public function text($text)
384
        {
385 1
                return $this->setText($text);
386
        }
387
388 1
        public function setCdata($text)
389
        {
390 1
                $this->context()->setCdata($text);
391
392 1
                return $this;
393
        }
394
395
        // Alias of setCdata().
396 1
        public function cdata($text)
397
        {
398 1
                return $this->setCdata($text);
399
        }
400
401 1
        public function remove(...$xpath)
402
        {
403 1
                $this->context()->remove(...$xpath);
404
405 1
                return $this;
406
        }
407
408
        private $context;
409
        private $contextEl;
410
411 1
        protected function context()
412
        {
413 1
                if ($this->document->dom->documentElement === null) {
414
                        // If the user has requested ['root' => null] at construction time
415
                        // the 'documentElement' property is null because we have not created
416
                        // a root node yet. Whether there is not a root node, the DOMDocument
417
                        // is promoted as root node.
418 1
                        if ($this->context === null) {
419 1
                                $this->context = new FluidContext($this->document, $this->handler, $this->document->dom);
420
                        }
421
422 1
                        return $this->context;
423
                }
424
425 1
                if ($this->contextEl !== $this->document->dom->documentElement) {
426
                        // The user can prepend a root node to the current root node.
427
                        // In this case we have to update the context with the new first root node.
428 1
                        $this->context   = new FluidContext($this->document, $this->handler, $this->document->dom->documentElement);
429 1
                        $this->contextEl = $this->document->dom->documentElement;
430
                }
431
432 1
                return $this->context;
433
        }
434
435 1
        protected function chooseContext($help_context, $new_context)
436
        {
437
                // If the two contextes are diffent, the user has requested
438
                // a switch of the context and we have to return it.
439 1
                if ($help_context !== $new_context) {
440 1
                        return $new_context;
441
                }
442
443 1
                return $this;
444
        }
445
}
446
447
class FluidNamespace
448
{
449
        const ID   = 'id'  ;
450
        const URI  = 'uri' ;
451
        const MODE = 'mode';
452
453
        const MODE_IMPLICIT = 0;
454
        const MODE_EXPLICIT = 1;
455
456
        private $config = [ self::ID   => '',
457
                            self::URI  => '',
458
                            self::MODE => self::MODE_EXPLICIT ];
459
460 1
        public function __construct($id, $uri, $mode = 1)
461
        {
462 1
                if (\is_array($id)) {
463 1
                        $args = $id;
464 1
                        $id   = $args[self::ID];
465 1
                        $uri  = $args[self::URI];
466
467 1
                        if (isset($args[self::MODE])) {
468 1
                                $mode = $args[self::MODE];
469
                        }
470
                }
471
472 1
                $this->config[self::ID]   = $id;
473 1
                $this->config[self::URI]  = $uri;
474 1
                $this->config[self::MODE] = $mode;
475 1
        }
476
477 1
        public function id()
478
        {
479 1
                return $this->config[self::ID];
480
        }
481
482 1
        public function uri()
483
        {
484 1
                return $this->config[self::URI];
485
        }
486
487 1
        public function mode()
488
        {
489 1
                return $this->config[self::MODE];
490
        }
491
492 1
        public function querify($xpath)
493
        {
494 1
                $id = $this->id();
495
496 1
                if (! empty($id)) {
497 1
                        $id .= ':';
498
                }
499
500
                // An XPath query may not start with a slash ('/').
501
                // Relative queries are an example '../target".
502 1
                $new_xpath = '';
503
504 1
                $nodes = \explode('/', $xpath);
505
506 1
                foreach ($nodes as $node) {
507
                        // An XPath query may have multiple slashes ('/')
508
                        // example: //target
509 1
                        if ($node) {
510 1
                                $new_xpath .= "{$id}{$node}";
511
                        }
512
513 1
                        $new_xpath .= '/';
514
                }
515
516
                // Removes the last appended slash.
517 1
                return \substr($new_xpath, 0, -1);
518
        }
519
}
520
521
} // END OF NAMESPACE FluidXml
522
523
namespace FluidXml\Core
524
{
525
526
use \FluidXml\FluidXml;
527
use \FluidXml\FluidNamespace;
528
529
use function \FluidXml\is_an_xml_string;
530
use function \FluidXml\domnodes_to_string;
531
532
interface FluidInterface
533
{
534
        /**
535
         * Executes an XPath query.
536
         *
537
         * ```php
538
         * $xml = fluidxml();
539
540
         * $xml->query("/doc/book[@id='123']");
541
         *
542
         * // Relative queries are valid.
543
         * $xml->query("/doc")->query("book[@id='123']");
544
         * ```
545
         *
546
         * @param string $xpath The XPath to execute.
547
         *
548
         * @return FluidContext The context associated to the DOMNodeList.
549
         */
550
        public function query(...$xpath);
551
        public function times($times, callable $fn = null);
552
        public function each(callable $fn);
553
554
        /**
555
         * Append a new node as child of the current context.
556
         *
557
         * ```php
558
         * $xml = fluidxml();
559
560
         * $xml->appendChild('title', 'The Theory Of Everything');
561
         * $xml->appendChild([ 'author' => 'S. Hawking' ]);
562
         *
563
         * $xml->appendChild('chapters', true)->appendChild('chapter', ['id'=> 1]);
564
         *
565
         * ```
566
         *
567
         * @param string|array $child The child/children to add.
568
         * @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...
569
         * @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...
570
         *                            or the context of the created node.
571
         *
572
         * @return FluidContext The context associated to the DOMNodeList.
573
         */
574
        public function appendChild($child, ...$optionals);
575
        public function prependSibling($sibling, ...$optionals);
576
        public function appendSibling($sibling, ...$optionals);
577
        public function setAttribute(...$arguments);
578
        public function setText($text);
579
        public function appendText($text);
580
        public function setCdata($text);
581
        public function appendCdata($text);
582
        public function remove(...$xpath);
583
        public function xml($strip = false);
584
        // Aliases:
585
        public function add($child, ...$optionals);
586
        public function prepend($sibling, ...$optionals);
587
        public function insertSiblingBefore($sibling, ...$optionals);
588
        public function append($sibling, ...$optionals);
589
        public function insertSiblingAfter($sibling, ...$optionals);
590
        public function attr(...$arguments);
591
        public function text($text);
592
}
593
594
trait ReservedCallTrait
595
{
596 1
        public function __call($method, $arguments)
597
        {
598 1
                $m = "{$method}_";
599
600 1
                if (\method_exists($this, $m)) {
601 1
                        return $this->$m(...$arguments);
602
                }
603
604
                throw new \Exception("Method '$method' not found.");
605
        }
606
}
607
608
trait ReservedCallStaticTrait
609
{
610 1
        public static function __callStatic($method, $arguments)
611
        {
612 1
                $m = "{$method}_";
613
614 1
                if (\method_exists(static::class, $m)) {
615 1
                        return static::$m(...$arguments);
616
                }
617
618
                throw new \Exception("Method '$method' not found.");
619
        }
620
}
621
622
trait NewableTrait
623
{
624
        // This method should be called 'new',
625
        // but for compatibility with PHP 5.6
626
        // it is shadowed by the __callStatic() method.
627 1
        public static function new_(...$arguments)
628
        {
629 1
                return new static(...$arguments);
630
        }
631
}
632
633
class FluidDocument
634
{
635
        public $dom;
636
        public $xpath;
637
        public $namespaces = [];
638
        public $handler;
639
}
640
641
class FluidRepeater
642
{
643
        private $document;
644
        private $handler;
645
        private $context;
646
        private $times;
647
648 1
        public function __construct($document, $handler, $context, $times)
649
        {
650 1
                $this->document = $document;
651 1
                $this->handler  = $handler;
652 1
                $this->context  = $context;
653 1
                $this->times    = $times;
654 1
        }
655
656 1
        public function __call($method, $arguments)
657
        {
658 1
                $nodes = [];
659 1
                $new_context = $this->context;
660
661 1
                for ($i = 0, $l = $this->times; $i < $l; ++$i) {
662 1
                        $new_context = $this->context->$method(...$arguments);
663 1
                        $nodes       = \array_merge($nodes, $new_context->asArray());
664
                }
665
666 1
                if ($new_context !== $this->context) {
667 1
                        return new FluidContext($this->document, $this->handler, $nodes);
668
                }
669
670 1
                return $this->context;
671
        }
672
}
673
674
class FluidInsertionHandler
675
{
676
        private $document;
677
        private $dom;
678
        private $namespaces;
679
680 1
        public function __construct($document)
681
        {
682 1
                $this->document   = $document;
683 1
                $this->dom        = $document->dom;
684 1
                $this->namespaces =& $document->namespaces;
685 1
        }
686
687 1
        public function insertElement(&$nodes, $element, &$optionals, $fn, $orig_context)
688
        {
689 1
                list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals);
690
691 1
                $new_nodes = [];
692
693 1
                foreach ($nodes as $n) {
694 1
                        foreach ($element as $k => $v) {
695
                                // I give up, it's a too complex job for only one method like me.
696 1
                                $cx        = $this->handleInsertion($n, $k, $v, $fn, $optionals);
697 1
                                $new_nodes = \array_merge($new_nodes, $cx);
698
                        }
699
                }
700
701 1
                $new_context = $this->newContext($new_nodes);
702
703
                // Setting the attributes is an help that the appendChild method
704
                // offers to the user and is the same of:
705
                // 1. appending a child switching the context
706
                // 2. setting the attributes over the new context.
707 1
                if (! empty($attributes)) {
708 1
                        $new_context->setAttribute($attributes);
709
                }
710
711 1
                if ($switch_context) {
712 1
                        return $new_context;
713
                }
714
715 1
                return $orig_context;
716
        }
717
718 1
        protected function newContext(&$context)
719
        {
720 1
                return new FluidContext($this->document, $this, $context);
721
        }
722
723 1
        protected function handleOptionals($element, &$optionals)
724
        {
725 1
                if (! \is_array($element)) {
726 1
                        $element = [ $element ];
727
                }
728
729 1
                $switch_context = false;
730 1
                $attributes     = [];
731
732 1
                foreach ($optionals as $opt) {
733 1
                        if (\is_array($opt)) {
734 1
                                $attributes = $opt;
735
736 1
                        } else if (\is_bool($opt)) {
737 1
                                $switch_context = $opt;
738
739 1
                        } else if (\is_string($opt)) {
740 1
                                $e = \array_pop($element);
741
742 1
                                $element[$e] = $opt;
743
744
                        } else {
745 1
                                throw new \Exception("Optional argument '$opt' not recognized.");
746
                        }
747
                }
748
749 1
                return [ $element, $attributes, $switch_context ];
750
        }
751
752
753 1
        protected function handleInsertion($parent, $k, $v, $fn, &$optionals)
754
        {
755
                // This is an highly optimized method.
756
                // Good code design would split this method in many different handlers
757
                // each one with its own checks. But it is too much expensive in terms
758
                // of performances for a core method like this, so this implementation
759
                // is prefered to collapse many identical checks to one.
760
761
                //////////////////////
762
                // Key is a string. //
763
                //////////////////////
764
765
                ///////////////////////////////////////////////////////
766 1
                $k_is_string    = \is_string($k);
767 1
                $v_is_string    = \is_string($v);
768 1
                $v_is_xml       = $v_is_string && is_an_xml_string($v);
769 1
                $k_is_special   = $k_is_string && $k[0] === '@';
770 1
                $k_isnt_special = ! $k_is_special;
771 1
                $v_isnt_string  = ! $v_is_string;
772 1
                $v_isnt_xml     = ! $v_is_xml;
773
                ///////////////////////////////////////////////////////
774
775 1
                if ($k_is_string && $k_isnt_special && $v_is_string && $v_isnt_xml) {
776 1
                        return $this->insertStringString($parent, $k, $v, $fn, $optionals);
777
                }
778
779 1
                if ($k_is_string && $k_isnt_special && $v_is_string && $v_is_xml) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
780
                        // TODO
781
                }
782
783
                //////////////////////////////////////////////
784 1
                $k_is_special_c = $k_is_special && $k === '@';
785
                //////////////////////////////////////////////
786
787 1
                if ($k_is_special_c && $v_is_string) {
788 1
                        return $this->insertSpecialContent($parent, $k, $v, $fn, $optionals);
789
                }
790
791
                /////////////////////////////////////////////////////
792 1
                $k_is_special_a = $k_is_special && ! $k_is_special_c;
793
                /////////////////////////////////////////////////////
794
795 1
                if ($k_is_special_a && $v_is_string) {
796 1
                        return $this->insertSpecialAttribute($parent, $k, $v, $fn, $optionals);
797
                }
798
799 1
                if ($k_is_string && $v_isnt_string) {
800 1
                        return $this->insertStringMixed($parent, $k, $v, $fn, $optionals);
801
                }
802
803
                ////////////////////////
804
                // Key is an integer. //
805
                ////////////////////////
806
807
                ////////////////////////////////
808 1
                $k_is_integer = \is_integer($k);
809 1
                $v_is_array   = \is_array($v);
810
                ////////////////////////////////
811
812 1
                if ($k_is_integer && $v_is_array) {
813 1
                        return $this->insertIntegerArray($parent, $k, $v, $fn, $optionals);
814
                }
815
816 1
                if ($k_is_integer && $v_is_string && $v_isnt_xml) {
817 1
                        return $this->insertIntegerString($parent, $k, $v, $fn, $optionals);
818
                }
819
820 1
                if ($k_is_integer && $v_is_string && $v_is_xml) {
821 1
                        return $this->insertIntegerXml($parent, $k, $v, $fn, $optionals);
822
                }
823
824
                //////////////////////////////////////////
825 1
                $v_is_domdoc = $v instanceof \DOMDocument;
826
                //////////////////////////////////////////
827
828 1
                if ($k_is_integer && $v_is_domdoc) {
829 1
                        return $this->insertIntegerDomdocument($parent, $k, $v, $fn, $optionals);
830
                }
831
832
                ///////////////////////////////////////////////
833 1
                $v_is_domnodelist = $v instanceof \DOMNodeList;
834
                ///////////////////////////////////////////////
835
836 1
                if ($k_is_integer && $v_is_domnodelist) {
837 1
                        return $this->insertIntegerDomnodelist($parent, $k, $v, $fn, $optionals);
838
                }
839
840
                ///////////////////////////////////////
841 1
                $v_is_domnode = $v instanceof \DOMNode;
842
                ///////////////////////////////////////
843
844 1
                if ($k_is_integer && ! $v_is_domdoc && $v_is_domnode) {
845 1
                        return $this->insertIntegerDomnode($parent, $k, $v, $fn, $optionals);
846
                }
847
848
                //////////////////////////////////////////////////
849 1
                $v_is_simplexml = $v instanceof \SimpleXMLElement;
850
                //////////////////////////////////////////////////
851
852 1
                if ($k_is_integer && $v_is_simplexml) {
853 1
                        return $this->insertIntegerSimplexml($parent, $k, $v, $fn, $optionals);
854
                }
855
856
                ////////////////////////////////////////
857 1
                $v_is_fluidxml = $v instanceof FluidXml;
858
                ////////////////////////////////////////
859
860 1
                if ($k_is_integer && $v_is_fluidxml) {
861 1
                        return $this->insertIntegerFluidxml($parent, $k, $v, $fn, $optionals);
862
                }
863
864
                ///////////////////////////////////////////
865 1
                $v_is_fluidcx = $v instanceof FluidContext;
866
                ///////////////////////////////////////////
867
868 1
                if ($k_is_integer && $v_is_fluidcx) {
869 1
                        return $this->insertIntegerFluidcontext($parent, $k, $v, $fn, $optionals);
870
                }
871
872 1
                throw new \Exception('XML document not supported.');
873
        }
874
875 1
        protected function createElement($name, $value = null)
876
        {
877
                // The DOMElement instance must be different for every node,
878
                // otherwise only one element is attached to the DOM.
879
880 1
                $id  = null;
881 1
                $uri = null;
882
883
                // The node name can contain the namespace id prefix.
884
                // Example: xsl:template
885 1
                $colon_pos = \strpos($name, ':');
886
887 1
                if ($colon_pos !== false) {
888 1
                        $id   = \substr($name, 0, $colon_pos);
889 1
                        $name = \substr($name, $colon_pos + 1);
890
                }
891
892 1
                if ($id !== null) {
893 1
                        $ns  = $this->namespaces[$id];
894 1
                        $uri = $ns->uri();
895
896 1
                        if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) {
897 1
                                $name = "{$id}:{$name}";
898
                        }
899
                }
900
901
                // Algorithm 1:
902 1
                $el = new \DOMElement($name, $value, $uri);
903
904
                // Algorithm 2:
905
                // $el = $dom->createElement($name, $value);
906
907 1
                return $el;
908
        }
909
910 1
        protected function attachNodes($parent, $nodes, $fn)
911
        {
912 1
                if (! \is_array($nodes) && ! $nodes instanceof \Traversable) {
913 1
                        $nodes = [ $nodes ];
914
                }
915
916 1
                $context = [];
917
918 1
                foreach ($nodes as $el) {
919 1
                        $el        = $this->dom->importNode($el, true);
920 1
                        $context[] = $fn($parent, $el);
921
                }
922
923 1
                return $context;
924
        }
925
926 1
        protected function insertSpecialContent($parent, $k, $v)
927
        {
928
                // The user has passed an element text content:
929
                // [ '@' => 'Element content.' ]
930
931
                // Algorithm 1:
932 1
                $this->newContext($parent)->appendText($v);
933
934
                // Algorithm 2:
935
                // $this->setText($v);
936
937
                // The user can specify multiple '@' special elements
938
                // so Algorithm 1 is the right choice.
939
940 1
                return [];
941
        }
942
943 1
        protected function insertSpecialAttribute($parent, $k, $v)
944
        {
945
                // The user has passed an attribute name and an attribute value:
946
                // [ '@attribute' => 'Attribute content' ]
947
948 1
                $attr = \substr($k, 1);
949 1
                $this->newContext($parent)->setAttribute($attr, $v);
950
951 1
                return [];
952
        }
953
954 1 View Code Duplication
        protected function insertStringString($parent, $k, $v, $fn)
955
        {
956
                // The user has passed an element name and an element value:
957
                // [ 'element' => 'Element content' ]
958
959 1
                $el = $this->createElement($k, $v);
960 1
                $el = $fn($parent, $el);
961
962 1
                return [ $el ];
963
        }
964
965 1 View Code Duplication
        protected function insertStringMixed($parent, $k, $v, $fn, &$optionals)
966
        {
967
                // The user has passed one of these two cases:
968
                // - [ 'element' => [...] ]
969
                // - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ]
970
971 1
                $el = $this->createElement($k);
972 1
                $el = $fn($parent, $el);
973
974
                // The new children elements must be created in the order
975
                // they are supplied, so 'appendChild' is the perfect operation.
976 1
                $this->newContext($el)->appendChild($v, ...$optionals);
977
978 1
                return [ $el ];
979
        }
980
981 1
        protected function insertIntegerArray($parent, $k, $v, $fn, &$optionals)
982
        {
983
                // The user has passed a wrapper array:
984
                // [ [...], ... ]
985
986 1
                $context = [];
987
988 1
                foreach ($v as $kk => $vv) {
989 1
                        $cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals);
990
991 1
                        $context = \array_merge($context, $cx);
992
                }
993
994 1
                return $context;
995
        }
996
997 1
        protected function insertIntegerString($parent, $k, $v, $fn)
998
        {
999
                // The user has passed a node name without a node value:
1000
                // [ 'element', ... ]
1001
1002 1
                $el = $this->createElement($v);
1003 1
                $el = $fn($parent, $el);
1004
1005 1
                return [ $el ];
1006
        }
1007
1008 1
        protected function insertIntegerXml($parent, $k, $v, $fn)
1009
        {
1010
                // The user has passed an XML document instance:
1011
                // [ '<tag></tag>', DOMNode, SimpleXMLElement, FluidXml ]
1012
1013 1
                $wrapper = new \DOMDocument();
1014 1
                $wrapper->formatOutput       = true;
1015 1
                $wrapper->preserveWhiteSpace = false;
1016
1017 1
                $v = \ltrim($v);
1018
1019 1
                if ($v[1] === '?') {
1020 1
                        $wrapper->loadXML($v);
1021 1
                        $nodes = $wrapper->childNodes;
1022
                } else {
1023
                        // A way to import strings with multiple root nodes.
1024 1
                        $wrapper->loadXML("<root>$v</root>");
1025
1026
                        // Algorithm 1:
1027 1
                        $nodes = $wrapper->documentElement->childNodes;
1028
1029
                        // Algorithm 2:
1030
                        // $dom_xp = new \DOMXPath($dom);
1031
                        // $nodes = $dom_xp->query('/root/*');
1032
                }
1033
1034 1
                return $this->attachNodes($parent, $nodes, $fn);
1035
        }
1036
1037 1
        protected function insertIntegerDomdocument($parent, $k, $v, $fn)
1038
        {
1039
                // A DOMDocument can have multiple root nodes.
1040
1041
                // Algorithm 1:
1042 1
                return $this->attachNodes($parent, $v->childNodes, $fn);
1043
1044
                // Algorithm 2:
1045
                // return $this->attachNodes($parent, $v->documentElement, $fn);
1046
        }
1047
1048 1
        protected function insertIntegerDomnodelist($parent, $k, $v, $fn)
1049
        {
1050 1
                return $this->attachNodes($parent, $v, $fn);
1051
        }
1052
1053 1
        protected function insertIntegerDomnode($parent, $k, $v, $fn)
1054
        {
1055 1
                return $this->attachNodes($parent, $v, $fn);
1056
        }
1057
1058 1
        protected function insertIntegerSimplexml($parent, $k, $v, $fn)
1059
        {
1060 1
                return $this->attachNodes($parent, \dom_import_simplexml($v), $fn);
1061
        }
1062
1063 1
        protected function insertIntegerFluidxml($parent, $k, $v, $fn)
1064
        {
1065 1
                return $this->attachNodes($parent, $v->dom()->documentElement, $fn);
1066
        }
1067
1068 1
        protected function insertIntegerFluidcontext($parent, $k, $v, $fn)
1069
        {
1070 1
                return $this->attachNodes($parent, $v->asArray(), $fn);
1071
        }
1072
}
1073
1074
class FluidContext implements FluidInterface, \ArrayAccess, \Iterator
1075
{
1076
        use NewableTrait,
1077
            ReservedCallTrait,          // For compatibility with PHP 5.6.
1078
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
1079
1080
        private $document;
1081
        private $handler;
1082
        private $nodes = [];
1083
        private $seek = 0;
1084
1085 1
        public function __construct($document, $handler, $context)
1086
        {
1087 1
                $this->document = $document;
1088 1
                $this->handler  = $handler;
1089
1090 1
                if (! \is_array($context) && ! $context instanceof \Traversable) {
1091
                        // DOMDocument, DOMElement and DOMNode are not iterable.
1092
                        // DOMNodeList and FluidContext are iterable.
1093 1
                        $context = [ $context ];
1094
                }
1095
1096 1
                foreach ($context as $n) {
1097 1
                        if (! $n instanceof \DOMNode) {
1098
                                throw new \Exception('Node type not recognized.');
1099
                        }
1100
1101 1
                        $this->nodes[] = $n;
1102
                }
1103 1
        }
1104
1105 1
        public function asArray()
1106
        {
1107 1
                return $this->nodes;
1108
        }
1109
1110
        // \ArrayAccess interface.
1111 1
        public function offsetSet($offset, $value)
1112
        {
1113
                // if (\is_null($offset)) {
1114
                //         $this->nodes[] = $value;
1115
                // } else {
1116
                //         $this->nodes[$offset] = $value;
1117
                // }
1118 1
                throw new \Exception('Setting a context element is not allowed.');
1119
        }
1120
1121
        // \ArrayAccess interface.
1122 1
        public function offsetExists($offset)
1123
        {
1124 1
                return isset($this->nodes[$offset]);
1125
        }
1126
1127
        // \ArrayAccess interface.
1128 1
        public function offsetUnset($offset)
1129
        {
1130
                // unset($this->nodes[$offset]);
1131 1
                \array_splice($this->nodes, $offset, 1);
1132 1
        }
1133
1134
        // \ArrayAccess interface.
1135 1
        public function offsetGet($offset)
1136
        {
1137 1
                if (isset($this->nodes[$offset])) {
1138 1
                        return $this->nodes[$offset];
1139
                }
1140
1141 1
                return null;
1142
        }
1143
1144
        // \Iterator interface.
1145 1
        public function rewind()
1146
        {
1147 1
                $this->seek = 0;
1148 1
        }
1149
1150
        // \Iterator interface.
1151 1
        public function current()
1152
        {
1153 1
                return $this->nodes[$this->seek];
1154
        }
1155
1156
        // \Iterator interface.
1157 1
        public function key()
1158
        {
1159 1
                return $this->seek;
1160
        }
1161
1162
        // \Iterator interface.
1163 1
        public function next()
1164
        {
1165 1
                ++$this->seek;
1166 1
        }
1167
1168
        // \Iterator interface.
1169 1
        public function valid()
1170
        {
1171 1
                return isset($this->nodes[$this->seek]);
1172
        }
1173
1174 1
        public function length()
1175
        {
1176 1
                return \count($this->nodes);
1177
        }
1178
1179 1
        public function query(...$xpath)
1180
        {
1181 1
                if (\is_array($xpath[0])) {
1182 1
                        $xpath = $xpath[0];
1183
                }
1184
1185 1
                $results = [];
1186
1187 1
                $xp = $this->document->xpath;
1188
1189 1
                foreach ($this->nodes as $n) {
1190 1
                        foreach ($xpath as $x) {
1191
                                // Returns a DOMNodeList.
1192 1
                                $res = $xp->query($x, $n);
1193
1194
                                // Algorithm 1:
1195
                                // $results = \array_merge($results, \iterator_to_array($res));
1196
1197
                                // Algorithm 2:
1198
                                // It is faster than \iterator_to_array and a lot faster
1199
                                // than \iterator_to_array + \array_merge.
1200 1
                                foreach ($res as $r) {
1201 1
                                        $results[] = $r;
1202
                                }
1203
1204
                                // Algorithm 3:
1205
                                // for ($i = 0, $l = $res->length; $i < $l; ++$i) {
1206
                                //         $results[] = $res->item($i);
1207
                                // }
1208
                        }
1209
                }
1210
1211
                // Performing over multiple sibling nodes a query that ascends
1212
                // the xpath, relative (../..) or absolute (//), returns identical
1213
                // matching results that must be collapsed in an unique result
1214
                // otherwise a subsequent operation is performed multiple times.
1215 1
                $results = $this->filterQueryResults($results);
1216
1217 1
                return $this->newContext($results);
1218
        }
1219
1220 1
        public function times($times, callable $fn = null)
1221
        {
1222 1
                if ($fn === null) {
1223 1
                        return new FluidRepeater($this->document, $this->handler, $this, $times);
1224
                }
1225
1226 1
                for ($i = 0; $i < $times; ++$i) {
1227 1
                        $this->callfn($fn, [$this, $i]);
1228
                }
1229
1230 1
                return $this;
1231
        }
1232
1233 1
        public function each(callable $fn)
1234
        {
1235 1
                foreach ($this->nodes as $i => $n) {
1236 1
                        $cx = $this->newContext($n);
1237
1238 1
                        $this->callfn($fn, [$cx, $i, $n]);
1239
                }
1240
1241 1
                return $this;
1242
        }
1243
1244
        // appendChild($child, $value?, $attributes? = [], $switchContext? = false)
1245 1
        public function appendChild($child, ...$optionals)
1246
        {
1247
                return $this->handler->insertElement($this->nodes, $child, $optionals, function($parent, $element) {
1248 1
                        return $parent->appendChild($element);
1249 1
                }, $this);
1250
        }
1251
1252
        // Alias of appendChild().
1253 1
        public function add($child, ...$optionals)
1254
        {
1255 1
                return $this->appendChild($child, ...$optionals);
1256
        }
1257
1258 1
        public function prependSibling($sibling, ...$optionals)
1259
        {
1260
                return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) {
1261 1
                        return $sibling->parentNode->insertBefore($element, $sibling);
1262 1
                }, $this);
1263
        }
1264
1265
        // Alias of prependSibling().
1266 1
        public function prepend($sibling, ...$optionals)
1267
        {
1268 1
                return $this->prependSibling($sibling, ...$optionals);
1269
        }
1270
1271
        // Alias of prependSibling().
1272 1
        public function insertSiblingBefore($sibling, ...$optionals)
1273
        {
1274 1
                return $this->prependSibling($sibling, ...$optionals);
1275
        }
1276
1277
        public function appendSibling($sibling, ...$optionals)
1278
        {
1279 1
                return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) {
1280
                        // If ->nextSibling is null, $element is simply appended as last sibling.
1281 1
                        return $sibling->parentNode->insertBefore($element, $sibling->nextSibling);
1282 1
                }, $this);
1283
        }
1284
1285
        // Alias of appendSibling().
1286 1
        public function append($sibling, ...$optionals)
1287
        {
1288 1
                return $this->appendSibling($sibling, ...$optionals);
1289
        }
1290
1291
        // Alias of appendSibling().
1292 1
        public function insertSiblingAfter($sibling, ...$optionals)
1293
        {
1294 1
                return $this->appendSibling($sibling, ...$optionals);
1295
        }
1296
1297
        // Arguments can be in the form of:
1298
        // setAttribute($name, $value)
1299
        // setAttribute(['name' => 'value', ...])
1300 1
        public function setAttribute(...$arguments)
1301
        {
1302
                // Default case is:
1303
                // [ 'name' => 'value', ... ]
1304 1
                $attrs = $arguments[0];
1305
1306
                // If the first argument is not an array,
1307
                // the user has passed two arguments:
1308
                // 1. is the attribute name
1309
                // 2. is the attribute value
1310 1
                if (! \is_array($arguments[0])) {
1311 1
                        $attrs = [$arguments[0] => $arguments[1]];
1312
                }
1313
1314 1
                foreach ($this->nodes as $n) {
1315 1
                        foreach ($attrs as $k => $v) {
1316
                                // Algorithm 1:
1317 1
                                $n->setAttribute($k, $v);
1318
1319
                                // Algorithm 2:
1320
                                // $n->setAttributeNode(new \DOMAttr($k, $v));
1321
1322
                                // Algorithm 3:
1323
                                // $n->appendChild(new \DOMAttr($k, $v));
1324
1325
                                // Algorithm 2 and 3 have a different behaviour
1326
                                // from Algorithm 1.
1327
                                // The attribute is still created or setted, but
1328
                                // changing the value of an existing attribute
1329
                                // changes even the order of that attribute
1330
                                // in the attribute list.
1331
                        }
1332
                }
1333
1334 1
                return $this;
1335
        }
1336
1337
        // Alias of setAttribute().
1338 1
        public function attr(...$arguments)
1339
        {
1340 1
                return $this->setAttribute(...$arguments);
1341
        }
1342
1343 1
        public function appendText($text)
1344
        {
1345 1
                foreach ($this->nodes as $n) {
1346 1
                        $n->appendChild(new \DOMText($text));
1347
                }
1348
1349 1
                return $this;
1350
        }
1351
1352 1
        public function appendCdata($text)
1353
        {
1354 1
                foreach ($this->nodes as $n) {
1355 1
                        $n->appendChild(new \DOMCDATASection($text));
1356
                }
1357
1358 1
                return $this;
1359
        }
1360
1361 1
        public function setText($text)
1362
        {
1363 1
                foreach ($this->nodes as $n) {
1364
                        // Algorithm 1:
1365 1
                        $n->nodeValue = $text;
1366
1367
                        // Algorithm 2:
1368
                        // foreach ($n->childNodes as $c) {
1369
                        //         $n->removeChild($c);
1370
                        // }
1371
                        // $n->appendChild(new \DOMText($text));
1372
1373
                        // Algorithm 3:
1374
                        // foreach ($n->childNodes as $c) {
1375
                        //         $n->replaceChild(new \DOMText($text), $c);
1376
                        // }
1377
                }
1378
1379 1
                return $this;
1380
        }
1381
1382
        // Alias of setText().
1383 1
        public function text($text)
1384
        {
1385 1
                return $this->setText($text);
1386
        }
1387
1388 1
        public function setCdata($text)
1389
        {
1390 1
                foreach ($this->nodes as $n) {
1391 1
                        $n->nodeValue = '';
1392 1
                        $n->appendChild(new \DOMCDATASection($text));
1393
                }
1394
1395 1
                return $this;
1396
        }
1397
1398
        // Alias of setCdata().
1399 1
        public function cdata($text)
1400
        {
1401 1
                return $this->setCdata($text);
1402
        }
1403
1404 1
        public function remove(...$xpath)
1405
        {
1406
                // Arguments can be empty, a string or an array of strings.
1407
1408 1
                if (empty($xpath)) {
1409
                        // The user has requested to remove the nodes of this context.
1410 1
                        $targets = $this->nodes;
1411
                } else {
1412 1
                        $targets = $this->query(...$xpath);
1413
                }
1414
1415 1
                foreach ($targets as $t) {
1416 1
                        $t->parentNode->removeChild($t);
1417
                }
1418
1419 1
                return $this;
1420
        }
1421
1422 1
        public function xml($strip = false)
1423
        {
1424 1
                return domnodes_to_string($this->nodes);
1425
        }
1426
1427 1
        protected function newContext(&$context)
1428
        {
1429 1
                return new FluidContext($this->document, $this->handler, $context);
1430
        }
1431
1432 1
        protected function filterQueryResults(&$results)
1433
        {
1434 1
                $set = [];
1435
1436 1
                foreach ($results as $r) {
1437 1
                        $found = false;
1438
1439 1
                        foreach ($set as $u) {
1440 1
                                if ($r === $u) {
1441 1
                                        $found = true;
1442
                                }
1443
                        }
1444
1445 1
                        if (! $found) {
1446 1
                                $set[] = $r;
1447
                        }
1448
                }
1449
1450 1
                return $set;
1451
        }
1452
1453 1
        protected function callfn($fn, $args)
1454
        {
1455 1
                if ($fn instanceof \Closure) {
1456 1
                        $bind = \array_shift($args);
1457
1458 1
                        $fn = $fn->bindTo($bind);
1459
1460
                        // It is faster than \call_user_func.
1461 1
                        return $fn(...$args);
1462
                }
1463
1464 1
                return \call_user_func($fn, ...$args);
1465
        }
1466
}
1467
1468
} // END OF NAMESPACE FluidXml\Core
1469