Passed
Push — master ( f21bed...142c4d )
by Arthur
02:05
created

DOMQuery::__call()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 5
nop 2
dl 0
loc 30
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * @file
4
 * This houses the class formerly called QueryPath.
5
 *
6
 * As of QueryPath 3.0.0, the class was renamed QueryPath::DOMQuery. This
7
 * was done for a few reasons:
8
 * - The library has been refactored, and it made more sense to call the top
9
 *   level class QueryPath. This is not the top level class.
10
 * - There have been requests for a JSONQuery class, which would be the
11
 *   natural complement of DOMQuery.
12
 */
13
14
namespace QueryPath;
15
16
use QueryPath\CSS\DOMTraverser;
17
use \QueryPath\CSS\QueryPathEventHandler;
18
use \Masterminds\HTML5;
19
use QueryPath\Entities;
20
use QueryPath\Exception;
21
use QueryPath\Helpers\QueryChecks;
22
use QueryPath\Helpers\QueryFilters;
23
use QueryPath\Helpers\QueryMutators;
24
25
/**
26
 * The DOMQuery object is the primary tool in this library.
27
 *
28
 * To create a new DOMQuery, use QueryPath::with() or qp() function.
29
 *
30
 * If you are new to these documents, start at the QueryPath.php page.
31
 * There you will find a quick guide to the tools contained in this project.
32
 *
33
 * A note on serialization: Query uses DOM classes internally, and those
34
 * do not serialize well at all. In addition, DOMQuery may contain many
35
 * extensions, and there is no guarantee that extensions can serialize. The
36
 * moral of the story: Don't serialize DOMQuery.
37
 *
38
 * @see     qp()
39
 * @see     QueryPath.php
40
 * @ingroup querypath_core
41
 */
