Passed
Push — master ( 35700c...56e7b5 )
by Andreas
16:57
created

midcom_helper_reflector_tree::resolve_path_parts()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 15
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 24
ccs 16
cts 16
cp 1
crap 3
rs 9.7666
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 6
    public function _root_objects_qb($deleted)
20
    {
21 6
        $schema_type = $this->mgdschema_class;
22 6
        $root_classes = self::get_root_classes();
23 6
        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 6
        $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 6
        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 6
        $upfield = midgard_object_class::get_property_up($schema_type);
36 6
        if (!empty($upfield)) {
37 6
            $uptype = $this->_mgd_reflector->get_midgard_type($upfield);
38 6
            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 6
                    $qb->add_constraint($upfield, '=', 0);
46 6
                    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 6
        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, $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 = self::get_parent($object);
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 = self::get_parent($parent);
92
        }
93
94 2
        $cache[$object->guid] = array_reverse($ret);
95 2
        return $cache[$object->guid];
96
    }
97
98
    /**
99
     * Get the parent object of given object
100
     *
101
     * Tries to utilize MidCOM DBA features first but can fallback on pure MgdSchema
102
     * as necessary
103
     *
104
     * NOTE: since this might fall back to pure MgdSchema never trust that MidCOM DBA features
105
     * are available, check for is_callable/method_exists first !
106
     *
107
     * @param midgard\portable\api\mgdobject $object the object to get parent for
108
     */
109 90
    public static function get_parent($object)
110
    {
111 90
        if (method_exists($object, 'get_parent')) {
112
            /**
113
             * The object might have valid reasons for returning empty value here, but we can't know if it's
114
             * because it's valid or because the get_parent* methods have not been overridden in the actually
115
             * used class
116
             */
117 90
            return $object->get_parent();
118
        }
119
120
        return false;
121
    }
122
123 20
    private static function _check_permissions(bool $deleted) : bool
124
    {
125
        // PONDER: Check for some generic user privilege instead  ??
126 20
        if (   $deleted
127 20
            && !midcom_connection::is_admin()
128 20
            && !midcom::get()->auth->is_component_sudo()) {
129
            debug_add('Non-admins are not allowed to list deleted objects', MIDCOM_LOG_ERROR);
130
            return false;
131
        }
132 20
        return true;
133
    }
134
135
    /**
136
     * Get children of given object
137
     *
138
     * @param midgard\portable\api\mgdobject $object object to get children for
139
     * @param boolean $deleted whether to get (only) deleted or not-deleted objects
140
     * @return array multidimensional array (keyed by classname) of objects or false on failure
141
     */
142 20
    public static function get_child_objects($object, $deleted = false)
143
    {
144 20
        if (!self::_check_permissions($deleted)) {
145
            return false;
146
        }
147 20
        $resolver = new self($object);
148 20
        $child_classes = $resolver->get_child_classes();
149 20
        if (!$child_classes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $child_classes 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...
150
            return false;
151
        }
152
153 20
        $child_objects = [];
154 20
        foreach ($child_classes as $schema_type) {
155 20
            $type_children = $resolver->_get_child_objects_type($schema_type, $object, $deleted);
156
            // PONDER: check for boolean false as result ??
157 20
            if (empty($type_children)) {
158 20
                continue;
159
            }
160 1
            $child_objects[$schema_type] = $type_children;
161
        }
162 20
        return $child_objects;
163
    }
164
165 96
    private function _get_type_qb(string $schema_type, bool $deleted)
166
    {
167 96
        if (empty($schema_type)) {
168
            debug_add('Passed schema_type argument is empty, this is fatal', MIDCOM_LOG_ERROR);
169
            return false;
170
        }
171 96
        if ($deleted) {
172
            $qb = new midgard_query_builder($schema_type);
173
            $qb->include_deleted();
174
            $qb->add_constraint('metadata.deleted', '<>', 0);
175
            return $qb;
176
        }
177
        // Figure correct MidCOM DBA class to use and get midcom QB
178 96
        $midcom_dba_classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type);
179 96
        if (empty($midcom_dba_classname)) {
180
            debug_add("MidCOM DBA does not know how to handle {$schema_type}", MIDCOM_LOG_ERROR);
181
            return false;
182
        }
