Completed
Push — master ( 0b04ee...e2db31 )
by Andreas
16:58
created

midgard_admin_asgard_navigation   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 26.02%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 210
c 4
b 1
f 0
dl 0
loc 406
ccs 57
cts 219
cp 0.2602
rs 2.24
wmc 77

13 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 25 8
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
B _draw_collapsed_element() 0 22 7
B draw() 0 49 6
A _draw_select_navigation() 0 15 3
B _process_root_types() 0 38 8
A _draw_type_list() 0 23 4

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
            $this->_object_path = array_column(midcom_helper_reflector_tree::resolve_path_parts($this->_object), 'object');
58
59
            // we go through the path bottom up and show the first root type we find
60 2
            foreach (array_reverse($this->_object_path) as $node) {
61 2
                foreach ($this->root_types as $root_type) {
62 2
                    if (   is_a($node, $root_type)
63 2
                        || midcom_helper_reflector::is_same_class($root_type, $node->__midcom_class_name__)) {
64 2
                        $this->expanded_root_types[] = $root_type;
65 2
                        break;
66
                    }
67
                }
68
            }
69
        }
70 2
    }
71
72
    protected function _is_collapsed($type, $total) : bool
73
    {
74
        return (   $total > $this->_config->get('max_navigation_entries')
75
                && empty($_GET['show_all_' . $type]));
76
    }
77
78 2
    protected function _list_child_elements($object, $level = 0)
79
    {
80 2
        if ($level > 25) {
81
            debug_add('Recursion level 25 exceeded, aborting', MIDCOM_LOG_ERROR);
82
            return;
83
        }
84 2
        $ref = midcom_helper_reflector_tree::get($object);
85
86 2
        $child_types = [];
87 2
        foreach ($ref->get_child_classes() as $class) {
88 2
            $qb = $ref->_child_objects_type_qb($class, $object, false);
89
90 2
            if (   !$qb
91 2
                || !($count = $qb->count_unchecked())) {
92 2
                continue;
93
            }
94 1
            midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $class);
95 1
            if ($this->_is_collapsed($class, $count)) {
96
                $qb->set_limit($this->_config->get('max_navigation_entries'));
97
            }
98 1
            $child_types[$class] = ['total' => $count, 'qb' => $qb];
99
        }
100
101 2
        if (!empty($child_types)) {
102 1
            echo "<ul>\n";
103 1
            foreach ($child_types as $type => $data) {
104 1
                $children = $data['qb']->execute();
105 1
                $label_mapping = [];
106 1
                foreach ($children as $i => $child) {
107 1
                    if (isset($this->shown_objects[$child->guid])) {
108
                        continue;
109
                    }
110
111 1
                    $ref = midcom_helper_reflector_tree::get($child);
112 1
                    $label_mapping[$i] = htmlspecialchars($ref->get_object_label($child));
113
                }
114
115 1
                asort($label_mapping);
116
117 1
                foreach ($label_mapping as $index => $label) {
118 1
                    $child = $children[$index];
119 1
                    $this->_draw_element($child, $label, $level);
120
                }
121 1
                if ($this->_is_collapsed($type, $data['total'])) {
122
                    $this->_draw_collapsed_element($level, $type, $data['total']);
123
                }
124
            }
125 1
            echo "</ul>\n";
126
        }
127 2
    }
128
129
    /**
130
     * Renders the given root objects to HTML and calls _list_child_elements()
131
     *
132
     * @param midcom_helper_reflector_tree $ref Reflector singleton
133
     */
134
    private function _list_root_elements(midcom_helper_reflector_tree $ref)
135
    {
136
        $qb = $ref->_root_objects_qb(false);
137
138
        if (   !$qb
139
            || !($total = $qb->count_unchecked())) {
140
            return;
141
        }
142
        midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $ref->mgdschema_class);
143
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
144
            $qb->set_limit($this->_config->get('max_navigation_entries'));
145
        }
146
147
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
148
149
        $root_objects = $qb->execute();
150
151
        $label_mapping = [];
152
        foreach ($root_objects as $i => $object) {
153
            $label_mapping[$i] = htmlspecialchars($ref->get_object_label($object));
154
        }
155
156
        asort($label_mapping);
157
        $autoexpand = (count($root_objects) == 1);
158
        foreach ($label_mapping as $index => $label) {
159
            $object = $root_objects[$index];
160
            $this->_draw_element($object, $label, 1, $autoexpand);
161
        }
162
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
163
            $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

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