42
class DOMQuery extends DOM
43
{
44
45
    use QueryFilters, QueryMutators, QueryChecks;
0 ignored issues
show
introduced by
The trait QueryPath\Helpers\QueryFilters requires some properties which are not provided by QueryPath\DOMQuery: $nextSibling, $previousSibling, $nodeType, $childNodes, $parentNode
Loading history...
introduced by
The trait QueryPath\Helpers\QueryMutators requires some properties which are not provided by QueryPath\DOMQuery: $ownerDocument, $nextSibling, $attributes, $documentElement, $nodeType, $childNodes, $value, $firstChild, $parentNode
Loading history...
Bug introduced by
The trait QueryPath\Helpers\QueryChecks requires the property $parentNode which is not provided by QueryPath\DOMQuery.
Loading history...
46
47
    /**
48
     * The last array of matches.
49
     */
50
    protected $last = []; // Last set of matches.
51
    private $ext = []; // Extensions array.
52
53
    /**
54
     * The number of current matches.
55
     *
56
     * @see count()
57
     */
58
    public $length = 0;
59
60
    /**
61
     * Get the effective options for the current DOMQuery object.
62
     *
63
     * This returns an associative array of all of the options as set
64
     * for the current DOMQuery object. This includes default options,
65
     * options directly passed in via {@link qp()} or the constructor,
66
     * an options set in the QueryPath::Options object.
67
     *
68
     * The order of merging options is this:
69
     *  - Options passed in using qp() are highest priority, and will
70
     *    override other options.
71
     *  - Options set with QueryPath::Options will override default options,
72
     *    but can be overridden by options passed into qp().
73
     *  - Default options will be used when no overrides are present.
74
     *
75
     * This function will return the options currently used, with the above option
76
     * overriding having been calculated already.
77
     *
78
     * @return array
79
     *  An associative array of options, calculated from defaults and overridden
80
     *  options.
81
     * @see   qp()
82
     * @see   QueryPath::Options::set()
83
     * @see   QueryPath::Options::merge()
84
     * @since 2.0
85
     */
86
    public function getOptions(): array
87
    {
88
        return $this->options;
89
    }
90
91
    /**
92
     * Select the root element of the document.
93
     *
94
     * This sets the current match to the document's root element. For
95
     * practical purposes, this is the same as:
96
     *
97
     * @code
98
     * qp($someDoc)->find(':root');
99
     * @endcode
100
     * However, since it doesn't invoke a parser, it has less overhead. It also
101
     * works in cases where the QueryPath has been reduced to zero elements (a
102
     * case that is not handled by find(':root') because there is no element
103
     * whose root can be found).
104
     *
105
     * @param string $selector
106
     *  A selector. If this is supplied, QueryPath will navigate to the
107
     *  document root and then run the query. (Added in QueryPath 2.0 Beta 2)
108
     * @return \QueryPath\DOMQuery
109
     *  The DOMQuery object, wrapping the root element (document element)
110
     *  for the current document.
111
     * @throws CSS\ParseException
112
     */
113
    public function top($selector = NULL): Query
114
    {
115
        return $this->inst($this->document->documentElement, $selector);
116
    }
117
118
    /**
119
     * Given a CSS Selector, find matching items.
120
     *
121
     * @param string $selector
122
     *   CSS 3 Selector
123
     * @return \QueryPath\DOMQuery
124
     * @see  filter()
125
     * @see  is()
126
     * @todo If a find() returns zero matches, then a subsequent find() will
127
     *   also return zero matches, even if that find has a selector like :root.
128
     *   The reason for this is that the {@link QueryPathEventHandler} does
129
     *   not set the root of the document tree if it cannot find any elements
130
     *   from which to determine what the root is. The workaround is to use
131
     *   {@link top()} to select the root element again.
132
     * @throws CSS\ParseException
133
     */
134
    public function find($selector): Query
135
    {
136
        $query = new DOMTraverser($this->matches);
0 ignored issues
show
Bug introduced by
$this->matches of type array is incompatible with the type SPLObjectStorage expected by parameter $splos of QueryPath\CSS\DOMTraverser::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

136
        $query = new DOMTraverser(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
137
        $query->find($selector);
138
        return $this->inst($query->matches(), NULL);
139
    }
140
141
    /**
142
     * @param $selector
143
     * @return $this
144
     * @throws CSS\ParseException
145
     */
146
    public function findInPlace($selector)
147
    {
148
        $query = new DOMTraverser($this->matches);
0 ignored issues
show
Bug introduced by
$this->matches of type array is incompatible with the type SPLObjectStorage expected by parameter $splos of QueryPath\CSS\DOMTraverser::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

148
        $query = new DOMTraverser(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
149
        $query->find($selector);
150
        $this->setMatches($query->matches());
151
152
        return $this;
153
    }
154
155
    /**
156
     * Execute an XPath query and store the results in the QueryPath.
157
     *
158
     * Most methods in this class support CSS 3 Selectors. Sometimes, though,
159
     * XPath provides a finer-grained query language. Use this to execute
160
     * XPath queries.
161
     *
162
     * Beware, though. DOMQuery works best on DOM Elements, but an XPath
163
     * query can return other nodes, strings, and values. These may not work with
164
     * other QueryPath functions (though you will be able to access the
165
     * values with {@link get()}).
166
     *
167
     * @param string $query
168
     *      An XPath query.
169
     * @param array $options
170
     *      Currently supported options are:
171
     *      - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
172
     *      in conjunction with 'namespace_uri'
173
     *      - 'namespace_uri': The URI to be used as the default namespace URI. Used
174
     *      with 'namespace_prefix'
175
     * @return \QueryPath\DOMQuery
176
     *      A DOMQuery object wrapping the results of the query.
177
     * @see    find()
178
     * @author M Butcher
179
     * @author Xavier Prud'homme
180
     * @throws CSS\ParseException
181
     */
182
    public function xpath($query, $options = [])
183
    {
184
        $xpath = new \DOMXPath($this->document);
185
186
        // Register a default namespace.
187
        if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) {
188
            $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
189
        }
190
191
        $found = new \SplObjectStorage();
192
        foreach ($this->matches as $item) {
193
            $nl = $xpath->query($query, $item);
194
            if ($nl->length > 0) {
195
                for ($i = 0; $i < $nl->length; ++$i) {
196
                    $found->attach($nl->item($i));
197
                }
198
            }
199
        }
200
201
        return $this->inst($found, NULL);
202
    }
203
204
    /**
205
     * Get the number of elements currently wrapped by this object.
206
     *
207
     * Note that there is no length property on this object.
208
     *
209
     * @return int
210
     *  Number of items in the object.
211
     * @deprecated QueryPath now implements Countable, so use count().
212
     */
213
    public function size()
214
    {
215
        return $this->matches->count();
216
    }
217
218
    /**
219
     * Get the number of elements currently wrapped by this object.
220
     *
221
     * Since DOMQuery is Countable, the PHP count() function can also
222
     * be used on a DOMQuery.
223
     *
224
     * @code
225
     * <?php
226
     *  count(qp($xml, 'div'));
227
     * ?>
228
     * @endcode
229
     *
230
     * @return int
231
     *  The number of matches in the DOMQuery.
232
     */
233
    public function count(): int
234
    {
235
        return $this->matches->count();
236
    }
237
238
    /**
239
     * Get one or all elements from this object.
240
     *
241
     * When called with no paramaters, this returns all objects wrapped by
242
     * the DOMQuery. Typically, these are DOMElement objects (unless you have
243
     * used map(), xpath(), or other methods that can select
244
     * non-elements).
245
     *
246
     * When called with an index, it will return the item in the DOMQuery with
247
     * that index number.
248
     *
249
     * Calling this method does not change the DOMQuery (e.g. it is
250
     * non-destructive).
251
     *
252
     * You can use qp()->get() to iterate over all elements matched. You can
253
     * also iterate over qp() itself (DOMQuery implementations must be Traversable).
254
     * In the later case, though, each item
255
     * will be wrapped in a DOMQuery object. To learn more about iterating
256
     * in QueryPath, see {@link examples/techniques.php}.
257
     *
258
     * @param int $index
259
     *   If specified, then only this index value will be returned. If this
260
     *   index is out of bounds, a NULL will be returned.
261
     * @param boolean $asObject
262
     *   If this is TRUE, an SplObjectStorage object will be returned
263
     *   instead of an array. This is the preferred method for extensions to use.
264
     * @return mixed
265
     *   If an index is passed, one element will be returned. If no index is
266
     *   present, an array of all matches will be returned.
267
     * @see eq()
268
     * @see SplObjectStorage
269
     */
270
    public function get($index = NULL, $asObject = false)
271
    {
272
        if ($index !== NULL) {
273
            return ($this->count() > $index) ? $this->getNthMatch($index) : NULL;
274
        }
275
        // Retain support for legacy.
276
        if (!$asObject) {
277
            $matches = [];
278
            foreach ($this->matches as $m) {
279
                $matches[] = $m;
280
            }
281
282
            return $matches;
283
        }
284
285
        return $this->matches;
286
    }
287
288
    /**
289
     * Get the namespace of the current element.
290
     *
291
     * If QP is currently pointed to a list of elements, this will get the
292
     * namespace of the first element.
293
     */
294
    public function ns()
295
    {
296
        return $this->get(0)->namespaceURI;
297
    }
298
299
    /**
300
     * Get the DOMDocument that we currently work with.
301
     *
302
     * This returns the current DOMDocument. Any changes made to this document will be
303
     * accessible to DOMQuery, as both will share access to the same object.
304
     *
305
     * @return DOMDocument
0 ignored issues
show
Bug introduced by
The type QueryPath\DOMDocument was not found. Did you mean DOMDocument? If so, make sure to prefix the type with \.
Loading history...
306
     */
307
    public function document()
308
    {
309
        return $this->document;
310
    }
311
312
    /**
313
     * On an XML document, load all XIncludes.
314
     *
315
     * @return \QueryPath\DOMQuery
316
     */
317
    public function xinclude()
318
    {
319
        $this->document->xinclude();
320
321
        return $this;
322
    }
323
324
    /**
325
     * Get all current elements wrapped in an array.
326
     * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
327
     * with no parameters.
328
     *
329
     * @return array
330
     *  An array of DOMNodes (typically DOMElements).
331
     */
332
    public function toArray()
333
    {
334
        return $this->get();
335
    }
336
337
    /**
338
     * Insert or retrieve a Data URL.
339
     *
340
     * When called with just $attr, it will fetch the result, attempt to decode it, and
341
     * return an array with the MIME type and the application data.
342
     *
343
     * When called with both $attr and $data, it will inject the data into all selected elements
344
     * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
345
     * the given PNG image into the selected elements.
346
     *
347
     * The current implementation only knows how to encode and decode Base 64 data.
348
     *
349
     * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
350
     *
351
     * @param string $attr
352
     *    The name of the attribute.
353
     * @param mixed $data
354
     *    The contents to inject as the data. The value can be any one of the following:
355
     *    - A URL: If this is given, then the subsystem will read the content from that URL. THIS
356
     *    MUST BE A FULL URL, not a relative path.
357
     *    - A string of data: If this is given, then the subsystem will encode the string.
358
     *    - A stream or file handle: If this is given, the stream's contents will be encoded
359
     *    and inserted as data.
360
     *    (Note that we make the assumption here that you would never want to set data to be
361
     *    a URL. If this is an incorrect assumption, file a bug.)
362
     * @param string $mime
363
     *    The MIME type of the document.
364
     * @param resource $context
365
     *    A valid context. Use this only if you need to pass a stream context. This is only necessary
366
     *    if $data is a URL. (See {@link stream_context_create()}).
367
     * @return \QueryPath\DOMQuery|string
368
     *    If this is called as a setter, this will return a DOMQuery object. Otherwise, it
369
     *    will attempt to fetch data out of the attribute and return that.
370
     * @see   http://en.wikipedia.org/wiki/Data:_URL
371
     * @see   attr()
372
     * @since 2.1
373
     */
374
    public function dataURL($attr, $data = NULL, $mime = 'application/octet-stream', $context = NULL)
375
    {
376
        if (is_null($data)) {
377
            // Attempt to fetch the data
378
            $data = $this->attr($attr);
379
            if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type QueryPath\DOMQuery; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

379
            if (empty($data) || is_array($data) || strpos(/** @scrutinizer ignore-type */ $data, 'data:') !== 0) {
Loading history...
380
                return;
381
            }
382
383
            // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
384
            $regex = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
385
            $matches = [];
386
            preg_match($regex, $data, $matches);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type QueryPath\DOMQuery; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

386
            preg_match($regex, /** @scrutinizer ignore-type */ $data, $matches);
Loading history...
387
388
            if (!empty($matches)) {
389
                $result = [
390
                    'mime' => $matches[1] . '/' . $matches[2],
391
                    'data' => base64_decode($matches[3]),
392
                ];
393
394
                return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type array<string,string> which is incompatible with the documented return type QueryPath\DOMQuery|string.
Loading history...
395
            }
396
        } else {
397
            $attVal = QueryPath::encodeDataURL($data, $mime, $context);
398
399
            return $this->attr($attr, $attVal);
400
        }
401
    }
402
403
    /**
404
     * Sort the contents of the QueryPath object.
405
     *
406
     * By default, this does not change the order of the elements in the
407
     * DOM. Instead, it just sorts the internal list. However, if TRUE
408
     * is passed in as the second parameter then QueryPath will re-order
409
     * the DOM, too.
410
     *
411
     * @attention
412
     * DOM re-ordering is done by finding the location of the original first
413
     * item in the list, and then placing the sorted list at that location.
414
     *
415
     * The argument $compartor is a callback, such as a function name or a
416
     * closure. The callback receives two DOMNode objects, which you can use
417
     * as DOMNodes, or wrap in QueryPath objects.
418
     *
419
     * A simple callback:
420
     * @code
421
     * <?php
422
     * $comp = function (\DOMNode $a, \DOMNode $b) {
423
     *   if ($a->textContent == $b->textContent) {
424
     *     return 0;
425
     *   }
426
     *   return $a->textContent > $b->textContent ? 1 : -1;
427
     * };
428
     * $qp = QueryPath::with($xml, $selector)->sort($comp);
429
     * ?>
430
     * @endcode
431
     *
432
     * The above sorts the matches into lexical order using the text of each node.
433
     * If you would prefer to work with QueryPath objects instead of DOMNode
434
     * objects, you may prefer something like this:
435
     *
436
     * @code
437
     * <?php
438
     * $comp = function (\DOMNode $a, \DOMNode $b) {
439
     *   $qpa = qp($a);
440
     *   $qpb = qp($b);
441
     *
442
     *   if ($qpa->text() == $qpb->text()) {
443
     *     return 0;
444
     *   }
445
     *   return $qpa->text()> $qpb->text()? 1 : -1;
446
     * };
447
     *
448
     * $qp = QueryPath::with($xml, $selector)->sort($comp);
449
     * ?>
450
     * @endcode
451
     *
452
     * @param callback $comparator
453
     *   A callback. This will be called during sorting to compare two DOMNode
454
     *   objects.
455
     * @param boolean $modifyDOM
456
     *   If this is TRUE, the sorted results will be inserted back into
457
     *   the DOM at the position of the original first element.
458
     * @return \QueryPath\DOMQuery
459
     *   This object.
460
     * @throws CSS\ParseException
461
     */
462
    public function sort($comparator, $modifyDOM = false): Query
463
    {
464
        // Sort as an array.
465
        $list = iterator_to_array($this->matches);
0 ignored issues
show
Bug introduced by
$this->matches of type array is incompatible with the type Traversable expected by parameter $iterator of iterator_to_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

465
        $list = iterator_to_array(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
466
467
        if (empty($list)) {
468
            return $this;
469
        }
470
471
        $oldFirst = $list[0];
472
473
        usort($list, $comparator);
474
475
        // Copy back into SplObjectStorage.
476
        $found = new \SplObjectStorage();
477
        foreach ($list as $node) {
478
            $found->attach($node);
479
        }
480
        //$this->setMatches($found);
481
482
483
        // Do DOM modifications only if necessary.
484
        if ($modifyDOM) {
485
            $placeholder = $oldFirst->ownerDocument->createElement('_PLACEHOLDER_');
486
            $placeholder = $oldFirst->parentNode->insertBefore($placeholder, $oldFirst);
487
            $len = count($list);
488
            for ($i = 0; $i < $len; ++$i) {
489
                $node = $list[$i];
490
                $node = $node->parentNode->removeChild($node);
491
                $placeholder->parentNode->insertBefore($node, $placeholder);
492
            }
493
            $placeholder->parentNode->removeChild($placeholder);
494
        }
495
496
        return $this->inst($found, NULL);
497
    }
498
499
    /**
500
     * Get an item's index.
501
     *
502
     * Given a DOMElement, get the index from the matches. This is the
503
     * converse of {@link get()}.
504
     *
505
     * @param DOMElement $subject
0 ignored issues
show
Bug introduced by
The type QueryPath\DOMElement was not found. Did you mean DOMElement? If so, make sure to prefix the type with \.
Loading history...
506
     *  The item to match.
507
     *
508
     * @return mixed
509
     *  The index as an integer (if found), or boolean FALSE. Since 0 is a
510
     *  valid index, you should use strong equality (===) to test..
511
     * @see get()
512
     * @see is()
513
     */
514
    public function index($subject)
515
    {
516
        $i = 0;
517
        foreach ($this->matches as $m) {
518
            if ($m === $subject) {
519
                return $i;
520
            }
521
            ++$i;
522
        }
523
524
        return false;
525
    }
526
527
    /**
528
     * The tag name of the first element in the list.
529
     *
530
     * This returns the tag name of the first element in the list of matches. If
531
     * the list is empty, an empty string will be used.
532
     *
533
     * @see replaceAll()
534
     * @see replaceWith()
535
     * @return string
536
     *  The tag name of the first element in the list.
537
     */
538
    public function tag()
539
    {
540
        return ($this->matches->count() > 0) ? $this->getFirstMatch()->tagName : '';
541
    }
542
543
    /**
544
     * Revert to the previous set of matches.
545
     *
546
     * <b>DEPRECATED</b> Do not use.
547
     *
548
     * This will revert back to the last set of matches (before the last
549
     * "destructive" set of operations). This undoes any change made to the set of
550
     * matched objects. Functions like find() and filter() change the
551
     * list of matched objects. The end() function will revert back to the last set of
552
     * matched items.
553
     *
554
     * Note that functions that modify the document, but do not change the list of
555
     * matched objects, are not "destructive". Thus, calling append('something')->end()
556
     * will not undo the append() call.
557
     *
558
     * Only one level of changes is stored. Reverting beyond that will result in
559
     * an empty set of matches. Example:
560
     *
561
     * @code
562
     * // The line below returns the same thing as qp(document, 'p');
563
     * qp(document, 'p')->find('div')->end();
564
     * // This returns an empty array:
565
     * qp(document, 'p')->end();
566
     * // This returns an empty array:
567
     * qp(document, 'p')->find('div')->find('span')->end()->end();
568
     * @endcode
569
     *
570
     * The last one returns an empty array because only one level of changes is stored.
571
     *
572
     * @return \QueryPath\DOMQuery
573
     *  A DOMNode object reflecting the list of matches prior to the last destructive
574
     *  operation.
575
     * @see        andSelf()
576
     * @see        add()
577
     * @deprecated This function will be removed.
578
     */
579
    public function end()
580
    {
581
        // Note that this does not use setMatches because it must set the previous
582
        // set of matches to empty array.
583
        $this->matches = $this->last;
584
        $this->last = new \SplObjectStorage();
585
586
        return $this;
587
    }
588
589
    /**
590
     * Combine the current and previous set of matched objects.
591
     *
592
     * Example:
593
     *
594
     * @code
595
     * qp(document, 'p')->find('div')->andSelf();
596
     * @endcode
597
     *
598
     * The code above will contain a list of all p elements and all div elements that
599
     * are beneath p elements.
600
     *
601
     * @see end();
602
     * @return \QueryPath\DOMQuery
603
     *  A DOMNode object with the results of the last two "destructive" operations.
604
     * @see add()
605
     * @see end()
606
     */
607
    public function andSelf()
608
    {
609
        // This is destructive, so we need to set $last:
610
        $last = $this->matches;
611
612
        foreach ($this->last as $item) {
613
            $this->matches->attach($item);
614
        }
615
616
        $this->last = $last;
617
618
        return $this;
619
    }
620
621
    /**
622
     * Set or get the markup for an element.
623
     *
624
     * If $markup is set, then the giving markup will be injected into each
625
     * item in the set. All other children of that node will be deleted, and this
626
     * new code will be the only child or children. The markup MUST BE WELL FORMED.
627
     *
628
     * If no markup is given, this will return a string representing the child
629
     * markup of the first node.
630
     *
631
     * <b>Important:</b> This differs from jQuery's html() function. This function
632
     * returns <i>the current node</i> and all of its children. jQuery returns only
633
     * the children. This means you do not need to do things like this:
634
     * @code$qp->parent()->html()@endcode.
635
     *
636
     * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
637
     *
638
     * @param string $markup
639
     *  The text to insert.
640
     * @return mixed
641
     *  A string if no markup was passed, or a DOMQuery if markup was passed.
642
     * @throws Exception
643
     * @throws QueryPath
644
     * @see xml()
645
     * @see text()
646
     * @see contents()
647
     */
648
    public function html($markup = NULL)
649
    {
650
        if (isset($markup)) {
651
652
            if ($this->options['replace_entities']) {
653
                $markup = Entities::replaceAllEntities($markup);
654
            }
655
656
            // Parse the HTML and insert it into the DOM
657
            //$doc = DOMDocument::loadHTML($markup);
658
            $doc = $this->document->createDocumentFragment();
659
            $doc->appendXML($markup);
660
            $this->removeChildren();
661
            $this->append($doc);
662
663
            return $this;
664
        }
665
        $length = $this->matches->count();
666
        if ($length === 0) {
667
            return NULL;
668
        }
669
        // Only return the first item -- that's what JQ does.
670
        $first = $this->getFirstMatch();
671
672
        // Catch cases where first item is not a legit DOM object.
673
        if (!($first instanceof \DOMNode)) {
674
            return NULL;
675
        }
676
677
        // Added by eabrand.
678
        if (!$first->ownerDocument->documentElement) {
679
            return NULL;
680
        }
681
682
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
683
            return $this->document->saveHTML();
684
        }
685
686
        // saveHTML cannot take a node and serialize it.
687
        return $this->document->saveXML($first);
688
    }
689
690
    /**
691
     * Write the QueryPath document to HTML5.
692
     *
693
     * See html()
694
     *
695
     * @param null $markup
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $markup is correct as it would always require null to be passed?
Loading history...
696
     * @return null|DOMQuery|string
697
     * @throws QueryPath
698
     * @throws \QueryPath\Exception
699
     */
700
    public function html5($markup = NULL)
701
    {
702
        $html5 = new HTML5($this->options);
703
704
        // append HTML to existing
705
        if ($markup === NULL) {
706
707
            // Parse the HTML and insert it into the DOM
708
            $doc = $html5->loadHTMLFragment($markup);
709
            $this->removeChildren();
710
            $this->append($doc);
711
712
            return $this;
713
        }
714
715
        $length = $this->count();
716
        if ($length === 0) {
717
            return NULL;
718
        }
719
        // Only return the first item -- that's what JQ does.
720
        $first = $this->getFirstMatch();
721
722
        // Catch cases where first item is not a legit DOM object.
723
        if (!($first instanceof \DOMNode)) {
724
            return NULL;
725
        }
726
727
        // Added by eabrand.
728
        if (!$first->ownerDocument->documentElement) {
729
            return NULL;
730
        }
731
732
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
733
            return $html5->saveHTML($this->document); //$this->document->saveHTML();
734
        }
735
736
        return $html5->saveHTML($first);
737
    }
738
739
    /**
740
     * Fetch the HTML contents INSIDE of the first DOMQuery item.
741
     *
742
     * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
743
     *
744
     * This gets all children of the first match in DOMQuery.
745
     *
746
     * Consider this fragment:
747
     *
748
     * @code
749
     * <div>
750
     * test <p>foo</p> test
751
     * </div>
752
     * @endcode
753
     *
754
     * We can retrieve just the contents of this code by doing something like
755
     * this:
756
     * @code
757
     * qp($xml, 'div')->innerHTML();
758
     * @endcode
759
     *
760
     * This would return the following:
761
     * @codetest <p>foo</p> test@endcode
762
     *
763
     * @return string
764
     *  Returns a string representation of the child nodes of the first
765
     *  matched element.
766
     * @see   html()
767
     * @see   innerXML()
768
     * @see   innerXHTML()
769
     * @since 2.0
770
     */
771
    public function innerHTML()
772
    {
773
        return $this->innerXML();
774
    }
775
776
    /**
777
     * Fetch child (inner) nodes of the first match.
778
     *
779
     * This will return the children of the present match. For an example,
780
     * see {@link innerHTML()}.
781
     *
782
     * @see   innerHTML()
783
     * @see   innerXML()
784
     * @return string
785
     *  Returns a string of XHTML that represents the children of the present
786
     *  node.
787
     * @since 2.0
788
     */
789
    public function innerXHTML()
790
    {
791
        $length = $this->matches->count();
792
        if ($length === 0) {
793
            return NULL;
794
        }
795
        // Only return the first item -- that's what JQ does.
796
        $first = $this->getFirstMatch();
797
798
        // Catch cases where first item is not a legit DOM object.
799
        if (!($first instanceof \DOMNode)) {
800
            return NULL;
801
        }
802
803
        if (!$first->hasChildNodes()) {
804
            return '';
805
        }
806
807
        $buffer = '';
808
        foreach ($first->childNodes as $child) {
809
            $buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
810
        }
811
812
        return $buffer;
813
    }
814
815
    /**
816
     * Fetch child (inner) nodes of the first match.
817
     *
818
     * This will return the children of the present match. For an example,
819
     * see {@link innerHTML()}.
820
     *
821
     * @see   innerHTML()
822
     * @see   innerXHTML()
823
     * @return string
824
     *  Returns a string of XHTML that represents the children of the present
825
     *  node.
826
     * @since 2.0
827
     */
828
    public function innerXML()
829
    {
830
        $length = $this->matches->count();
831
        if ($length === 0) {
832
            return NULL;
833
        }
834
        // Only return the first item -- that's what JQ does.
835
        $first = $this->getFirstMatch();
836
837
        // Catch cases where first item is not a legit DOM object.
838
        if (!($first instanceof \DOMNode)) {
839
            return NULL;
840
        }
841
842
        if (!$first->hasChildNodes()) {
843
            return '';
844
        }
845
846
        $buffer = '';
847
        foreach ($first->childNodes as $child) {
848
            $buffer .= $this->document->saveXML($child);
849
        }
850
851
        return $buffer;
852
    }
853
854
    /**
855
     * Get child elements as an HTML5 string.
856
     *
857
     * TODO: This is a very simple alteration of innerXML. Do we need better
858
     * support?
859
     */
860
    public function innerHTML5()
861
    {
862
        $length = $this->matches->count();
863
        if ($length === 0) {
864
            return NULL;
865
        }
866
        // Only return the first item -- that's what JQ does.
867
        $first = $this->getFirstMatch();
868
869
        // Catch cases where first item is not a legit DOM object.
870
        if (!($first instanceof \DOMNode)) {
871
            return NULL;
872
        }
873
874
        if (!$first->hasChildNodes()) {
875
            return '';
876
        }
877
878
        $html5 = new HTML5($this->options);
879
        $buffer = '';
880
        foreach ($first->childNodes as $child) {
881
            $buffer .= $html5->saveHTML($child);
882
        }
883
884
        return $buffer;
885
    }
886
887
    /**
888
     * Retrieve the text of each match and concatenate them with the given separator.
889
     *
890
     * This has the effect of looping through all children, retrieving their text
891
     * content, and then concatenating the text with a separator.
892
     *
893
     * @param string $sep
894
     *  The string used to separate text items. The default is a comma followed by a
895
     *  space.
896
     * @param boolean $filterEmpties
897
     *  If this is true, empty items will be ignored.
898
     * @return string
899
     *  The text contents, concatenated together with the given separator between
900
     *  every pair of items.
901
     * @see   implode()
902
     * @see   text()
903
     * @since 2.0
904
     */
905
    public function textImplode($sep = ', ', $filterEmpties = true): string
906
    {
907
        $tmp = [];
908
        foreach ($this->matches as $m) {
909
            $txt = $m->textContent;
910
            $trimmed = trim($txt);
911
            // If filter empties out, then we only add items that have content.
912
            if ($filterEmpties) {
913
                if (strlen($trimmed) > 0) {
914
                    $tmp[] = $txt;
915
                }
916
            } // Else add all content, even if it's empty.
917
            else {
918
                $tmp[] = $txt;
919
            }
920
        }
921
922
        return implode($sep, $tmp);
923
    }
924
925
    /**
926
     * Get the text contents from just child elements.
927
     *
928
     * This is a specialized variant of textImplode() that implodes text for just the
929
     * child elements of the current element.
930
     *
931
     * @param string $separator
932
     *  The separator that will be inserted between found text content.
933
     * @return string
934
     *  The concatenated values of all children.
935
     * @throws CSS\ParseException
936
     */
937
    public function childrenText($separator = ' '): string
938
    {
939
        // Branch makes it non-destructive.
940
        return $this->branch()->xpath('descendant::text()')->textImplode($separator);
941
    }
942
943
    /**
944
     * Get or set the text contents of a node.
945
     *
946
     * @param string $text
947
     *  If this is not NULL, this value will be set as the text of the node. It
948
     *  will replace any existing content.
949
     * @return mixed
950
     *  A DOMQuery if $text is set, or the text content if no text
951
     *  is passed in as a pram.
952
     * @see html()
953
     * @see xml()
954
     * @see contents()
955
     */
956
    public function text($text = NULL)
957
    {
958
        if (isset($text)) {
959
            $this->removeChildren();
960
            foreach ($this->matches as $m) {
961
                $m->appendChild($this->document->createTextNode($text));
962
            }
963
964
            return $this;
965
        }
966
        // Returns all text as one string:
967
        $buf = '';
968
        foreach ($this->matches as $m) {
969
            $buf .= $m->textContent;
970
        }
971
972
        return $buf;
973
    }
974
975
    /**
976
     * Get or set the text before each selected item.
977
     *
978
     * If $text is passed in, the text is inserted before each currently selected item.
979
     *
980
     * If no text is given, this will return the concatenated text after each selected element.
981
     *
982
     * @code
983
     * <?php
984
     * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
985
     *
986
     * // This will return 'Foo'
987
     * qp($xml, 'a')->textBefore();
988
     *
989
     * // This will insert 'Baz' right before <b/>.
990
     * qp($xml, 'b')->textBefore('Baz');
991
     * ?>
992
     * @endcode
993
     *
994
     * @param string $text
995
     *  If this is set, it will be inserted before each node in the current set of
996
     *  selected items.
997
     * @return mixed
998
     *  Returns the DOMQuery object if $text was set, and returns a string (possibly empty)
999
     *  if no param is passed.
1000
     * @throws Exception
1001
     * @throws QueryPath
1002
     */
1003
    public function textBefore($text = NULL)
1004
    {
1005
        if (isset($text)) {
1006
            $textNode = $this->document->createTextNode($text);
1007
1008
            return $this->before($textNode);
1009
        }
1010
        $buffer = '';
1011
        foreach ($this->matches as $m) {
1012
            $p = $m;
1013
            while (isset($p->previousSibling) && $p->previousSibling->nodeType === XML_TEXT_NODE) {
1014
                $p = $p->previousSibling;
1015
                $buffer .= $p->textContent;
1016
            }
1017
        }
1018
1019
        return $buffer;
1020
    }
1021
1022
    public function textAfter($text = NULL)
1023
    {
1024
        if (isset($text)) {
1025
            $textNode = $this->document->createTextNode($text);
1026
1027
            return $this->after($textNode);
1028
        }
1029
        $buffer = '';
1030
        foreach ($this->matches as $m) {
1031
            $n = $m;
1032
            while (isset($n->nextSibling) && $n->nextSibling->nodeType === XML_TEXT_NODE) {
1033
                $n = $n->nextSibling;
1034
                $buffer .= $n->textContent;
1035
            }
1036
        }
1037
1038
        return $buffer;
1039
    }
1040
1041
    /**
1042
     * Set or get the value of an element's 'value' attribute.
1043
     *
1044
     * The 'value' attribute is common in HTML form elements. This is a
1045
     * convenience function for accessing the values. Since this is not  common
1046
     * task on the server side, this method may be removed in future releases. (It
1047
     * is currently provided for jQuery compatibility.)
1048
     *
1049
     * If a value is provided in the params, then the value will be set for all
1050
     * matches. If no params are given, then the value of the first matched element
1051
     * will be returned. This may be NULL.
1052
     *
1053
     * @deprecated Just use attr(). There's no reason to use this on the server.
1054
     * @see        attr()
1055
     * @param string $value
1056
     * @return mixed
1057
     *  Returns a DOMQuery if a string was passed in, and a string if no string
1058
     *  was passed in. In the later case, an error will produce NULL.
1059
     */
1060
    public function val($value = NULL)
1061
    {
1062
        if (isset($value)) {
1063
            $this->attr('value', $value);
1064
1065
            return $this;
1066
        }
1067
1068
        return $this->attr('value');
1069
    }
1070
1071
    /**
1072
     * Set or get XHTML markup for an element or elements.
1073
     *
1074
     * This differs from {@link html()} in that it processes (and produces)
1075
     * strictly XML 1.0 compliant markup.
1076
     *
1077
     * Like {@link xml()} and {@link html()}, this functions as both a
1078
     * setter and a getter.
1079
     *
1080
     * This is a convenience function for fetching HTML in XML format.
1081
     * It does no processing of the markup (such as schema validation).
1082
     *
1083
     * @param string $markup
1084
     *  A string containing XML data.
1085
     * @return mixed
1086
     *  If markup is passed in, a DOMQuery is returned. If no markup is passed
1087
     *  in, XML representing the first matched element is returned.
1088
     * @see html()
1089
     * @see innerXHTML()
1090
     */
1091
    public function xhtml($markup = NULL)
1092
    {
1093
1094
        // XXX: This is a minor reworking of the original xml() method.
1095
        // This should be refactored, probably.
1096
        // See http://github.com/technosophos/querypath/issues#issue/10
1097
1098
        $omit_xml_decl = $this->options['omit_xml_declaration'];
1099
        if ($markup === true) {
0 ignored issues
show
introduced by
The condition $markup === true is always false.
Loading history...
1100
            // Basically, we handle the special case where we don't
1101
            // want the XML declaration to be displayed.
1102
            $omit_xml_decl = true;
1103
        } elseif (isset($markup)) {
1104
            return $this->xml($markup);
1105
        }
1106
1107
        $length = $this->matches->count();
1108
        if ($length === 0) {
1109
            return NULL;
1110
        }
1111
1112
        // Only return the first item -- that's what JQ does.
1113
        $first = $this->getFirstMatch();
1114
        // Catch cases where first item is not a legit DOM object.
1115
        if (!($first instanceof \DOMNode)) {
1116
            return NULL;
1117
        }
1118
1119
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
1120
1121
            // Has the unfortunate side-effect of stripping doctype.
1122
            //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
1123
            $text = $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
1124
        } else {
1125
            $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
1126
        }
1127
1128
        // Issue #47: Using the old trick for removing the XML tag also removed the
1129
        // doctype. So we remove it with a regex:
1130
        if ($omit_xml_decl) {
1131
            $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
1132
        }
1133
1134
        // This is slightly lenient: It allows for cases where code incorrectly places content
1135
        // inside of these supposedly unary elements.
1136
        $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
1137
        $text = preg_replace($unary, '<\\1\\2 />', $text);
1138
1139
        // Experimental: Support for enclosing CDATA sections with comments to be both XML compat
1140
        // and HTML 4/5 compat
1141
        $cdata = '/(<!\[CDATA\[|\]\]>)/i';
1142
        $replace = $this->options['escape_xhtml_js_css_sections'];
1143
        $text = preg_replace($cdata, $replace, $text);
1144
1145
        return $text;
1146
    }