183
184 96
        return call_user_func([$midcom_dba_classname, 'new_query_builder']);
185
    }
186
187
    /**
188
     * Figure out constraint(s) to use to get child objects
189
     */
190 94
    private function _get_link_fields(string $schema_type, $for_object) : array
191
    {
192 94
        static $cache = [];
193 94
        $cache_key = $schema_type . '-' . get_class($for_object);
194 94
        if (empty($cache[$cache_key])) {
195 7
            $ref = new midgard_reflection_property($schema_type);
196
197
            $linkfields = [
198 7
                'up' => midgard_object_class::get_property_up($schema_type),
199 7
                'parent' => midgard_object_class::get_property_parent($schema_type)
200
            ];
201 7
            $linkfields = array_filter($linkfields);
202 7
            $data = [];
203 7
            foreach ($linkfields as $link_type => $field) {
204
                $info = [
205 7
                    'name' => $field,
206 7
                    'type' => $ref->get_midgard_type($field),
207 7
                    'target' => $ref->get_link_target($field)
208
                ];
209 7
                $linked_class = $ref->get_link_name($field);
210 7
                if (   empty($linked_class)
211 7
                    && $info['type'] === MGD_TYPE_GUID) {
212
                    // Guid link without class specification, valid for all classes
213 7
                    if (empty($info['target'])) {
214 7
                        $info['target'] = 'guid';
215
                    }
216 5
                } elseif (!self::is_same_class($linked_class, get_class($for_object))) {
217
                    // This link points elsewhere
218 2
                    continue;
219
                }
220 7
                $data[$link_type] = $info;
221
            }
222 7
            $cache[$cache_key] = $data;
223
        }
224 94
        return $cache[$cache_key];
225
    }
226
227
    /**
228
     * Creates a QB instance for _get_child_objects_type
229
     */
230 94
    public function _child_objects_type_qb($schema_type, $for_object, $deleted)
231
    {
232 94
        if (!is_object($for_object)) {
233
            debug_add('Passed for_object argument is not object, this is fatal', MIDCOM_LOG_ERROR);
234
            return false;
235
        }
236 94
        $qb = $this->_get_type_qb($schema_type, $deleted);
237 94
        if (!$qb) {
238
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
239
            return false;
240
        }
241
242 94
        $linkfields = $this->_get_link_fields($schema_type, $for_object);
243
244 94
        if (empty($linkfields)) {
245
            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);
246
            return false;
247
        }
248
249 94
        $multiple_links = false;
250 94
        if (count($linkfields) > 1) {
251
            $multiple_links = true;
252
            $qb->begin_group('OR');
253
        }
254
255 94
        foreach ($linkfields as $link_type => $field_data) {
256 94
            $field_target = $field_data['target'];
257 94
            $field_type = $field_data['type'];
258 94
            $field = $field_data['name'];
259
260 94
            if (   !$field_target
261 94
                || !isset($for_object->$field_target)) {
262
                // Why return false ???
263
                return false;
264
            }
265 94
            switch ($field_type) {
266
                case MGD_TYPE_STRING:
267
                case MGD_TYPE_GUID:
268 94
                    $qb->add_constraint($field, '=', (string) $for_object->$field_target);
269 94
                    break;
270
                case MGD_TYPE_INT:
271
                case MGD_TYPE_UINT:
272 91
                    if ($link_type == 'up') {
273 91
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
274 85
                    } elseif ($link_type == 'parent') {
275 85
                        $up_property = midgard_object_class::get_property_up($schema_type);
276 85
                        if (!empty($up_property)) {
277
                            //we only return direct children (otherwise they would turn up twice in recursive queries)
278 85
                            $qb->begin_group('AND');
279 85
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
280 85
                            $qb->add_constraint($up_property, '=', 0);
281 85
                            $qb->end_group();
282
                        } else {
283 85
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
284
                        }
285
                    } else {
286
                        $qb->begin_group('AND');
287
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
288
                        // make sure we don't accidentally find other objects with the same id
289
                        $qb->add_constraint($field . '.guid', '=', (string) $for_object->guid);
290
                        $qb->end_group();
291
                    }
292 91
                    break;
293
                default:
294
                    debug_add("Do not know how to handle linked field '{$field}', has type {$field_type}", MIDCOM_LOG_INFO);
295
296
                    // Why return false ???
297
                    return false;
298
            }
299
        }