198
        $css_class = $this->get_css_classes($object, /** @scrutinizer ignore-type */ $ref->mgdschema_class);
Loading history...
199
200
        $mode = $this->_request_data['default_mode'];
201
        if (str_contains($css_class, 'readonly')) {
202
            $mode = 'view';
203
        }
204
205
        $this->shown_objects[$object->guid] = true;
206
207
        echo "    <li class=\"{$css_class}\">";
208
209
        $icon = $ref->get_object_icon($object);
210
211
        if (trim($label) == '') {
212
            $label = $ref->get_class_label() . ' #' . $object->id;
213
        }
214
        $label = html_entity_decode($label);
215
216
        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";
217
        if (   $selected
218
            || $autoexpand) {
219
            $this->_list_child_elements($object, $level + 1);
220
        }
221
        echo "    </li>\n";
222
    }
223
224
    private function _draw_plugins()
225
    {
226
        $customdata = midcom::get()->componentloader->get_all_manifest_customdata('asgard_plugin');
227
        foreach ($customdata as $component => $plugin_config) {
228
            $this->_request_data['section_url'] = midcom_connection::get_url('self') . "__mfa/asgard_{$component}/";
229
            $this->_request_data['section_name'] = $this->_i18n->get_string($component, $component);
230
            $class = $plugin_config['class'];
231
232
            if (!midcom::get()->auth->can_user_do("{$component}:access", null, $class)) {
233
                // Disabled plugin
234
                continue;
235
            }
236
237
            if (   method_exists($class, 'navigation')
238
                && ($this->_request_data['plugin_name'] == "asgard_{$component}")) {
239
                $this->_request_data['expanded'] = true;
240
                midcom_show_style('midgard_admin_asgard_navigation_section_header');
241
                call_user_func([$class, 'navigation']);
242
            } else {
243
                $this->_request_data['expanded'] = false;
244
                midcom_show_style('midgard_admin_asgard_navigation_section_header');
245
            }
246
247
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
248
        }
249
    }
250
251 1
    private function _is_selected($object) : bool
252
    {
253 1
        foreach ($this->_object_path as $path_object) {
254 1
            if ($object->guid == $path_object->guid) {
255
                return true;
256
            }
257
        }
258 1
        return false;
259
    }
260
261 1
    protected function get_css_classes($object, string $mgdschema_class) : string
262
    {
263 1
        $css_class = get_class($object) . " {$mgdschema_class}";
264
265
        // Populate common properties
266 1
        $css_class = midcom::get()->metadata->get_object_classes($object, $css_class);
267
268 1
        if ($this->_is_selected($object)) {
269
            $css_class .= ' selected';
270
        }
271 1
        if (   is_object($this->_object)
272 1
            && (   $object->guid == $this->_object->guid
273 1
                || (   is_a($this->_object, midcom_db_parameter::class)
274 1
                    && $object->guid == $this->_object->parentguid))) {
275
            $css_class .= ' current';
276
        }
277 1
        if ( !$object->can_do('midgard:update')) {
278
            $css_class .= ' readonly';
279
        }
280 1
        return $css_class;
281
    }
282
283
    /**
284
     * Apply visibility restrictions from various sources
285
     *
286
     * @return array Alphabetically sorted list of class => title pairs
287
     */
288
    private function _process_root_types() : array