1147
1148
    /**
1149
     * Set or get the XML markup for an element or elements.
1150
     *
1151
     * Like {@link html()}, this functions in both a setter and a getter mode.
1152
     *
1153
     * In setter mode, the string passed in will be parsed and then appended to the
1154
     * elements wrapped by this DOMNode object.When in setter mode, this parses
1155
     * the XML using the DOMFragment parser. For that reason, an XML declaration
1156
     * is not necessary.
1157
     *
1158
     * In getter mode, the first element wrapped by this DOMNode object will be
1159
     * converted to an XML string and returned.
1160
     *
1161
     * @param string $markup
1162
     *  A string containing XML data.
1163
     * @return mixed
1164
     *  If markup is passed in, a DOMQuery is returned. If no markup is passed
1165
     *  in, XML representing the first matched element is returned.
1166
     * @see xhtml()
1167
     * @see html()
1168
     * @see text()
1169
     * @see content()
1170
     * @see innerXML()
1171
     */
1172
    public function xml($markup = NULL)
1173
    {
1174
        $omit_xml_decl = $this->options['omit_xml_declaration'];
1175
        if ($markup === true) {
0 ignored issues
show
introduced by
The condition $markup === true is always false.
Loading history...
1176
            // Basically, we handle the special case where we don't
1177
            // want the XML declaration to be displayed.
1178
            $omit_xml_decl = true;
1179
        } elseif (isset($markup)) {
1180
            if ($this->options['replace_entities']) {
1181
                $markup = Entities::replaceAllEntities($markup);
1182
            }
1183
            $doc = $this->document->createDocumentFragment();
1184
            $doc->appendXML($markup);
1185
            $this->removeChildren();
1186
            $this->append($doc);
1187
1188
            return $this;
1189
        }
1190
        $length = $this->matches->count();
1191
        if ($length === 0) {
1192
            return NULL;
1193
        }
1194
        // Only return the first item -- that's what JQ does.
1195
        $first = $this->getFirstMatch();
1196
1197
        // Catch cases where first item is not a legit DOM object.
1198
        if (!($first instanceof \DOMNode)) {
1199
            return NULL;
1200
        }
1201
1202
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
1203
1204
            return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
1205
        }
