Completed
Push — master ( fbf680...001884 )
by Daniele
02:32
created

FluidInsertionHandler   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 394
Duplicated Lines 6.35 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%
Metric Value
dl 25
loc 394
ccs 149
cts 149
cp 1
wmc 72
lcom 1
cbo 1
rs 5.5667

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B insertElement() 0 25 5
A newContext() 0 4 1
B handleOptionals() 0 28 6
D handleInsertion() 0 121 36
B createElement() 0 34 4
A attachNodes() 0 15 4
A insertSpecialContent() 0 16 1
A insertSpecialAttribute() 0 10 1
A insertStringString() 10 10 1
A insertStringMixed() 15 15 1
A insertIntegerArray() 0 15 2
A insertIntegerString() 0 10 1
B insertIntegerXml() 0 28 2
A insertIntegerDomdocument() 0 10 1
A insertIntegerDomnodelist() 0 4 1
A insertIntegerDomnode() 0 4 1
A insertIntegerSimplexml() 0 4 1
A insertIntegerFluidxml() 0 4 1
A insertIntegerFluidcontext() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FluidInsertionHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FluidInsertionHandler, and based on these observations, apply Extract Interface, too.

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 ...$optionals Accepted values are:
558
         *                      - a boolean for requesting the context switch
559
         *                      - a string which is the element text content
560
         *                      - an array containing the attributes to set on the element.
561
         *
562
         * @return FluidContext The context associated to the DOMNodeList.
563
         */
564
        public function appendChild($child, ...$optionals);
565
        public function prependSibling($sibling, ...$optionals);
566
        public function appendSibling($sibling, ...$optionals);
567
        public function setAttribute(...$arguments);
568
        public function setText($text);
569
        public function appendText($text);
570
        public function setCdata($text);
571
        public function appendCdata($text);
572
        public function remove(...$xpath);
573
        public function xml($strip = false);
574
        // Aliases:
575
        public function add($child, ...$optionals);
576
        public function prepend($sibling, ...$optionals);
577
        public function insertSiblingBefore($sibling, ...$optionals);
578
        public function append($sibling, ...$optionals);
579
        public function insertSiblingAfter($sibling, ...$optionals);
580
        public function attr(...$arguments);
581
        public function text($text);
