Passed
Push — master ( 5e38fb...651208 )
by Andreas
21:49
created

midcom_helper_reflector_tree   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Test Coverage

Coverage 63.64%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 82
eloc 241
c 4
b 0
f 0
dl 0
loc 478
ccs 154
cts 242
cp 0.6364
rs 2

17 Methods

Rating   Name   Duplication   Size   Complexity  
B _root_objects_qb() 0 34 8
A resolve_path() 0 4 1
A _check_permissions() 0 10 4
A get_child_objects() 0 16 4
A resolve_path_parts() 0 24 3
A _get_type_qb() 0 20 4
B _get_link_fields() 0 35 7
C _child_objects_type_qb() 0 72 15
A get_parent_class() 0 8 2
A add_schema_sorts_to_qb() 0 10 3
A _get_child_objects_type() 0 12 2
A is_link_to_current_class() 0 13 4
B get_tree() 0 33 6
A get_child_classes() 0 7 2
B _resolve_root_classes() 0 51 9
A get_root_classes() 0 7 2
A _resolve_child_classes() 0 27 6

How to fix   Complexity   

Complex Class

Complex classes like midcom_helper_reflector_tree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use midcom_helper_reflector_tree, and based on these observations, apply Extract Interface, too.

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(bool $deleted)
20
    {
21 5
        $schema_type = $this->mgdschema_class;
22 5
        $root_classes = self::get_root_classes();
23 5
        if (!in_array($schema_type, $root_classes)) {
24
            debug_add("Type {$schema_type} is not a \"root\" type", MIDCOM_LOG_ERROR);
25
            return false;
26
        }
27
28 5
        $qb = $this->_get_type_qb($schema_type, $deleted);
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

28
        $qb = $this->_get_type_qb(/** @scrutinizer ignore-type */ $schema_type, $deleted);
Loading history...
29 5
        if (!$qb) {
30
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
31
            return false;
32
        }
33
34
        // Figure out constraint to use to get root level objects
35 5
        $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

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

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

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

464
            $children = self::get_child_objects(/** @scrutinizer ignore-type */ $parent);
Loading history...
465
        } catch (midcom_error $e) {
466
            return $tree;
467
        }
468
469
        foreach ($children as $class => $objects) {
470
            $reflector = parent::get($class);
471
472
            foreach ($objects as $object) {
473
                if (array_key_exists($object->guid, $shown_guids)) {
474
                    //we might see objects twice if they have both up and parent
475
                    continue;
476
                }
477
                $shown_guids[$object->guid] = true;
478
479
                $leaf = [
480
                    'title' => $reflector->get_object_label($object),
481
                    'icon' => $reflector->get_object_icon($object),
482
                    'class' => $class
483
                ];
484
                $grandchildren = self::get_tree($object);
485
                if (!empty($grandchildren)) {
486
                    $leaf['children'] = $grandchildren;
487
                }
488
                $tree[] = $leaf;
489
            }
490
        }
491
        return $tree;
492
    }
493
}
494