1206
1207
        return $this->document->saveXML($first);
1208
    }
1209
1210
    /**
1211
     * Send the XML document to the client.
1212
     *
1213
     * Write the document to a file path, if given, or
1214
     * to stdout (usually the client).
1215
     *
1216
     * This prints the entire document.
1217
     *
1218
     * @param string $path
1219
     *  The path to the file into which the XML should be written. if
1220
     *  this is NULL, data will be written to STDOUT, which is usually
1221
     *  sent to the remote browser.
1222
     * @param int $options
1223
     *  (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
1224
     * @return \QueryPath\DOMQuery
1225
     *  The DOMQuery object, unmodified.
1226
     * @see xml()
1227
     * @see innerXML()
1228
     * @see writeXHTML()
1229
     * @throws Exception
1230
     *  In the event that a file cannot be written, an Exception will be thrown.
1231
     */
1232
    public function writeXML($path = NULL, $options = NULL)
1233
    {
1234
        if ($path === NULL) {
1235
            print $this->document->saveXML(NULL, $options);
1236
        } else {
1237
            try {
1238
                set_error_handler([IOException::class, 'initializeFromError']);
1239
                $this->document->save($path, $options);
1240
            } catch (Exception $e) {
1241
                restore_error_handler();
1242
                throw $e;
1243
            }
1244
            restore_error_handler();
1245
        }
1246
1247
        return $this;
1248
    }
