Passed
Push — master ( 598445...fef947 )
by Andreas
11:12
created

midgard_admin_asgard_navigation   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Test Coverage

Coverage 26.96%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 204
dl 0
loc 388
ccs 55
cts 204
cp 0.2696
rs 2.56
c 2
b 1
f 0
wmc 73

13 Methods

Rating   Name   Duplication   Size   Complexity  
B draw() 0 49 6
A _process_root_types() 0 34 6
B __construct() 0 25 8
A _draw_plugins() 0 24 5
A _is_collapsed() 0 4 2
B get_css_classes() 0 20 7
A _draw_element() 0 28 5
A _is_selected() 0 8 3
B _list_root_elements() 0 33 7
C _list_child_elements() 0 48 12
A _draw_type_list() 0 21 3
B _draw_collapsed_element() 0 22 7
A _draw_select_navigation() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like midgard_admin_asgard_navigation 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 midgard_admin_asgard_navigation, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package midgard.admin.asgard
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
 * Navigation class for Asgard
11
 *
12
 * @package midgard.admin.asgard
13
 */
14
class midgard_admin_asgard_navigation
15
{
16
    use midcom_baseclasses_components_base;
0 ignored issues
show
introduced by
The trait midcom_baseclasses_components_base requires some properties which are not provided by midgard_admin_asgard_navigation: $i18n, $head
Loading history...
17
18
    public array $root_types = [];
19
20
    /**
21
     * @var midgard\portable\api\mgdobject
22
     */
23
    protected $_object;
24
25
    /**
26
     * Object path to the current object.
27
     */
28
    private array $_object_path = [];
29
30
    private array $_request_data = [];
31
    private array $expanded_root_types = [];
32
    protected array $shown_objects = [];
33
34 2
    public function __construct(?object $object, array &$request_data)
35
    {
36 2
        $this->_component = 'midgard.admin.asgard';
37
38 2
        $this->_object = $object;
39 2
        $this->_request_data =& $request_data;
40
41 2
        $this->root_types = midcom_helper_reflector_tree::get_root_classes();
42
43 2
        if (array_key_exists('current_type', $request_data)) {
44
            $expanded_type = $request_data['current_type'];
45
            if (!in_array($expanded_type, $this->root_types)) {
46
                $expanded_type = midcom_helper_reflector_tree::get($expanded_type)->get_parent_class();
47
            }
48
            $this->expanded_root_types[] = $expanded_type;
49 2
        } elseif (isset($this->_object)) {
50 2
            $this->_object_path = array_column(midcom_helper_reflector_tree::resolve_path_parts($object), 'object');
51
52
            // we go through the path bottom up and show the first root type we find
53 2
            foreach (array_reverse($this->_object_path) as $node) {
54 2
                foreach ($this->root_types as $root_type) {
55 2
                    if (   $node instanceof $root_type
56 2
                        || midcom_helper_reflector::is_same_class($root_type, $node->__midcom_class_name__)) {
57 2
                        $this->expanded_root_types[] = $root_type;
58 2
                        break;
59
                    }
60
                }
61
            }
62
        }
63
    }
64
65
    protected function _is_collapsed(string $type, int $total) : bool
66
    {
67
        return (   $total > $this->_config->get('max_navigation_entries')
68
                && empty($_GET['show_all_' . $type]));
69
    }
70
71 2
    protected function _list_child_elements(object $object, int $level = 0)
72
    {
73 2
        if ($level > 25) {
74
            debug_add('Recursion level 25 exceeded, aborting', MIDCOM_LOG_ERROR);
75
            return;
76
        }
77 2
        $ref = midcom_helper_reflector_tree::get($object);
78
79 2
        $child_types = [];
80 2
        foreach ($ref->get_child_classes() as $class) {
81 2
            $qb = $ref->_child_objects_type_qb($class, $object, false);
82
83 2
            if (   !$qb
84 2
                || !($count = $qb->count_unchecked())) {
85 2
                continue;
86
            }
87 1
            midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $class);
88 1
            if ($this->_is_collapsed($class, $count)) {
89
                $qb->set_limit($this->_config->get('max_navigation_entries'));
90
            }
91 1
            $child_types[$class] = ['total' => $count, 'qb' => $qb];
92
        }
93
94 2
        if (!empty($child_types)) {
95 1
            echo "<ul>\n";
96 1
            foreach ($child_types as $type => $data) {
97 1
                $children = $data['qb']->execute();
98 1
                $label_mapping = [];
99 1
                foreach ($children as $i => $child) {
100 1
                    if (isset($this->shown_objects[$child->guid])) {
101
                        continue;
102
                    }
103
104 1
                    $ref = midcom_helper_reflector_tree::get($child);
105 1
                    $label_mapping[$i] = htmlspecialchars($ref->get_object_label($child));
0 ignored issues
show
Bug introduced by
It seems like $ref->get_object_label($child) can also be of type null; however, parameter $string of htmlspecialchars() 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

105
                    $label_mapping[$i] = htmlspecialchars(/** @scrutinizer ignore-type */ $ref->get_object_label($child));
Loading history...
106
                }
107
108 1
                asort($label_mapping);
109
110 1
                foreach ($label_mapping as $index => $label) {
111 1
                    $child = $children[$index];
112 1
                    $this->_draw_element($child, $label, $level);
113
                }
114 1
                if ($this->_is_collapsed($type, $data['total'])) {
115
                    $this->_draw_collapsed_element($level, $type, $data['total']);
116
                }
117
            }
118 1
            echo "</ul>\n";
119
        }
120
    }
