Passed
Push — master ( 0edcd9...8997f4 )
by Andreas
57:25
created

midcom_helper_reflector_tree::get_root_classes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 4
cts 5
cp 0.8
crap 2.032
rs 10
c 0
b 0
f 0
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 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
                    '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 94
        return $cache[$cache_key];
195
    }
196
197
    /**
198
     * Creates a QB instance for _get_child_objects_type
199
     */
200 94
    public function _child_objects_type_qb(string $schema_type, object $for_object, bool $deleted)
201
    {
202 94
        $qb = $this->_get_type_qb($schema_type, $deleted);
203 94
        if (!$qb) {
204
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
205
            return false;
206
        }
207
208 94
        $linkfields = $this->_get_link_fields($schema_type, get_class($for_object));
209
210 94
        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 94
        $multiple_links = count($linkfields) > 1;
216 94
        if ($multiple_links) {
217
            $qb->begin_group('OR');
218
        }
219
220 94
        foreach ($linkfields as $link_type => $field_data) {
221 94
            $field_target = $field_data['target'];
222 94
            $field_type = $field_data['type'];
223 94
            $field = $field_data['name'];
224
225 94
            if (   !$field_target
226 94
                || !isset($for_object->$field_target)) {
227
                // Why return false ???
228
                return false;
229
            }
230 94
            switch ($field_type) {
231
                case MGD_TYPE_STRING:
232
                case MGD_TYPE_GUID:
233 94
                    $qb->add_constraint($field, '=', (string) $for_object->$field_target);
234 94
                    break;
235
                case MGD_TYPE_INT:
236
                case MGD_TYPE_UINT:
237 91
                    if ($link_type == 'up') {
238 91
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
239
                    } else {
240 85
                        if (!empty($linkfields['up']['name'])) {
241
                            //we only return direct children (otherwise they would turn up twice in recursive queries)
242
                            $qb->begin_group('AND');
243
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
244
                            $qb->add_constraint($linkfields['up']['name'], '=', 0);
245
                            $qb->end_group();
246
                        } else {
247 85
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
248
                        }
249
                    }
250 91
                    break;
251
                default:
252
                    debug_add("Do not know how to handle linked field '{$field}', has type {$field_type}", MIDCOM_LOG_INFO);
253
254
                    // Why return false ???
255
                    return false;
256
            }
257
        }
258
259 94
        if ($multiple_links) {
260
            $qb->end_group();
261
        }
262
263 94
        return $qb;
264
    }
265
266
    /**
267
     * Get the parent class of the class this reflector was instantiated for
268
     *
269
     * @return string class name (or false if the type has no parent)
270
     */
271 2
    public function get_parent_class() : ?string
272
    {
273 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

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