582
}
583
584
trait ReservedCallTrait
585
{
586 1
        public function __call($method, $arguments)
587
        {
588 1
                $m = "{$method}_";
589
590 1
                if (\method_exists($this, $m)) {
591 1
                        return $this->$m(...$arguments);
592
                }
593
594 1
                throw new \Exception("Method '$method' not found.");
595
        }
596
}
597
598
trait ReservedCallStaticTrait
599
{
600 1
        public static function __callStatic($method, $arguments)
601
        {
602 1
                $m = "{$method}_";
603
604 1
                if (\method_exists(static::class, $m)) {
605 1
                        return static::$m(...$arguments);
606
                }
607
608 1
                throw new \Exception("Method '$method' not found.");
609
        }
610
}
611
612
trait NewableTrait
613
{
614
        // This method should be called 'new',
615
        // but for compatibility with PHP 5.6
616
        // it is shadowed by the __callStatic() method.
617 1
        public static function new_(...$arguments)
618
        {
619 1
                return new static(...$arguments);
620
        }
621
}
622
623
class FluidDocument
624
{
625
        public $dom;
626
        public $xpath;
627
        public $namespaces = [];
628
        public $handler;
629
}
630
631
class FluidRepeater
632
{
633
        private $document;
634
        private $handler;
635
        private $context;
636
        private $times;
637
638 1
        public function __construct($document, $handler, $context, $times)
639
        {
640 1
                $this->document = $document;
641 1
                $this->handler  = $handler;
642 1
                $this->context  = $context;
643 1
                $this->times    = $times;
644 1
        }
645
646 1
        public function __call($method, $arguments)
647
        {
648 1
                $nodes = [];
649 1
                $new_context = $this->context;
650
651 1
                for ($i = 0, $l = $this->times; $i < $l; ++$i) {
652 1
                        $new_context = $this->context->$method(...$arguments);
653 1
                        $nodes       = \array_merge($nodes, $new_context->asArray());
654
                }
655
656 1
                if ($new_context !== $this->context) {
657 1
                        return new FluidContext($this->document, $this->handler, $nodes);
658
                }
659
660 1
                return $this->context;
661
        }
662
}
663
664
class FluidInsertionHandler
665
{
666
        private $document;
667
        private $dom;
668
        private $namespaces;
669
670 1
        public function __construct($document)
671
        {
672 1
                $this->document   = $document;
673 1
                $this->dom        = $document->dom;
674 1
                $this->namespaces =& $document->namespaces;
675 1
        }
676
677 1
        public function insertElement(&$nodes, $element, &$optionals, $fn, $orig_context)
678
        {
679 1
                list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals);
680
681 1
                $new_nodes = [];
682
683 1
                foreach ($nodes as $n) {
684 1
                        foreach ($element as $k => $v) {
685 1
                                $cx        = $this->handleInsertion($n, $k, $v, $fn, $optionals);
686 1
                                $new_nodes = \array_merge($new_nodes, $cx);
687
                        }
688
                }
689
690 1
                $new_context = $this->newContext($new_nodes);
691
692
                // Setting the attributes is an help that the appendChild method
693
                // offers to the user and is the same of:
694
                // 1. appending a child switching the context
695
                // 2. setting the attributes over the new context.
696 1
                if (! empty($attributes)) {
697 1
                        $new_context->setAttribute($attributes);
698
                }
699
700 1
                return $switch_context ? $new_context : $orig_context;
701
        }
702
703 1
        protected function newContext(&$context)
704
        {
705 1
                return new FluidContext($this->document, $this, $context);
706
        }
707
708 1
        protected function handleOptionals($element, &$optionals)
709
        {
710 1
                if (! \is_array($element)) {
711 1
                        $element = [ $element ];
712
                }
713
714 1
                $switch_context = false;
715 1
                $attributes     = [];
716
717 1
                foreach ($optionals as $opt) {
718 1
                        if (\is_array($opt)) {
719 1
                                $attributes = $opt;
720
721 1
                        } else if (\is_bool($opt)) {
722 1
                                $switch_context = $opt;
723
724 1
                        } else if (\is_string($opt)) {
725 1
                                $e = \array_pop($element);
726
727 1
                                $element[$e] = $opt;
728
729
                        } else {
730 1
                                throw new \Exception("Optional argument '$opt' not recognized.");
731
                        }
732
                }
733
734 1
                return [ $element, $attributes, $switch_context ];
735
        }
736
737
738 1
        protected function handleInsertion($parent, $k, $v, $fn, &$optionals)