1249
1250
    /**
1251
     * Writes HTML to output.
1252
     *
1253
     * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
1254
     * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
1255
     *
1256
     * Write the document to stdout (usually the client) or to a file.
1257
     *
1258
     * @param string $path
1259
     *  The path to the file into which the XML should be written. if
1260
     *  this is NULL, data will be written to STDOUT, which is usually
1261
     *  sent to the remote browser.
1262
     * @return \QueryPath\DOMQuery
1263
     *  The DOMQuery object, unmodified.
1264
     * @see html()
1265
     * @see innerHTML()
1266
     * @throws Exception
1267
     *  In the event that a file cannot be written, an Exception will be thrown.
1268
     */
1269
    public function writeHTML($path = NULL)
1270
    {
1271
        if ($path === NULL) {
1272
            print $this->document->saveHTML();
1273
        } else {
1274
            try {
1275
                set_error_handler(['\QueryPath\ParseException', 'initializeFromError']);
1276
                $this->document->saveHTMLFile($path);
1277
            } catch (Exception $e) {
1278
                restore_error_handler();
1279
                throw $e;
1280
            }
1281
            restore_error_handler();
1282
        }
1283
1284
        return $this;
1285
    }
1286
1287
    /**
1288
     * Write the document to HTML5.
1289
     *
1290
     * This works the same as the other write* functions, but it encodes the output
1291
     * as HTML5 with UTF-8.
1292
     *
1293
     * @see html5()
1294
     * @see innerHTML5()
1295
     * @throws Exception
1296
     *  In the event that a file cannot be written, an Exception will be thrown.
1297
     */
