QueryMutators::after()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 16
rs 9.9332
1
<?php
2
3
namespace QueryPath\Helpers;
4
5
6
use QueryPath\CSS\ParseException;
7
use QueryPath\CSS\QueryPathEventHandler;
8
use QueryPath\DOMQuery;
9
use QueryPath\Exception;
10
use QueryPath\Query;
11
use QueryPath\QueryPath;
12
13
trait QueryMutators
14
{
15
16
    /**
17
     * Empty everything within the specified element.
18
     *
19
     * A convenience function for removeChildren(). This is equivalent to jQuery's
20
     * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
21
     * function name.
22
     *
23
     * @return \QueryPath\DOMQuery
24
     *  The DOMQuery object with the newly emptied elements.
25
     * @see        removeChildren()
26
     * @since      2.1
27
     * @author     eabrand
28
     * @deprecated The removeChildren() function is the preferred method.
29
     */
30
    public function emptyElement(): Query
31
    {
32
        $this->removeChildren();
33
34
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
35
    }
36
37
    /**
38
     * Insert the given markup as the last child.
39
     *
40
     * The markup will be inserted into each match in the set.
41
     *
42
     * The same element cannot be inserted multiple times into a document. DOM
43
     * documents do not allow a single object to be inserted multiple times
44
     * into the DOM. To insert the same XML repeatedly, we must first clone
45
     * the object. This has one practical implication: Once you have inserted
46
     * an element into the object, you cannot further manipulate the original
47
     * element and expect the changes to be replciated in the appended object.
48
     * (They are not the same -- there is no shared reference.) Instead, you
49
     * will need to retrieve the appended object and operate on that.
50
     *
51
     * @param mixed $data
52
     *  This can be either a string (the usual case), or a DOM Element.
53
     * @return \QueryPath\DOMQuery
54
     *  The DOMQuery object.
55
     * @see appendTo()
56
     * @see prepend()
57
     * @throws QueryPath::Exception
58
     *  Thrown if $data is an unsupported object type.
59
     * @throws Exception
60
     */
61
    public function append($data): Query
62
    {
63
        $data = $this->prepareInsert($data);
0 ignored issues
show
Bug introduced by
It seems like prepareInsert() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

63
        /** @scrutinizer ignore-call */ 
64
        $data = $this->prepareInsert($data);
Loading history...
64
        if (isset($data)) {
65
            if (empty($this->document->documentElement) && $this->matches->count() === 0) {
66
                // Then we assume we are writing to the doc root
67
                $this->document->appendChild($data);
68
                $found = new \SplObjectStorage();
69
                $found->attach($this->document->documentElement);
70
                $this->setMatches($found);
0 ignored issues
show
Bug introduced by
It seems like setMatches() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

70
                $this->/** @scrutinizer ignore-call */ 
71
                       setMatches($found);
Loading history...
71
            } else {
72
                // You can only append in item once. So in cases where we
73
                // need to append multiple times, we have to clone the node.
74
                foreach ($this->matches as $m) {
75
                    // DOMDocumentFragments are even more troublesome, as they don't
76
                    // always clone correctly. So we have to clone their children.
77
                    if ($data instanceof \DOMDocumentFragment) {
78
                        foreach ($data->childNodes as $n) {
79
                            $m->appendChild($n->cloneNode(true));
80
                        }
81
                    } else {
82
                        // Otherwise a standard clone will do.
83
                        $m->appendChild($data->cloneNode(true));
84
                    }
85
86
                }
87
            }
88
89
        }
90
91
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
92
    }
93
94
    /**
95
     * Insert the given markup as the first child.
96
     *
97
     * The markup will be inserted into each match in the set.
98
     *
99
     * @param mixed $data
100
     *  This can be either a string (the usual case), or a DOM Element.
101
     * @return \QueryPath\DOMQuery
102
     * @see append()
103
     * @see before()
104
     * @see after()
105
     * @see prependTo()
106
     * @throws QueryPath::Exception
107
     *  Thrown if $data is an unsupported object type.
108
     * @throws Exception
109
     */
110
    public function prepend($data): Query
111
    {
112
        $data = $this->prepareInsert($data);
113
        if (isset($data)) {
114
            foreach ($this->matches as $m) {
115
                $ins = $data->cloneNode(true);
116
                if ($m->hasChildNodes()) {
117
                    $m->insertBefore($ins, $m->childNodes->item(0));
118
                } else {
119
                    $m->appendChild($ins);
120
                }
121
            }
122
        }
123
124
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
125
    }
126
127
    /**
128
     * Take all nodes in the current object and prepend them to the children nodes of
129
     * each matched node in the passed-in DOMQuery object.
130
     *
131
     * This will iterate through each item in the current DOMQuery object and
132
     * add each item to the beginning of the children of each element in the
133
     * passed-in DOMQuery object.
134
     *
135
     * @see insertBefore()
136
     * @see insertAfter()
137
     * @see prepend()
138
     * @see appendTo()
139
     * @param DOMQuery $dest
140
     *  The destination DOMQuery object.
141
     * @return \QueryPath\DOMQuery
142
     *  The original DOMQuery, unmodified. NOT the destination DOMQuery.
143
     * @throws QueryPath::Exception
144
     *  Thrown if $data is an unsupported object type.
145
     */
146
    public function prependTo(Query $dest)
147
    {
148
        foreach ($this->matches as $m) {
149
            $dest->prepend($m);
0 ignored issues
show
Bug introduced by
The method prepend() does not exist on QueryPath\Query. It seems like you code against a sub-type of QueryPath\Query such as QueryPath\DOMQuery. ( Ignorable by Annotation )

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

149
            $dest->/** @scrutinizer ignore-call */ 
150
                   prepend($m);
Loading history...
150
        }
151
152
        return $this;
153
    }
154
155
    /**
156
     * Insert the given data before each element in the current set of matches.
157
     *
158
     * This will take the give data (XML or HTML) and put it before each of the items that
159
     * the DOMQuery object currently contains. Contrast this with after().
160
     *
161
     * @param mixed $data
162
     *  The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
163
     *  or the other usual suspects. (See {@link qp()}).
164
     * @return \QueryPath\DOMQuery
165
     *  Returns the DOMQuery with the new modifications. The list of elements currently
166
     *  selected will remain the same.
167
     * @see insertBefore()
168
     * @see after()
169
     * @see append()
170
     * @see prepend()
171
     * @throws QueryPath::Exception
172
     *  Thrown if $data is an unsupported object type.
173
     * @throws Exception
174
     */
175
    public function before($data): Query
176
    {
177
        $data = $this->prepareInsert($data);
178
        foreach ($this->matches as $m) {
179
            $ins = $data->cloneNode(true);
180
            $m->parentNode->insertBefore($ins, $m);
181
        }
182
183
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
184
    }
185
186
    /**
187
     * Insert the current elements into the destination document.
188
     * The items are inserted before each element in the given DOMQuery document.
189
     * That is, they will be siblings with the current elements.
190
     *
191
     * @param Query $dest
192
     *  Destination DOMQuery document.
193
     * @return \QueryPath\DOMQuery
194
     *  The current DOMQuery object, unaltered. Only the destination DOMQuery
195
     *  object is altered.
196
     * @see before()
197
     * @see insertAfter()
198
     * @see appendTo()
199
     * @throws QueryPath::Exception
200
     *  Thrown if $data is an unsupported object type.
201
     */
202
    public function insertBefore(Query $dest): Query
203
    {
204
        foreach ($this->matches as $m) {
205
            $dest->before($m);
0 ignored issues
show
Bug introduced by
The method before() does not exist on QueryPath\Query. It seems like you code against a sub-type of QueryPath\Query such as QueryPath\DOMQuery. ( Ignorable by Annotation )

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

205
            $dest->/** @scrutinizer ignore-call */ 
206
                   before($m);
Loading history...
206
        }
207
208
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
209
    }
210
211
    /**
212
     * Insert the contents of the current DOMQuery after the nodes in the
213
     * destination DOMQuery object.
214
     *
215
     * @param Query $dest
216
     *  Destination object where the current elements will be deposited.
217
     * @return \QueryPath\DOMQuery
218
     *  The present DOMQuery, unaltered. Only the destination object is altered.
219
     * @see after()
220
     * @see insertBefore()
221
     * @see append()
222
     * @throws QueryPath::Exception
223
     *  Thrown if $data is an unsupported object type.
224
     */
225
    public function insertAfter(Query $dest): Query
226
    {
227
        foreach ($this->matches as $m) {
228
            $dest->after($m);
0 ignored issues
show
Bug introduced by
The method after() does not exist on QueryPath\Query. It seems like you code against a sub-type of QueryPath\Query such as QueryPath\DOMQuery. ( Ignorable by Annotation )

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

228
            $dest->/** @scrutinizer ignore-call */ 
229
                   after($m);
Loading history...
229
        }
230
231
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
232
    }
233
234
    /**
235
     * Insert the given data after each element in the current DOMQuery object.
236
     *
237
     * This inserts the element as a peer to the currently matched elements.
238
     * Contrast this with {@link append()}, which inserts the data as children
239
     * of matched elements.
240
     *
241
     * @param mixed $data
242
     *  The data to be appended.
243
     * @return \QueryPath\DOMQuery
244
     *  The DOMQuery object (with the items inserted).
245
     * @see before()
246
     * @see append()
247
     * @throws QueryPath::Exception
248
     *  Thrown if $data is an unsupported object type.
249
     * @throws Exception
250
     */
251
    public function after($data): Query
252
    {
253
        if (empty($data)) {
254
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
255
        }
256
        $data = $this->prepareInsert($data);
257
        foreach ($this->matches as $m) {
258
            $ins = $data->cloneNode(true);
259
            if (isset($m->nextSibling)) {
260
                $m->parentNode->insertBefore($ins, $m->nextSibling);
261
            } else {
262
                $m->parentNode->appendChild($ins);
263
            }
264
        }
265
266
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
267
    }
268
269
    /**
270
     * Replace the existing element(s) in the list with a new one.
271
     *
272
     * @param mixed $new
273
     *  A DOMElement or XML in a string. This will replace all elements
274
     *  currently wrapped in the DOMQuery object.
275
     * @return \QueryPath\DOMQuery
276
     *  The DOMQuery object wrapping <b>the items that were removed</b>.
277
     *  This remains consistent with the jQuery API.
278
     * @throws Exception
279
     * @throws ParseException
280
     * @throws QueryPath
281
     * @see append()
282
     * @see prepend()
283
     * @see before()
284
     * @see after()
285
     * @see remove()
286
     * @see replaceAll()
287
     */
288
    public function replaceWith($new): Query
289
    {
290
        $data = $this->prepareInsert($new);
291
        $found = new \SplObjectStorage();
292
        foreach ($this->matches as $m) {
293
            $parent = $m->parentNode;
294
            $parent->insertBefore($data->cloneNode(true), $m);
295
            $found->attach($parent->removeChild($m));
296
        }
297
298
        return $this->inst($found, NULL);
0 ignored issues
show
Bug introduced by
It seems like inst() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

298
        return $this->/** @scrutinizer ignore-call */ inst($found, NULL);
Loading history...
299
    }
300
301
    /**
302
     * Remove the parent element from the selected node or nodes.
303
     *
304
     * This takes the given list of nodes and "unwraps" them, moving them out of their parent
305
     * node, and then deleting the parent node.
306
     *
307
     * For example, consider this:
308
     *
309
     * @code
310
     *   <root><wrapper><content/></wrapper></root>
311
     * @endcode
312
     *
313
     * Now we can run this code:
314
     * @code
315
     *   qp($xml, 'content')->unwrap();
316
     * @endcode
317
     *
318
     * This will result in:
319
     *
320
     * @code
321
     *   <root><content/></root>
322
     * @endcode
323
     * This is the opposite of wrap().
324
     *
325
     * <b>The root element cannot be unwrapped.</b> It has no parents.
326
     * If you attempt to use unwrap on a root element, this will throw a
327
     * QueryPath::Exception. (You can, however, "Unwrap" a child that is
328
     * a direct descendant of the root element. This will remove the root
329
     * element, and replace the child as the root element. Be careful, though.
330
     * You cannot set more than one child as a root element.)
331
     *
332
     * @return \QueryPath\DOMQuery
333
     *  The DOMQuery object, with the same element(s) selected.
334
     * @throws Exception
335
     * @see    wrap()
336
     * @since  2.1
337
     * @author mbutcher
338
     */
339
    public function unwrap(): Query
340
    {
341
        // We do this in two loops in order to
342
        // capture the case where two matches are
343
        // under the same parent. Othwerwise we might
344
        // remove a match before we can move it.
345
        $parents = new \SplObjectStorage();
346
        foreach ($this->matches as $m) {
347
348
            // Cannot unwrap the root element.
349
            if ($m->isSameNode($m->ownerDocument->documentElement)) {
350
                throw new \QueryPath\Exception('Cannot unwrap the root element.');
351
            }
352
353
            // Move children to peer of parent.
354
            $parent = $m->parentNode;
355
            $old = $parent->removeChild($m);
356
            $parent->parentNode->insertBefore($old, $parent);
357
            $parents->attach($parent);
358
        }
359
360
        // Now that all the children are moved, we
361
        // remove all of the parents.
362
        foreach ($parents as $ele) {
363
            $ele->parentNode->removeChild($ele);
364
        }
365
366
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
367
    }
368
369
    /**
370
     * Wrap each element inside of the given markup.
371
     *
372
     * Markup is usually a string, but it can also be a DOMNode, a document
373
     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
374
     * the first item in the list will be used.)
375
     *
376
     * @param mixed $markup
377
     *  Markup that will wrap each element in the current list.
378
     * @return \QueryPath\DOMQuery
379
     *  The DOMQuery object with the wrapping changes made.
380
     * @throws Exception
381
     * @throws QueryPath
382
     * @see wrapAll()
383
     * @see wrapInner()
384
     */
385
    public function wrap($markup): Query
386
    {
387
        $data = $this->prepareInsert($markup);
388
389
        // If the markup passed in is empty, we don't do any wrapping.
390
        if (empty($data)) {
391
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
392
        }
393
394
        foreach ($this->matches as $m) {
395
            if ($data instanceof \DOMDocumentFragment) {
396
                $copy = $data->firstChild->cloneNode(true);
0 ignored issues
show
Bug introduced by
The method cloneNode() does not exist on null. ( Ignorable by Annotation )

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

396
                /** @scrutinizer ignore-call */ 
397
                $copy = $data->firstChild->cloneNode(true);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
397
            } else {
398
                $copy = $data->cloneNode(true);
399
            }
400
401
            // XXX: Should be able to avoid doing this over and over.
402
            if ($copy->hasChildNodes()) {
403
                $deepest = $this->deepestNode($copy);
0 ignored issues
show
Bug introduced by
The method deepestNode() does not exist on QueryPath\Helpers\QueryMutators. Did you maybe mean deepest()? ( Ignorable by Annotation )

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

403
                /** @scrutinizer ignore-call */ 
404
                $deepest = $this->deepestNode($copy);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
404
                // FIXME: Does this need a different data structure?
405
                $bottom = $deepest[0];
406
            } else {
407
                $bottom = $copy;
408
            }
409
410
            $parent = $m->parentNode;
411
            $parent->insertBefore($copy, $m);
412
            $m = $parent->removeChild($m);
413
            $bottom->appendChild($m);
414
        }
415
416
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
417
    }
418
419
    /**
420
     * Wrap all elements inside of the given markup.
421
     *
422
     * So all elements will be grouped together under this single marked up
423
     * item. This works by first determining the parent element of the first item
424
     * in the list. It then moves all of the matching elements under the wrapper
425
     * and inserts the wrapper where that first element was found. (This is in
426
     * accordance with the way jQuery works.)
427
     *
428
     * Markup is usually XML in a string, but it can also be a DOMNode, a document
429
     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
430
     * the first item in the list will be used.)
431
     *
432
     * @param string $markup
433
     *  Markup that will wrap all elements in the current list.
434
     * @return \QueryPath\DOMQuery
435
     *  The DOMQuery object with the wrapping changes made.
436
     * @throws Exception
437
     * @throws QueryPath
438
     * @see wrap()
439
     * @see wrapInner()
440
     */
441
    public function wrapAll($markup)
442
    {
443
        if ($this->matches->count() === 0) {
444
            return;
445
        }
446
447
        $data = $this->prepareInsert($markup);
448
449
        if (empty($data)) {
450
            return $this;
451
        }
452
453
        if ($data instanceof \DOMDocumentFragment) {
454
            $data = $data->firstChild->cloneNode(true);
455
        } else {
456
            $data = $data->cloneNode(true);
457
        }
458
459
        if ($data->hasChildNodes()) {
460
            $deepest = $this->deepestNode($data);
461
            // FIXME: Does this need fixing?
462
            $bottom = $deepest[0];
463
        } else {
464
            $bottom = $data;
465
        }
466
467
        $first = $this->getFirstMatch();
0 ignored issues
show
Bug introduced by
It seems like getFirstMatch() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

467
        /** @scrutinizer ignore-call */ 
468
        $first = $this->getFirstMatch();
Loading history...
468
        $parent = $first->parentNode;
469
        $parent->insertBefore($data, $first);
470
        foreach ($this->matches as $m) {
471
            $bottom->appendChild($m->parentNode->removeChild($m));
472
        }
473
474
        return $this;
475
    }
476
477
    /**
478
     * Wrap the child elements of each item in the list with the given markup.
479
     *
480
     * Markup is usually a string, but it can also be a DOMNode, a document
481
     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
482
     * the first item in the list will be used.)
483
     *
484
     * @param string $markup
485
     *  Markup that will wrap children of each element in the current list.
486
     * @return \QueryPath\DOMQuery
487
     *  The DOMQuery object with the wrapping changes made.
488
     * @see wrap()
489
     * @see wrapAll()
490
     * @throws \QueryPath\Exception
491
     * @throws QueryPath
492
     */
493
    public function wrapInner($markup)
494
    {
495
        $data = $this->prepareInsert($markup);
496
497
        // No data? Short circuit.
498
        if (empty($data)) {
499
            return $this;
500
        }
501
502
        foreach ($this->matches as $m) {
503
            if ($data instanceof \DOMDocumentFragment) {
504
                $wrapper = $data->firstChild->cloneNode(true);
505
            } else {
506
                $wrapper = $data->cloneNode(true);
507
            }
508
509
            if ($wrapper->hasChildNodes()) {
510
                $deepest = $this->deepestNode($wrapper);
511
                // FIXME: ???
512
                $bottom = $deepest[0];
513
            } else {
514
                $bottom = $wrapper;
515
            }
516
517
            if ($m->hasChildNodes()) {
518
                while ($m->firstChild) {
519
                    $kid = $m->removeChild($m->firstChild);
520
                    $bottom->appendChild($kid);
521
                }
522
            }
523
524
            $m->appendChild($wrapper);
525
        }
526
527
        return $this;
528
    }
529
530
    /**
531
     * Reduce the set of matches to the deepest child node in the tree.
532
     *
533
     * This loops through the matches and looks for the deepest child node of all of
534
     * the matches. "Deepest", here, is relative to the nodes in the list. It is
535
     * calculated as the distance from the starting node to the most distant child
536
     * node. In other words, it is not necessarily the farthest node from the root
537
     * element, but the farthest note from the matched element.
538
     *
539
     * In the case where there are multiple nodes at the same depth, all of the
540
     * nodes at that depth will be included.
541
     *
542
     * @return \QueryPath\DOMQuery
543
     *  The DOMQuery wrapping the single deepest node.
544
     * @throws ParseException
545
     */
546
    public function deepest(): Query
547
    {
548
        $deepest = 0;
549
        $winner = new \SplObjectStorage();
550
        foreach ($this->matches as $m) {
551
            $local_deepest = 0;
552
            $local_ele = $this->deepestNode($m, 0, NULL, $local_deepest);
553
554
            // Replace with the new deepest.
555
            if ($local_deepest > $deepest) {
556
                $winner = new \SplObjectStorage();
557
                foreach ($local_ele as $lele) {
558
                    $winner->attach($lele);
559
                }
560
                $deepest = $local_deepest;
561
            } // Augument with other equally deep elements.
562
            elseif ($local_deepest === $deepest) {
563
                foreach ($local_ele as $lele) {
564
                    $winner->attach($lele);
565
                }
566
            }
567
        }
568
569
        return $this->inst($winner, NULL);
570
    }
571
572
    /**
573
     * Add a class to all elements in the current DOMQuery.
574
     *
575
     * This searchers for a class attribute on each item wrapped by the current
576
     * DOMNode object. If no attribute is found, a new one is added and its value
577
     * is set to $class. If a class attribute is found, then the value is appended
578
     * on to the end.
579
     *
580
     * @param string $class
581
     *  The name of the class.
582
     * @return \QueryPath\DOMQuery
583
     *  Returns the DOMQuery object.
584
     * @see css()
585
     * @see attr()
586
     * @see removeClass()
587
     * @see hasClass()
588
     */
589
    public function addClass($class)
590
    {
591
        foreach ($this->matches as $m) {
592
            if ($m->hasAttribute('class')) {
593
                $val = $m->getAttribute('class');
594
                $m->setAttribute('class', $val . ' ' . $class);
595
            } else {
596
                $m->setAttribute('class', $class);
597
            }
598
        }
599
600
        return $this;
601
    }
602
603
    /**
604
     * Remove the named class from any element in the DOMQuery that has it.
605
     *
606
     * This may result in the entire class attribute being removed. If there
607
     * are other items in the class attribute, though, they will not be removed.
608
     *
609
     * Example:
610
     * Consider this XML:
611
     *
612
     * @code
613
     * <element class="first second"/>
614
     * @endcode
615
     *
616
     * Executing this fragment of code will remove only the 'first' class:
617
     * @code
618
     * qp(document, 'element')->removeClass('first');
619
     * @endcode
620
     *
621
     * The resulting XML will be:
622
     * @code
623
     * <element class="second"/>
624
     * @endcode
625
     *
626
     * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
627
     *
628
     * @param string $class
629
     *  The class name to remove.
630
     * @return \QueryPath\DOMQuery
631
     *  The modified DOMNode object.
632
     * @see attr()
633
     * @see addClass()
634
     * @see hasClass()
635
     */
636
    public function removeClass($class = false): Query
637
    {
638
        if (empty($class)) {
639
            foreach ($this->matches as $m) {
640
                $m->removeAttribute('class');
641
            }
642
        } else {
643
            $to_remove = array_filter(explode(' ', $class));
644
            foreach ($this->matches as $m) {
645
                if ($m->hasAttribute('class')) {
646
                    $vals = array_filter(explode(' ', $m->getAttribute('class')));
647
                    $buf = [];
648
                    foreach ($vals as $v) {
649
                        if (!in_array($v, $to_remove)) {
650
                            $buf[] = $v;
651
                        }
652
                    }
653
                    if (empty($buf)) {
654
                        $m->removeAttribute('class');
655
                    } else {
656
                        $m->setAttribute('class', implode(' ', $buf));
657
                    }
658
                }
659
            }
660
        }
661
662
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
663
    }
664
665
    /**
666
     * Detach any items from the list if they match the selector.
667
     *
668
     * In other words, each item that matches the selector will be removed
669
     * from the DOM document. The returned DOMQuery wraps the list of
670
     * removed elements.
671
     *
672
     * If no selector is specified, this will remove all current matches from
673
     * the document.
674
     *
675
     * @param string $selector
676
     *  A CSS Selector.
677
     * @return \QueryPath\DOMQuery
678
     *  The Query path wrapping a list of removed items.
679
     * @see    replaceAll()
680
     * @see    replaceWith()
681
     * @see    removeChildren()
682
     * @since  2.1
683
     * @author eabrand
684
     * @throws ParseException
685
     */
686
    public function detach($selector = NULL): Query
687
    {
688
        if (NULL !== $selector) {
689
            $this->find($selector);
0 ignored issues
show
Bug introduced by
It seems like find() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

689
            $this->/** @scrutinizer ignore-call */ 
690
                   find($selector);
Loading history...
690
        }
691
692
        $found = new \SplObjectStorage();
693
        $this->last = $this->matches;
0 ignored issues
show
Bug Best Practice introduced by
The property last does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
694
        foreach ($this->matches as $item) {
695
            // The item returned is (according to docs) different from
696
            // the one passed in, so we have to re-store it.
697
            $found->attach($item->parentNode->removeChild($item));
698
        }
699
700
        return $this->inst($found, NULL);
701
    }
702
703
    /**
704
     * Attach any items from the list if they match the selector.
705
     *
706
     * If no selector is specified, this will remove all current matches from
707
     * the document.
708
     *
709
     * @param DOMQuery $dest
710
     *  A DOMQuery Selector.
711
     * @return \QueryPath\DOMQuery
712
     *  The Query path wrapping a list of removed items.
713
     * @see    replaceAll()
714
     * @see    replaceWith()
715
     * @see    removeChildren()
716
     * @since  2.1
717
     * @author eabrand
718
     * @throws QueryPath
719
     * @throws Exception
720
     */
721
    public function attach(DOMQuery $dest): Query
722
    {
723
        foreach ($this->last as $m) {
724
            $dest->append($m);
725
        }
726
727
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
728
    }
729
730
    /**
731
     * Append the current elements to the destination passed into the function.
732
     *
733
     * This cycles through all of the current matches and appends them to
734
     * the context given in $destination. If a selector is provided then the
735
     * $destination is queried (using that selector) prior to the data being
736
     * appended. The data is then appended to the found items.
737
     *
738
     * @param DOMQuery $dest
739
     *  A DOMQuery object that will be appended to.
740
     * @return \QueryPath\DOMQuery
741
     *  The original DOMQuery, unaltered. Only the destination DOMQuery will
742
     *  be modified.
743
     * @see append()
744
     * @see prependTo()
745
     * @throws QueryPath::Exception
746
     *  Thrown if $data is an unsupported object type.
747
     * @throws Exception
748
     */
749
    public function appendTo(DOMQuery $dest): Query
750
    {
751
        foreach ($this->matches as $m) {
752
            $dest->append($m);
753
        }
754
755
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
756
    }
757
758
    /**
759
     * Remove any items from the list if they match the selector.
760
     *
761
     * In other words, each item that matches the selector will be remove
762
     * from the DOM document. The returned DOMQuery wraps the list of
763
     * removed elements.
764
     *
765
     * If no selector is specified, this will remove all current matches from
766
     * the document.
767
     *
768
     * @param string $selector
769
     *  A CSS Selector.
770
     * @return \QueryPath\DOMQuery
771
     *  The Query path wrapping a list of removed items.
772
     * @see replaceAll()
773
     * @see replaceWith()
774
     * @see removeChildren()
775
     * @throws ParseException
776
     */
777
    public function remove($selector = NULL): Query
778
    {
779
        if (!empty($selector)) {
780
            // Do a non-destructive find.
781
            $query = new QueryPathEventHandler($this->matches);
782
            $query->find($selector);
783
            $matches = $query->getMatches();
784
        } else {
785
            $matches = $this->matches;
786
        }
787
788
        $found = new \SplObjectStorage();
789
        foreach ($matches as $item) {
790
            // The item returned is (according to docs) different from
791
            // the one passed in, so we have to re-store it.
792
            $found->attach($item->parentNode->removeChild($item));
793
        }
794
795
        // Return a clone DOMQuery with just the removed items. If
796
        // no items are found, this will return an empty DOMQuery.
797
        return count($found) === 0 ? new static() : new static($found);
0 ignored issues
show
Bug Best Practice introduced by
The expression return count($found) ===...() : new static($found) returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
Unused Code introduced by
The call to QueryPath\Helpers\QueryMutators::__construct() has too many arguments starting with $found. ( Ignorable by Annotation )

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

797
        return count($found) === 0 ? new static() : /** @scrutinizer ignore-call */ new static($found);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
798
    }
799
800
    /**
801
     * This replaces everything that matches the selector with the first value
802
     * in the current list.
803
     *
804
     * This is the reverse of replaceWith.
805
     *
806
     * Unlike jQuery, DOMQuery cannot assume a default document. Consequently,
807
     * you must specify the intended destination document. If it is omitted, the
808
     * present document is assumed to be tthe document. However, that can result
809
     * in undefined behavior if the selector and the replacement are not sufficiently
810
     * distinct.
811
     *
812
     * @param string $selector
813
     *  The selector.
814
     * @param \DOMDocument $document
815
     *  The destination document.
816
     * @return \QueryPath\DOMQuery
817
     *  The DOMQuery wrapping the modified document.
818
     * @deprecated Due to the fact that this is not a particularly friendly method,
819
     *  and that it can be easily replicated using {@see replaceWith()}, it is to be
820
     *  considered deprecated.
821
     * @see        remove()
822
     * @see        replaceWith()
823
     * @throws ParseException
824
     */
825
    public function replaceAll($selector, \DOMDocument $document): Query
826
    {
827
        $replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
828
829
        $c = new QueryPathEventHandler($document);
830
        $c->find($selector);
831
        $temp = $c->getMatches();
832
        foreach ($temp as $item) {
833
            $node = $replacement->cloneNode();
834
            $node = $document->importNode($node);
835
            $item->parentNode->replaceChild($node, $item);
836
        }
837
838
        return QueryPath::with($document, NULL, $this->options);
839
    }
840
841
    /**
842
     * Add more elements to the current set of matches.
843
     *
844
     * This begins the new query at the top of the DOM again. The results found
845
     * when running this selector are then merged into the existing results. In
846
     * this way, you can add additional elements to the existing set.
847
     *
848
     * @param string $selector
849
     *  A valid selector.
850
     * @return \QueryPath\DOMQuery
851
     *  The DOMQuery object with the newly added elements.
852
     * @see append()
853
     * @see after()
854
     * @see andSelf()
855
     * @see end()
856
     */
857
    public function add($selector): Query
858
    {
859
860
        // This is destructive, so we need to set $last:
861
        $this->last = $this->matches;
0 ignored issues
show
Bug Best Practice introduced by
The property last does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
862
863
        foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) {
864
            $this->matches->attach($item);
865
        }
866
867
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
868
    }