739
        {
740
                // This is an highly optimized method.
741
                // Good code design would split this method in many different handlers
742
                // each one with its own checks. But it is too much expensive in terms
743
                // of performances for a core method like this, so this implementation
744
                // is prefered to collapse many identical checks to one.
745
746
                //////////////////////
747
                // Key is a string. //
748
                //////////////////////
749
750
                ///////////////////////////////////////////////////////
751 1
                $k_is_string    = \is_string($k);
752 1
                $v_is_string    = \is_string($v);
753 1
                $v_is_xml       = $v_is_string && is_an_xml_string($v);
754 1
                $k_is_special   = $k_is_string && $k[0] === '@';
755 1
                $k_isnt_special = ! $k_is_special;
756 1
                $v_isnt_string  = ! $v_is_string;
757 1
                $v_isnt_xml     = ! $v_is_xml;
758
                ///////////////////////////////////////////////////////
759
760 1
                if ($k_is_string && $k_isnt_special && $v_is_string && $v_isnt_xml) {
761 1
                        return $this->insertStringString($parent, $k, $v, $fn, $optionals);
762
                }
763
764
                // if ($k_is_string && $k_isnt_special && $v_is_string && $v_is_xml) {
765
                        // TODO
766
                // }
767
768
                //////////////////////////////////////////////
769 1
                $k_is_special_c = $k_is_special && $k === '@';
770
                //////////////////////////////////////////////
771
772 1
                if ($k_is_special_c && $v_is_string) {
773 1
                        return $this->insertSpecialContent($parent, $k, $v, $fn, $optionals);
774
                }
775
776
                /////////////////////////////////////////////////////
777 1
                $k_is_special_a = $k_is_special && ! $k_is_special_c;
778
                /////////////////////////////////////////////////////
779
780 1
                if ($k_is_special_a && $v_is_string) {
781 1
                        return $this->insertSpecialAttribute($parent, $k, $v, $fn, $optionals);
782
                }
783
784 1
                if ($k_is_string && $v_isnt_string) {
785 1
                        return $this->insertStringMixed($parent, $k, $v, $fn, $optionals);
786
                }
787
788
                ////////////////////////
789
                // Key is an integer. //
790
                ////////////////////////
791
792
                ////////////////////////////////
793 1
                $k_is_integer = \is_integer($k);
794 1
                $v_is_array   = \is_array($v);
795
                ////////////////////////////////
796
797 1
                if ($k_is_integer && $v_is_array) {
798 1
                        return $this->insertIntegerArray($parent, $k, $v, $fn, $optionals);
799
                }
800
801 1
                if ($k_is_integer && $v_is_string && $v_isnt_xml) {
802 1
                        return $this->insertIntegerString($parent, $k, $v, $fn, $optionals);
803
                }
804
805 1
                if ($k_is_integer && $v_is_string && $v_is_xml) {
806 1
                        return $this->insertIntegerXml($parent, $k, $v, $fn, $optionals);
807
                }
808
809
                //////////////////////////////////////////
810 1
                $v_is_domdoc = $v instanceof \DOMDocument;
811
                //////////////////////////////////////////
812
813 1
                if ($k_is_integer && $v_is_domdoc) {
814 1
                        return $this->insertIntegerDomdocument($parent, $k, $v, $fn, $optionals);
815
                }
816
817
                ///////////////////////////////////////////////
818 1
                $v_is_domnodelist = $v instanceof \DOMNodeList;
819
                ///////////////////////////////////////////////
820
821 1
                if ($k_is_integer && $v_is_domnodelist) {
822 1
                        return $this->insertIntegerDomnodelist($parent, $k, $v, $fn, $optionals);
823
                }
824
825
                ///////////////////////////////////////
826 1
                $v_is_domnode = $v instanceof \DOMNode;
827
                ///////////////////////////////////////
828
829 1
                if ($k_is_integer && ! $v_is_domdoc && $v_is_domnode) {
830 1
                        return $this->insertIntegerDomnode($parent, $k, $v, $fn, $optionals);
831
                }
832
833
                //////////////////////////////////////////////////
834 1
                $v_is_simplexml = $v instanceof \SimpleXMLElement;
835
                //////////////////////////////////////////////////
836
837 1
                if ($k_is_integer && $v_is_simplexml) {
838 1
                        return $this->insertIntegerSimplexml($parent, $k, $v, $fn, $optionals);
839
                }
840
841
                ////////////////////////////////////////
842 1
                $v_is_fluidxml = $v instanceof FluidXml;
843
                ////////////////////////////////////////
844
845 1
                if ($k_is_integer && $v_is_fluidxml) {
846 1
                        return $this->insertIntegerFluidxml($parent, $k, $v, $fn, $optionals);
847
                }
848
849
                ///////////////////////////////////////////
850 1
                $v_is_fluidcx = $v instanceof FluidContext;
851
                ///////////////////////////////////////////
852
853 1
                if ($k_is_integer && $v_is_fluidcx) {
854 1
                        return $this->insertIntegerFluidcontext($parent, $k, $v, $fn, $optionals);
855
                }
856
857 1
                throw new \Exception('XML document not supported.');
858
        }