289
    {
290
        // Get the types that might have special display conditions
291
        // @TODO: Should this just include to the configuration selection, although it would break the consistency
292
        // of other similar preference sets, which simply override the global settings?
293
        $selected = midgard_admin_asgard_plugin::get_preference('midgard_types') ?: $this->_config->get('midgard_types');
294
295
        // Get the inclusion/exclusion model
296
        $model = midgard_admin_asgard_plugin::get_preference('midgard_types_model') ?: $this->_config->get('midgard_types_model');
297
        $exclude = ($model == 'exclude');
298
299
        $label_mapping = midgard_admin_asgard_plugin::get_root_classes();
300
301
        if (preg_match_all('/\|([a-z0-9\.\-_]+)/', $selected, $regs)) {
302
            $types = array_flip($regs[1]);
303
            if ($exclude) {
304
                $label_mapping = array_diff_key($label_mapping, $types);
305
            } else {
306
                $label_mapping = array_intersect_key($label_mapping, $types);
307
            }
308
        }
309
310
        // Get the possible regular expression
311
        $regexp = midgard_admin_asgard_plugin::get_preference('midgard_types_regexp') ?: $this->_config->get('midgard_types_regexp');
312
313
        // "Convert" quickly to PERL regular expression
314
        if (!preg_match('/^[\/|]/', $regexp)) {
0 ignored issues
show
Bug introduced by
It seems like $regexp can also be of type false; however, parameter $subject of preg_match() 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

314
        if (!preg_match('/^[\/|]/', /** @scrutinizer ignore-type */ $regexp)) {
Loading history...
315
            $regexp = "/{$regexp}/";
316
        }
317
318
        // If the regular expression has been set, check which types should be shown
319
        if ($regexp !== '//') {
320
            $label_mapping = array_filter($label_mapping, function ($root_type) use ($regexp, $exclude) {
321
                return preg_match($regexp, $root_type) == $exclude;
322
            }, ARRAY_FILTER_USE_KEY);
323
        }
324
325
        return $label_mapping;
326
    }
327
328
    public function draw()
329
    {
330
        $this->_request_data['chapter_name'] = midcom::get()->config->get('midcom_site_title');
331
        midcom_show_style('midgard_admin_asgard_navigation_chapter');
332
333
        $this->_draw_plugins();
334
335
        if (!midcom::get()->auth->can_user_do('midgard.admin.asgard:manage_objects', null, 'midgard_admin_asgard_plugin')) {
336
            return;
337
        }
338
339
        $label_mapping = $this->_process_root_types();
340
341
        $expanded_types = array_intersect(array_keys($label_mapping), $this->expanded_root_types);
342
343
        /*
344
         * Use a dropdown for displaying the navigation if at least one type is expanded
345
         * and the user has the corresponding preference set. That way, you expanded types
346
         * can take up the maximum available space while all types are still accessible with one
347
         * click if nothing is expanded
348
         */
349
        $types_shown = false;
350
        if (    !empty($expanded_types)
351
             && midgard_admin_asgard_plugin::get_preference('navigation_type') === 'dropdown') {
352
            $this->_draw_select_navigation();
353
            $types_shown = true;
354
        }
355
356
        foreach ($expanded_types as $root_type) {
357
            $this->_request_data['section_url'] = midcom_connection::get_url('self') . "__mfa/asgard/{$root_type}";
358
            $this->_request_data['section_name'] = $label_mapping[$root_type];
359
            $this->_request_data['expanded'] = true;
360
            midcom_show_style('midgard_admin_asgard_navigation_section_header');
361
            $ref = midcom_helper_reflector_tree::get($root_type);
362
            $this->_list_root_elements($ref);
363
364
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
365
        }
366
367
        if (!$types_shown) {
368
            $this->_request_data['section_name'] = $this->_l10n->get('midgard objects');
369
            $this->_request_data['section_url'] = null;
370
            $this->_request_data['expanded'] = true;
371
            midcom_show_style('midgard_admin_asgard_navigation_section_header');
372
            $collapsed_types = array_diff_key($label_mapping, array_flip($expanded_types));
373
374
            $this->_draw_type_list($collapsed_types);
375
376
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
377
        }
378
    }
379
380
    private function _draw_type_list(array $types)
381
    {
382
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
383
384
        foreach ($types as $type => $label) {
385
            $url = midcom_connection::get_url('self') . "__mfa/asgard/{$type}/";
386
            echo "    <li class=\"mgdschema-type\">";
387
388
            $dbaclass = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($type);
389
            if (   $dbaclass
390
                && class_exists($dbaclass)) {
391
                $object = new $dbaclass;
392
            } else {
393
                $object = new $type;
394
            }
395
            $icon = midcom_helper_reflector::get_object_icon($object);
396
397
            echo "<a href=\"" . $url . "\" title=\"{$label}\">{$icon}{$label}</a>\n";
398
399
            echo "    </li>\n";
400
        }
401
402
        echo "</ul>\n";
403
    }
404
405
    private function _draw_select_navigation()
406
    {
407
        if (!empty($this->_object_path)) {
408
            $this->_request_data['root_object'] = $this->_object_path[0];
409
            $this->_request_data['navigation_type'] = $this->_object_path[0]->__mgdschema_class_name__;
410
        } elseif (isset($this->expanded_root_types[0])) {
411
            $this->_request_data['navigation_type'] = $this->expanded_root_types[0];
412
        } else {
413
            $this->_request_data['navigation_type'] = '';
414
        }
415
416
        $this->_request_data['label_mapping'] = midgard_admin_asgard_plugin::get_root_classes();
417
        $this->_request_data['expanded_root_types'] = $this->expanded_root_types;
418
419
        midcom_show_style('midgard_admin_asgard_navigation_sections');
420
    }
421
}
422