Completed
Push — master ( 3f00ec...1044f8 )
by Daniele
02:34
created

SaveableTrait   A

Complexity

Total Complexity 2

Size/Duplication

Total Lines 13
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0

Test Coverage

Coverage 100%
Metric Value
wmc 2
lcom 0
cbo 0
dl 0
loc 13
ccs 5
cts 5
cp 1
rs 10

1 Method

Rating   Name   Duplication   Size   Complexity  
A save() 0 10 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\SaveableTrait;
50
use \FluidXml\Core\ReservedCallTrait;
51
use \FluidXml\Core\ReservedCallStaticTrait;
52
53
/**
54
 * Constructs a new FluidXml instance.
55
 *
56
 * ```php
57
 * $xml = fluidxml();
58
 * // is the same of
59
 * $xml = new FluidXml();
60
 *
61
 * $xml = fluidxml([
62
 *
63
 *   'root'       => 'doc',
64
 *
65
 *   'version'    => '1.0',
66
 *
67
 *   'encoding'   => 'UTF-8',
68
 *
69
 *   'stylesheet' => null ]);
70
 * ```
71
 *
72
 * @param array $arguments Options that influence the construction of the XML document.
73
 *
74
 * @return FluidXml A new FluidXml instance.
75
 */
76
function fluidify(...$arguments)
77
{
78 1
        return FluidXml::load(...$arguments);
79
}
80
81
function fluidxml(...$arguments)
82
{
83 1
        return new FluidXml(...$arguments);
84
}
85
86
function fluidns(...$arguments)
87
{
88 1
        return new FluidNamespace(...$arguments);
89
}
90
91
function is_an_xml_string($string)
92
{
93
        // Removes any empty new line at the beginning,
94
        // otherwise the first character check may fail.
95 1
        $string = \ltrim($string);
96
97 1
        return $string[0] === '<';
98
}
99
100
function domdocument_to_string_without_headers(\DOMDocument $dom)
101
{
102 1
        return $dom->saveXML($dom->documentElement);
103
}
104
105
function domnodelist_to_string(\DOMNodeList $nodelist)
106
{
107 1
        $nodes = [];
108
109 1
        foreach ($nodelist as $n) {
110 1
                $nodes[] = $n;
111
        }
112
113 1
        return domnodes_to_string($nodes);
114
}
115
116
function domnodes_to_string(array $nodes)
117
{
118 1
        $dom = $nodes[0]->ownerDocument;
119 1
        $xml = '';
120
121 1
        foreach ($nodes as $n) {
122 1
                $xml .= $dom->saveXML($n) . PHP_EOL;
123
        }
124
125 1
        return \rtrim($xml);
126
}
127
128
function simplexml_to_string_without_headers(\SimpleXMLElement $element)
129
{
130 1
        $dom = \dom_import_simplexml($element);
131
132 1
        return $dom->ownerDocument->saveXML($dom);
133
}
134
135
/**
136
 * @method FluidXml namespace(...$arguments)
137
 */