859
860 1
        protected function createElement($name, $value = null)
861
        {
862
                // The DOMElement instance must be different for every node,
863
                // otherwise only one element is attached to the DOM.
864
865 1
                $id  = null;
866 1
                $uri = null;
867
868
                // The node name can contain the namespace id prefix.
869
                // Example: xsl:template
870 1
                $colon_pos = \strpos($name, ':');
871
872 1
                if ($colon_pos !== false) {
873 1
                        $id   = \substr($name, 0, $colon_pos);
874 1
                        $name = \substr($name, $colon_pos + 1);
875
                }
876
877 1
                if ($id !== null) {
878 1
                        $ns  = $this->namespaces[$id];
879 1
                        $uri = $ns->uri();
880
881 1
                        if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) {
882 1
                                $name = "{$id}:{$name}";
883
                        }
884
                }
885
886
                // Algorithm 1:
887 1
                $el = new \DOMElement($name, $value, $uri);
888
889
                // Algorithm 2:
890
                // $el = $dom->createElement($name, $value);
891
892 1
                return $el;
893
        }
894
895 1
        protected function attachNodes($parent, $nodes, $fn)
896
        {
897 1
                if (! \is_array($nodes) && ! $nodes instanceof \Traversable) {
898 1
                        $nodes = [ $nodes ];
899
                }
900
901 1
                $context = [];
902
903 1
                foreach ($nodes as $el) {
904 1
                        $el        = $this->dom->importNode($el, true);
905 1
                        $context[] = $fn($parent, $el);
906
                }
907
908 1
                return $context;
909
        }
910
911 1
        protected function insertSpecialContent($parent, $k, $v)
912
        {
913
                // The user has passed an element text content:
914
                // [ '@' => 'Element content.' ]
915
916
                // Algorithm 1:
917 1
                $this->newContext($parent)->appendText($v);
918
919
                // Algorithm 2:
920
                // $this->setText($v);
921
922
                // The user can specify multiple '@' special elements
923
                // so Algorithm 1 is the right choice.
924
925 1
                return [];
926
        }
927
928 1
        protected function insertSpecialAttribute($parent, $k, $v)
929
        {
930
                // The user has passed an attribute name and an attribute value:
931
                // [ '@attribute' => 'Attribute content' ]
932
933 1
                $attr = \substr($k, 1);
934 1
                $this->newContext($parent)->setAttribute($attr, $v);
935
936 1
                return [];
937
        }
938
939 1 View Code Duplication
        protected function insertStringString($parent, $k, $v, $fn)
940
        {
941
                // The user has passed an element name and an element value:
942
                // [ 'element' => 'Element content' ]
943
944 1
                $el = $this->createElement($k, $v);
945 1
                $el = $fn($parent, $el);
946
947 1
                return [ $el ];
948
        }
949
950 1 View Code Duplication
        protected function insertStringMixed($parent, $k, $v, $fn, &$optionals)
951
        {
952
                // The user has passed one of these two cases:
953
                // - [ 'element' => [...] ]
954
                // - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ]
955
956 1
                $el = $this->createElement($k);
957 1
                $el = $fn($parent, $el);
958
959
                // The new children elements must be created in the order
960
                // they are supplied, so 'appendChild' is the perfect operation.
961 1
                $this->newContext($el)->appendChild($v, ...$optionals);
962
963 1
                return [ $el ];
964
        }
965
966 1
        protected function insertIntegerArray($parent, $k, $v, $fn, &$optionals)
967
        {
968
                // The user has passed a wrapper array:
969
                // [ [...], ... ]
970
971 1
                $context = [];
972
973 1
                foreach ($v as $kk => $vv) {
974 1
                        $cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals);
975
976 1
                        $context = \array_merge($context, $cx);
977
                }
978
979 1
                return $context;
980
        }
