Completed
Push — master ( 966347...8b759a )
by Gilles
08:12
created

InnerNode::insertAfter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3.0416
1
<?php
2
namespace PHPHtmlParser\Dom;
3
4
use PHPHtmlParser\Exceptions\ChildNotFoundException;
5
use PHPHtmlParser\Exceptions\CircularException;
6
use stringEncode\Encode;
7
8
/**
9
 * Inner node of the html tree, might have children.
10
 *
11
 * @package PHPHtmlParser\Dom
12
 */
13
abstract class InnerNode extends ArrayNode
14
{
15
16
    /**
17
     * An array of all the children.
18
     *
19
     * @var array
20
     */
21
    protected $children = [];
22
23
    /**
24
     * Sets the encoding class to this node and propagates it
25
     * to all its children.
26
     *
27
     * @param Encode $encode
28
     * @return void
29
     */
30 186
    public function propagateEncoding(Encode $encode): void
31
    {
32 186
        $this->encode = $encode;
33 186
        $this->tag->setEncoding($encode);
34
        // check children
35 186
        foreach ($this->children as $id => $child) {
36
            /** @var AbstractNode $node */
37 186
            $node = $child['node'];
38 186
            $node->propagateEncoding($encode);
39
        }
40 186
    }
41
42
    /**
43
     * Checks if this node has children.
44
     *
45
     * @return bool
46
     */
47 390
    public function hasChildren(): bool
48
    {
49 390
        return ! empty($this->children);
50
    }
51
52
    /**
53
     * Returns the child by id.
54
     *
55
     * @param int $id
56
     * @return AbstractNode
57
     * @throws ChildNotFoundException
58
     */
59 333
    public function getChild(int $id): AbstractNode
60
    {
61 333
        if ( ! isset($this->children[$id])) {
62 3
            throw new ChildNotFoundException("Child '$id' not found in this node.");
63
        }
64
65 330
        return $this->children[$id]['node'];
66
    }
67
68
    /**
69
     * Returns a new array of child nodes
70
     *
71
     * @return array
72
     */
73 12
    public function getChildren(): array
74
    {
75 12
        $nodes = [];
76
        try {
77 12
            $child = $this->firstChild();
78
            do {
79 12
                $nodes[] = $child;
80 12
                $child   = $this->nextChild($child->id());
81 12
            } while ( ! is_null($child));
82 12
        } catch (ChildNotFoundException $e) {
83
            // we are done looking for children
84
        }
85
86 12
        return $nodes;
87
    }
88
89
    /**
90
     * Counts children
91
     *
92
     * @return int
93
     */
94 6
    public function countChildren(): int
95
    {
96 6
        return count($this->children);
97
    }
98
99
    /**
100
     * Adds a child node to this node and returns the id of the child for this
101
     * parent.
102
     *
103
     * @param AbstractNode $child
104
     * @param Int $before
105
     * @return bool
106
     * @throws CircularException
107
     */
108 384
    public function addChild(AbstractNode $child, int $before = -1): bool
109
    {
110 384
        $key = null;
111
112
        // check integrity
113 384
        if ($this->isAncestor($child->id())) {
114 3
            throw new CircularException('Can not add child. It is my ancestor.');
115
        }
116
117
        // check if child is itself
118 384
        if ($child->id() == $this->id) {
119 3
            throw new CircularException('Can not set itself as a child.');
120
        }
121
122 381
		$next = null;
123
124 381
        if ($this->hasChildren()) {
125 366
			if (isset($this->children[$child->id()])) {
126
				// we already have this child
127 354
				return false;
128
			}
129
130 297
			if ($before >= 0) {
131 9
				if (!isset($this->children[$before])) {
132
					return false;
133
				}
134
135 9
				$key = $this->children[$before]['prev'];
136
137 9
				if($key){
138 6
					$this->children[$key]['next'] = $child->id();
139
				}
140
141 9
				$this->children[$before]['prev'] = $child->id();
142 9
				$next = $before;
143
			} else {
144 297
				$sibling = $this->lastChild();
145 297
				$key = $sibling->id();
146
147 297
				$this->children[$key]['next'] = $child->id();
148
			}
149
        }
150
151 381
		$keys = array_keys($this->children);
152
153
		$insert = [
154 381
			'node' => $child,
155 381
			'next' => $next,
156 381
			'prev' => $key,
157
		];
158
159 381
		$index = $key ? (array_search($key, $keys, true) + 1) : 0;
160 381
		array_splice($keys, $index, 0, $child->id());
161
162 381
		$children = array_values($this->children);
163 381
		array_splice($children, $index, 0, [$insert]);
164
165
		// add the child
166 381
		$this->children = array_combine($keys, $children);
167
168
        // tell child I am the new parent
169 381
        $child->setParent($this);
170
171
        //clear any cache
172 381
        $this->clear();
173
174 381
        return true;
175
    }
176
177
	/**
178
	 * Insert element before child with provided id
179
	 *
180
	 * @param AbstractNode $child
181
	 * @param int $id
182
	 * @return bool
183
	 */
184 6
	public function insertBefore(AbstractNode $child, int $id): bool
185
	{
186 6
		return $this->addChild($child, $id);
187
	}
188
189
	/**
190
	 * Insert element before after with provided id
191
	 *
192
	 * @param AbstractNode $child
193
	 * @param int $id
194
	 * @return bool
195
	 */
196 6
	public function insertAfter(AbstractNode $child, int $id): bool
197
	{
198 6
		if (!isset($this->children[$id])) {
199
			return false;
200
		}
201
202 6
		if ($this->children[$id]['next']) {
203 3
			return $this->addChild($child, $this->children[$id]['next']);
204
		}
205
206 3
		return $this->addChild($child);
207
	}
208
209
    /**
210
     * Removes the child by id.
211
     *
212
     * @param int $id
213
     * @return InnerNode
214
     * @chainable
215
     */
216 21
    public function removeChild(int $id): InnerNode
217
    {
218 21
        if ( ! isset($this->children[$id])) {
219 3
            return $this;
220
        }
221
222
        // handle moving next and previous assignments.
223 18
        $next = $this->children[$id]['next'];
224 18
        $prev = $this->children[$id]['prev'];
225 18
        if ( ! is_null($next)) {
226 9
            $this->children[$next]['prev'] = $prev;
227
        }
228 18
        if ( ! is_null($prev)) {
229 9
            $this->children[$prev]['next'] = $next;
230
        }
231
232
        // remove the child
233 18
        unset($this->children[$id]);
234
235
        //clear any cache
236 18
        $this->clear();
237
238 18
        return $this;
239
    }
240
241
    /**
242
     * Check if has next Child
243
     *
244
     * @param int $id
245
     * @return mixed
246
     */
247 6
    public function hasNextChild(int $id)
248
    {
249 6
        $child= $this->getChild($id);
250 3
        return $this->children[$child->id()]['next'];
251
    }
252
253
    /**
254
     * Attempts to get the next child.
255
     *
256
     * @param int $id
257
     * @return AbstractNode
258
     * @uses $this->getChild()
259
     * @throws ChildNotFoundException
260
     */
261 291
    public function nextChild(int $id): AbstractNode
262
    {
263 291
        $child = $this->getChild($id);
264 291
        $next  = $this->children[$child->id()]['next'];
265 291
        if (is_null($next)) {
266 270
            throw new ChildNotFoundException("Child '$id' next not found in this node.");
267
        }
268
269 261
        return $this->getChild($next);
270
    }
271
272
    /**
273
     * Attempts to get the previous child.
274
     *
275
     * @param int $id
276
     * @return AbstractNode
277
     * @uses $this->getChild()
278
     * @throws ChildNotFoundException
279
     */
280 12
    public function previousChild(int $id): AbstractNode
281
    {
282 12
        $child = $this->getchild($id);
283 12
        $next  = $this->children[$child->id()]['prev'];
284 12
        if (is_null($next)) {
285 3
            throw new ChildNotFoundException("Child '$id' previous not found in this node.");
286
        }
287
288 9
        return $this->getChild($next);
289
    }
290
291
    /**
292
     * Checks if the given node id is a child of the
293
     * current node.
294
     *
295
     * @param int $id
296
     * @return bool
297
     */
298 372
    public function isChild(int $id): bool
299
    {
300 372
        foreach ($this->children as $childId => $child) {
301 36
            if ($id == $childId) {
302 24
                return true;
303
            }
304
        }
305
306 372
        return false;
307
    }
308
309
    /**
310
     * Removes the child with id $childId and replace it with the new child
311
     * $newChild.
312
     *
313
     * @param int $childId
314
     * @param AbstractNode $newChild
315
     * @throws ChildNotFoundException
316
     * @return void
317
     */
318 6
    public function replaceChild(int $childId, AbstractNode $newChild): void
319
    {
320 6
        $oldChild = $this->children[$childId];
321
322 6
        $newChild->prev = $oldChild['prev'];
0 ignored issues
show
Bug introduced by
The property prev does not seem to exist in PHPHtmlParser\Dom\AbstractNode.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
323 6
        $newChild->next = $oldChild['next'];
0 ignored issues
show
Bug introduced by
The property next does not seem to exist in PHPHtmlParser\Dom\AbstractNode.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
324
325 6
        $keys = array_keys($this->children);
326 6
        $index = array_search($childId, $keys, true);
327 6
        $keys[$index] = $newChild->id();
328 6
        $this->children = array_combine($keys, $this->children);
329 6
        $this->children[$newChild->id()] = array(
330 6
            'prev' => $oldChild['prev'],
331 6
            'node' => $newChild,
332 6
            'next' => $oldChild['next']
333
        );
334
335 6
        if ($oldChild['prev'] && isset($this->children[$newChild->prev])) {
0 ignored issues
show
Documentation introduced by
The property prev does not exist on object<PHPHtmlParser\Dom\AbstractNode>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
336
            $this->children[$oldChild['prev']]['next'] = $newChild->id();
337
        }
338
339 6
        if ($oldChild['next'] && isset($this->children[$newChild->next])) {
0 ignored issues
show
Documentation introduced by
The property next does not exist on object<PHPHtmlParser\Dom\AbstractNode>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
340 3
            $this->children[$oldChild['next']]['prev'] = $newChild->id();
341
        }
342 6
    }
343
344
    /**
345
     * Shortcut to return the first child.
346
     *
347
     * @return AbstractNode
348
     * @uses $this->getChild()
349
     */
350 282
    public function firstChild(): AbstractNode
351
    {
352 282
        reset($this->children);
353 282
        $key = key($this->children);
354
355 282
        return $this->getChild($key);
356
    }
357
358
    /**
359
     * Attempts to get the last child.
360
     *
361
     * @return AbstractNode
362
     */
363 297
    public function lastChild(): AbstractNode
364
    {
365 297
        end($this->children);
366 297
        $key = key($this->children);
367
368 297
        return $this->getChild($key);
369
    }
370
371
    /**
372
     * Checks if the given node id is a descendant of the
373
     * current node.
374
     *
375
     * @param int $id
376
     * @return bool
377
     */
378 372
    public function isDescendant(int $id): bool
379
    {
380 372
        if ($this->isChild($id)) {
381 6
            return true;
382
        }
383
384 372
        foreach ($this->children as $childId => $child) {
385
            /** @var InnerNode $node */
386 18
            $node = $child['node'];
387 18
            if ($node instanceof InnerNode &&
388 18
                $node->hasChildren() &&
389 18
                $node->isDescendant($id)
390
            ) {
391 8
                return true;
392
            }
393
        }
394
395 372
        return false;
396
    }
397
398
    /**
399
     * Sets the parent node.
400
     *
401
     * @param InnerNode $parent
402
     * @return AbstractNode
403
     * @throws CircularException
404
     * @chainable
405
     */
406 372
    public function setParent(InnerNode $parent): AbstractNode
407
    {
408
        // check integrity
409 372
        if ($this->isDescendant($parent->id())) {
410 3
            throw new CircularException('Can not add descendant "'.$parent->id().'" as my parent.');
411
        }
412
413 372
        return parent::setParent($parent);
414
    }
415
}
416