Completed
Push — master ( 5213ea...b697ca )
by Daniele
04:53
created

FluidContext::each()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
crap 2
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 1
                        $file     = $document;
152 1
                        $document = \file_get_contents($file);
153
154
                        // file_get_contents returns false if it can't read.
155 1
                        if (! $document) {
156 1
                                throw new \Exception("File '$file' not accessible.");
157
                        }
158
                }
159
160 1
                return (new FluidXml(['root' => null]))->appendChild($document);
161
        }
162
163 1
        public function __construct($root = null, $options = [])
164
        {
165 1
                $defaults = [ 'root'       => self::ROOT_NODE,
166 1
                              'version'    => '1.0',
167 1
                              'encoding'   => 'UTF-8',
168
                              'stylesheet' => null ];
169
170 1
                if (\is_string($root)) {
171
                        // The root option can be specified as first argument
172
                        // because it is the most common.
173 1
                        $defaults['root'] = $root;
174 1
                } else if (\is_array($root)) {
175
                        // If the first argument is an array, the user has skipped
176
                        // the root option and is passing a bunch of options all together.
177 1
                        $options = $root;
178
                }
179
180 1
                $opts = \array_merge($defaults, $options);
181
182 1
                $this->document = new FluidDocument();
183 1
                $doc            = $this->document;
184
185 1
                $doc->dom = new \DOMDocument($opts['version'], $opts['encoding']);
186 1
                $doc->dom->formatOutput       = true;
187 1
                $doc->dom->preserveWhiteSpace = false;
188
189 1
                $doc->xpath    = new \DOMXPath($doc->dom);
190
191 1
                $this->handler = new FluidInsertionHandler($doc);
192
193 1
                if (! empty($opts['root'])) {
194 1
                        $this->appendSibling($opts['root']);
195
                }
196
197 1
                if (! empty($opts['stylesheet'])) {
198
                        $attrs = 'type="text/xsl" '
199 1
                               . "encoding=\"{$opts['encoding']}\" "
200 1
                               . 'indent="yes" '
201 1
                               . "href=\"{$opts['stylesheet']}\"";
202 1
                        $stylesheet = new \DOMProcessingInstruction('xml-stylesheet', $attrs);
203
204 1
                        $doc->dom->insertBefore($stylesheet, $doc->dom->documentElement);
205
                }
206 1
        }
207
208 1
        public function xml($strip = false)
209
        {
210 1
                if ($strip) {
211 1
                        return domdocument_to_string_without_headers($this->document->dom);
212
                }
213
214 1
                return $this->document->dom->saveXML();
215
        }
216
217 1
        public function dom()
218
        {
219 1
                return $this->document->dom;
220
        }
221
222 1
        public function namespaces()
223
        {
224 1
                return $this->document->namespaces;
225
        }
226
227
        // This method should be called 'namespace',
228
        // but for compatibility with PHP 5.6
229
        // it is shadowed by the __call() method.
230 1
        protected function namespace_(...$arguments)
231
        {
232 1
                $namespaces = [];
233
234 1
                if (\is_string($arguments[0])) {
235 1
                        $args = [ $arguments[0], $arguments[1] ];
236
237 1
                        if (isset($arguments[2])) {
238 1
                                $args[] = $arguments[2];
239
                        }
240
241 1
                        $namespaces[] = new FluidNamespace(...$args);
242 1
                } else if (\is_array($arguments[0])) {
243 1
                        $namespaces = $arguments[0];
244
                } else {
245 1
                        $namespaces = $arguments;
246
                }
247
248 1
                foreach ($namespaces as $n) {
249 1
                        $this->document->namespaces[$n->id()] = $n;
250 1
                        $this->document->xpath->registerNamespace($n->id(), $n->uri());
251
                }
252
253 1
                return $this;
254
        }
255
256 1
        public function query(...$xpath)
257
        {
258 1
                return $this->context()->query(...$xpath);
259
        }
260
261 1
        public function times($times, callable $fn = null)
262
        {
263 1
                return $this->context()->times($times, $fn);
264
        }
265
266 1
        public function each(callable $fn)
267
        {
268 1
                return $this->context()->each($fn);
269
        }
270
271 1
        public function appendChild($child, ...$optionals)
272
        {
273
                // If the user has requested ['root' => null] at construction time
274
                // 'context()' promotes DOMDocument as root node.
275 1
                $context     = $this->context();
276 1
                $new_context = $context->appendChild($child, ...$optionals);
277
278 1
                return $this->chooseContext($context, $new_context);
279
        }
280
281
        // Alias of appendChild().
282 1
        public function add($child, ...$optionals)
283
        {
284 1
                return $this->appendChild($child, ...$optionals);
285
        }
286
287 1 View Code Duplication
        public function prependSibling($sibling, ...$optionals)
288
        {
289 1
                if ($this->document->dom->documentElement === null) {
290
                        // If the document doesn't have at least one root node,
291
                        // the sibling creation fails. In this case we replace
292
                        // the sibling creation with the creation of a generic node.
293 1
                        return $this->appendChild($sibling, ...$optionals);
294
                }
295
296 1
                $context     = $this->context();
297 1
                $new_context = $context->prependSibling($sibling, ...$optionals);
298
299 1
                return $this->chooseContext($context, $new_context);
300
        }
301
302
        // Alias of prependSibling().