981
982 1
        protected function insertIntegerString($parent, $k, $v, $fn)
983
        {
984
                // The user has passed a node name without a node value:
985
                // [ 'element', ... ]
986
987 1
                $el = $this->createElement($v);
988 1
                $el = $fn($parent, $el);
989
990 1
                return [ $el ];
991
        }
992
993 1
        protected function insertIntegerXml($parent, $k, $v, $fn)
994
        {
995
                // The user has passed an XML document instance:
996
                // [ '<tag></tag>', DOMNode, SimpleXMLElement, FluidXml ]
997
998 1
                $wrapper = new \DOMDocument();
999 1
                $wrapper->formatOutput       = true;
1000 1
                $wrapper->preserveWhiteSpace = false;
1001
1002 1
                $v = \ltrim($v);
1003
1004 1
                if ($v[1] === '?') {
1005 1
                        $wrapper->loadXML($v);
1006 1
                        $nodes = $wrapper->childNodes;
1007
                } else {
1008
                        // A way to import strings with multiple root nodes.
1009 1
                        $wrapper->loadXML("<root>$v</root>");
1010
1011
                        // Algorithm 1:
1012 1
                        $nodes = $wrapper->documentElement->childNodes;
1013
1014
                        // Algorithm 2:
1015
                        // $dom_xp = new \DOMXPath($dom);
1016
                        // $nodes = $dom_xp->query('/root/*');
1017
                }
1018
1019 1
                return $this->attachNodes($parent, $nodes, $fn);
1020
        }
1021
1022 1
        protected function insertIntegerDomdocument($parent, $k, $v, $fn)
1023
        {
1024
                // A DOMDocument can have multiple root nodes.
1025
1026
                // Algorithm 1:
1027 1
                return $this->attachNodes($parent, $v->childNodes, $fn);
1028
1029
                // Algorithm 2:
1030
                // return $this->attachNodes($parent, $v->documentElement, $fn);
1031
        }
1032
1033 1
        protected function insertIntegerDomnodelist($parent, $k, $v, $fn)
1034
        {
1035 1
                return $this->attachNodes($parent, $v, $fn);
1036
        }
1037
1038 1
        protected function insertIntegerDomnode($parent, $k, $v, $fn)
1039
        {
1040 1
                return $this->attachNodes($parent, $v, $fn);
1041
        }
1042
1043 1
        protected function insertIntegerSimplexml($parent, $k, $v, $fn)
1044
        {
1045 1
                return $this->attachNodes($parent, \dom_import_simplexml($v), $fn);
1046
        }
1047
1048 1
        protected function insertIntegerFluidxml($parent, $k, $v, $fn)
1049
        {
1050 1
                return $this->attachNodes($parent, $v->dom()->documentElement, $fn);
1051
        }
1052
1053 1
        protected function insertIntegerFluidcontext($parent, $k, $v, $fn)
1054
        {
1055 1
                return $this->attachNodes($parent, $v->asArray(), $fn);
1056
        }