869
870
    /**
871
     * Remove all child nodes.
872
     *
873
     * This is equivalent to jQuery's empty() function. (However, empty() is a
874
     * PHP built-in, and cannot be used as a method name.)
875
     *
876
     * @return \QueryPath\DOMQuery
877
     *  The DOMQuery object with the child nodes removed.
878
     * @see replaceWith()
879
     * @see replaceAll()
880
     * @see remove()
881
     */
882
    public function removeChildren(): Query
883
    {
884
        foreach ($this->matches as $m) {
885
            while ($kid = $m->firstChild) {
886
                $m->removeChild($kid);
887
            }
888
        }
889
890
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\Helpers\QueryMutators which is incompatible with the type-hinted return QueryPath\Query.
Loading history...
891
    }
892
893
    /**
894
     * Get/set an attribute.
895
     * - If no parameters are specified, this returns an associative array of all
896
     *   name/value pairs.
897
     * - If both $name and $value are set, then this will set the attribute name/value
898
     *   pair for all items in this object.
899
     * - If $name is set, and is an array, then
900
     *   all attributes in the array will be set for all items in this object.
901
     * - If $name is a string and is set, then the attribute value will be returned.
902
     *
903
     * When an attribute value is retrieved, only the attribute value of the FIRST
904
     * match is returned.
905
     *
906
     * @param mixed $name
907
     *   The name of the attribute or an associative array of name/value pairs.
908
     * @param string $value
909
     *   A value (used only when setting an individual property).
910
     * @return mixed
911
     *   If this was a setter request, return the DOMQuery object. If this was
912
     *   an access request (getter), return the string value.
913
     * @see removeAttr()
914
     * @see tag()
915
     * @see hasAttr()
916
     * @see hasClass()
917
     */