303 1
        public function prepend($sibling, ...$optionals)
304
        {
305 1
                return $this->prependSibling($sibling, ...$optionals);
306
        }
307
308
        // Alias of prependSibling().
309 1
        public function insertSiblingBefore($sibling, ...$optionals)
310
        {
311 1
                return $this->prependSibling($sibling, ...$optionals);
312
        }
313
314 1 View Code Duplication
        public function appendSibling($sibling, ...$optionals)
315
        {
316 1
                if ($this->document->dom->documentElement === null) {
317
                        // If the document doesn't have at least one root node,
318
                        // the sibling creation fails. In this case we replace
319
                        // the sibling creation with the creation of a generic node.
320 1
                        return $this->appendChild($sibling, ...$optionals);
321
                }
322
323 1
                $context     = $this->context();
324 1
                $new_context = $context->appendSibling($sibling, ...$optionals);
325
326 1
                return $this->chooseContext($context, $new_context);
327
        }
328
329
        // Alias of appendSibling().
330 1
        public function append($sibling, ...$optionals)
331
        {
332 1
                return $this->appendSibling($sibling, ...$optionals);
333
        }
334
335
        // Alias of appendSibling().
336 1
        public function insertSiblingAfter($sibling, ...$optionals)
337
        {
338 1
                return $this->appendSibling($sibling, ...$optionals);
339
        }
340
341 1
        public function setAttribute(...$arguments)
342
        {
343 1
                $this->context()->setAttribute(...$arguments);
344
345 1
                return $this;
346
        }
347
348
        // Alias of setAttribute().
349 1
        public function attr(...$arguments)
350
        {
351 1
                return $this->setAttribute(...$arguments);
352
        }
353
354 1
        public function appendText($text)
355
        {
356 1
                $this->context()->appendText($text);
357
358 1
                return $this;
359
        }
360
361 1
        public function appendCdata($text)
362
        {
363 1
                $this->context()->appendCdata($text);
364
365 1
                return $this;
366
        }
367
368 1
        public function setText($text)
369
        {
370 1
                $this->context()->setText($text);
371
372 1
                return $this;
373
        }
374
375
        // Alias of setText().
376 1
        public function text($text)
377
        {
378 1
                return $this->setText($text);
379
        }
380
381 1
        public function setCdata($text)
382
        {
383 1
                $this->context()->setCdata($text);
384
385 1
                return $this;
386
        }
387
388
        // Alias of setCdata().
389 1
        public function cdata($text)
390
        {
391 1
                return $this->setCdata($text);
392
        }
393
394 1
        public function remove(...$xpath)
395
        {
396 1
                $this->context()->remove(...$xpath);
397
398 1
                return $this;
399
        }
400
401
        private $context;
402
        private $contextEl;
403
404 1
        protected function context()
405
        {
406 1
                $el = $this->document->dom->documentElement;
407
408 1
                if ($el === null) {
409
                        // Whether there is not a root node
410
                        // the DOMDocument is promoted as root node.
411 1
                        $el = $this->document->dom;
412
                }
413
414 1
                if ($this->context === null || $el !== $this->contextEl) {
415
                        // The user can prepend a root node to the current root node.
416
                        // In this case we have to update the context with the new first root node.
417 1
                        $this->context   = new FluidContext($this->document, $this->handler, $el);
418 1
                        $this->contextEl = $el;
419
                }
420
421 1
                return $this->context;
422
        }
423
424 1
        protected function chooseContext($help_context, $new_context)
425
        {
426
                // If the two contextes are diffent, the user has requested
427
                // a switch of the context and we have to return it.
428 1
                if ($help_context !== $new_context) {
429 1
                        return $new_context;
430
                }
431
432 1
                return $this;
433
        }
