Passed
Push — master ( 40899a...60cef9 )
by Andreas
22:38
created

midgard_admin_asgard_navigation   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 435
Duplicated Lines 0 %

Test Coverage

Coverage 24.37%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 227
c 2
b 0
f 0
dl 0
loc 435
ccs 58
cts 238
cp 0.2437
rs 2
wmc 84

13 Methods

Rating   Name   Duplication   Size   Complexity  
B draw() 0 49 6
A _draw_select_navigation() 0 23 4
D _process_root_types() 0 57 13
B __construct() 0 27 9
A _draw_plugins() 0 24 5
A _is_collapsed() 0 4 2
B get_css_classes() 0 20 7
B _list_root_elements() 0 33 7
A _draw_element() 0 29 5
A _is_selected() 0 8 3
C _list_child_elements() 0 48 12
A _draw_type_list() 0 23 4
B _draw_collapsed_element() 0 22 7

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 extends midcom_baseclasses_components_purecode
15
{
16
    /**
17
     * Root types
18
     *
19
     * @var array
20
     */
21
    public $root_types = [];
22
23
    /**
24
     * Some object
25
     *
26
     * @var midgard\portable\api\mgdobject
27
     */
28
    protected $_object;
29
30
    /**
31
     * Object path to the current object.
32
     *
33
     * @var Array
34
     */
35
    private $_object_path = [];
36
37
    private $_request_data = [];
38
    private $expanded_root_types = [];
39
    protected $shown_objects = [];
40
41 2
    public function __construct($object, &$request_data)
42
    {
43 2
        parent::__construct();
44
45 2
        $this->_object = $object;
46 2
        $this->_request_data =& $request_data;
47
48 2
        $this->root_types = midcom_helper_reflector_tree::get_root_classes();
49
50 2
        if (array_key_exists('current_type', $this->_request_data)) {
51
            $expanded_type = $this->_request_data['current_type'];
52
            if (!in_array($expanded_type, $this->root_types)) {
53
                $expanded_type = midcom_helper_reflector_tree::get($expanded_type)->get_parent_class();
54
            }
55
            $this->expanded_root_types[] = $expanded_type;
56 2
        } elseif (isset($this->_object)) {
57 2
            foreach (midcom_helper_reflector_tree::resolve_path_parts($this->_object) as $part) {
58 2
                $this->_object_path[] = $part['object'];
59
            }
60
61
            // we go through the path bottom up and show the first root type we find
62 2
            foreach (array_reverse($this->_object_path) as $node) {
63 2
                foreach ($this->root_types as $root_type) {
64 2
                    if (    is_a($node, $root_type)
65 2
                        || midcom_helper_reflector::is_same_class($root_type, $node->__midcom_class_name__)) {
66 2
                        $this->expanded_root_types[] = $root_type;
67 2
                        break;
68
                    }
69
                }
70
            }
71
        }
72 2
    }
73
74
    protected function _is_collapsed($type, $total) : bool
75
    {
76
        return (   $total > $this->_config->get('max_navigation_entries')
77
                && empty($_GET['show_all_' . $type]));
78
    }
79
80 2
    protected function _list_child_elements($object, $level = 0)
81
    {
82 2
        if ($level > 25) {
83
            debug_add('Recursion level 25 exceeded, aborting', MIDCOM_LOG_ERROR);
84
            return;
85
        }
86 2
        $ref = midcom_helper_reflector_tree::get($object);
87
88 2
        $child_types = [];
89 2
        foreach ($ref->get_child_classes() as $class) {
90 2
            $qb = $ref->_child_objects_type_qb($class, $object, false);
91
92 2
            if (   !$qb
93 2
                || !($count = $qb->count_unchecked())) {
94 2
                continue;
95
            }
96 1
            midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $class);
97 1
            if ($this->_is_collapsed($class, $count)) {
98
                $qb->set_limit($this->_config->get('max_navigation_entries'));
99
            }
100 1
            $child_types[$class] = ['total' => $count, 'qb' => $qb];
101
        }
102
103 2
        if (!empty($child_types)) {
104 1
            echo "<ul>\n";
105 1
            foreach ($child_types as $type => $data) {
106 1
                $children = $data['qb']->execute();
107 1
                $label_mapping = [];
108 1
                foreach ($children as $i => $child) {
109 1
                    if (isset($this->shown_objects[$child->guid])) {
110
                        continue;
111
                    }
112
113 1
                    $ref = midcom_helper_reflector_tree::get($child);
114 1
                    $label_mapping[$i] = htmlspecialchars($ref->get_object_label($child));
115
                }
116
117 1
                asort($label_mapping);
118
119 1
                foreach ($label_mapping as $index => $label) {
120 1
                    $child = $children[$index];
121 1
                    $this->_draw_element($child, $label, $level);
122
                }
123 1
                if ($this->_is_collapsed($type, $data['total'])) {
124
                    $this->_draw_collapsed_element($level, $type, $data['total']);
125
                }
126
            }
127 1
            echo "</ul>\n";
128
        }
129 2
    }