121
122
    /**
123
     * Renders the given root objects to HTML and calls _list_child_elements()
124
     */
125
    private function _list_root_elements(midcom_helper_reflector_tree $ref)
126
    {
127
        $qb = $ref->_root_objects_qb();
128
129
        if (   !$qb
130
            || !($total = $qb->count_unchecked())) {
131
            return;
132
        }
133
        midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $ref->mgdschema_class);
134
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
135
            $qb->set_limit($this->_config->get('max_navigation_entries'));
136
        }
137
138
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
139
140
        $root_objects = $qb->execute();
141
142
        $label_mapping = [];
143
        foreach ($root_objects as $i => $object) {
144
            $label_mapping[$i] = htmlspecialchars($ref->get_object_label($object));
0 ignored issues
show
Bug introduced by
It seems like $ref->get_object_label($object) can also be of type null; however, parameter $string of htmlspecialchars() 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

144
            $label_mapping[$i] = htmlspecialchars(/** @scrutinizer ignore-type */ $ref->get_object_label($object));
Loading history...
145
        }
146
147
        asort($label_mapping);
148
        $autoexpand = (count($root_objects) == 1);
149
        foreach ($label_mapping as $index => $label) {
150
            $object = $root_objects[$index];
151
            $this->_draw_element($object, $label, 1, $autoexpand);
152
        }
153
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
154
            $this->_draw_collapsed_element(0, $ref->mgdschema_class, $total);
155
        }
156
157
        echo "</ul>\n";
158
    }
159
160
    private function _draw_collapsed_element(int $level, string $type, int $total)
161
    {
162
        $ref = midcom_helper_reflector::get($type);
163
        if (!empty($this->_object_path[$level])) {
164
            if ($this->_object_path[$level]->__mgdschema_class_name__ == $type) {
165
                $object = $this->_object_path[$level];
166
            } elseif ($level == 0) {
167
                // this is the case where our object has parents, but we're in its type view directly
168
                foreach ($this->_object_path as $candidate) {
169
                    if ($candidate->__mgdschema_class_name__ == $type) {
170
                        $object = $candidate;
171
                        break;
172
                    }
173
                }
174
            }
175
            if (!empty($object)) {
176
                $label = htmlspecialchars($ref->get_object_label($object));
0 ignored issues
show
Bug introduced by
It seems like $ref->get_object_label($object) can also be of type null; however, parameter $string of htmlspecialchars() 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

176
                $label = htmlspecialchars(/** @scrutinizer ignore-type */ $ref->get_object_label($object));
Loading history...
177
                $this->_draw_element($object, $label, $level);
178
            }
179
        }
180
        $icon = midcom_helper_reflector::get_object_icon(new $type);
181
        echo '<li><a class="expand-type-children" href="?show_all_' . $type . '=1">' . $icon . ' ' . sprintf($this->_l10n->get('show all %s %s entries'), $total, $ref->get_class_label()) . '</a></li>';
182
    }
183
184
    protected function _draw_element(object $object, string $label, int $level, bool $autoexpand = false)