434
}
435
436
class FluidNamespace
437
{
438
        const ID   = 'id'  ;
439
        const URI  = 'uri' ;
440
        const MODE = 'mode';
441
442
        const MODE_IMPLICIT = 0;
443
        const MODE_EXPLICIT = 1;
444
445
        private $config = [ self::ID   => '',
446
                            self::URI  => '',
447
                            self::MODE => self::MODE_EXPLICIT ];
448
449 1
        public function __construct($id, $uri, $mode = 1)
450
        {
451 1
                if (\is_array($id)) {
452 1
                        $args = $id;
453 1
                        $id   = $args[self::ID];
454 1
                        $uri  = $args[self::URI];
455
456 1
                        if (isset($args[self::MODE])) {
457 1
                                $mode = $args[self::MODE];
458
                        }
459
                }
460
461 1
                $this->config[self::ID]   = $id;
462 1
                $this->config[self::URI]  = $uri;
463 1
                $this->config[self::MODE] = $mode;
464 1
        }
465
466 1
        public function id()
467
        {
468 1
                return $this->config[self::ID];
469
        }
470
471 1
        public function uri()
472
        {
473 1
                return $this->config[self::URI];
474
        }
475
476 1
        public function mode()
477
        {
478 1
                return $this->config[self::MODE];
479
        }
480
481 1
        public function __invoke($xpath)
482
        {
483 1
                $id = $this->id();
484
485 1
                if (! empty($id)) {
486 1
                        $id .= ':';
487
                }
488
489
                // An XPath query may not start with a slash ('/').
490
                // Relative queries are an example '../target".
491 1
                $new_xpath = '';
492
493 1
                $nodes = \explode('/', $xpath);
494
495 1
                foreach ($nodes as $node) {
496 1
                        if (! empty($node)) {
497
                                // An XPath query can have multiple slashes.
498
                                // Example: //target
499 1
                                $new_xpath .= "{$id}{$node}";
500
                        }
501
502 1
                        $new_xpath .= '/';
503
                }
504
505
                // Removes the last appended slash.
506 1
                return \substr($new_xpath, 0, -1);
507
        }
508
}
509
510
} // END OF NAMESPACE FluidXml
511
512
namespace FluidXml\Core
513
{
514
515
use \FluidXml\FluidXml;
516
use \FluidXml\FluidNamespace;
517
518
use function \FluidXml\is_an_xml_string;
519
use function \FluidXml\domnodes_to_string;
520
521
interface FluidInterface
522
{
523
        /**
524
         * Executes an XPath query.
525
         *
526
         * ```php
527
         * $xml = fluidxml();
528
529
         * $xml->query("/doc/book[@id='123']");
530
         *
531
         * // Relative queries are valid.
532
         * $xml->query("/doc")->query("book[@id='123']");
533
         * ```
534
         *
535
         * @param string $xpath The XPath to execute.
536
         *
537
         * @return FluidContext The context associated to the DOMNodeList.
538
         */
539
        public function query(...$xpath);
540
        public function times($times, callable $fn = null);
541
        public function each(callable $fn);
542
543
        /**
544
         * Append a new node as child of the current context.
545
         *
546
         * ```php
547
         * $xml = fluidxml();
548
549
         * $xml->appendChild('title', 'The Theory Of Everything');
550
         * $xml->appendChild([ 'author' => 'S. Hawking' ]);
551
         *
552
         * $xml->appendChild('chapters', true)->appendChild('chapter', ['id'=> 1]);
553
         *
554
         * ```
555
         *
556
         * @param string|array $child The child/children to add.
557
         * @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...
558
         * @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...
559
         *                            or the context of the created node.
560
         *
561
         * @return FluidContext The context associated to the DOMNodeList.
562
         */
563
        public function appendChild($child, ...$optionals);
564
        public function prependSibling($sibling, ...$optionals);
565
        public function appendSibling($sibling, ...$optionals);
566
        public function setAttribute(...$arguments);
567
        public function setText($text);
568
        public function appendText($text);
569
        public function setCdata($text);
570
        public function appendCdata($text);
571
        public function remove(...$xpath);
572
        public function xml($strip = false);
573
        // Aliases:
574
        public function add($child, ...$optionals);
575
        public function prepend($sibling, ...$optionals);
576
        public function insertSiblingBefore($sibling, ...$optionals);
577
        public function append($sibling, ...$optionals);
578
        public function insertSiblingAfter($sibling, ...$optionals);
579
        public function attr(...$arguments);
580
        public function text($text);
581
}
582
583
trait ReservedCallTrait
584
{
585 1
        public function __call($method, $arguments)
586
        {
587 1
                $m = "{$method}_";
588
589 1
                if (\method_exists($this, $m)) {
590 1
                        return $this->$m(...$arguments);
591
                }
592
593 1
                throw new \Exception("Method '$method' not found.");
594
        }
595
}
596
597
trait ReservedCallStaticTrait
598
{
599 1
        public static function __callStatic($method, $arguments)
600
        {
601 1
                $m = "{$method}_";
602
603 1
                if (\method_exists(static::class, $m)) {
604 1
                        return static::$m(...$arguments);
605
                }
606
607 1
                throw new \Exception("Method '$method' not found.");
608
        }
609
}
610
611
trait NewableTrait
612
{
613
        // This method should be called 'new',
614
        // but for compatibility with PHP 5.6
615
        // it is shadowed by the __callStatic() method.
616 1
        public static function new_(...$arguments)
617
        {
618 1
                return new static(...$arguments);
619
        }
620
}
621
622
class FluidDocument
623
{
624
        public $dom;
625
        public $xpath;
626
        public $namespaces = [];
627
        public $handler;
628
}
629
630
class FluidRepeater
631
{
632
        private $document;
633
        private $handler;
634
        private $context;
635
        private $times;
636
637 1
        public function __construct($document, $handler, $context, $times)
638
        {
639 1
                $this->document = $document;
640 1
                $this->handler  = $handler;
641 1
                $this->context  = $context;
642 1
                $this->times    = $times;
643 1
        }
644
645 1
        public function __call($method, $arguments)
646
        {
647 1
                $nodes = [];
648 1
                $new_context = $this->context;
649
650 1
                for ($i = 0, $l = $this->times; $i < $l; ++$i) {
651 1
                        $new_context = $this->context->$method(...$arguments);
652 1
                        $nodes       = \array_merge($nodes, $new_context->asArray());
653
                }
654
655 1
                if ($new_context !== $this->context) {
656 1
                        return new FluidContext($this->document, $this->handler, $nodes);
657
                }
658
659 1
                return $this->context;
660
        }
661
}
662
663
class FluidInsertionHandler
664
{
665
        private $document;
666
        private $dom;
667
        private $namespaces;
668
669 1
        public function __construct($document)
670
        {
671 1
                $this->document   = $document;
672 1
                $this->dom        = $document->dom;
673 1
                $this->namespaces =& $document->namespaces;
674 1
        }
675
676 1
        public function insertElement(&$nodes, $element, &$optionals, $fn, $orig_context)
677
        {
678 1
                list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals);
679
680 1
                $new_nodes = [];
681
682 1
                foreach ($nodes as $n) {
683 1
                        foreach ($element as $k => $v) {
684 1
                                $cx        = $this->handleInsertion($n, $k, $v, $fn, $optionals);
685 1
                                $new_nodes = \array_merge($new_nodes, $cx);
686
                        }
687
                }
688
689 1
                $new_context = $this->newContext($new_nodes);
690
691
                // Setting the attributes is an help that the appendChild method
692
                // offers to the user and is the same of:
693
                // 1. appending a child switching the context
694
                // 2. setting the attributes over the new context.
695 1
                if (! empty($attributes)) {
696 1
                        $new_context->setAttribute($attributes);
697
                }
698
699 1
                return $switch_context ? $new_context : $orig_context;
700
        }
701
702 1
        protected function newContext(&$context)
703
        {
704 1
                return new FluidContext($this->document, $this, $context);
705
        }
706
707 1
        protected function handleOptionals($element, &$optionals)
708
        {
709 1
                if (! \is_array($element)) {
710 1
                        $element = [ $element ];
711
                }
712
713 1
                $switch_context = false;
714 1
                $attributes     = [];
715
716 1
                foreach ($optionals as $opt) {
717 1
                        if (\is_array($opt)) {
718 1
                                $attributes = $opt;
719
720 1
                        } else if (\is_bool($opt)) {
721 1
                                $switch_context = $opt;
722
723 1
                        } else if (\is_string($opt)) {
724 1
                                $e = \array_pop($element);
725
726 1
                                $element[$e] = $opt;
727
728
                        } else {
729 1
                                throw new \Exception("Optional argument '$opt' not recognized.");
730
                        }
731
                }
732
733 1
                return [ $element, $attributes, $switch_context ];
734
        }
735
736
737 1
        protected function handleInsertion($parent, $k, $v, $fn, &$optionals)
738
        {
739
                // This is an highly optimized method.
740
                // Good code design would split this method in many different handlers
741
                // each one with its own checks. But it is too much expensive in terms
742
                // of performances for a core method like this, so this implementation
743
                // is prefered to collapse many identical checks to one.
744
745
                //////////////////////
746
                // Key is a string. //
747
                //////////////////////
748
749
                ///////////////////////////////////////////////////////
750 1
                $k_is_string    = \is_string($k);
751 1
                $v_is_string    = \is_string($v);
752 1
                $v_is_xml       = $v_is_string && is_an_xml_string($v);
753 1
                $k_is_special   = $k_is_string && $k[0] === '@';
754 1
                $k_isnt_special = ! $k_is_special;
755 1
                $v_isnt_string  = ! $v_is_string;
756 1
                $v_isnt_xml     = ! $v_is_xml;
757
                ///////////////////////////////////////////////////////
758
759 1
                if ($k_is_string && $k_isnt_special && $v_is_string && $v_isnt_xml) {
760 1
                        return $this->insertStringString($parent, $k, $v, $fn, $optionals);
761
                }
762
763 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...
764
                        // TODO
765
                }
766
767
                //////////////////////////////////////////////
768 1
                $k_is_special_c = $k_is_special && $k === '@';
769
                //////////////////////////////////////////////
770
771 1
                if ($k_is_special_c && $v_is_string) {
772 1
                        return $this->insertSpecialContent($parent, $k, $v, $fn, $optionals);
773
                }
774
775
                /////////////////////////////////////////////////////
776 1
                $k_is_special_a = $k_is_special && ! $k_is_special_c;
777
                /////////////////////////////////////////////////////
778
779 1
                if ($k_is_special_a && $v_is_string) {
780 1
                        return $this->insertSpecialAttribute($parent, $k, $v, $fn, $optionals);
781
                }
782
783 1
                if ($k_is_string && $v_isnt_string) {
784 1
                        return $this->insertStringMixed($parent, $k, $v, $fn, $optionals);
785
                }
786
787
                ////////////////////////
788
                // Key is an integer. //
789
                ////////////////////////
790
791
                ////////////////////////////////
792 1
                $k_is_integer = \is_integer($k);
793 1
                $v_is_array   = \is_array($v);
794
                ////////////////////////////////
795
796 1
                if ($k_is_integer && $v_is_array) {
797 1
                        return $this->insertIntegerArray($parent, $k, $v, $fn, $optionals);
798
                }
799
800 1
                if ($k_is_integer && $v_is_string && $v_isnt_xml) {
801 1
                        return $this->insertIntegerString($parent, $k, $v, $fn, $optionals);
802
                }
803
804 1
                if ($k_is_integer && $v_is_string && $v_is_xml) {
805 1
                        return $this->insertIntegerXml($parent, $k, $v, $fn, $optionals);
806
                }
807
808
                //////////////////////////////////////////
809 1
                $v_is_domdoc = $v instanceof \DOMDocument;
810
                //////////////////////////////////////////
811
812 1
                if ($k_is_integer && $v_is_domdoc) {
813 1
                        return $this->insertIntegerDomdocument($parent, $k, $v, $fn, $optionals);
814
                }
815
816
                ///////////////////////////////////////////////
817 1
                $v_is_domnodelist = $v instanceof \DOMNodeList;
818
                ///////////////////////////////////////////////
819
820 1
                if ($k_is_integer && $v_is_domnodelist) {
821 1
                        return $this->insertIntegerDomnodelist($parent, $k, $v, $fn, $optionals);
822
                }
823
824
                ///////////////////////////////////////
825 1
                $v_is_domnode = $v instanceof \DOMNode;
826
                ///////////////////////////////////////
827
828 1
                if ($k_is_integer && ! $v_is_domdoc && $v_is_domnode) {
829 1
                        return $this->insertIntegerDomnode($parent, $k, $v, $fn, $optionals);
830
                }
831
832
                //////////////////////////////////////////////////
833 1
                $v_is_simplexml = $v instanceof \SimpleXMLElement;
834
                //////////////////////////////////////////////////
835
836 1
                if ($k_is_integer && $v_is_simplexml) {
837 1
                        return $this->insertIntegerSimplexml($parent, $k, $v, $fn, $optionals);
838
                }
839
840
                ////////////////////////////////////////
841 1
                $v_is_fluidxml = $v instanceof FluidXml;
842
                ////////////////////////////////////////
843
844 1
                if ($k_is_integer && $v_is_fluidxml) {
845 1
                        return $this->insertIntegerFluidxml($parent, $k, $v, $fn, $optionals);
846
                }
847
848
                ///////////////////////////////////////////
849 1
                $v_is_fluidcx = $v instanceof FluidContext;
850
                ///////////////////////////////////////////
851
852 1
                if ($k_is_integer && $v_is_fluidcx) {
853 1
                        return $this->insertIntegerFluidcontext($parent, $k, $v, $fn, $optionals);
854
                }
855
856 1
                throw new \Exception('XML document not supported.');
857
        }
