Completed
Push — master ( 2850eb...7f616c )
by Chauncey
10:27
created

HierarchicalCollection::getPage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Charcoal\Object;
4
5
use InvalidArgumentException;
6
7
// From 'charcoal-core'
8
use Charcoal\Model\Collection as CharcoalCollection;
9
10
// From 'charcoal-object'
11
use Charcoal\Object\HierarchicalInterface;
12
13
/**
14
 * A Hierarchical Model Collection
15
 *
16
 * Sorts and flattens a collection of hierarchically-interrelated models.
17
 *
18
 * This class is not recommended. Currently, only designed and used by
19
 * {@see \Charcoal\Support\Property\HierarchicalObjectProperty} and
20
 * {@see \Charcoal\Support\Admin\Widget\HierarchicalTableWidget}.
21
 */
22
class HierarchicalCollection extends CharcoalCollection
23
{
24
    /**
25
     * The current page (slice).
26
     *
27
     * @var integer
28
     */
29
    protected $page = 0;
30
31
    /**
32
     * The number of objects per page (slice).
33
     *
34
     * @var integer
35
     */
36
    protected $numPerPage = 0;
37
38
    /**
39
     * Create a new hierarchically-sorted collection.
40
     *
41
     * @param  array|Traversable|null $objs Array of objects to pre-populate this collection.
42
     * @param  boolean                $sort Whether to sort the collection immediately.
43
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
44
     */
45
    public function __construct($objs = [], $sort = true)
46
    {
47
        if ($objs) {
48
            $this->merge($objs);
49
50
            if ($sort) {
51
                $this->sortTree();
52
            }
53
        }
54
    }
55
56
    /**
57
     * Sort the hierarchical collection of objects.
58
     *
59
     * @return self
60
     */
61
    public function sortTree()
62
    {
63
        $level   = 0;
64
        $count   = 0;
65
        $pageNum = $this->getPage();
66
        $perPage = $this->getNumPerPage();
67
68
        $sortedObjects = [];
69
        $rootObjects   = [];
70
        $childObjects  = [];
71
72
        foreach ($this->objects as $object) {
73
            // Repair bad hierarchy.
74
            if ($object->hasMaster() && $object->getMaster()->id() === $object->id()) {
75
                $object->setMaster(0);
76
                $object->update([ 'master' ]);
77
            }
78
79
            if ($object->hasMaster()) {
80
                $childObjects[$object->getMaster()->id()][] = $object;
81
            } else {
82
                $rootObjects[] = $object;
83
            }
84
        }
85
86
        if (empty($rootObjects) && !empty($childObjects)) {
87
            foreach ($childObjects as $parentId => $children) {
88
                $parentObj = $children[0]->getMaster();
89
                $parentObj->auxiliary = true;
90
91
                $rootObjects[] = $parentObj;
92
            }
93
        }
94
95
        $this->objects = &$rootObjects;
96
97
        if ($perPage < 1) {
98
            foreach ($this->objects as $object) {
99
                $object->level = $level;
100
                $sortedObjects[$object->id()] = $object;
101
102
                $count++;
103
104
                if (isset($childObjects[$object->id()])) {
105
                    $this->sortDescendantObjects(
106
                        $object,
107
                        $childObjects,
108
                        $count,
109
                        ($level + 1),
110
                        $sortedObjects
111
                    );
112
                }
113
            }
114
        } else {
115
            $start = (( $pageNum - 1 ) * $perPage);
116
            $end   = ($start + $perPage);
117
118
            foreach ($this->objects as $object) {
119
                if ($count >= $end) {
120
                    break;
121
                }
122
123
                if ($count >= $start) {
124
                    $object->level = $level;
125
                    $sortedObjects[$object->id()] = $object;
126
                }
127
128
                $count++;
129
130
                if (isset($childObjects[$object->id()])) {
131
                    $this->sortDescendantObjects(
132
                        $object,
133
                        $childObjects,
134
                        $count,
135
                        ($level + 1),
136
                        $sortedObjects
137
                    );
138
                }
139
            }
140
141
            // If we are on the last page, display orphaned descendants.
142
            if ($childObjects && $count < $end) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $childObjects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
143
                foreach ($childObjects as $orphans) {
144
                    foreach ($orphans as $descendants) {
145
                        if ($count >= $end) {
146
                            break;
147
                        }
148
149
                        if ($count >= $start) {
150
                            $descendants->level = 0;
151
                            $sortedObjects[$descendants->id()] = $descendants;
152
                        }
153
154
                        $count++;
155
                    }
156
                }
157
            }
158
        }
159
160
        $this->objects = $sortedObjects;
161
162
        return $this;
163
    }
164
165
    /**
166
     * Given an object, display the nested hierarchy of descendants.
167
     *
168
     * @param  HierarchicalInterface   $parentObj     The parent object from which to append its
169
     *     descendants for display.
170
     * @param  HierarchicalInterface[] $childObjects  The list of descendants by parent object ID.
171
     *     Passed by reference.
172
     * @param  integer                 $count         The current count of objects to display,
173
     *     for pagination. Passed by reference.
174
     * @param  integer                 $level         The level directly below the $parentObj.
175
     * @param  HierarchicalInterface[] $sortedObjects The list of objects to be displayed.
176
     *     Passed by reference.
177
     * @return void
178
     */