1057
}
1058
1059
class FluidContext implements FluidInterface, \ArrayAccess, \Iterator
1060
{
1061
        use NewableTrait,
1062
            ReservedCallTrait,          // For compatibility with PHP 5.6.
1063
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
1064
1065
        private $document;
1066
        private $handler;
1067
        private $nodes = [];
1068
        private $seek = 0;
1069
1070 1
        public function __construct($document, $handler, $context)
1071
        {
1072 1
                $this->document = $document;
1073 1
                $this->handler  = $handler;
1074
1075 1
                if (! \is_array($context) && ! $context instanceof \Traversable) {
1076
                        // DOMDocument, DOMElement and DOMNode are not iterable.
1077
                        // DOMNodeList and FluidContext are iterable.
1078 1
                        $context = [ $context ];
1079
                }
1080
1081 1
                foreach ($context as $n) {
1082 1
                        if (! $n instanceof \DOMNode) {
1083 1
                                throw new \Exception('Node type not recognized.');
1084
                        }
1085
1086 1
                        $this->nodes[] = $n;
1087
                }
1088 1
        }
1089
1090 1
        public function asArray()
1091
        {
1092 1
                return $this->nodes;
1093
        }
1094
1095
        // \ArrayAccess interface.
1096 1
        public function offsetSet($offset, $value)
1097
        {
1098
                // if (\is_null($offset)) {
1099
                //         $this->nodes[] = $value;
1100
                // } else {
1101
                //         $this->nodes[$offset] = $value;
1102
                // }
1103 1
                throw new \Exception('Setting a context element is not allowed.');
1104
        }
1105
1106
        // \ArrayAccess interface.
1107 1
        public function offsetExists($offset)
1108
        {
1109 1
                return isset($this->nodes[$offset]);
1110
        }
1111
1112
        // \ArrayAccess interface.
1113 1
        public function offsetUnset($offset)
1114
        {
1115
                // unset($this->nodes[$offset]);
1116 1
                \array_splice($this->nodes, $offset, 1);
1117 1
        }
1118
1119
        // \ArrayAccess interface.
1120 1
        public function offsetGet($offset)
1121
        {
1122 1
                if (isset($this->nodes[$offset])) {
1123 1
                        return $this->nodes[$offset];
1124
                }
1125
1126 1
                return null;
1127
        }
1128
1129
        // \Iterator interface.
1130 1
        public function rewind()
1131
        {
1132 1
                $this->seek = 0;
1133 1
        }
1134
1135
        // \Iterator interface.
1136 1
        public function current()
1137
        {
1138 1
                return $this->nodes[$this->seek];
1139
        }
1140
1141
        // \Iterator interface.
1142 1
        public function key()
1143
        {
1144 1
                return $this->seek;
1145
        }
1146
1147
        // \Iterator interface.
1148 1
        public function next()
1149
        {
1150 1
                ++$this->seek;
1151 1
        }
1152
1153
        // \Iterator interface.
1154 1
        public function valid()
1155
        {
1156 1
                return isset($this->nodes[$this->seek]);
1157
        }
1158
1159 1
        public function length()
1160
        {
1161 1
                return \count($this->nodes);
1162
        }
1163
1164 1
        public function query(...$xpath)
1165
        {
1166 1
                if (\is_array($xpath[0])) {
1167 1
                        $xpath = $xpath[0];
1168
                }
1169
1170 1
                $results = [];
1171
1172 1
                $xp = $this->document->xpath;
1173
1174 1
                foreach ($this->nodes as $n) {
1175 1
                        foreach ($xpath as $x) {
1176
                                // Returns a DOMNodeList.
1177 1
                                $res = $xp->query($x, $n);
1178
1179
                                // Algorithm 1:
1180
                                // $results = \array_merge($results, \iterator_to_array($res));
1181
1182
                                // Algorithm 2:
1183
                                // It is faster than \iterator_to_array and a lot faster
1184
                                // than \iterator_to_array + \array_merge.
1185 1
                                foreach ($res as $r) {
1186 1
                                        $results[] = $r;
1187
                                }
1188
1189
                                // Algorithm 3:
1190
                                // for ($i = 0, $l = $res->length; $i < $l; ++$i) {
1191
                                //         $results[] = $res->item($i);
1192
                                // }
1193
                        }
1194
                }
1195
1196
                // Performing over multiple sibling nodes a query that ascends
1197
                // the xpath, relative (../..) or absolute (//), returns identical
1198
                // matching results that must be collapsed in an unique result
1199
                // otherwise a subsequent operation is performed multiple times.
1200 1
                $results = $this->filterQueryResults($results);
1201
1202 1
                return $this->newContext($results);
1203
        }
1204
1205 1
        public function times($times, callable $fn = null)
1206
        {
1207 1
                if ($fn === null) {
1208 1
                        return new FluidRepeater($this->document, $this->handler, $this, $times);
1209
                }
1210
1211 1
                for ($i = 0; $i < $times; ++$i) {
1212 1
                        $this->callfn($fn, [$this, $i]);
1213
                }
1214
1215 1
                return $this;
1216
        }
1217
1218 1
        public function each(callable $fn)
1219
        {
1220 1
                foreach ($this->nodes as $i => $n) {
1221 1
                        $cx = $this->newContext($n);
1222
1223 1
                        $this->callfn($fn, [$cx, $i, $n]);
1224
                }
1225
1226 1
                return $this;
1227
        }
1228
1229
        // appendChild($child, $value?, $attributes? = [], $switchContext? = false)
1230 1
        public function appendChild($child, ...$optionals)
1231
        {
1232
                return $this->handler->insertElement($this->nodes, $child, $optionals, function($parent, $element) {
1233 1
                        return $parent->appendChild($element);
1234 1
                }, $this);
1235
        }
1236
1237
        // Alias of appendChild().
1238 1
        public function add($child, ...$optionals)
1239
        {
1240 1
                return $this->appendChild($child, ...$optionals);
1241
        }
1242
1243 1
        public function prependSibling($sibling, ...$optionals)
1244
        {
1245
                return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) {
1246 1
                        return $sibling->parentNode->insertBefore($element, $sibling);
1247 1
                }, $this);