858
859 1
        protected function createElement($name, $value = null)
860
        {
861
                // The DOMElement instance must be different for every node,
862
                // otherwise only one element is attached to the DOM.
863
864 1
                $id  = null;
865 1
                $uri = null;
866
867
                // The node name can contain the namespace id prefix.
868
                // Example: xsl:template
869 1
                $colon_pos = \strpos($name, ':');
870
871 1
                if ($colon_pos !== false) {
872 1
                        $id   = \substr($name, 0, $colon_pos);
873 1
                        $name = \substr($name, $colon_pos + 1);
874
                }
875
876 1
                if ($id !== null) {
877 1
                        $ns  = $this->namespaces[$id];
878 1
                        $uri = $ns->uri();
879
880 1
                        if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) {
881 1
                                $name = "{$id}:{$name}";
882
                        }
883
                }
884
885
                // Algorithm 1:
886 1
                $el = new \DOMElement($name, $value, $uri);
887
888
                // Algorithm 2:
889
                // $el = $dom->createElement($name, $value);
890
891 1
                return $el;
892
        }
893
894 1
        protected function attachNodes($parent, $nodes, $fn)
895
        {
896 1
                if (! \is_array($nodes) && ! $nodes instanceof \Traversable) {
897 1
                        $nodes = [ $nodes ];
898
                }
899
900 1
                $context = [];
901
902 1
                foreach ($nodes as $el) {
903 1
                        $el        = $this->dom->importNode($el, true);
904 1
                        $context[] = $fn($parent, $el);
905
                }
906
907 1
                return $context;
908
        }
