Passed
Push — master ( fdc20c...c27313 )
by Andreas
51:09 queued 22:57
created

midcom_helper_reflector_tree::get_child_objects()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.246

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 13
c 2
b 0
f 0
nc 5
nop 2
dl 0
loc 23
ccs 11
cts 14
cp 0.7856
crap 5.246
rs 9.5222
1
<?php
2
/**
3
 * @package midcom.helper.reflector
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * The Grand Unified Reflector, Tree information
11
 *
12
 * @package midcom.helper.reflector
13
 */
14
class midcom_helper_reflector_tree extends midcom_helper_reflector
15
{
16
    /**
17
     * Creates a QB instance for root objects
18
     */
19 5
    public function _root_objects_qb()
20
    {
21 5
        $schema_type = $this->mgdschema_class;
22
23 5
        $qb = $this->_get_type_qb($schema_type, false);
0 ignored issues
show
Bug introduced by
It seems like $schema_type can also be of type null; however, parameter $schema_type of midcom_helper_reflector_tree::_get_type_qb() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

23
        $qb = $this->_get_type_qb(/** @scrutinizer ignore-type */ $schema_type, false);
Loading history...
24 5
        if (!$qb) {
25
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
26
            return false;
27
        }
28
29
        // Figure out constraint to use to get root level objects
30 5
        if ($upfield = midgard_object_class::get_property_up($schema_type)) {
0 ignored issues
show
Bug introduced by
It seems like $schema_type can also be of type null; however, parameter $classname of midgard_object_class::get_property_up() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

30
        if ($upfield = midgard_object_class::get_property_up(/** @scrutinizer ignore-type */ $schema_type)) {
Loading history...
31 5
            $uptype = $this->_mgd_reflector->get_midgard_type($upfield);
32 5
            switch ($uptype) {
33
                case MGD_TYPE_STRING:
34
                case MGD_TYPE_GUID:
35
                    $qb->add_constraint($upfield, '=', '');
36
                    break;
37
                case MGD_TYPE_INT:
38
                case MGD_TYPE_UINT:
39 5
                    $qb->add_constraint($upfield, '=', 0);
40 5
                    break;
41
                default:
42
                    debug_add("Do not know how to handle upfield '{$upfield}' has type {$uptype}", MIDCOM_LOG_ERROR);
43
                    return false;
44
            }
45
        }
46 5
        return $qb;
47
    }
48
49
    /**
50
     * Get rendered path for object
51
     *
52
     * @param midgard\portable\api\mgdobject $object The object to get path for
53
     * @param string $separator the string used to separate path components
54
     */
55 1
    public static function resolve_path($object, string $separator = ' &gt; ') : string
56
    {
57 1
        $parts = self::resolve_path_parts($object);
58 1
        return implode($separator, array_column($parts, 'label'));
59
    }
60
61
    /**
62
     * Get path components for object
63
     *
64
     * @param midgard\portable\api\mgdobject $object The object to get path for
65
     */
66 3
    public static function resolve_path_parts($object) : array
67
    {
68 3
        static $cache = [];
69 3
        if (isset($cache[$object->guid])) {
70 1
            return $cache[$object->guid];
71
        }
72
73 2
        $ret = [];
74 2
        $ret[] = [
75 2
            'object' => $object,
76 2
            'label' => parent::get($object)->get_object_label($object),
77
        ];
78
79 2
        $parent = $object->get_parent();
80 2
        while (is_object($parent)) {
81 1
            $ret[] = [
82 1
                'object' => $parent,
83 1
                'label' => parent::get($parent)->get_object_label($parent),
84
            ];
85 1
            $parent = $parent->get_parent();
86
        }
87
88 2
        $cache[$object->guid] = array_reverse($ret);
89 2
        return $cache[$object->guid];
90
    }
91
92 20
    private static function _check_permissions(bool $deleted) : bool
93
    {
94
        // PONDER: Check for some generic user privilege instead  ??
95 20
        if (   $deleted
96 20
            && !midcom_connection::is_admin()
97 20
            && !midcom::get()->auth->is_component_sudo()) {
98
            debug_add('Non-admins are not allowed to list deleted objects', MIDCOM_LOG_ERROR);
99
            return false;
100
        }
101 20
        return true;
102
    }
103
104
    /**
105
     * Get children of given object
106
     *
107
     * @param midgard\portable\api\mgdobject $object object to get children for
108
     * @param boolean $deleted whether to get (only) deleted or not-deleted objects
109
     * @return array multidimensional array (keyed by classname) of objects
110
     */
111 20
    public static function get_child_objects(object $object, bool $deleted = false) : array
112
    {
113 20
        if (!self::_check_permissions($deleted)) {
114
            return [];
115
        }
116 20
        $resolver = new self($object);
117
118 20
        $child_objects = [];
119 20
        foreach ($resolver->get_child_classes() as $schema_type) {
120 20
            $qb = $resolver->_child_objects_type_qb($schema_type, $object, $deleted);
121 20
            if (!$qb) {
122
                debug_add('Could not get QB instance', MIDCOM_LOG_ERROR);
123
                continue;
124
            }
125
126
            // Sort by title and name if available
127 20
            self::add_schema_sorts_to_qb($qb, $schema_type);
128
129 20
            if ($type_children = $qb->execute()) {
130 1
                $child_objects[$schema_type] = $type_children;
131
            }
132
        }
133 20
        return $child_objects;
134
    }
135
136 96
    private function _get_type_qb(string $schema_type, bool $deleted)
137
    {
138 96
        if (empty($schema_type)) {
139
            debug_add('Passed schema_type argument is empty, this is fatal', MIDCOM_LOG_ERROR);
140
            return false;
141
        }
142 96
        if ($deleted) {
143
            $qb = new midgard_query_builder($schema_type);
144
            $qb->include_deleted();
145
            $qb->add_constraint('metadata.deleted', '<>', 0);
146
            return $qb;
147
        }
148
        // Figure correct MidCOM DBA class to use and get midcom QB
149 96
        $midcom_dba_classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type);
150 96
        if (empty($midcom_dba_classname)) {
151
            debug_add("MidCOM DBA does not know how to handle {$schema_type}", MIDCOM_LOG_ERROR);
152
            return false;
153
        }
154
155 96
        return $midcom_dba_classname::new_query_builder();
156
    }
157
158
    /**
159
     * Figure out constraint(s) to use to get child objects
160
     */
161 94
    private function _get_link_fields(string $schema_type, $classname) : array
162
    {
163 94
        static $cache = [];
164 94
        $cache_key = $schema_type . '-' . $classname;
165 94
        if (empty($cache[$cache_key])) {
166 6
            $ref = new midgard_reflection_property($schema_type);
167
168
            $linkfields = [
169 6
                'up' => midgard_object_class::get_property_up($schema_type),
170 6
                'parent' => midgard_object_class::get_property_parent($schema_type)
171
            ];
172 6
            $linkfields = array_filter($linkfields);
173 6
            $data = [];
174 6
            foreach ($linkfields as $link_type => $field) {
175
                $info = [
176 6
                    'name' => $field,
177 6
                    'type' => $ref->get_midgard_type($field),
178 6
                    'target' => $ref->get_link_target($field)
179
                ];
180 6
                $linked_class = $ref->get_link_name($field);
181 6
                if (   empty($linked_class)
182 6
                    && $info['type'] === MGD_TYPE_GUID) {
183
                    // Guid link without class specification, valid for all classes
184 6
                    if (empty($info['target'])) {
185 6
                        $info['target'] = 'guid';
186
                    }
187 4
                } elseif (!self::is_same_class($linked_class, $classname)) {
188
                    // This link points elsewhere
189 2
                    continue;
190
                }
191 6
                $data[$link_type] = $info;
192
            }
193 6
            $cache[$cache_key] = $data;
194
        }
195 94
        return $cache[$cache_key];
196
    }
197
198
    /**
199
     * Creates a QB instance for _get_child_objects_type
200
     */
201 94
    public function _child_objects_type_qb(string $schema_type, object $for_object, bool $deleted)
202
    {
203 94
        $qb = $this->_get_type_qb($schema_type, $deleted);
204 94
        if (!$qb) {
205
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
206
            return false;
207
        }
208
209 94
        $linkfields = $this->_get_link_fields($schema_type, get_class($for_object));
210
211 94
        if (empty($linkfields)) {
212
            debug_add("Class '{$schema_type}' has no valid link properties pointing to class '" . get_class($for_object) . "', this should not happen here", MIDCOM_LOG_ERROR);
213
            return false;
214
        }
215
216 94
        $multiple_links = false;
217 94
        if (count($linkfields) > 1) {
218
            $multiple_links = true;
219
            $qb->begin_group('OR');
220
        }
221
222 94
        foreach ($linkfields as $link_type => $field_data) {
223 94
            $field_target = $field_data['target'];
224 94
            $field_type = $field_data['type'];
225 94
            $field = $field_data['name'];
226
227 94
            if (   !$field_target
228 94
                || !isset($for_object->$field_target)) {
229
                // Why return false ???
230
                return false;
231
            }
232 94
            switch ($field_type) {
233
                case MGD_TYPE_STRING:
234
                case MGD_TYPE_GUID:
235 94
                    $qb->add_constraint($field, '=', (string) $for_object->$field_target);
236 94
                    break;
237
                case MGD_TYPE_INT:
238
                case MGD_TYPE_UINT:
239 91
                    if ($link_type == 'up') {
240 91
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
241 85
                    } elseif ($link_type == 'parent') {
242 85
                        $up_property = midgard_object_class::get_property_up($schema_type);
243 85
                        if (!empty($up_property)) {
244
                            //we only return direct children (otherwise they would turn up twice in recursive queries)
245 85
                            $qb->begin_group('AND');
246 85
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
247 85
                            $qb->add_constraint($up_property, '=', 0);
248 85
                            $qb->end_group();
249
                        } else {
250 85
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
251
                        }
252
                    } else {
253
                        $qb->begin_group('AND');
254
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
255
                        // make sure we don't accidentally find other objects with the same id
256
                        $qb->add_constraint($field . '.guid', '=', (string) $for_object->guid);
257
                        $qb->end_group();
258
                    }
259 91
                    break;
260
                default:
261
                    debug_add("Do not know how to handle linked field '{$field}', has type {$field_type}", MIDCOM_LOG_INFO);
262
263
                    // Why return false ???
264
                    return false;
265
            }
266
        }
267
268 94
        if ($multiple_links) {
269
            $qb->end_group();
270
        }
271
272 94
        return $qb;
273
    }
274
275
    /**
276
     * Get the parent class of the class this reflector was instantiated for
277
     *
278
     * @return string class name (or false if the type has no parent)
279
     */
280 2
    public function get_parent_class() : ?string
281
    {
282 2
        $parent_property = midgard_object_class::get_property_parent($this->mgdschema_class);
0 ignored issues
show
Bug introduced by
It seems like $this->mgdschema_class can also be of type null; however, parameter $classname of midgard_object_class::get_property_parent() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

282
        $parent_property = midgard_object_class::get_property_parent(/** @scrutinizer ignore-type */ $this->mgdschema_class);
Loading history...
283 2
        if (!$parent_property) {
284 2
            return null;
285
        }
286
        $ref = new midgard_reflection_property($this->mgdschema_class);
287
        return $ref->get_link_name($parent_property);
288
    }
289
290
    /**
291
     * Get the child classes of the class this reflector was instantiated for
292
     */
293 95
    public function get_child_classes() : array
294
    {
295 95
        static $child_classes_all = [];
296 95
        if (!isset($child_classes_all[$this->mgdschema_class])) {
297 4
            $child_classes_all[$this->mgdschema_class] = $this->_resolve_child_classes();
298
        }
299 95
        return $child_classes_all[$this->mgdschema_class];
300
    }
301
302
    /**
303
     * Resolve the child classes of the class this reflector was instantiated for, used by get_child_classes()
304
     */
305 4
    private function _resolve_child_classes() : array
306
    {
307 4
        $child_class_exceptions_neverchild = $this->_config->get('child_class_exceptions_neverchild');
308
309
        // Safety against misconfiguration
310 4
        if (!is_array($child_class_exceptions_neverchild)) {
311
            debug_add("config->get('child_class_exceptions_neverchild') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
312
            $child_class_exceptions_neverchild = [];
313
        }
314 4
        $child_classes = [];
315 4
        $types = array_diff(midcom_connection::get_schema_types(), $child_class_exceptions_neverchild);
316 4
        foreach ($types as $schema_type) {
317 4
            $parent_property = midgard_object_class::get_property_parent($schema_type);
318 4
            $up_property = midgard_object_class::get_property_up($schema_type);
319
320 4
            if (   $this->is_link_to_current_class($parent_property, $schema_type)
321 4
                || $this->is_link_to_current_class($up_property, $schema_type)) {
322 4
                $child_classes[] = $schema_type;
323
            }
324
        }
325
326
        //make sure children of the same type come out on top
327 4
        if ($key = array_search($this->mgdschema_class, $child_classes)) {
328 2
            unset($child_classes[$key]);
329 2
            array_unshift($child_classes, $this->mgdschema_class);
330
        }
331 4
        return $child_classes;
332
    }
333
334 4
    private function is_link_to_current_class($property, string $prospect_type) : bool
335
    {
336 4
        if (empty($property)) {
337 4
            return false;
338
        }
339
340 4
        $ref = new midgard_reflection_property($prospect_type);
341 4
        $link_class = $ref->get_link_name($property);
342 4
        if (   empty($link_class)
343 4
            && $ref->get_midgard_type($property) === MGD_TYPE_GUID) {
344 4
            return true;
345
        }
346 4
        return self::is_same_class($link_class, $this->mgdschema_class);
347
    }
348
349
    /**
350
     * Get an array of "root level" classes
351
     */
352 12
    public static function get_root_classes() : array
353
    {
354 12
        static $root_classes = false;
355 12
        if (empty($root_classes)) {
356
            $root_classes = self::_resolve_root_classes();
357
        }
358 12
        return $root_classes;
359
    }
360
361
    /**
362
     * Resolves the "root level" classes, used by get_root_classes()
363
     */
364
    private static function _resolve_root_classes() : array
365
    {
366
        $root_exceptions_notroot = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get('root_class_exceptions_notroot');
367
        // Safety against misconfiguration
368
        if (!is_array($root_exceptions_notroot)) {
369
            debug_add("config->get('root_class_exceptions_notroot') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
370
            $root_exceptions_notroot = [];
371
        }
372
        $root_classes = [];
373
        $types = array_diff(midcom_connection::get_schema_types(), $root_exceptions_notroot);
374
        foreach ($types as $schema_type) {
375
            // Class extensions mapping
376
            $schema_type = self::class_rewrite($schema_type);
377
378
            // Make sure we only add classes once
379
            if (in_array($schema_type, $root_classes)) {
380
                // Already listed
381
                continue;
382
            }
383
384
            if (midgard_object_class::get_property_parent($schema_type)) {
385
                // type has parent set, thus cannot be root type
386
                continue;
387
            }
388
389
            if (!midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type)) {
390
                // Not a MidCOM DBA object, skip
391
                continue;
392
            }
393
394
            $root_classes[] = $schema_type;
395
        }
396
397
        usort($root_classes, 'strnatcmp');
398
        return $root_classes;
399
    }
400
401
    /**
402
     * Add default ("title" and "name") sorts to a QB instance
403
     *
404
     * @param midgard_query_builder $qb QB instance
405
     * @param string $schema_type valid mgdschema class name
406
     */
407 20
    public static function add_schema_sorts_to_qb($qb, string $schema_type)
408
    {
409
        // Sort by "title" and "name" if available
410 20
        $ref = self::get($schema_type);
411 20
        $dummy = new $schema_type();
412 20
        if ($title_property = $ref->get_title_property($dummy)) {
413 20
            $qb->add_order($title_property);
414
        }
415 20
        if ($name_property = $ref->get_name_property($dummy)) {
416 15
            $qb->add_order($name_property);
417
        }
418 20
    }
419
420
    /**
421
     * List object children
422
     *
423
     * @param midcom_core_dbaobject $parent
424
     */
425
    public static function get_tree(midcom_core_dbaobject $parent) : array
426
    {
427
        static $shown_guids = [];
428
        $tree = [];
429
        try {
430
            $children = self::get_child_objects($parent);
431
        } catch (midcom_error $e) {
432
            return $tree;
433
        }
434
435
        foreach ($children as $class => $objects) {
436
            $reflector = parent::get($class);
437
438
            foreach ($objects as $object) {
439
                if (array_key_exists($object->guid, $shown_guids)) {
440
                    //we might see objects twice if they have both up and parent
441
                    continue;
442
                }
443
                $shown_guids[$object->guid] = true;
444
445
                $leaf = [
446
                    'title' => $reflector->get_object_label($object),
447
                    'icon' => $reflector->get_object_icon($object),
448
                    'class' => $class
449
                ];
450
                $grandchildren = self::get_tree($object);
451
                if (!empty($grandchildren)) {
452
                    $leaf['children'] = $grandchildren;
453
                }
454
                $tree[] = $leaf;
455
            }
456
        }
457
        return $tree;
458
    }
459
}
460