1248
        }
1249
1250
        // Alias of prependSibling().
1251 1
        public function prepend($sibling, ...$optionals)
1252
        {
1253 1
                return $this->prependSibling($sibling, ...$optionals);
1254
        }
1255
1256
        // Alias of prependSibling().
1257 1
        public function insertSiblingBefore($sibling, ...$optionals)
1258
        {
1259 1
                return $this->prependSibling($sibling, ...$optionals);
1260
        }
1261
1262
        public function appendSibling($sibling, ...$optionals)
1263
        {
1264 1
                return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) {
1265
                        // If ->nextSibling is null, $element is simply appended as last sibling.
1266 1
                        return $sibling->parentNode->insertBefore($element, $sibling->nextSibling);
1267 1
                }, $this);
1268
        }
1269
1270
        // Alias of appendSibling().
1271 1
        public function append($sibling, ...$optionals)
1272
        {
1273 1
                return $this->appendSibling($sibling, ...$optionals);
1274
        }
1275
1276
        // Alias of appendSibling().
1277 1
        public function insertSiblingAfter($sibling, ...$optionals)
1278
        {
1279 1
                return $this->appendSibling($sibling, ...$optionals);
1280
        }
1281
1282
        // Arguments can be in the form of:
1283
        // setAttribute($name, $value)
1284
        // setAttribute(['name' => 'value', ...])
1285 1
        public function setAttribute(...$arguments)
1286
        {
1287
                // Default case is:
1288
                // [ 'name' => 'value', ... ]
1289 1
                $attrs = $arguments[0];
1290
1291
                // If the first argument is not an array,
1292
                // the user has passed two arguments:
1293
                // 1. is the attribute name
1294
                // 2. is the attribute value
1295 1
                if (! \is_array($arguments[0])) {
1296 1
                        $attrs = [$arguments[0] => $arguments[1]];
1297
                }
1298
1299 1
                foreach ($this->nodes as $n) {
1300 1
                        foreach ($attrs as $k => $v) {
1301
                                // Algorithm 1:
1302 1
                                $n->setAttribute($k, $v);
1303
1304
                                // Algorithm 2:
1305
                                // $n->setAttributeNode(new \DOMAttr($k, $v));
1306
1307
                                // Algorithm 3:
1308
                                // $n->appendChild(new \DOMAttr($k, $v));
1309
1310
                                // Algorithm 2 and 3 have a different behaviour
1311
                                // from Algorithm 1.
1312
                                // The attribute is still created or setted, but
1313
                                // changing the value of an existing attribute
1314
                                // changes even the order of that attribute
1315
                                // in the attribute list.
1316
                        }
1317
                }
1318
1319 1
                return $this;
1320
        }