918
    public function attr($name = NULL, $value = NULL)
919
    {
920
        // Default case: Return all attributes as an assoc array.
921
        if (is_null($name)) {
922
            if ($this->matches->count() === 0) {
923
                return NULL;
924
            }
925
            $ele = $this->getFirstMatch();
926
            $buffer = [];
927
928
            // This does not appear to be part of the DOM
929
            // spec. Nor is it documented. But it works.
930
            foreach ($ele->attributes as $name => $attrNode) {
931
                $buffer[$name] = $attrNode->value;
932
            }
933
934
            return $buffer;
935
        }
936
937
        // multi-setter
938
        if (is_array($name)) {
939
            foreach ($name as $k => $v) {
940
                foreach ($this->matches as $m) {
941
                    $m->setAttribute($k, $v);
942
                }
943
            }
944
945
            return $this;
946
        }
947
        // setter
948
        if (isset($value)) {
949
            foreach ($this->matches as $m) {
950
                $m->setAttribute($name, $value);
951
            }
952
953
            return $this;
954
        }
955
956
        //getter
957
        if ($this->matches->count() === 0) {
958
            return NULL;
959
        }
960
961
        // Special node type handler:
962
        if ($name === 'nodeType') {
963
            return $this->getFirstMatch()->nodeType;
964
        }
965
966
        // Always return first match's attr.
967
        return $this->getFirstMatch()->getAttribute($name);
968
    }