909
910 1
        protected function insertSpecialContent($parent, $k, $v)
911
        {
912
                // The user has passed an element text content:
913
                // [ '@' => 'Element content.' ]
914
915
                // Algorithm 1:
916 1
                $this->newContext($parent)->appendText($v);
917
918
                // Algorithm 2:
919
                // $this->setText($v);
920
921
                // The user can specify multiple '@' special elements
922
                // so Algorithm 1 is the right choice.
923
924 1
                return [];
925
        }
926
927 1
        protected function insertSpecialAttribute($parent, $k, $v)
928
        {
929
                // The user has passed an attribute name and an attribute value:
930
                // [ '@attribute' => 'Attribute content' ]
931
932 1
                $attr = \substr($k, 1);
933 1
                $this->newContext($parent)->setAttribute($attr, $v);
934
935 1
                return [];
936
        }
937
938 1 View Code Duplication
        protected function insertStringString($parent, $k, $v, $fn)
939
        {
940
                // The user has passed an element name and an element value:
941
                // [ 'element' => 'Element content' ]
942
943 1
                $el = $this->createElement($k, $v);
944 1
                $el = $fn($parent, $el);
945
946 1
                return [ $el ];
947
        }
948
949 1 View Code Duplication
        protected function insertStringMixed($parent, $k, $v, $fn, &$optionals)
950
        {
951
                // The user has passed one of these two cases:
952
                // - [ 'element' => [...] ]
953
                // - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ]
954
955 1
                $el = $this->createElement($k);
956 1
                $el = $fn($parent, $el);
957
958
                // The new children elements must be created in the order
959
                // they are supplied, so 'appendChild' is the perfect operation.
960 1
                $this->newContext($el)->appendChild($v, ...$optionals);
961
962 1
                return [ $el ];
963
        }
964
965 1
        protected function insertIntegerArray($parent, $k, $v, $fn, &$optionals)