185
    {
186
        $ref = midcom_helper_reflector_tree::get($object);
187
188
        $selected = $this->_is_selected($object);
189
        $css_class = $this->get_css_classes($object, $ref->mgdschema_class);
190
191
        $mode = $this->_request_data['default_mode'];
192
        if (str_contains($css_class, 'readonly')) {
193
            $mode = 'view';
194
        }
195
196
        $this->shown_objects[$object->guid] = true;
197
198
        echo "    <li class=\"{$css_class}\">";
199
200
        $icon = $ref->get_object_icon($object);
201
202
        if (trim($label) == '') {
203
            $label = $ref->get_class_label() . ' #' . $object->id;
204
        }
205
206
        echo "<a href=\"" . midcom_connection::get_url('self') . "__mfa/asgard/object/{$mode}/{$object->guid}/\" title=\"GUID: {$object->guid}, ID: {$object->id}\">{$icon}{$label}</a>\n";
207
        if (   $selected
208
            || $autoexpand) {
209
            $this->_list_child_elements($object, $level + 1);
210
        }
211
        echo "    </li>\n";
212
    }
213
214
    private function _draw_plugins()
215
    {
216
        $customdata = midcom::get()->componentloader->get_all_manifest_customdata('asgard_plugin');
217
        foreach ($customdata as $component => $plugin_config) {
218
            $this->_request_data['section_url'] = midcom_connection::get_url('self') . "__mfa/asgard_{$component}/";
219
            $this->_request_data['section_name'] = $this->_i18n->get_string($component, $component);
220
            $class = $plugin_config['class'];
221
222
            if (!midcom::get()->auth->can_user_do("{$component}:access", class: $class)) {
223
                // Disabled plugin
224
                continue;
225
            }
226
227
            if (   method_exists($class, 'navigation')
228
                && ($this->_request_data['plugin_name'] == "asgard_{$component}")) {
229
                $this->_request_data['expanded'] = true;
230
                midcom_show_style('midgard_admin_asgard_navigation_section_header');
231
                $class::navigation();
232
            } else {
233
                $this->_request_data['expanded'] = false;
234
                midcom_show_style('midgard_admin_asgard_navigation_section_header');
235
            }
236
237
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
238
        }
239
    }
240
241 1
    private function _is_selected(object $object) : bool
242
    {
243 1
        foreach ($this->_object_path as $path_object) {
244 1
            if ($object->guid == $path_object->guid) {
245
                return true;
246
            }
247
        }
248 1
        return false;
249
    }
250
251 1
    protected function get_css_classes(object $object, string $mgdschema_class) : string
252
    {
253 1
        $css_class = get_class($object) . " {$mgdschema_class}";
254
255
        // Populate common properties
256 1
        $css_class = midcom::get()->metadata->get_object_classes($object, $css_class);
257
258 1
        if ($this->_is_selected($object)) {
259
            $css_class .= ' selected';
260
        }
261 1
        if (   is_object($this->_object)
262 1
            && (   $object->guid == $this->_object->guid
263 1
                || (   $this->_object instanceof midcom_db_parameter
264 1
                    && $object->guid == $this->_object->parentguid))) {
265
            $css_class .= ' current';
266
        }
267 1
        if ( !$object->can_do('midgard:update')) {
268
            $css_class .= ' readonly';
269
        }
270 1
        return $css_class;
271
    }
272
273
    /**
274
     * Apply visibility restrictions from various sources
275
     *
276
     * @return array Alphabetically sorted list of class => title pairs
277
     */
278
    private function _process_root_types() : array
279
    {
280
        // Get the inclusion/exclusion model
281
        $exclude = midgard_admin_asgard_plugin::get_preference('midgard_types_model') == 'exclude';
282
283
        $label_mapping = midgard_admin_asgard_plugin::get_root_classes();
284
285
        // Get the types that might have special display conditions
286
        // @TODO: Should this just include to the configuration selection, although it would break the consistency
287
        // of other similar preference sets, which simply override the global settings?
288
        if (   ($selected = midgard_admin_asgard_plugin::get_preference('midgard_types'))
289
            && preg_match_all('/\|([a-z0-9\.\-_]+)/', $selected, $regs)) {
290
            $types = array_flip($regs[1]);
291
            if ($exclude) {
292
                $label_mapping = array_diff_key($label_mapping, $types);
293
            } else {
294
                $label_mapping = array_intersect_key($label_mapping, $types);
295
            }
296
        }
297
298
        // Get the possible regular expression
299
        if ($regexp = midgard_admin_asgard_plugin::get_preference('midgard_types_regexp')) {
300
            // "Convert" quickly to PERL regular expression
301
            if (!preg_match('/^[\/|]/', $regexp)) {
302
                $regexp = "/{$regexp}/";
303
            }
304
305
            // If the regular expression has been set, check which types should be shown
306
            $label_mapping = array_filter($label_mapping, function ($root_type) use ($regexp, $exclude) {
307
                return preg_match($regexp, $root_type) == $exclude;
308
            }, ARRAY_FILTER_USE_KEY);
309
        }
310
311
        return $label_mapping;
312
    }