969
970
    /**
971
     * Set/get a CSS value for the current element(s).
972
     * This sets the CSS value for each element in the DOMQuery object.
973
     * It does this by setting (or getting) the style attribute (without a namespace).
974
     *
975
     * For example, consider this code:
976
     *
977
     * @code
978
     * <?php
979
     * qp(HTML_STUB, 'body')->css('background-color','red')->html();
980
     * ?>
981
     * @endcode
982
     * This will return the following HTML:
983
     * @code
984
     * <body style="background-color: red"/>
985
     * @endcode
986
     *
987
     * If no parameters are passed into this function, then the current style
988
     * element will be returned unparsed. Example:
989
     * @code
990
     * <?php
991
     * qp(HTML_STUB, 'body')->css('background-color','red')->css();
992
     * ?>
993
     * @endcode
994
     * This will return the following:
995
     * @code
996
     * background-color: red
997
     * @endcode
998
     *
999
     * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
1000
     * (In previous versions of QueryPath, a call to css() overwrite the existing style
1001
     * values).
1002
     *
1003
     * @param mixed $name
1004
     *  If this is a string, it will be used as a CSS name. If it is an array,
1005
     *  this will assume it is an array of name/value pairs of CSS rules. It will
1006
     *  apply all rules to all elements in the set.
1007
     * @param string $value
1008
     *  The value to set. This is only set if $name is a string.
1009
     * @return \QueryPath\DOMQuery
1010
     */