966
        {
967
                // The user has passed a wrapper array:
968
                // [ [...], ... ]
969
970 1
                $context = [];
971
972 1
                foreach ($v as $kk => $vv) {
973 1
                        $cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals);
974
975 1
                        $context = \array_merge($context, $cx);
976
                }
977
978 1
                return $context;
979
        }
980
981 1
        protected function insertIntegerString($parent, $k, $v, $fn)
982
        {
983
                // The user has passed a node name without a node value:
984
                // [ 'element', ... ]
985
986 1
                $el = $this->createElement($v);
987 1
                $el = $fn($parent, $el);
988
989 1
                return [ $el ];
990
        }
991
992 1
        protected function insertIntegerXml($parent, $k, $v, $fn)
993
        {
994
                // The user has passed an XML document instance:
995
                // [ '<tag></tag>', DOMNode, SimpleXMLElement, FluidXml ]
996
997 1
                $wrapper = new \DOMDocument();
998 1
                $wrapper->formatOutput       = true;
999 1
                $wrapper->preserveWhiteSpace = false;
1000
1001 1
                $v = \ltrim($v);
1002
1003 1
                if ($v[1] === '?') {
1004 1
                        $wrapper->loadXML($v);
1005 1
                        $nodes = $wrapper->childNodes;
1006
                } else {
1007
                        // A way to import strings with multiple root nodes.
1008 1
                        $wrapper->loadXML("<root>$v</root>");
1009
1010
                        // Algorithm 1:
1011 1
                        $nodes = $wrapper->documentElement->childNodes;
1012
1013
                        // Algorithm 2:
1014
                        // $dom_xp = new \DOMXPath($dom);
1015
                        // $nodes = $dom_xp->query('/root/*');
1016
                }
1017
1018 1
                return $this->attachNodes($parent, $nodes, $fn);
1019
        }
1020
1021 1
        protected function insertIntegerDomdocument($parent, $k, $v, $fn)
1022
        {
1023
                // A DOMDocument can have multiple root nodes.
1024
1025
                // Algorithm 1:
1026 1
                return $this->attachNodes($parent, $v->childNodes, $fn);
1027
1028
                // Algorithm 2:
1029
                // return $this->attachNodes($parent, $v->documentElement, $fn);
1030
        }
1031
1032 1
        protected function insertIntegerDomnodelist($parent, $k, $v, $fn)
1033
        {
1034 1
                return $this->attachNodes($parent, $v, $fn);
1035
        }
1036
1037 1
        protected function insertIntegerDomnode($parent, $k, $v, $fn)
1038
        {
1039 1
                return $this->attachNodes($parent, $v, $fn);
1040
        }
1041
1042 1
        protected function insertIntegerSimplexml($parent, $k, $v, $fn)
1043
        {
1044 1
                return $this->attachNodes($parent, \dom_import_simplexml($v), $fn);
1045
        }
1046
1047 1
        protected function insertIntegerFluidxml($parent, $k, $v, $fn)
1048
        {
1049 1
                return $this->attachNodes($parent, $v->dom()->documentElement, $fn);
1050
        }
1051
1052 1
        protected function insertIntegerFluidcontext($parent, $k, $v, $fn)
1053
        {
1054 1
                return $this->attachNodes($parent, $v->asArray(), $fn);
1055
        }