1321
1322
        // Alias of setAttribute().
1323 1
        public function attr(...$arguments)
1324
        {
1325 1
                return $this->setAttribute(...$arguments);
1326
        }
1327
1328 1
        public function appendText($text)
1329
        {
1330 1
                foreach ($this->nodes as $n) {
1331 1
                        $n->appendChild(new \DOMText($text));
1332
                }
1333
1334 1
                return $this;
1335
        }
1336
1337 1
        public function appendCdata($text)
1338
        {
1339 1
                foreach ($this->nodes as $n) {
1340 1
                        $n->appendChild(new \DOMCDATASection($text));
1341
                }
1342
1343 1
                return $this;
1344
        }
1345
1346 1
        public function setText($text)
1347
        {
1348 1
                foreach ($this->nodes as $n) {
1349
                        // Algorithm 1:
1350 1
                        $n->nodeValue = $text;
1351
1352
                        // Algorithm 2:
1353
                        // foreach ($n->childNodes as $c) {
1354
                        //         $n->removeChild($c);
1355
                        // }
1356
                        // $n->appendChild(new \DOMText($text));
1357
1358
                        // Algorithm 3:
1359
                        // foreach ($n->childNodes as $c) {
1360
                        //         $n->replaceChild(new \DOMText($text), $c);
1361
                        // }
1362
                }
1363
1364 1
                return $this;
1365
        }
1366
1367
        // Alias of setText().
1368 1
        public function text($text)
1369
        {
1370 1
                return $this->setText($text);
1371
        }
1372
1373 1
        public function setCdata($text)
1374
        {
1375 1
                foreach ($this->nodes as $n) {
1376 1
                        $n->nodeValue = '';
1377 1
                        $n->appendChild(new \DOMCDATASection($text));
1378
                }
1379
1380 1
                return $this;
1381
        }
1382
1383
        // Alias of setCdata().
1384 1
        public function cdata($text)
1385
        {
1386 1
                return $this->setCdata($text);
1387
        }
1388
1389 1
        public function remove(...$xpath)
1390
        {
1391
                // Arguments can be empty, a string or an array of strings.
1392
1393 1
                if (empty($xpath)) {
1394
                        // The user has requested to remove the nodes of this context.
1395 1
                        $targets = $this->nodes;
1396
                } else {
1397 1
                        $targets = $this->query(...$xpath);
1398
                }
1399
1400 1
                foreach ($targets as $t) {
1401 1
                        $t->parentNode->removeChild($t);
1402
                }
1403
1404 1
                return $this;
1405
        }
1406
1407 1
        public function xml($strip = false)
1408
        {
1409 1
                return domnodes_to_string($this->nodes);
1410
        }
1411
1412 1
        protected function newContext(&$context)
1413
        {
1414 1
                return new FluidContext($this->document, $this->handler, $context);
1415
        }
1416
1417 1
        protected function filterQueryResults(&$results)
1418
        {
1419 1
                $set = [];
1420
1421 1
                foreach ($results as $r) {
1422 1
                        $found = false;
1423
1424 1
                        foreach ($set as $u) {
1425 1
                                $found = ($r === $u) || $found;
1426
                        }
1427
1428 1
                        if (! $found) {
1429 1
                                $set[] = $r;
1430
                        }
1431
                }
1432
1433 1
                return $set;
1434
        }
1435
1436 1
        protected function callfn($fn, $args)
1437
        {
1438 1
                if ($fn instanceof \Closure) {
1439 1
                        $bind = \array_shift($args);
1440
1441 1
                        $fn = $fn->bindTo($bind);
1442
1443
                        // It is faster than \call_user_func.
1444 1
                        return $fn(...$args);
1445
                }
1446
1447 1
                return \call_user_func($fn, ...$args);
1448
        }
1449
}
1450
1451
} // END OF NAMESPACE FluidXml\Core
1452