1011
    public function css($name = NULL, $value = '')
1012
    {
1013
        if (empty($name)) {
1014
            return $this->attr('style');
1015
        }
1016
1017
        // Get any existing CSS.
1018
        $css = [];
1019
        foreach ($this->matches as $match) {
1020
            $style = $match->getAttribute('style');
1021
            if (!empty($style)) {
1022
                // XXX: Is this sufficient?
1023
                $style_array = explode(';', $style);
1024
                foreach ($style_array as $item) {
1025
                    $item = trim($item);
1026
1027
                    // Skip empty attributes.
1028
                    if ($item === '') {
1029
                        continue;
1030
                    }
1031
1032
                    [$css_att, $css_val] = explode(':', $item, 2);
1033
                    $css[$css_att] = trim($css_val);
1034
                }
1035
            }
1036
        }
1037
1038
        if (is_array($name)) {
1039
            // Use array_merge instead of + to preserve order.
1040
            $css = array_merge($css, $name);
1041
        } else {
1042
            $css[$name] = $value;
1043
        }
1044
1045
        // Collapse CSS into a string.
1046
        $format = '%s: %s;';
1047
        $css_string = '';
1048
        foreach ($css as $n => $v) {
1049
            $css_string .= sprintf($format, $n, trim($v));
1050
        }
1051
1052
        $this->attr('style', $css_string);
1053
1054
        return $this;
1055
    }
1056
}