1056
}
1057
1058
class FluidContext implements FluidInterface, \ArrayAccess, \Iterator
1059
{
1060
        use NewableTrait,
1061
            ReservedCallTrait,          // For compatibility with PHP 5.6.
1062
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
1063
1064
        private $document;
1065
        private $handler;
1066
        private $nodes = [];
1067
        private $seek = 0;
1068
1069 1
        public function __construct($document, $handler, $context)
1070
        {
1071 1
                $this->document = $document;
1072 1
                $this->handler  = $handler;
1073
1074 1
                if (! \is_array($context) && ! $context instanceof \Traversable) {
1075
                        // DOMDocument, DOMElement and DOMNode are not iterable.
1076
                        // DOMNodeList and FluidContext are iterable.
1077 1
                        $context = [ $context ];
1078
                }
1079
1080 1
                foreach ($context as $n) {
1081 1
                        if (! $n instanceof \DOMNode) {
1082 1
                                throw new \Exception('Node type not recognized.');
1083
                        }
1084
1085 1
                        $this->nodes[] = $n;
1086
                }
1087 1
        }
1088
1089 1
        public function asArray()
1090
        {
1091 1
                return $this->nodes;
1092
        }
1093
1094
        // \ArrayAccess interface.
1095 1
        public function offsetSet($offset, $value)
1096
        {
1097
                // if (\is_null($offset)) {
1098
                //         $this->nodes[] = $value;
1099
                // } else {
1100
                //         $this->nodes[$offset] = $value;
1101
                // }
1102 1
                throw new \Exception('Setting a context element is not allowed.');
1103
        }
1104
1105
        // \ArrayAccess interface.
1106 1
        public function offsetExists($offset)
1107
        {
1108 1
                return isset($this->nodes[$offset]);
1109
        }
1110
1111
        // \ArrayAccess interface.
1112 1
        public function offsetUnset($offset)
1113
        {
1114
                // unset($this->nodes[$offset]);
1115 1
                \array_splice($this->nodes, $offset, 1);
1116 1
        }
1117
1118
        // \ArrayAccess interface.
1119 1
        public function offsetGet($offset)
1120
        {
1121 1
                if (isset($this->nodes[$offset])) {
1122 1
                        return $this->nodes[$offset];
1123
                }
1124
1125 1
                return null;
1126
        }
1127
1128
        // \Iterator interface.
1129 1
        public function rewind()
1130
        {
1131 1
                $this->seek = 0;
1132 1
        }
1133
1134
        // \Iterator interface.
1135 1
        public function current()
1136
        {
1137 1
                return $this->nodes[$this->seek];
1138
        }
1139
1140
        // \Iterator interface.
1141 1
        public function key()
1142
        {
1143 1
                return $this->seek;
1144
        }
1145
1146
        // \Iterator interface.
1147 1
        public function next()
1148
        {
1149 1
                ++$this->seek;
1150 1
        }
1151
1152
        // \Iterator interface.
1153 1
        public function valid()
1154
        {
1155 1
                return isset($this->nodes[$this->seek]);
1156
        }
1157
1158 1
        public function length()
1159
        {
1160 1
                return \count($this->nodes);
1161
        }
1162
1163 1
        public function query(...$xpath)
1164
        {
1165 1
                if (\is_array($xpath[0])) {
1166 1
                        $xpath = $xpath[0];
1167
                }
1168
1169 1
                $results = [];
1170
1171 1
                $xp = $this->document->xpath;
1172
1173 1
                foreach ($this->nodes as $n) {
1174 1
                        foreach ($xpath as $x) {
1175
                                // Returns a DOMNodeList.
1176 1
                                $res = $xp->query($x, $n);
1177
1178
                                // Algorithm 1:
1179
                                // $results = \array_merge($results, \iterator_to_array($res));
1180
1181
                                // Algorithm 2:
1182
                                // It is faster than \iterator_to_array and a lot faster
1183
                                // than \iterator_to_array + \array_merge.
1184 1
                                foreach ($res as $r) {
1185 1
                                        $results[] = $r;
1186
                                }
1187
1188
                                // Algorithm 3:
1189
                                // for ($i = 0, $l = $res->length; $i < $l; ++$i) {
1190
                                //         $results[] = $res->item($i);
1191
                                // }
1192
                        }
1193
                }
1194
1195
                // Performing over multiple sibling nodes a query that ascends
1196
                // the xpath, relative (../..) or absolute (//), returns identical
1197
                // matching results that must be collapsed in an unique result
1198
                // otherwise a subsequent operation is performed multiple times.
1199 1
                return $this->newContext($this->filterQueryResults($results));
0 ignored issues
show
Bug introduced by
$this->filterQueryResults($results) cannot be passed to newcontext() as the parameter $context expects a reference.
Loading history...
1200
        }
1201
1202 1
        public function times($times, callable $fn = null)
1203
        {
1204 1
                if ($fn === null) {
1205 1
                        return new FluidRepeater($this->document, $this->handler, $this, $times);
1206
                }
1207
1208 1
                for ($i = 0; $i < $times; ++$i) {
1209 1
                        $this->callfn($fn, [$this, $i]);
1210
                }
1211
1212 1
                return $this;
1213
        }
1214
1215 1
        public function each(callable $fn)
1216
        {
1217 1
                foreach ($this->nodes as $i => $n) {
1218 1
                        $cx = $this->newContext($n);
1219
1220 1
                        $this->callfn($fn, [$cx, $i, $n]);
1221
                }
1222
1223 1
                return $this;
1224
        }
1225
1226
        // appendChild($child, $value?, $attributes? = [], $switchContext? = false)
1227 1
        public function appendChild($child, ...$optionals)
1228
        {
1229
                return $this->handler->insertElement($this->nodes, $child, $optionals, function($parent, $element) {
1230 1
                        return $parent->appendChild($element);
1231 1
                }, $this);
1232
        }
1233
1234
        // Alias of appendChild().
1235 1
        public function add($child, ...$optionals)
1236
        {
1237 1
                return $this->appendChild($child, ...$optionals);
1238
        }
1239
1240 1
        public function prependSibling($sibling, ...$optionals)
1241
        {
1242
                return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) {
1243 1
                        return $sibling->parentNode->insertBefore($element, $sibling);
1244 1
                }, $this);
1245
        }
1246
1247
        // Alias of prependSibling().
1248 1
        public function prepend($sibling, ...$optionals)
1249
        {
1250 1
                return $this->prependSibling($sibling, ...$optionals);
1251
        }
1252
1253
        // Alias of prependSibling().
1254 1
        public function insertSiblingBefore($sibling, ...$optionals)
1255
        {
1256 1
                return $this->prependSibling($sibling, ...$optionals);
1257
        }
1258
1259
        public function appendSibling($sibling, ...$optionals)
1260
        {
1261 1
                return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) {
1262
                        // If ->nextSibling is null, $element is simply appended as last sibling.
1263 1
                        return $sibling->parentNode->insertBefore($element, $sibling->nextSibling);
1264 1
                }, $this);
1265
        }
1266
1267
        // Alias of appendSibling().
1268 1
        public function append($sibling, ...$optionals)
1269
        {
1270 1
                return $this->appendSibling($sibling, ...$optionals);
1271
        }
1272
1273
        // Alias of appendSibling().
1274 1
        public function insertSiblingAfter($sibling, ...$optionals)
1275
        {
1276 1
                return $this->appendSibling($sibling, ...$optionals);
1277
        }