179
    private function sortDescendantObjects(
180
        HierarchicalInterface $parentObj,
181
        array &$childObjects,
182
        &$count,
183
        $level,
184
        array &$sortedObjects
185
    ) {
186
        $pageNum = $this->getPage();
187
        $perPage = $this->getNumPerPage();
188
189
        if ($perPage < 1) {
190
            foreach ($childObjects[$parentObj->id()] as $object) {
0 ignored issues
show
Bug introduced by
The method id() does not seem to exist on object<Charcoal\Object\HierarchicalInterface>.

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...
Bug introduced by
The expression $childObjects[$parentObj->id()] of type object<Charcoal\Object\HierarchicalInterface> is not traversable.
Loading history...
191 View Code Duplication
                if ($count === 0 && $object->hasMaster()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
192
                    $myParents = [];
193
                    $myParent  = $object->getMaster();
194
                    while ($myParent) {
195
                        $myParents[] = $myParent;
196
197
                        if (!$myParent->hasMaster()) {
198
                            break;
199
                        }
200
201
                        $myParent = $myParent->getMaster();
202
                    }
203
204
                    $numParents = count($myParents);
205
                    while ($myParent = array_pop($myParents)) {
206
                        $myParent->level = ($level - $numParents);
207
                        $sortedObjects[$myParent->id()] = $myParent;
208
                        $numParents--;
209
                    }
210
                }
211
212
                $object->level = $level;
213
                $sortedObjects[$object->id()] = $object;
214
215
                $count++;
216
217
                if (isset($childObjects[$object->id()])) {
218
                    $this->sortDescendantObjects(
219
                        $object,
220
                        $childObjects,
221
                        $count,
222
                        ($level + 1),
223
                        $sortedObjects
224
                    );
225
                }
226
            }
227
        } else {
228
            $start = (( $pageNum - 1 ) * $perPage);
229
            $end   = ($start + $perPage);
230
231
            foreach ($childObjects[$parentObj->id()] as $object) {
0 ignored issues
show
Bug introduced by
The method id() does not seem to exist on object<Charcoal\Object\HierarchicalInterface>.

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...
Bug introduced by
The expression $childObjects[$parentObj->id()] of type object<Charcoal\Object\HierarchicalInterface> is not traversable.
Loading history...
232
                if ($count >= $end) {
233
                    break;
234
                }
235
236
                // If the page starts in a subtree, print the parents.
237 View Code Duplication
                if ($count === $start && $object->hasMaster()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
238
                    $myParents = [];
239
                    $myParent  = $object->getMaster();
240
                    while ($myParent) {
241
                        $myParents[] = $myParent;
242
243
                        if (!$myParent->hasMaster()) {
244
                            break;
245
                        }
246
247
                        $myParent = $myParent->getMaster();
248
                    }
249
250
                    $numParents = count($myParents);
251
                    while ($myParent = array_pop($myParents)) {
252
                        $myParent->level = ($level - $numParents);
253
                        $sortedObjects[$myParent->id()] = $myParent;
254
                        $numParents--;
255
                    }
256
                }
257
258
                if ($count >= $start) {
259
                    $object->level = $level;
260
                    $sortedObjects[$object->id()] = $object;
261
                }
262
263
                $count++;
264
265
                if (isset($childObjects[$object->id()])) {
266
                    $this->sortDescendantObjects(
267
                        $object,
268
                        $childObjects,
269
                        $count,
270
                        ($level + 1),
271
                        $sortedObjects
272
                    );
273
                }
274
            }
275
        }
276
277
        // Required in order to keep track of orphans
278
        unset($childObjects[$parentObj->id()]);
279
    }
280
281
    /**
282
     * @param  integer $page The current page. Start at 0.
283
     * @throws InvalidArgumentException If the parameter is not numeric or < 0.
284
     * @return Pagination (Chainable)
285
     */
286 View Code Duplication
    public function setPage($page)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
287
    {
288
        if (!is_numeric($page)) {
289
            throw new InvalidArgumentException(
290
                'Page number needs to be numeric.'
291
            );
292
        }
293
294
        $page = (int)$page;
295
        if ($page < 0) {
296
            throw new InvalidArgumentException(
297
                'Page number needs to be >= 0.'
298
            );
299
        }
300
301
        $this->page = $page;
302
303
        return $this;
304
    }
305
306
    /**
307
     * @return integer
308
     */
309
    public function getPage()
310
    {
311
        return $this->page;
312
    }
313
314
    /**
315
     * @param  integer $num The number of results to retrieve, per page.
316
     * @throws InvalidArgumentException If the parameter is not numeric or < 0.
317
     * @return Pagination (Chainable)
318
     */
319 View Code Duplication
    public function setNumPerPage($num)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
320
    {
321
        if (!is_numeric($num)) {
322
            throw new InvalidArgumentException(
323
                'Num-per-page needs to be numeric.'
324
            );
325
        }
326
327
        $num = (int)$num;
328
329
        if ($num < 0) {
330
            throw new InvalidArgumentException(
331
                'Num-per-page needs to be >= 0.'
332
            );
333
        }
334
335
        $this->numPerPage = $num;
336
337
        return $this;
338
    }
339
340
    /**
341
     * @return integer
342
     */
343
    public function getNumPerPage()
344
    {
345
        return $this->numPerPage;
346
    }
347
348
    /**
349
     * Determine if the given value is acceptable for the collection.
350
     *
351
     * @param  mixed $value The value being vetted.
352
     * @return boolean
353
     */
354
    public function isAcceptable($value)
355
    {
356
        return ($value instanceof HierarchicalInterface);
357
    }
358
}
359