Passed
Push — master ( 3657f2...b17fdb )
by Andreas
24:28 queued 05:07
created

midcom_helper_reflector::get_search_properties()   B

Complexity

Conditions 10
Paths 25

Size

Total Lines 54
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 10.0056

Importance

Changes 0
Metric Value
cc 10
eloc 31
c 0
b 0
f 0
nc 25
nop 0
dl 0
loc 54
ccs 25
cts 26
cp 0.9615
crap 10.0056
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use midgard\portable\storage\connection;
10
use midgard\portable\api\mgdobject;
11
12
/**
13
 * The Grand Unified Reflector
14
 *
15
 * @package midcom.helper.reflector
16
 */
17
class midcom_helper_reflector extends midcom_baseclasses_components_purecode
18
{
19
    public $mgdschema_class = '';
20
21
    /**
22
     * @var midgard_reflection_property
23
     */
24
    protected $_mgd_reflector;
25
26
    protected $_dummy_object;
27
28
    private static $_cache = [
29
        'l10n' => [],
30
        'instance' => [],
31
        'title' => [],
32
        'name' => [],
33
        'fieldnames' => [],
34
        'object_icon_map' => null,
35
        'create_type_map' => null
36
    ];
37
38
    /**
39
     * Constructor, takes classname or object, resolved MgdSchema root class automagically
40
     *
41
     * @param string|mgdobject $src classname or object
42
     */
43 191
    public function __construct($src)
44
    {
45 191
        parent::__construct();
46
47
        // Resolve root class name
48 191
        $this->mgdschema_class = self::resolve_baseclass($src);
49
        // Could not resolve root class name
50 191
        if (empty($this->mgdschema_class)) {
51
            // Handle object vs string
52
            $original_class = (is_object($src)) ? get_class($src) : $src;
53
            throw new midcom_error("Could not determine MgdSchema baseclass for '{$original_class}'");
54
        }
55
56
        // Instantiate midgard reflector
57 191
        $this->_mgd_reflector = new midgard_reflection_property($this->mgdschema_class);
58
59
        // Instantiate dummy object
60 191
        $this->_dummy_object = new $this->mgdschema_class;
61 191
    }
62
63
    /**
64
     * Get cached reflector instance
65
     *
66
     * @param mixed $src Object or classname
67
     * @return static
68
     */
69 395
    public static function &get($src)
70
    {
71 395
        $identifier = get_called_class() . (is_object($src) ? get_class($src) : $src);
72
73 395
        if (!isset(self::$_cache['instance'][$identifier])) {
74 36
            self::$_cache['instance'][$identifier] = new static($src);
75
        }
76 395
        return self::$_cache['instance'][$identifier];
77
    }
78
79
    /**
80
     * Get object's (mgdschema) fieldnames.
81
     *
82
     * @param object $object Object The object to query
83
     */
84 111
    public static function get_object_fieldnames(object $object) : array
85
    {
86 111
        $classname = get_class($object);
87 111
        $metadata = false;
88
89 111
        if (midcom::get()->dbclassloader->is_midcom_db_object($object)) {
90 101
            $classname = $object->__mgdschema_class_name__;
91 88
        } elseif ($object instanceof midcom_helper_metadata) {
92 78
            $metadata = true;
93 78
            $classname = $object->object->__mgdschema_class_name__;
94
        }
95
96 111
        if (is_subclass_of($classname, mgdobject::class)) {
97 111
            $cm = connection::get_em()->getClassMetadata($classname);
98 111
            return $cm->get_schema_properties($metadata);
0 ignored issues
show
introduced by
The method get_schema_properties() does not exist on Doctrine\ORM\Mapping\ClassMetadata. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

98
            return $cm->/** @scrutinizer ignore-call */ get_schema_properties($metadata);
Loading history...
99
        }
100 5
        return array_keys(get_object_vars($object));
101
    }
102
103 150
    public function property_exists(string $property, bool $metadata = false) : bool
104
    {
105 150
        return $this->_mgd_reflector->property_exists($property, $metadata);
106
    }
107
108
    /**
109
     * Gets a midcom_helper_l10n instance for component governing the type
110
     */
111 67
    public function get_component_l10n() : midcom_services_i18n_l10n
112
    {
113 67
        if (!isset(self::$_cache['l10n'][$this->mgdschema_class])) {
114 11
            if ($component = midcom::get()->dbclassloader->get_component_for_class($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 midcom_services_dbclassl...t_component_for_class() 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

114
            if ($component = midcom::get()->dbclassloader->get_component_for_class(/** @scrutinizer ignore-type */ $this->mgdschema_class)) {
Loading history...
115 10
                self::$_cache['l10n'][$this->mgdschema_class] = $this->_i18n->get_l10n($component);
116
            } else {
117 1
                debug_add("Could not resolve component for class {$this->mgdschema_class}, using our own l10n", MIDCOM_LOG_INFO);
118 1
                self::$_cache['l10n'][$this->mgdschema_class] = $this->_l10n;
119
            }
120
        }
121
122 67
        return self::$_cache['l10n'][$this->mgdschema_class];
123
    }
124
125
    /**
126
     * Get the localized label of the class
127
     *
128
     * @todo remove any hardcoded class names/prefixes
129
     */
130 67
    public function get_class_label() : string
131
    {
132 67
        $component_l10n = $this->get_component_l10n();
133 67
        $use_classname = $this->mgdschema_class;
134
135 67
        $midcom_dba_classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($use_classname);
136
137 67
        if (!empty($midcom_dba_classname)) {
138 67
            $use_classname = $midcom_dba_classname;
139
        }
140
141 67
        $use_classname = preg_replace('/_(db|dba)$/', '', $use_classname);
142
143 67
        $label = $component_l10n->get($use_classname);
144 67
        if ($label == $use_classname) {
145
            // Class string not localized, try Bergie's way to pretty-print
146 67
            $classname_parts = explode('_', $use_classname);
147 67
            if (count($classname_parts) >= 3) {
148
                // Drop first two parts of class name
149 67
                array_shift($classname_parts);
150 67
                array_shift($classname_parts);
151
            }
152
            // FIXME: Remove hardcoded class prefixes
153 67
            $use_label = preg_replace('/(openpsa|notifications)_/', '', implode('_', $classname_parts));
154
155 67
            $use_label = str_replace('_', ' ', $use_label);
156 67
            $label = $component_l10n->get($use_label);
157 67
            if ($use_label == $label) {
158 62
                $label = ucwords($use_label);
159
            }
160
        }
161 67
        return $label;
162
    }
163
164
    /**
165
     * Get property name to use as label
166
     */
167 23
    public function get_label_property() : string
168
    {
169 23
        $midcom_class = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($this->mgdschema_class);
170 23
        $obj = ($midcom_class) ? new $midcom_class : new $this->mgdschema_class;
171
172 23
        if (method_exists($obj, 'get_label_property')) {
173 3
            return $obj->get_label_property();
174
        }
175 20
        return $this->get_property('title', $obj) ??
176 4
            $this->get_property('name', $obj) ??
177 20
            'guid';
178
    }
179
180
    /**
181
     * Get the object label property value
182
     */
183 55
    public function get_object_label(object $object) : ?string
184
    {
185 55
        if ($object instanceof mgdobject) {
186
            try {
187 2
                $obj = midcom::get()->dbfactory->convert_midgard_to_midcom($object);
188
            } catch (midcom_error $e) {
189 2
                return null;
190
            }
191
        } else {
192 53
            $obj = $object;
193
        }
194 55
        if (method_exists($obj, 'get_label')) {
195 43
            return $obj->get_label();
196
        }
197
198 14
        $properties = array_flip($obj->get_properties());
199 14
        if (empty($properties)) {
200
            debug_add("Could not list object properties, aborting", MIDCOM_LOG_ERROR);
201
            return null;
202
        }
203 14
        if (isset($properties['title'])) {
204 12
            return $obj->title;
205
        }
206 3
        if (isset($properties['name'])) {
207 2
            return $obj->name;
208
        }
209 1
        if ($obj->id > 0) {
210
            return $this->get_class_label() . ' #' . $obj->id;
211
        }
212 1
        return '';
213
    }
214
215
    /**
216
     * Get the name of the create icon image
217
     */
218 28
    public static function get_create_icon(string $type) : string
219
    {
220 28
        if (is_callable([$type, 'get_create_icon'])) {
221
            // class has static method to tell us the answer ? great !
222
            return $type::get_create_icon();
223
        }
224 28
        return self::get_icon($type, 'create_type');
225
    }
226
227
    /**
228
     * heuristics magic (instead of adding something here, take a look at
229
     * config keys "create_type_magic" and "object_icon_magic")
230
     */
231 36
    private static function get_icon(string $object_class, string $mode) : string
232
    {
233 36
        $object_baseclass = self::resolve_baseclass($object_class);
234 36
        if (null === self::$_cache[$mode . '_map']) {
235 2
            self::$_cache[$mode . '_map'] = self::_get_icon_map($mode . '_magic', $mode === 'create_type' ? 'file-o' : 'file');
236
        }
237 36
        $map = self::$_cache[$mode . '_map'];
238
239
        switch (true) {
240 36
            case (isset($map[$object_class])):
241 18
                return $map[$object_class];
242
243 32
            case (isset($map[$object_baseclass])):
244 2
                return $map[$object_baseclass];
245
246 32
            case (str_contains($object_class, 'person')):
247 4
                return $mode === 'create_type' ? 'user-o' : 'user';
248
249 30
            case (str_contains($object_class, 'event')):
250 2
                return 'calendar-o';
251
252 28
            case (str_contains($object_class, 'member')):
253 28
            case (str_contains($object_class, 'organization')):
254 27
            case (str_contains($object_class, 'group')):
255 6
                return 'users';
256
257 26
            case (str_contains($object_class, 'element')):
258 1
                return 'file-code-o';
259
260
            default:
261 25
                return $map['__default__'];
262
        }
263
    }
264
265
    /**
266
     * Get the object icon
267
     */
268 12
    public static function get_object_icon(object $obj) : string
269
    {
270 12
        if (method_exists($obj, 'get_icon')) {
271
            // object knows it's icon, how handy!
272 4
            $icon = $obj->get_icon();
273
        } else {
274 9
            $icon = self::get_icon(get_class($obj), 'object_icon');
275
        }
276
277 12
        return '<i class="fa fa-' . $icon . '"></i>';
278
    }
279
280 2
    private static function _get_icon_map(string $config_key, string $fallback) : array
281
    {
282 2
        $config = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config');
283 2
        $icons2classes = $config->get($config_key);
284 2
        $icon_map = [];
285
        //sanity
286 2
        if (!is_array($icons2classes)) {
287
            throw new midcom_error('Config key "' . $config_key . '" is not an array');
288
        }
289 2
        foreach ($icons2classes as $icon => $classes) {
290 2
            $icon_map = array_merge($icon_map, array_fill_keys($classes, $icon));
291
        }
292 2
        if (!isset($icon_map['__default__'])) {
293
            $icon_map['__default__'] = $fallback;
294
        }
295 2
        return $icon_map;
296
    }
297
298
    /**
299
     * Get class properties to use as search fields in choosers or other direct DB searches
300
     */
301 15
    public function get_search_properties() : array
302
    {
303
        // Return cached results if we have them
304 15
        static $cache = [];
305 15
        if (isset($cache[$this->mgdschema_class])) {
306 9
            return $cache[$this->mgdschema_class];
307
        }
308 7
        debug_add("Starting analysis for class {$this->mgdschema_class}");
309
310 7
        $properties = self::get_object_fieldnames($this->_dummy_object);
311
312
        $default_properties = [
313 7
            'title' => true,
314
            'tag' => true,
315
            'firstname' => true,
316
            'lastname' => true,
317
            'official' => true,
318
            'username' => true,
319
        ];
320
321 7
        $search_properties = array_intersect_key($default_properties, array_flip($properties));
322
323 7
        foreach ($properties as $property) {
324 7
            if (str_contains($property, 'name')) {
325 6
                $search_properties[$property] = true;
326
            }
327
            // TODO: More per property heuristics
328
        }
329
        // TODO: parent and up heuristics
330
331 7
        $label_prop = $this->get_label_property();
332
333 7
        if (    $label_prop != 'guid'
334 7
             && $this->_mgd_reflector->property_exists($label_prop)) {
335 6
            $search_properties[$label_prop] = true;
336
        }
337
338
        // Exceptions - always search these fields
339 7
        $always_search_all = $this->_config->get('always_search_fields') ?: [];
340 7
        if (!empty($always_search_all[$this->mgdschema_class])) {
341 1
            $fields = array_intersect($always_search_all[$this->mgdschema_class], $properties);
342 1
            $search_properties += array_flip($fields);
343
        }
344
345
        // Exceptions - never search these fields
346 7
        $never_search_all = $this->_config->get('never_search_fields') ?: [];
347 7
        if (!empty($never_search_all[$this->mgdschema_class])) {
348
            $search_properties = array_diff_key($search_properties, array_flip($never_search_all[$this->mgdschema_class]));
349
        }
350
351 7
        $search_properties = array_keys($search_properties);
352 7
        debug_print_r("Search properties for {$this->mgdschema_class}: ", $search_properties);
353 7
        $cache[$this->mgdschema_class] = $search_properties;
354 7
        return $search_properties;
355
    }
356
357
    /**
358
     * Gets a list of link properties and the links target info
359
     *
360
     * Link info key specification
361
     *     'class' string link target class name
362
     *     'target' string link target property (of target class)
363
     *
364
     * @return array multidimensional array keyed by property, values are arrays with link info (or false in case of failure)
365
     */
366 4
    public function get_link_properties() : array
367
    {
368
        // Return cached results if we have them
369 4
        static $cache = [];
370 4
        if (isset($cache[$this->mgdschema_class])) {
371 1
            return $cache[$this->mgdschema_class];
372
        }
373 3
        debug_add("Starting analysis for class {$this->mgdschema_class}");
374
375
        // Shorthands
376 3
        $ref = $this->_mgd_reflector;
377 3
        $obj = $this->_dummy_object;
378
379
        // Get property list and start checking (or abort on error)
380 3
        $links = [];
381 3
        foreach (self::get_object_fieldnames($obj) as $property) {
382 3
            if ($property == 'guid') {
383
                // GUID, even though of type MGD_TYPE_GUID, is never a link
384 3
                continue;
385
            }
386
387 3
            if (   !$ref->is_link($property)
388 3
                && $ref->get_midgard_type($property) != MGD_TYPE_GUID) {
389 3
                continue;
390
            }
391 3
            debug_add("Processing property '{$property}'");
392
            $linkinfo = [
393 3
                'class' => $ref->get_link_name($property),
394 3
                'target' => $ref->get_link_target($property),
395 3
                'type' => $ref->get_midgard_type($property),
396
            ];
397
398 3
            if (!$linkinfo['target'] && $linkinfo['type'] == MGD_TYPE_GUID) {
399 1
                $linkinfo['target'] = 'guid';
400
            }
401
402 3
            $links[$property] = $linkinfo;
403
        }
404
405 3
        debug_print_r("Links for {$this->mgdschema_class}: ", $links);
406 3
        $cache[$this->mgdschema_class] = $links;
407 3
        return $links;
408
    }
409
410
    /**
411
     * Map extended classes
412
     *
413
     * For example org.openpsa.* components often expand core objects,
414
     * in config we specify which classes we wish to substitute with which
415
     *
416
     * @param string $schema_type classname to check rewriting for
417
     * @return string new classname (or original in case no rewriting is to be done)
418
     */
419 38
    public static function class_rewrite(string $schema_type) : string
420
    {
421 38
        static $extends = false;
422 38
        if ($extends === false) {
423
            $extends = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get('class_extends');
424
            // Safety against misconfiguration
425
            if (!is_array($extends)) {
426
                throw new midcom_error("config->get('class_extends') did not return array, invalid configuration ??");
427
            }
428
        }
429 38
        if (   isset($extends[$schema_type])
430 38
            && class_exists($extends[$schema_type])) {
431 2
            return $extends[$schema_type];
432
        }
433 37
        return $schema_type;
434
    }
435
436
    /**
437
     * See if two MgdSchema classes are the same
438
     *
439
     * NOTE: also takes into account the various extended class scenarios
440
     *
441
     * @param string $class_one first class to compare
442
     * @param string $class_two second class to compare
443
     */
444 11
    public static function is_same_class($class_one, $class_two) : bool
445
    {
446 11
        $one = self::resolve_baseclass($class_one);
447 11
        $two = self::resolve_baseclass($class_two);
448 11
        return $one == $two;
449
    }
450
451
    /**
452
     * Get the MgdSchema classname for given class
453
     *
454
     * @param mixed $classname either string (class name) or object
455
     * @return string the base class name
456
     */
457 225
    public static function resolve_baseclass($classname) : ?string
458
    {
459 225
        static $cached = [];
460
461 225
        if (is_object($classname)) {
462 173
            $class_instance = $classname;
463 173
            $classname = get_class($classname);
464
        }
465
466 225
        if (empty($classname)) {
467
            return null;
468
        }
469
470 225
        if (isset($cached[$classname])) {
471 205
            return $cached[$classname];
472
        }
473
474 36
        if (!isset($class_instance)) {
475 10
            $class_instance = new $classname();
476
        }
477
478
        // Check for decorators first
479 36
        if (!empty($class_instance->__mgdschema_class_name__)) {
480 29
            $parent_class = $class_instance->__mgdschema_class_name__;
481
        } else {
482 7
            $parent_class = $classname;
483
        }
484
485 36
        $cached[$classname] = self::class_rewrite($parent_class);
486
487 36
        return $cached[$classname];
488
    }
489
490 389
    private function get_property(string $type, object $object) : ?string
491
    {
492
        // Cache results per class within request
493 389
        $key = get_class($object);
494 389
        if (array_key_exists($key, self::$_cache[$type])) {
495 374
            return self::$_cache[$type][$key];
496
        }
497 51
        self::$_cache[$type][$key] = null;
498
499
        // Configured properties
500 51
        $exceptions = $this->_config->get($type . '_exceptions');
501 51
        foreach ($exceptions as $class => $property) {
502 51
            if (midcom::get()->dbfactory->is_a($object, $class)) {
503 8
                if (   $property !== false
504 8
                    && !$this->_mgd_reflector->property_exists($property)) {
505
                    debug_add("Matched class '{$key}' to '{$class}' via is_a but property '{$property}' does not exist", MIDCOM_LOG_ERROR);
506
                } else {
507 8
                    self::$_cache[$type][$key] = $property;
508
                }
509 8
                return self::$_cache[$type][$key];
510
            }
511
        }
512
        // The simple heuristic
513 46
        if ($this->_mgd_reflector->property_exists($type)) {
514 21
            self::$_cache[$type][$key] = $type;
515
        }
516 46
        return self::$_cache[$type][$key];
517
    }
518
519
    /**
520
     * Resolve the "name" property of given object
521
     *
522
     * @param object $object the object to get the name property for
523
     */
524 304
    public static function get_name_property(object $object) : ?string
525
    {
526 304
        return self::get($object)->get_property('name', $object);
527
    }
528
529
    /**
530
     * Resolve the "title" of given object
531
     *
532
     * NOTE: This is distinctly different from get_object_label, which will always return something
533
     * even if it's just the class name and GUID, also it will for some classes include extra info (like datetimes)
534
     * which we do not want here.
535
     *
536
     * @param object $object the object to get the name property for
537
     */
538 184
    public static function get_object_title(object $object) : ?string
539
    {
540 184
        if ($title_property = self::get_title_property($object)) {
541 181
            return (string) $object->{$title_property};
542
        }
543
        // Could not resolve valid property
544 3
        return null;
545
    }
546
547
    /**
548
     * Resolve the "title" property of given object
549
     *
550
     * NOTE: This is distinctly different from get_label_property, which will always return something
551
     * even if it's just the guid
552
     *
553
     * @param object $object The object to get the title property for
554
     */
555 206
    public static function get_title_property(object $object) : ?string
556
    {
557 206
        return self::get($object)->get_property('title', $object);
558
    }
559
}
560