1298
    public function writeHTML5($path = NULL)
1299
    {
1300
        $html5 = new HTML5();
1301
        if ($path === NULL) {
1302
            // Print the document to stdout.
1303
            print $html5->saveHTML($this->document);
1304
1305
            return;
1306
        }
1307
1308
        $html5->save($this->document, $path);
1309
    }
1310
1311
    /**
1312
     * Write an XHTML file to output.
1313
     *
1314
     * Typically, you should use this instead of {@link writeHTML()}.
1315
     *
1316
     * Currently, this functions identically to {@link toXML()} <i>except that</i>
1317
     * it always uses closing tags (e.g. always @code<script></script>@endcode,
1318
     * never @code<script/>@endcode). It will
1319
     * write the file as well-formed XML. No XHTML schema validation is done.
1320
     *
1321
     * @see   writeXML()
1322
     * @see   xml()
1323
     * @see   writeHTML()
1324
     * @see   innerXHTML()
1325
     * @see   xhtml()
1326
     * @param string $path
1327
     *  The filename of the file to write to.
1328
     * @return \QueryPath\DOMQuery
1329
     *  Returns the DOMQuery, unmodified.
1330
     * @throws Exception
1331
     *  In the event that the output file cannot be written, an exception is
1332
     *  thrown.
1333
     * @since 2.0
1334
     */
