Passed
Push — master ( 207826...bd5086 )
by Andreas
18:02
created

midcom_helper_reflector_tree   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Test Coverage

Coverage 64%

Importance

Changes 9
Bugs 1 Features 0
Metric Value
eloc 169
c 9
b 1
f 0
dl 0
loc 359
ccs 112
cts 175
cp 0.64
rs 3.52
wmc 61

14 Methods

Rating   Name   Duplication   Size   Complexity  
A resolve_path() 0 4 1
B _root_objects_qb() 0 28 7
A _get_type_qb() 0 20 4
A _check_permissions() 0 10 4
A get_child_objects() 0 23 5
A resolve_path_parts() 0 24 3
B _child_objects_type_qb() 0 39 8
A get_parent_class() 0 8 2
A add_schema_sorts_to_qb() 0 9 3
A get_tree() 0 27 5
A _resolve_root_classes() 0 30 5
A get_child_classes() 0 20 5
B _get_link_fields() 0 32 7
A get_root_classes() 0 7 2

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()
20
    {
21 5
        $schema_type = $this->mgdschema_class;
22
23 5
        $qb = $this->_get_type_qb($schema_type, false);
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)) {
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 21
    private static function _check_permissions(bool $deleted) : bool
93
    {
94
        // PONDER: Check for some generic user privilege instead  ??
95 21
        if (   $deleted
96 21
            && !midcom_connection::is_admin()
97 21
            && !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 21
        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 21
    public static function get_child_objects(object $object, bool $deleted = false) : array
112
    {
113 21
        if (!self::_check_permissions($deleted)) {
114
            return [];
115
        }
116 21
        $resolver = new self($object);
117
118 21
        $child_objects = [];
119 21
        foreach ($resolver->get_child_classes() as $schema_type) {
120 21
            $qb = $resolver->_child_objects_type_qb($schema_type, $object, $deleted);
121 21
            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 21
            self::add_schema_sorts_to_qb($qb, $schema_type);
128
129 21
            if ($type_children = $qb->execute()) {
130 1
                $child_objects[$schema_type] = $type_children;
131
            }
132
        }
133 21
        return $child_objects;
134
    }
135
136 106
    private function _get_type_qb(string $schema_type, bool $deleted)
137
    {
138 106
        if (empty($schema_type)) {
139
            debug_add('Passed schema_type argument is empty, this is fatal', MIDCOM_LOG_ERROR);
140
            return false;
141
        }
142 106
        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 106
        $midcom_dba_classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type);
150 106
        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 106
        return $midcom_dba_classname::new_query_builder();
156
    }
157
158
    /**
159
     * Figure out constraint(s) to use to get child objects
160
     */
161 104
    private function _get_link_fields(string $schema_type, string $target_class) : array
162
    {
163 104
        static $cache = [];
164 104
        $cache_key = $schema_type . '-' . $target_class;
165 104
        if (empty($cache[$cache_key])) {
166 6
            $ref = new midgard_reflection_property($schema_type);
167
168 6
            $linkfields = array_filter([
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
            $data = [];
173 6
            foreach ($linkfields as $link_type => $field) {
174
                $info = [
175 6
                    'name' => $field,
176 6
                    'target' => $ref->get_link_target($field)
177
                ];
178
179 6
                if ($linked_class = $ref->get_link_name($field)) {
180 5
                    if (!self::is_same_class($linked_class, $target_class)) {
181
                        // This link points elsewhere
182 5
                        continue;
183
                    }
184 6
                } elseif ($ref->get_midgard_type($field) === MGD_TYPE_GUID && empty($info['target'])) {
185
                    // Guid link without class specification, valid for all classes
186 6
                    $info['target'] = 'guid';
187
                }
188 6
                $data[$link_type] = $info;
189
            }
190 6
            $cache[$cache_key] = $data;
191
        }
192 104
        return $cache[$cache_key];
193
    }
194
195
    /**
196
     * Creates a QB instance for _get_child_objects_type
197
     */
198 104
    public function _child_objects_type_qb(string $schema_type, object $for_object, bool $deleted)
199
    {
200 104
        $linkfields = $this->_get_link_fields($schema_type, get_class($for_object));
201 104
        if (empty($linkfields)) {
202
            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);
203
            return false;
204
        }
205 104
        $multiple_links = count($linkfields) > 1;
206
207 104
        $qb = $this->_get_type_qb($schema_type, $deleted);
208 104
        if (!$qb) {
209
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
210
            return false;
211
        }
212
213 104
        if ($multiple_links) {
214
            $qb->begin_group('OR');
215
        }
216
217 104
        foreach ($linkfields as $link_type => $field_data) {
218 104
            $field_target = $field_data['target'];
219 104
            $field = $field_data['name'];
220
221 104
            if ($link_type == 'parent' && !empty($linkfields['up']['name'])) {
222
                //we only return direct children (otherwise they would turn up twice in recursive queries)
223
                $qb->begin_group('AND');
224
                    $qb->add_constraint($field, '=', $for_object->$field_target);
225
                    $qb->add_constraint($linkfields['up']['name'], '=', 0);
226
                $qb->end_group();
227
            } else {
228 104
                $qb->add_constraint($field, '=', $for_object->$field_target);
229
            }
230
        }
231
232 104
        if ($multiple_links) {
233
            $qb->end_group();
234
        }
235
236 104
        return $qb;
237
    }
238
239
    /**
240
     * Get the parent class of the class this reflector was instantiated for
241
     *
242
     * @return string class name (or false if the type has no parent)
243
     */
244 2
    public function get_parent_class() : ?string
245
    {
246 2
        $parent_property = midgard_object_class::get_property_parent($this->mgdschema_class);
247 2
        if (!$parent_property) {
248 2
            return null;
249
        }
250
        $ref = new midgard_reflection_property($this->mgdschema_class);
251
        return $ref->get_link_name($parent_property);
252
    }
253
254
    /**
255
     * Get the child classes of the class this reflector was instantiated for
256
     */
257 105
    public function get_child_classes() : array
258
    {
259 105
        static $cache = [];
260 105
        if (!isset($cache[$this->mgdschema_class])) {
261 4
            $cache[$this->mgdschema_class] = [];
262
263 4
            $types = array_diff(midcom_connection::get_schema_types(), $this->_config->get_array('child_class_exceptions_neverchild'));
264 4
            foreach ($types as $schema_type) {
265 4
                if ($this->_get_link_fields($schema_type, $this->mgdschema_class)) {
266 4
                    $cache[$this->mgdschema_class][] = $schema_type;
267
                }
268
            }
269
270
            //make sure children of the same type come out on top
271 4
            if ($key = array_search($this->mgdschema_class, $cache[$this->mgdschema_class])) {
272 2
                unset($cache[$this->mgdschema_class][$key]);
273 2
                array_unshift($cache[$this->mgdschema_class], $this->mgdschema_class);
274
            }
275
        }
276 105
        return $cache[$this->mgdschema_class];
277
    }
278
279
    /**
280
     * Get an array of "root level" classes
281
     */
282 12
    public static function get_root_classes() : array
283
    {
284 12
        static $root_classes = false;
285 12
        if (empty($root_classes)) {
286
            $root_classes = self::_resolve_root_classes();
287
        }
288 12
        return $root_classes;
289
    }
290
291
    /**
292
     * Resolves the "root level" classes, used by get_root_classes()
293
     */
294
    private static function _resolve_root_classes() : array
295
    {
296
        $root_exceptions_notroot = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get_array('root_class_exceptions_notroot');
297
        $root_classes = [];
298
        $types = array_diff(midcom_connection::get_schema_types(), $root_exceptions_notroot);
299
        foreach ($types as $schema_type) {
300
            // Class extensions mapping
301
            $schema_type = self::class_rewrite($schema_type);
302
303
            // Make sure we only add classes once
304
            if (in_array($schema_type, $root_classes)) {
305
                // Already listed
306
                continue;
307
            }
308
309
            if (midgard_object_class::get_property_parent($schema_type)) {
310
                // type has parent set, thus cannot be root type
311
                continue;
312
            }
313
314
            if (!midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type)) {
315
                // Not a MidCOM DBA object, skip
316
                continue;
317
            }
318
319
            $root_classes[] = $schema_type;
320
        }
321
322
        usort($root_classes, 'strnatcmp');
323
        return $root_classes;
324
    }
325
326
    /**
327
     * Add default ("title" and "name") sorts to a QB instance
328
     *
329
     * @param midgard_query_builder $qb QB instance
330
     */
331 21
    public static function add_schema_sorts_to_qb($qb, string $schema_type)
332
    {
333
        // Sort by "title" and "name" if available
334 21
        $dummy = new $schema_type();
335 21
        if ($title_property = self::get_title_property($dummy)) {
336 21
            $qb->add_order($title_property);
337
        }
338 21
        if ($name_property = self::get_name_property($dummy)) {
339 16
            $qb->add_order($name_property);
340
        }
341 21
    }
342
343
    /**
344
     * List object children
345
     */
346
    public static function get_tree(midcom_core_dbaobject $parent) : array
347
    {
348
        static $shown_guids = [];
349
        $tree = [];
350
351
        foreach (self::get_child_objects($parent) as $class => $objects) {
352
            $reflector = parent::get($class);
353
354
            foreach ($objects as $object) {
355
                if (array_key_exists($object->guid, $shown_guids)) {
356
                    //we might see objects twice if they have both up and parent
357
                    continue;
358
                }
359
                $shown_guids[$object->guid] = true;
360
361
                $leaf = [
362
                    'title' => $reflector->get_object_label($object),
363
                    'icon' => $reflector->get_object_icon($object),
364
                    'class' => $class
365
                ];
366
                if ($grandchildren = self::get_tree($object)) {
367
                    $leaf['children'] = $grandchildren;
368
                }
369
                $tree[] = $leaf;
370
            }
371
        }
372
        return $tree;
373
    }
374
}
375