Passed
Push — master ( d9e25d...dc012b )
by Jan
05:48
created

AbstractStructuralDBElement::addChild()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\Entity\Base;
24
25
use App\Entity\Attachments\AttachmentContainingDBElement;
26
use App\Entity\Parameters\ParametersTrait;
27
use App\Validator\Constraints\NoneOfItsChildren;
28
use function count;
29
use Doctrine\Common\Collections\ArrayCollection;
30
use Doctrine\Common\Collections\Collection;
31
use Doctrine\ORM\Mapping as ORM;
32
use function get_class;
33
use InvalidArgumentException;
34
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
35
use Symfony\Component\Serializer\Annotation\Groups;
36
37
/**
38
 * All elements with the fields "id", "name" and "parent_id" (at least).
39
 *
40
 * This class is for managing all database objects with a structural design.
41
 * All these sub-objects must have the table columns 'id', 'name' and 'parent_id' (at least)!
42
 * The root node has always the ID '0'.
43
 * It's allowed to have instances of root elements, but if you try to change
44
 * an attribute of a root element, you will get an exception!
45
 *
46
 * @ORM\MappedSuperclass(repositoryClass="App\Repository\StructuralDBElementRepository")
47
 *
48
 * @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"})
49
 *
50
 * @UniqueEntity(fields={"name", "parent"}, ignoreNull=false, message="structural.entity.unique_name")
51
 */