130
131
    /**
132
     * Renders the given root objects to HTML and calls _list_child_elements()
133
     *
134
     * @param midcom_helper_reflector_tree $ref Reflector singleton
135
     */
136
    private function _list_root_elements(midcom_helper_reflector_tree $ref)
137
    {
138
        $qb = $ref->_root_objects_qb(false);
139
140
        if (   !$qb
141
            || !($total = $qb->count_unchecked())) {
142
            return;
143
        }
144
        midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $ref->mgdschema_class);
145
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
146
            $qb->set_limit($this->_config->get('max_navigation_entries'));
147
        }
148
149
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
150
151
        $root_objects = $qb->execute();
152
153
        $label_mapping = [];
154
        foreach ($root_objects as $i => $object) {
155
            $label_mapping[$i] = htmlspecialchars($ref->get_object_label($object));
156
        }
157
158
        asort($label_mapping);
159
        $autoexpand = (count($root_objects) == 1);
160
        foreach ($label_mapping as $index => $label) {
161
            $object = $root_objects[$index];
162
            $this->_draw_element($object, $label, 1, $autoexpand);
163
        }
164
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
165
            $this->_draw_collapsed_element(0, $ref->mgdschema_class, $total);
0 ignored issues
show
Bug introduced by
It seems like $ref->mgdschema_class can also be of type null; however, parameter $type of midgard_admin_asgard_nav...raw_collapsed_element() 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

165
            $this->_draw_collapsed_element(0, /** @scrutinizer ignore-type */ $ref->mgdschema_class, $total);
Loading history...
166
        }
167
168
        echo "</ul>\n";
169
    }
170
171
    private function _draw_collapsed_element(int $level, string $type, int $total)
172
    {
173
        $ref = midcom_helper_reflector::get($type);
174
        if (!empty($this->_object_path[$level])) {
175
            if ($this->_object_path[$level]->__mgdschema_class_name__ == $type) {
176
                $object = $this->_object_path[$level];
177
            } elseif ($level == 0) {
178
                // this is the case where our object has parents, but we're in its type view directly
179
                foreach ($this->_object_path as $candidate) {
180
                    if ($candidate->__mgdschema_class_name__ == $type) {
181
                        $object = $candidate;
182
                        break;
183
                    }
184
                }
185
            }
186
            if (!empty($object)) {
187
                $label = htmlspecialchars($ref->get_object_label($object));
188
                $this->_draw_element($object, $label, $level);
189
            }
190
        }
191
        $icon = midcom_helper_reflector::get_object_icon(new $type);
192
        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>';
193
    }
194
195
    protected function _draw_element($object, string $label, int $level, bool $autoexpand = false)
196
    {
197
        $ref = midcom_helper_reflector_tree::get($object);
198
199
        $selected = $this->_is_selected($object);
200
        $css_class = $this->get_css_classes($object, $ref->mgdschema_class);
0 ignored issues
show
Bug introduced by
It seems like $ref->mgdschema_class can also be of type null; however, parameter $mgdschema_class of midgard_admin_asgard_navigation::get_css_classes() 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

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