1278
1279
        // Arguments can be in the form of:
1280
        // setAttribute($name, $value)
1281
        // setAttribute(['name' => 'value', ...])
1282 1
        public function setAttribute(...$arguments)
1283
        {
1284
                // Default case is:
1285
                // [ 'name' => 'value', ... ]
1286 1
                $attrs = $arguments[0];
1287
1288
                // If the first argument is not an array,
1289
                // the user has passed two arguments:
1290
                // 1. is the attribute name
1291
                // 2. is the attribute value
1292 1
                if (! \is_array($arguments[0])) {
1293 1
                        $attrs = [$arguments[0] => $arguments[1]];
1294
                }
1295
1296 1
                foreach ($this->nodes as $n) {
1297 1
                        foreach ($attrs as $k => $v) {
1298
                                // Algorithm 1:
1299 1
                                $n->setAttribute($k, $v);
1300
1301
                                // Algorithm 2:
1302
                                // $n->setAttributeNode(new \DOMAttr($k, $v));
1303
1304
                                // Algorithm 3:
1305
                                // $n->appendChild(new \DOMAttr($k, $v));
1306
1307
                                // Algorithm 2 and 3 have a different behaviour
1308
                                // from Algorithm 1.
1309
                                // The attribute is still created or setted, but
1310
                                // changing the value of an existing attribute
1311
                                // changes even the order of that attribute
1312
                                // in the attribute list.
1313
                        }
1314
                }
1315
1316 1
                return $this;
1317
        }
1318
1319
        // Alias of setAttribute().
1320 1
        public function attr(...$arguments)
1321
        {
1322 1
                return $this->setAttribute(...$arguments);
1323
        }
1324
1325 1
        public function appendText($text)
1326
        {
1327 1
                foreach ($this->nodes as $n) {
1328 1
                        $n->appendChild(new \DOMText($text));
1329
                }
1330
1331 1
                return $this;
1332
        }
1333
1334 1
        public function appendCdata($text)
1335
        {
1336 1
                foreach ($this->nodes as $n) {
1337 1
                        $n->appendChild(new \DOMCDATASection($text));
1338
                }
1339
1340 1
                return $this;
1341
        }
1342
1343 1
        public function setText($text)
1344
        {
1345 1
                foreach ($this->nodes as $n) {
1346
                        // Algorithm 1:
1347 1
                        $n->nodeValue = $text;
1348
1349
                        // Algorithm 2:
1350
                        // foreach ($n->childNodes as $c) {
1351
                        //         $n->removeChild($c);
1352
                        // }
1353
                        // $n->appendChild(new \DOMText($text));
1354
1355
                        // Algorithm 3:
1356
                        // foreach ($n->childNodes as $c) {
1357
                        //         $n->replaceChild(new \DOMText($text), $c);
1358
                        // }
1359
                }
1360
1361 1
                return $this;
1362
        }
1363
1364
        // Alias of setText().
1365 1
        public function text($text)
1366
        {
1367 1
                return $this->setText($text);
1368
        }
1369
1370 1
        public function setCdata($text)
1371
        {
1372 1
                foreach ($this->nodes as $n) {
1373 1
                        $n->nodeValue = '';
1374 1
                        $n->appendChild(new \DOMCDATASection($text));
1375
                }
1376
1377 1
                return $this;
1378
        }
1379
1380
        // Alias of setCdata().
1381 1
        public function cdata($text)
1382
        {
1383 1
                return $this->setCdata($text);
1384
        }
1385
1386 1
        public function remove(...$xpath)
1387
        {
1388
                // Arguments can be empty, a string or an array of strings.
1389
1390 1
                if (empty($xpath)) {
1391
                        // The user has requested to remove the nodes of this context.
1392 1
                        $targets = $this->nodes;
1393
                } else {
1394 1
                        $targets = $this->query(...$xpath);
1395
                }
1396
1397 1
                foreach ($targets as $t) {
1398 1
                        $t->parentNode->removeChild($t);
1399
                }
1400
1401 1
                return $this;
1402
        }
1403
1404 1
        public function xml($strip = false)
1405
        {
1406 1
                return domnodes_to_string($this->nodes);
1407
        }
1408
1409 1
        protected function newContext(&$context)
1410
        {
1411 1
                return new FluidContext($this->document, $this->handler, $context);
1412
        }
1413
1414 1
        protected function filterQueryResults(&$results)
1415
        {
1416 1
                $set = [];
1417
1418 1
                foreach ($results as $r) {
1419 1
                        $found = false;
1420
1421 1
                        foreach ($set as $u) {
1422 1
                                $found = ($r === $u) || $found;
1423
                        }
1424
1425 1
                        if (! $found) {
1426 1
                                $set[] = $r;
1427
                        }
1428
                }
1429
1430 1
                return $set;
1431
        }
1432
1433 1
        protected function callfn($fn, $args)
1434
        {
1435 1
                if ($fn instanceof \Closure) {
1436 1
                        $bind = \array_shift($args);
1437
1438 1
                        $fn = $fn->bindTo($bind);
1439
1440
                        // It is faster than \call_user_func.
1441 1
                        return $fn(...$args);
1442
                }
1443
1444 1
                return \call_user_func($fn, ...$args);
1445
        }
1446
}
1447
1448
} // END OF NAMESPACE FluidXml\Core
1449