52
abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
53
{
54
    use ParametersTrait;
55
56
    public const ID_ROOT_ELEMENT = 0;
57
58
    /**
59
     * This is a not standard character, so build a const, so a dev can easily use it.
60
     */
61
    public const PATH_DELIMITER_ARROW = ' → ';
62
63
    /**
64
     * @var string The comment info for this element
65
     * @ORM\Column(type="text")
66
     * @Groups({"simple", "extended", "full"})
67
     */
68
    protected string $comment = '';
69
70
    /**
71
     * @var bool If this property is set, this element can not be selected for part properties.
72
     *           Useful if this element should be used only for grouping, sorting.
73
     * @ORM\Column(type="boolean")
74
     */
75
    protected bool $not_selectable = false;
76
77
    /**
78
     * @var int
79
     */
80
    protected int $level = 0;
81
82
    /**
83
     * We can not define the mapping here or we will get an exception. Unfortunately we have to do the mapping in the
84
     * subclasses.
85
     *
86
     * @var AbstractStructuralDBElement[]|Collection
87
     * @Groups({"include_children"})
88
     */
89
    protected $children;
90
91
    /**
92
     * @var AbstractStructuralDBElement
93
     * @NoneOfItsChildren()
94
     * @Groups({"include_parents"})
95
     */
96
    protected $parent = null;
97
98
    /** @var string[] all names of all parent elements as a array of strings,
99
     *  the last array element is the name of the element itself
100
     */
101
    private array $full_path_strings = [];
102
103
    public function __construct()
104
    {
105
        parent::__construct();
106
        $this->children = new ArrayCollection();
107
        $this->parameters = new ArrayCollection();
108
    }
109
110
    public function __clone()
111
    {
112
        if ($this->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
113
            //Deep clone parameters
114
            $parameters = $this->parameters;
115
            $this->parameters = new ArrayCollection();
116
            foreach ($parameters as $parameter) {
117
                $this->addParameter(clone $parameter);
118
            }
119
        }
120
        parent::__clone();
121
    }
122
123
    /******************************************************************************
124
     * StructuralDBElement constructor.
125
     *****************************************************************************/
126
127
    /**
128
     * Check if this element is a child of another element (recursive).
129
     *
130
     * @param AbstractStructuralDBElement $another_element the object to compare
131
     *                                                     IMPORTANT: both objects to compare must be from the same class (for example two "Device" objects)!
132
     *
133
     * @return bool true, if this element is child of $another_element
134
     *
135
     * @throws InvalidArgumentException if there was an error
136
     */
137
    public function isChildOf(self $another_element): bool
138
    {
139
        $class_name = static::class;
140
141
        //Check if both elements compared, are from the same type
142
        // (we have to check inheritance, or we get exceptions when using doctrine entities (they have a proxy type):
143
        if (!is_a($another_element, $class_name) && !is_a($this, get_class($another_element))) {
144
            throw new InvalidArgumentException('isChildOf() only works for objects of the same type!');
145
        }
146
147
        if (null === $this->getParent()) { // this is the root node
148
            return false;
149
        }
150
151
        //If the parent element is equal to the element we want to compare, return true
152
        if ($this->getParent()->getID() === null || $this->getParent()->getID() === null) {
153
            //If the IDs are not yet defined, we have to compare the objects itself
154
            if ($this->getParent() === $another_element) {
155
                return true;
156
            }
157
        } else { //If the IDs are defined, we can compare the IDs
158
            if ($this->getParent()->getID() === $another_element->getID()) {
159
                return true;
160
            }
161
        }
162
163
        //Otherwise, check recursively
164
        return $this->parent->isChildOf($another_element);
165
    }
166
167
    /**
168
     * Checks if this element is an root element (has no parent).
169
     *
170
     * @return bool true if the this element is an root element
171
     */
172
    public function isRoot(): bool
173
    {
174
        return null === $this->parent;
175
    }
176
177
    /******************************************************************************
178
     *
179
     * Getters
180
     *
181
     ******************************************************************************/
182
183
    /**
184
     * Get the parent of this element.
185
     *
186
     * @return AbstractStructuralDBElement|null The parent element. Null if this element, does not have a parent.
187
     */
188
    public function getParent(): ?self
189
    {
190
        return $this->parent;
191
    }
192
193
    /**
194
     *  Get the comment of the element.
195
196
     *
197
     * @return string the comment
198
     */
199
    public function getComment(): ?string
200
    {
201
        return $this->comment;
202
    }
203
204
    /**
205
     * Get the level.
206
     *
207
     * The level of the root node is -1.
208
     *
209
     * @return int the level of this element (zero means a most top element
210
     *             [a sub element of the root node])
211
     */
212
    public function getLevel(): int
213
    {
214
        /*
215
         * Only check for nodes that have a parent. In the other cases zero is correct.
216
         */
217
        if (0 === $this->level && null !== $this->parent) {
218
            $element = $this->parent;
219
            while (null !== $element) {
220
                /** @var AbstractStructuralDBElement $element */
221
                $element = $element->parent;
222
                ++$this->level;
223
            }
224
        }
225
226
        return $this->level;
227
    }
228
229
    /**
230
     * Get the full path.
231
     *
232
     * @param string $delimiter the delimiter of the returned string
233
     *
234
     * @return string the full path (incl. the name of this element), delimited by $delimiter
235
     */
236
    public function getFullPath(string $delimiter = self::PATH_DELIMITER_ARROW): string
237
    {
238
        if (empty($this->full_path_strings)) {
239
            $this->full_path_strings = [];
240
            $this->full_path_strings[] = $this->getName();
241
            $element = $this;
242
243
            $overflow = 20; //We only allow 20 levels depth
244
245
            while (null !== $element->parent && $overflow >= 0) {
246
                $element = $element->parent;
247
                $this->full_path_strings[] = $element->getName();
248
                //Decrement to prevent mem overflow.
249
                --$overflow;
250
            }
251
252
            $this->full_path_strings = array_reverse($this->full_path_strings);
253
        }
254
255
        return implode($delimiter, $this->full_path_strings);
256
    }
257
258
    /**
259
     * Gets the path to this element (including the element itself).
260
     *
261
     * @return self[] An array with all (recursively) parent elements (including this one),
262
     *                ordered from the lowest levels (root node) first to the highest level (the element itself)
263
     */
264
    public function getPathArray(): array
265
    {
266
        $tmp = [];
267
        $tmp[] = $this;
268
269
        //We only allow 20 levels depth
270
        while (!end($tmp)->isRoot() && count($tmp) < 20) {
271
            $tmp[] = end($tmp)->parent;
272
        }
273
274
        return array_reverse($tmp);
275
    }
276
277
    /**
278
     * Get all sub elements of this element.
279
     *
280
     * @return Collection<static>|iterable all subelements as an array of objects (sorted by their full path)
281
     * @psalm-return Collection<int, static>
282
     */
283
    public function getSubelements(): iterable
284
    {
285
        return $this->children ?? new ArrayCollection();
286
    }
287
288
    /**
289
     * @see getSubelements()
290
     * @return Collection<static>|iterable
291
     * @psalm-return Collection<int, static>
292
     */
293
    public function getChildren(): iterable
294
    {
295
        return $this->getSubelements();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getSubelements() returns the type iterable which is incompatible with the documented return type Doctrine\Common\Collections\Collection.
Loading history...
296
    }
297
298
    public function isNotSelectable(): bool
299
    {
300
        return $this->not_selectable;
301
    }
302
303
    /******************************************************************************
304
     *
305
     * Setters
306
     *
307
     ******************************************************************************/
308
309
    /**
310
     * Sets the new parent object.
311
     *
312
     * @param  AbstractStructuralDBElement|null  $new_parent  The new parent object
313
     *
314
     * @return AbstractStructuralDBElement
315
     */
316
    public function setParent(?self $new_parent): self
317
    {
318
        /*
319
        if ($new_parent->isChildOf($this)) {
320
            throw new \InvalidArgumentException('You can not use one of the element childs as parent!');
321
        } */
322
323
        $this->parent = $new_parent;
324
325
        //Add this element as child to the new parent
326
        if (null !== $new_parent) {
327
            $new_parent->getChildren()->add($this);
328
        }
329
330
        return $this;
331
    }
332
333
    /**
334
     *  Set the comment.
335
     *
336
     * @param  string|null  $new_comment  the new comment
337
     *
338
     * @return AbstractStructuralDBElement
339
     */
340
    public function setComment(?string $new_comment): self
341
    {
342
        $this->comment = $new_comment;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Adds the given element as child to this element.
349
     * @param  static  $child
350
     * @return $this
351
     */
352
    public function addChild(self $child): self
353
    {
354
        $this->children->add($child);
355
        //Children get this element as parent
356
        $child->setParent($this);
357
        return $this;
358
    }
359
360
    /**
361
     * Removes the given element as child from this element.
362
     * @param  static  $child
363
     * @return $this
364
     */
365
    public function removeChild(self $child): self
366
    {
367
        $this->children->removeElement($child);
368
        //Children has no parent anymore
369
        $child->setParent(null);
370
        return $this;
371
    }
372
373
    /**
374
     * @return AbstractStructuralDBElement
375
     */
376
    public function setNotSelectable(bool $not_selectable): self
377
    {
378
        $this->not_selectable = $not_selectable;
379
380
        return $this;
381
    }
382
383
    public function clearChildren(): self
384
    {
385
        $this->children = new ArrayCollection();
386
387
        return $this;
388
    }
389
}
390