313
314
    public function draw()
315
    {
316
        $this->_request_data['chapter_name'] = midcom::get()->config->get('midcom_site_title');
317
        midcom_show_style('midgard_admin_asgard_navigation_chapter');
318
319
        $this->_draw_plugins();
320
321
        if (!midcom::get()->auth->can_user_do('midgard.admin.asgard:manage_objects', class: 'midgard_admin_asgard_plugin')) {
322
            return;
323
        }
324
325
        $label_mapping = $this->_process_root_types();
326
327
        $expanded_types = array_intersect(array_keys($label_mapping), $this->expanded_root_types);
328
329
        /*
330
         * Use a dropdown for displaying the navigation if at least one type is expanded
331
         * and the user has the corresponding preference set. That way, you expanded types
332
         * can take up the maximum available space while all types are still accessible with one
333
         * click if nothing is expanded
334
         */
335
        $types_shown = false;
336
        if (    !empty($expanded_types)
337
             && midgard_admin_asgard_plugin::get_preference('navigation_type') === 'dropdown') {
338
            $this->_draw_select_navigation();
339
            $types_shown = true;
340
        }
341
342
        foreach ($expanded_types as $root_type) {
343
            $this->_request_data['section_url'] = midcom_connection::get_url('self') . "__mfa/asgard/{$root_type}";
344
            $this->_request_data['section_name'] = $label_mapping[$root_type];
345
            $this->_request_data['expanded'] = true;
346
            midcom_show_style('midgard_admin_asgard_navigation_section_header');
347
            $ref = midcom_helper_reflector_tree::get($root_type);
348
            $this->_list_root_elements($ref);
349
350
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
351
        }
352
353
        if (!$types_shown) {
354
            $this->_request_data['section_name'] = $this->_l10n->get('midgard objects');
355
            $this->_request_data['section_url'] = null;
356
            $this->_request_data['expanded'] = true;
357
            midcom_show_style('midgard_admin_asgard_navigation_section_header');
358
            $collapsed_types = array_diff_key($label_mapping, array_flip($expanded_types));
359
360
            $this->_draw_type_list($collapsed_types);
361
362
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
363
        }
364
    }
365
366
    private function _draw_type_list(array $types)
367
    {
368
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
369
370
        foreach ($types as $type => $label) {
371
            $url = midcom_connection::get_url('self') . "__mfa/asgard/{$type}/";
372
            echo "    <li class=\"mgdschema-type\">";
373
374
            if ($dbaclass = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($type)) {
375
                $object = new $dbaclass;
376
            } else {
377
                $object = new $type;
378
            }
379
            $icon = midcom_helper_reflector::get_object_icon($object);
380
381
            echo "<a href=\"" . $url . "\" title=\"{$label}\">{$icon}{$label}</a>\n";
382
383
            echo "    </li>\n";
384
        }
385
386
        echo "</ul>\n";
387
    }
388
389
    private function _draw_select_navigation()
390
    {
391
        if (!empty($this->_object_path)) {
392
            $this->_request_data['root_object'] = $this->_object_path[0];
393
            $this->_request_data['navigation_type'] = $this->_object_path[0]->__mgdschema_class_name__;
394
        } else {
395
            $this->_request_data['navigation_type'] = $this->expanded_root_types[0] ?? '';
396
        }
397
398
        $this->_request_data['label_mapping'] = midgard_admin_asgard_plugin::get_root_classes();
399
        $this->_request_data['expanded_root_types'] = $this->expanded_root_types;
400
401
        midcom_show_style('midgard_admin_asgard_navigation_sections');
402
    }
403
}
404