300
301 94
        if ($multiple_links) {
302
            $qb->end_group();
303
        }
304
305 94
        return $qb;
306
    }
307
308
    /**
309
     * Used by get_child_objects
310
     *
311
     * @return array of objects
312
     */
313 20
    public function _get_child_objects_type($schema_type, $for_object, $deleted)
314
    {
315 20
        $qb = $this->_child_objects_type_qb($schema_type, $for_object, $deleted);
316 20
        if (!$qb) {
317
            debug_add('Could not get QB instance', MIDCOM_LOG_ERROR);
318
            return false;
319
        }
320
321
        // Sort by title and name if available
322 20
        self::add_schema_sorts_to_qb($qb, $schema_type);
323
324 20
        return $qb->execute();
325
    }
326
327
    /**
328
     * Get the parent class of the class this reflector was instantiated for
329
     *
330
     * @return string class name (or false if the type has no parent)
331
     */
332 2
    public function get_parent_class()
333
    {
334 2
        $parent_property = midgard_object_class::get_property_parent($this->mgdschema_class);
335 2
        if (!$parent_property) {
336 2
            return false;
337
        }
338
        $ref = new midgard_reflection_property($this->mgdschema_class);
339
        return $ref->get_link_name($parent_property);
340
    }
341
342
    /**
343
     * Get the child classes of the class this reflector was instantiated for
344
     */
345 95
    public function get_child_classes() : array
346
    {
347 95
        static $child_classes_all = [];
348 95
        if (!isset($child_classes_all[$this->mgdschema_class])) {
349 5
            $child_classes_all[$this->mgdschema_class] = $this->_resolve_child_classes();
350
        }
351 95
        return $child_classes_all[$this->mgdschema_class];
352
    }
353
354
    /**
355
     * Resolve the child classes of the class this reflector was instantiated for, used by get_child_classes()
356
     */
357 5
    private function _resolve_child_classes() : array