138
class FluidXml implements FluidInterface
139
{
140
        use NewableTrait,
141
            SaveableTrait,
142
            ReservedCallTrait,          // For compatibility with PHP 5.6.
143
            ReservedCallStaticTrait;    // For compatibility with PHP 5.6.
144
145
        const ROOT_NODE = 'doc';
146
147
        private $document;
148
        private $handler;
149
150 1
        public static function load($document)
151
        {
152 1
                if (\is_string($document) && ! is_an_xml_string($document)) {
153 1
                        $file     = $document;
154 1
                        $document = \file_get_contents($file);
155
156
                        // file_get_contents returns false if it can't read.
157 1
                        if (! $document) {
158 1
                                throw new \Exception("File '$file' not accessible.");
159
                        }
160
                }
161
162 1
                return (new FluidXml(['root' => null]))->appendChild($document);
163
        }
164
165 1
        public function __construct($root = null, $options = [])
166
        {
167 1
                $defaults = [ 'root'       => self::ROOT_NODE,
168 1
                              'version'    => '1.0',
169 1
                              'encoding'   => 'UTF-8',
170
                              'stylesheet' => null ];
171
172 1
                if (\is_string($root)) {
173
                        // The root option can be specified as first argument
174
                        // because it is the most common.
175 1
                        $defaults['root'] = $root;
176 1
                } else if (\is_array($root)) {
177
                        // If the first argument is an array, the user has skipped
178
                        // the root option and is passing a bunch of options all together.
179 1
                        $options = $root;
180
                }
181
182 1
                $opts = \array_merge($defaults, $options);
183
184 1
                $this->document = new FluidDocument();
185 1
                $doc            = $this->document;
186
187 1
                $doc->dom = new \DOMDocument($opts['version'], $opts['encoding']);
188 1
                $doc->dom->formatOutput       = true;
189 1
                $doc->dom->preserveWhiteSpace = false;
190
191 1
                $doc->xpath    = new \DOMXPath($doc->dom);
192
193 1
                $this->handler = new FluidInsertionHandler($doc);
194
195 1
                if (! empty($opts['root'])) {
196 1
                        $this->appendSibling($opts['root']);
197
                }
198
199 1
                if (! empty($opts['stylesheet'])) {
200
                        $attrs = 'type="text/xsl" '
201 1
                               . "encoding=\"{$opts['encoding']}\" "
202 1
                               . 'indent="yes" '
203 1
                               . "href=\"{$opts['stylesheet']}\"";
204 1
                        $stylesheet = new \DOMProcessingInstruction('xml-stylesheet', $attrs);
205
206 1
                        $doc->dom->insertBefore($stylesheet, $doc->dom->documentElement);
207
                }
208 1
        }
209
210 1
        public function xml($strip = false)
211
        {
212 1
                if ($strip) {
213 1
                        return domdocument_to_string_without_headers($this->document->dom);
214
                }
215
216 1
                return $this->document->dom->saveXML();
217
        }
218
219 1
        public function dom()
220
        {
221 1
                return $this->document->dom;
222
        }
223
224 1
        public function namespaces()
225
        {
226 1
                return $this->document->namespaces;
227
        }
228
229
        // This method should be called 'namespace',
230
        // but for compatibility with PHP 5.6
231
        // it is shadowed by the __call() method.
232 1
        protected function namespace_(...$arguments)
233
        {
234 1
                $namespaces = [];
235
236 1
                if (\is_string($arguments[0])) {
237 1
                        $args = [ $arguments[0], $arguments[1] ];
238
239 1
                        if (isset($arguments[2])) {
240 1
                                $args[] = $arguments[2];
241
                        }
242
243 1
                        $namespaces[] = new FluidNamespace(...$args);
244 1
                } else if (\is_array($arguments[0])) {
245 1
                        $namespaces = $arguments[0];
246
                } else {
247 1
                        $namespaces = $arguments;
248
                }
249
250 1
                foreach ($namespaces as $n) {
251 1
                        $this->document->namespaces[$n->id()] = $n;
252 1
                        $this->document->xpath->registerNamespace($n->id(), $n->uri());
253
                }
254
255 1
                return $this;
256
        }
257
258 1
        public function query(...$xpath)
259
        {
260 1
                return $this->context()->query(...$xpath);
261
        }
262
263 1
        public function times($times, callable $fn = null)
264
        {
265 1
                return $this->context()->times($times, $fn);
266
        }
267
268 1
        public function each(callable $fn)
269
        {
270 1
                return $this->context()->each($fn);
271
        }
272
273 1
        public function appendChild($child, ...$optionals)
274
        {
275
                // If the user has requested ['root' => null] at construction time
276
                // 'context()' promotes DOMDocument as root node.
277 1
                $context     = $this->context();
278 1
                $new_context = $context->appendChild($child, ...$optionals);
279
280 1
                return $this->chooseContext($context, $new_context);
281
        }
282
283
        // Alias of appendChild().
284 1
        public function add($child, ...$optionals)
285
        {
286 1
                return $this->appendChild($child, ...$optionals);
287
        }
288
289 1 View Code Duplication
        public function prependSibling($sibling, ...$optionals)
290
        {
291 1
                if ($this->document->dom->documentElement === null) {
292
                        // If the document doesn't have at least one root node,
293
                        // the sibling creation fails. In this case we replace
294
                        // the sibling creation with the creation of a generic node.
295 1
                        return $this->appendChild($sibling, ...$optionals);
296
                }
297
298 1
                $context     = $this->context();
299 1
                $new_context = $context->prependSibling($sibling, ...$optionals);
300
301 1
                return $this->chooseContext($context, $new_context);
302
        }
303
304
        // Alias of prependSibling().
305 1
        public function prepend($sibling, ...$optionals)
306
        {
307 1
                return $this->prependSibling($sibling, ...$optionals);
308
        }
309
310
        // Alias of prependSibling().
311 1
        public function insertSiblingBefore($sibling, ...$optionals)
312
        {
313 1
                return $this->prependSibling($sibling, ...$optionals);
314
        }
315
316 1 View Code Duplication
        public function appendSibling($sibling, ...$optionals)
317
        {
318 1
                if ($this->document->dom->documentElement === null) {
319
                        // If the document doesn't have at least one root node,
320
                        // the sibling creation fails. In this case we replace
321
                        // the sibling creation with the creation of a generic node.
322 1
                        return $this->appendChild($sibling, ...$optionals);
323
                }
324
325 1
                $context     = $this->context();
326 1
                $new_context = $context->appendSibling($sibling, ...$optionals);
327
328 1
                return $this->chooseContext($context, $new_context);
329
        }
330
331
        // Alias of appendSibling().
332 1
        public function append($sibling, ...$optionals)
333
        {
334 1
                return $this->appendSibling($sibling, ...$optionals);
335
        }
336
337
        // Alias of appendSibling().
338 1
        public function insertSiblingAfter($sibling, ...$optionals)
339
        {
340 1
                return $this->appendSibling($sibling, ...$optionals);
341
        }
342
343 1
        public function setAttribute(...$arguments)
344
        {
345 1
                $this->context()->setAttribute(...$arguments);
346
347 1
                return $this;
348
        }
349
350
        // Alias of setAttribute().
351 1
        public function attr(...$arguments)
352
        {
353 1
                return $this->setAttribute(...$arguments);
354
        }
355
356 1
        public function appendText($text)
357
        {
358 1
                $this->context()->appendText($text);
359
360 1
                return $this;
361
        }
362
363 1
        public function appendCdata($text)
364
        {
365 1
                $this->context()->appendCdata($text);
366
367 1
                return $this;
368
        }
369
370 1
        public function setText($text)
371
        {
372 1
                $this->context()->setText($text);
373
374 1
                return $this;
375
        }
376
377
        // Alias of setText().
378 1
        public function text($text)
379
        {
380 1
                return $this->setText($text);
381
        }
382
383 1
        public function setCdata($text)
384
        {
385 1
                $this->context()->setCdata($text);
386
387 1
                return $this;
388
        }
389
390
        // Alias of setCdata().
391 1
        public function cdata($text)
392
        {
393 1
                return $this->setCdata($text);
394
        }
395
396 1
        public function remove(...$xpath)
397
        {
398 1
                $this->context()->remove(...$xpath);
399
400 1
                return $this;
401
        }
402
403
        private $context;
404
        private $contextEl;
405
406 1
        protected function context()
407
        {
408 1
                $el = $this->document->dom->documentElement;
409
410 1
                if ($el === null) {
411
                        // Whether there is not a root node
412
                        // the DOMDocument is promoted as root node.
413 1
                        $el = $this->document->dom;
414
                }
415
416 1
                if ($this->context === null || $el !== $this->contextEl) {
417
                        // The user can prepend a root node to the current root node.
418
                        // In this case we have to update the context with the new first root node.
419 1
                        $this->context   = new FluidContext($this->document, $this->handler, $el);
420 1
                        $this->contextEl = $el;
421
                }
422
423 1
                return $this->context;
424
        }
425
426 1
        protected function chooseContext($help_context, $new_context)
427
        {
428
                // If the two contextes are diffent, the user has requested
429
                // a switch of the context and we have to return it.
430 1
                if ($help_context !== $new_context) {
431 1
                        return $new_context;
432
                }
433
434 1
                return $this;
435
        }
436
}
437
438
class FluidNamespace
439
{
440
        const ID   = 'id'  ;
441
        const URI  = 'uri' ;
442
        const MODE = 'mode';
443
444
        const MODE_IMPLICIT = 0;
445
        const MODE_EXPLICIT = 1;
446
447
        private $config = [ self::ID   => '',
448
                            self::URI  => '',
449
                            self::MODE => self::MODE_EXPLICIT ];
450
451 1
        public function __construct($id, $uri, $mode = 1)
452
        {
453 1
                if (\is_array($id)) {
454 1
                        $args = $id;
455 1
                        $id   = $args[self::ID];
456 1
                        $uri  = $args[self::URI];
457
458 1
                        if (isset($args[self::MODE])) {
459 1
                                $mode = $args[self::MODE];
460
                        }
461
                }
462
463 1
                $this->config[self::ID]   = $id;
464 1
                $this->config[self::URI]  = $uri;
465 1
                $this->config[self::MODE] = $mode;
466 1
        }
467
468 1
        public function id()
469
        {
470 1
                return $this->config[self::ID];
471
        }
472
473 1
        public function uri()
474
        {
475 1
                return $this->config[self::URI];
476
        }
477
478 1
        public function mode()
479
        {
480 1
                return $this->config[self::MODE];
481
        }
482
483 1
        public function __invoke($xpath)
484
        {
485 1
                $id = $this->id();
486
487 1
                if (! empty($id)) {
488 1
                        $id .= ':';
489
                }
490
491
                // An XPath query may not start with a slash ('/').
492
                // Relative queries are an example '../target".
493 1
                $new_xpath = '';
494
495 1
                $nodes = \explode('/', $xpath);
496
497 1
                foreach ($nodes as $node) {
498 1
                        if (! empty($node)) {
499
                                // An XPath query can have multiple slashes.
500
                                // Example: //target
501 1
                                $new_xpath .= "{$id}{$node}";
502
                        }
503
504 1
                        $new_xpath .= '/';
505
                }
506
507
                // Removes the last appended slash.
508 1
                return \substr($new_xpath, 0, -1);
509
        }
510
}
511
512
} // END OF NAMESPACE FluidXml
513
514
namespace FluidXml\Core
515
{
516
517
use \FluidXml\FluidXml;
518
use \FluidXml\FluidNamespace;
519
520
use function \FluidXml\is_an_xml_string;
521
use function \FluidXml\domnodes_to_string;
522
523
interface FluidInterface
524
{
525
        /**
526
         * Executes an XPath query.
527
         *
528
         * ```php
529
         * $xml = fluidxml();
530
531
         * $xml->query("/doc/book[@id='123']");
532
         *
533
         * // Relative queries are valid.
534
         * $xml->query("/doc")->query("book[@id='123']");
535
         * ```
536
         *
537
         * @param string $xpath The XPath to execute.
538
         *
539
         * @return FluidContext The context associated to the DOMNodeList.
540
         */
541
        public function query(...$xpath);
542
        public function times($times, callable $fn = null);
543
        public function each(callable $fn);
544
545
        /**
546
         * Append a new node as child of the current context.
547
         *
548
         * ```php
549
         * $xml = fluidxml();
550
551
         * $xml->appendChild('title', 'The Theory Of Everything');
552
         * $xml->appendChild([ 'author' => 'S. Hawking' ]);
553
         *
554
         * $xml->appendChild('chapters', true)->appendChild('chapter', ['id'=> 1]);
555
         *
556
         * ```
557
         *
558
         * @param string|array $child The child/children to add.
559
         * @param ...$optionals Accepted values are:
560
         *                      - a boolean for requesting the context switch
561
         *                      - a string which is the element text content
562
         *                      - an array containing the attributes to set on the element.
563
         *
564
         * @return FluidContext The context associated to the DOMNodeList.
565
         */
566
        public function appendChild($child, ...$optionals);
567
        public function prependSibling($sibling, ...$optionals);
568
        public function appendSibling($sibling, ...$optionals);
569
        public function setAttribute(...$arguments);
570
        public function setText($text);
571
        public function appendText($text);
572
        public function setCdata($text);
573
        public function appendCdata($text);
574
        public function remove(...$xpath);
575
        public function xml($strip = false);
576
        public function save($file, $strip = false);
577
        // Aliases:
578
        public function add($child, ...$optionals);
579
        public function prepend($sibling, ...$optionals);
580
        public function insertSiblingBefore($sibling, ...$optionals);
581
        public function append($sibling, ...$optionals);
582
        public function insertSiblingAfter($sibling, ...$optionals);
583
        public function attr(...$arguments);
584
        public function text($text);
585
}
586
587
trait ReservedCallTrait
588
{
589 1
        public function __call($method, $arguments)
590
        {
591 1
                $m = "{$method}_";
592
593 1
                if (\method_exists($this, $m)) {
594 1
                        return $this->$m(...$arguments);
595
                }
596
597 1
                throw new \Exception("Method '$method' not found.");
598
        }
599
}
600
601
trait ReservedCallStaticTrait
602
{
603 1
        public static function __callStatic($method, $arguments)
604
        {
605 1
                $m = "{$method}_";
606
607 1
                if (\method_exists(static::class, $m)) {
608 1
                        return static::$m(...$arguments);
609
                }
610
611 1
                throw new \Exception("Method '$method' not found.");
612
        }
613
}
614
615
trait NewableTrait
616
{
617
        // This method should be called 'new',
618
        // but for compatibility with PHP 5.6
619
        // it is shadowed by the __callStatic() method.
620 1
        public static function new_(...$arguments)
621
        {
622 1
                return new static(...$arguments);
623
        }
624
}
625
626
trait SaveableTrait
627
{
628 1
        public function save($file, $strip = false)
629
        {
630 1
                $status = \file_put_contents($file, $this->xml($strip));
0 ignored issues
show
Bug introduced by
It seems like xml() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

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