1335
    public function writeXHTML($path = NULL)
1336
    {
1337
        return $this->writeXML($path, LIBXML_NOEMPTYTAG);
1338
    }
1339
1340
    /**
1341
     * Branch the base DOMQuery into another one with the same matches.
1342
     *
1343
     * This function makes a copy of the DOMQuery object, but keeps the new copy
1344
     * (initially) pointed at the same matches. This object can then be queried without
1345
     * changing the original DOMQuery. However, changes to the elements inside of this
1346
     * DOMQuery will show up in the DOMQuery from which it is branched.
1347
     *
1348
     * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
1349
     * the current DOMNode object and makes a copy of all of its matches. You continue
1350
     * to operate on the same DOMNode object, but the elements inside of the DOMQuery
1351
     * are copies of those before the call to cloneAll().
1352
     *
1353
     * This, on the other hand, copies <i>the DOMQuery</i>, but keeps valid
1354
     * references to the document and the wrapped elements. A new query branch is
1355
     * created, but any changes will be written back to the same document.
1356
     *
1357
     * In practice, this comes in handy when you want to do multiple queries on a part
1358
     * of the document, but then return to a previous set of matches. (see {@link QPTPL}
1359
     * for examples of this in practice).
1360
     *
1361
     * Example:
1362
     *
1363
     * @code
1364
     * <?php
1365
     * $qp = qp( QueryPath::HTML_STUB);
1366
     * $branch = $qp->branch();
1367
     * $branch->find('title')->text('Title');
1368
     * $qp->find('body')->text('This is the body')->writeHTML;
1369
     * ?>
1370
     * @endcode
1371
     *
1372
     * Notice that in the code, each of the DOMQuery objects is doing its own
1373
     * query. However, both are modifying the same document. The result of the above
1374
     * would look something like this:
1375
     *
1376
     * @code
1377
     * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1378
     * <html xmlns="http://www.w3.org/1999/xhtml">
1379
     * <head>
1380
     *    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
1381
     *    <title>Title</title>
1382
     * </head>
1383
     * <body>This is the body</body>
1384
     * </html>
1385
     * @endcode
1386
     *
1387
     * Notice that while $qp and $banch were performing separate queries, they
1388
     * both modified the same document.
1389
     *
1390
     * In jQuery or a browser-based solution, you generally do not need a branching
1391
     * function because there is (implicitly) only one document. In QueryPath, there
1392
     * is no implicit document. Every document must be explicitly specified (and,
1393
     * in most cases, parsed -- which is costly). Branching makes it possible to
1394
     * work on one document with multiple DOMNode objects.
1395
     *
1396
     * @param string $selector
1397
     *  If a selector is passed in, an additional {@link find()} will be executed
1398
     *  on the branch before it is returned. (Added in QueryPath 2.0.)
1399
     * @return \QueryPath\DOMQuery
1400
     *  A copy of the DOMQuery object that points to the same set of elements that
1401
     *  the original DOMQuery was pointing to.
1402
     * @since 1.1
1403
     * @see   cloneAll()
1404
     * @see   find()
1405
     * @throws CSS\ParseException
1406
     */