358
    {
359 5
        $child_class_exceptions_neverchild = $this->_config->get('child_class_exceptions_neverchild');
360
361
        // Safety against misconfiguration
362 5
        if (!is_array($child_class_exceptions_neverchild)) {
363
            debug_add("config->get('child_class_exceptions_neverchild') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
364
            $child_class_exceptions_neverchild = [];
365
        }
366 5
        $child_classes = [];
367 5
        $types = array_diff(midcom_connection::get_schema_types(), $child_class_exceptions_neverchild);
368 5
        foreach ($types as $schema_type) {
369 5
            $parent_property = midgard_object_class::get_property_parent($schema_type);
370 5
            $up_property = midgard_object_class::get_property_up($schema_type);
371
372 5
            if (   $this->is_link_to_current_class($parent_property, $schema_type)
373 5
                || $this->is_link_to_current_class($up_property, $schema_type)) {
374 5
                $child_classes[] = $schema_type;
375
            }
376
        }
377
378
        //make sure children of the same type come out on top
379 5
        if ($key = array_search($this->mgdschema_class, $child_classes)) {
380
            unset($child_classes[$key]);
381
            array_unshift($child_classes, $this->mgdschema_class);
382
        }
383 5
        return $child_classes;
384
    }
385
386 5
    private function is_link_to_current_class($property, string $prospect_type) : bool
387
    {
388 5
        if (empty($property)) {
389 5
            return false;
390
        }
391
392 5
        $ref = new midgard_reflection_property($prospect_type);
393 5
        $link_class = $ref->get_link_name($property);
394 5
        if (   empty($link_class)
395 5
            && $ref->get_midgard_type($property) === MGD_TYPE_GUID) {
396 5
            return true;
397
        }
398 5
        return self::is_same_class($link_class, $this->mgdschema_class);
399
    }
400
401
    /**
402
     * Get an array of "root level" classes
403
     */
404 13
    public static function get_root_classes() : array
405
    {
406 13
        static $root_classes = false;
407 13
        if (empty($root_classes)) {
408 1
            $root_classes = self::_resolve_root_classes();
409
        }
410 13
        return $root_classes;
411
    }
412
413
    /**
414
     * Resolves the "root level" classes, used by get_root_classes()
415
     */
416 1
    private static function _resolve_root_classes() : array
417
    {
418 1
        $root_exceptions_notroot = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get('root_class_exceptions_notroot');
419
        // Safety against misconfiguration
420 1
        if (!is_array($root_exceptions_notroot)) {
421
            debug_add("config->get('root_class_exceptions_notroot') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
422
            $root_exceptions_notroot = [];
423
        }
424 1
        $root_classes = [];
425 1
        $types = array_diff(midcom_connection::get_schema_types(), $root_exceptions_notroot);
426 1
        foreach ($types as $schema_type) {
427
            // Class extensions mapping
428 1
            $schema_type = self::class_rewrite($schema_type);
429
430
            // Make sure we only add classes once
431 1
            if (in_array($schema_type, $root_classes)) {
432
                // Already listed
433
                continue;
434
            }
435
436 1
            if (midgard_object_class::get_property_parent($schema_type)) {
437
                // type has parent set, thus cannot be root type
438 1
                continue;
439
            }
440
441 1
            if (!midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type)) {
442
                // Not a MidCOM DBA object, skip
443
                continue;
444
            }
445
446 1
            $root_classes[] = $schema_type;
447
        }
448
449 1
        $root_exceptions_forceroot = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get('root_class_exceptions_forceroot');
450
        // Safety against misconfiguration
451 1
        if (!is_array($root_exceptions_forceroot)) {
452
            debug_add("config->get('root_class_exceptions_forceroot') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
453
            $root_exceptions_forceroot = [];
454
        }
455 1
        $root_exceptions_forceroot = array_diff($root_exceptions_forceroot, $root_classes);
456 1
        foreach ($root_exceptions_forceroot as $schema_type) {
457
            if (!class_exists($schema_type)) {
458
                // Not a valid class
459
                debug_add("Type {$schema_type} has been listed to always be root class, but the class does not exist", MIDCOM_LOG_WARN);
460
                continue;
461
            }
462
            $root_classes[] = $schema_type;
463
        }
464
465 1
        usort($root_classes, 'strnatcmp');
466 1
        return $root_classes;
467
    }
468
469
    /**
470
     * Add default ("title" and "name") sorts to a QB instance
471
     *
472
     * @param midgard_query_builder $qb QB instance
473
     * @param string $schema_type valid mgdschema class name
474
     */
475 20
    public static function add_schema_sorts_to_qb($qb, $schema_type)
476
    {
477
        // Sort by "title" and "name" if available
478 20
        $ref = self::get($schema_type);
479 20
        $dummy = new $schema_type();
480 20
        if ($title_property = $ref->get_title_property($dummy)) {
481 20
            $qb->add_order($title_property);
482
        }
483 20
        if ($name_property = $ref->get_name_property($dummy)) {
484 15
            $qb->add_order($name_property);
485
        }
486 20
    }
487
488
    /**
489
     * List object children
490
     *
491
     * @param midcom_core_dbaobject $parent
492
     */
493
    public static function get_tree(midcom_core_dbaobject $parent) : array
494
    {
495
        static $shown_guids = [];
496
        $tree = [];
497
        try {
498
            $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

498
            $children = self::get_child_objects(/** @scrutinizer ignore-type */ $parent);
Loading history...
499
        } catch (midcom_error $e) {
500
            return $tree;
501
        }
502
503
        foreach ($children as $class => $objects) {
504
            $reflector = parent::get($class);
505
506
            foreach ($objects as $object) {
507
                if (array_key_exists($object->guid, $shown_guids)) {
508
                    //we might see objects twice if they have both up and parent
509
                    continue;
510
                }
511
                $shown_guids[$object->guid] = true;
512
513
                $leaf = [
514
                    'title' => $reflector->get_object_label($object),
515
                    'icon' => $reflector->get_object_icon($object),
516
                    'class' => $class
517
                ];
518
                $grandchildren = self::get_tree($object);
519
                if (!empty($grandchildren)) {
520
                    $leaf['children'] = $grandchildren;
521
                }
522
                $tree[] = $leaf;
523
            }
524
        }
525
        return $tree;
526
    }
527
}
528