1407
    public function branch($selector = NULL)
1408
    {
1409
        $temp = QueryPath::with($this->matches, NULL, $this->options);
1410
        //if (isset($selector)) $temp->find($selector);
1411
        $temp->document = $this->document;
1412
        if (isset($selector)) {
1413
            $temp->findInPlace($selector);
1414
        }
1415
1416
        return $temp;
1417
    }
1418
1419
    /**
1420
     * @param $matches
1421
     * @param $selector
1422
     * @return DOMQuery
1423
     * @throws CSS\ParseException
1424
     */
1425
    protected function inst($matches, $selector): Query
1426
    {
1427
        $dolly = clone $this;
1428
        $dolly->setMatches($matches);
1429
1430
        if (isset($selector)) {
1431
            $dolly->findInPlace($selector);
1432
        }
1433
1434
        return $dolly;
1435
    }
1436
1437
    /**
1438
     * Perform a deep clone of each node in the DOMQuery.
1439
     *
1440
     * @attention
1441
     *   This is an in-place modification of the current QueryPath object.
1442
     *
1443
     * This does not clone the DOMQuery object, but instead clones the
1444
     * list of nodes wrapped by the DOMQuery. Every element is deeply
1445
     * cloned.
1446
     *
1447
     * This method is analogous to jQuery's clone() method.
1448
     *
1449
     * This is a destructive operation, which means that end() will revert
1450
     * the list back to the clone's original.
1451
     * @see qp()
1452
     * @return \QueryPath\DOMQuery
1453
     */
1454
    public function cloneAll(): Query
1455
    {
1456
        $found = new \SplObjectStorage();
1457
        foreach ($this->matches as $m) {
1458
            $found->attach($m->cloneNode(true));
1459
        }
1460
        $this->setMatches($found);
1461
1462
        return $this;
1463
    }
1464
1465
    /**
1466
     * Clone the DOMQuery.
1467
     *
1468
     * This makes a deep clone of the elements inside of the DOMQuery.
1469
     *
1470
     * This clones only the QueryPathImpl, not all of the decorators. The
1471
     * clone operator in PHP should handle the cloning of the decorators.
1472
     */
1473
    public function __clone()
1474
    {
1475
        //XXX: Should we clone the document?
1476
1477
        // Make sure we clone the kids.
1478
        $this->cloneAll();
1479
    }
1480
1481
    /**
1482
     * Call extension methods.
1483
     *
1484
     * This function is used to invoke extension methods. It searches the
1485
     * registered extenstensions for a matching function name. If one is found,
1486
     * it is executed with the arguments in the $arguments array.
1487
     *
1488
     * @throws \ReflectionException
1489
     * @throws QueryPath::Exception
1490
     *  An exception is thrown if a non-existent method is called.
1491
     * @throws Exception
1492
     */
1493
    public function __call($name, $arguments)
1494
    {
1495
1496
        if (!ExtensionRegistry::$useRegistry) {
1497
            throw new Exception("No method named $name found (Extensions disabled).");
1498
        }
1499
1500
        // Loading of extensions is deferred until the first time a
1501
        // non-core method is called. This makes constructing faster, but it
1502
        // may make the first invocation of __call() slower (if there are
1503
        // enough extensions.)
1504
        //
1505
        // The main reason for moving this out of the constructor is that most
1506
        // new DOMQuery instances do not use extensions. Charging qp() calls
1507
        // with the additional hit is not a good idea.
1508
        //
1509
        // Also, this will at least limit the number of circular references.
1510
        if (empty($this->ext)) {
1511
            // Load the registry
1512
            $this->ext = ExtensionRegistry::getExtensions($this);
1513
        }
1514
1515
        // Note that an empty ext registry indicates that extensions are disabled.
1516
        if (!empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
1517
            $owner = ExtensionRegistry::getMethodClass($name);
1518
            $method = new \ReflectionMethod($owner, $name);
1519
1520
            return $method->invokeArgs($this->ext[$owner], $arguments);
1521
        }
1522
        throw new Exception("No method named $name found. Possibly missing an extension.");
1523
    }
1524
1525
    /**
1526
     * Get an iterator for the matches in this object.
1527
     *
1528
     * @return Iterable
1529
     *  Returns an iterator.
1530
     */
1531
    public function getIterator()
1532
    {
1533
        $i = new QueryPathIterator($this->matches);
0 ignored issues
show
Bug introduced by
$this->matches of type array is incompatible with the type Traversable expected by parameter $iterator of QueryPath\QueryPathIterator::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1533
        $i = new QueryPathIterator(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
1534
        $i->options = $this->options;
1535
1536
        return $i;
1537
    }
1538
}
1539