Passed
Push — master ( 0ab4bc...d2b377 )
by Andreas
25:09 queued 04:16
created

autocomplete   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 284
Duplicated Lines 0 %

Test Coverage

Coverage 68%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 163
dl 0
loc 284
ccs 119
cts 175
cp 0.68
rs 3.04
c 2
b 0
f 0
wmc 67

15 Methods

Rating   Name   Duplication   Size   Complexity  
A build_label() 0 9 3
A __construct() 0 4 1
A get_results() 0 30 6
A get_widget_config() 0 8 1
A get_objects() 0 3 1
A prepare_qb() 0 24 6
A sort_items() 0 9 3
A apply_constraints() 0 14 6
A get_querystring() 0 23 6
B get_search_constraints() 0 41 10
A verify_request() 0 19 6
A add_head_elements() 0 17 3
A create_item_label() 0 13 6
A sanitize_label() 0 3 1
B get_property_string() 0 27 8

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * @copyright CONTENT CONTROL GmbH, http://www.contentcontrol-berlin.de
4
 */
5
6
namespace midcom\datamanager\helper;
7
8
use midcom;
9
use midcom_connection;
10
use midcom_core_query;
11
use midcom_core_account;
12
use midcom_db_person;
13
use midcom_error;
14
use midcom_helper_reflector;
15
use midcom_baseclasses_components_configuration;
16
17
/**
18
 * Experimental autocomplete helper
19
 */
20
class autocomplete
21
{
22
    /**
23
     * The request data we're working on
24
     *
25
     * @var array
26
     */
27
    private $request;
28
29 2
    public function __construct(array $data)
30
    {
31 2
        $this->request = $data;
32 2
        $this->verify_request();
33 2
    }
34
35 2
    private function verify_request()
36
    {
37 2
        if (!class_exists($this->request['class'])) {
38
            throw new midcom_error("Class {$this->request['class']} could not be loaded");
39
        }
40
41 2
        if (empty($this->request['searchfields'])) {
42
            throw new midcom_error("No fields to search for defined");
43
        }
44
45 2
        if (empty($this->request["term"])) {
46
            throw new midcom_error("Empty query string.");
47
        }
48
49 2
        if (!isset($this->request['titlefield'])) {
50 2
            $this->request['titlefield'] = null;
51
        }
52 2
        if (!isset($this->request['result_headers'])) {
53 1
            $this->request['result_headers'] = [];
54
        }
55 2
    }
56
57 1
    private function prepare_qb()
58
    {
59 1
        $qb = $this->request['class']::new_query_builder();
60
61 1
        if (!empty($this->request['constraints'])) {
62
            $this->apply_constraints($qb, $this->request['constraints']);
63
        }
64
65 1
        $constraints = $this->get_search_constraints();
66 1
        if (!empty($constraints)) {
67 1
            $qb->begin_group('OR');
68 1
            $this->apply_constraints($qb, $constraints);
69 1
            $qb->end_group();
70
        }
71
72 1
        if (!empty($this->request['orders'])) {
73
            ksort($this->request['orders']);
74
            foreach ($this->request['orders'] as $data) {
75
                foreach ($data as $field => $order) {
76
                    $qb->add_order($field, $order);
77
                }
78
            }
79
        }
80 1
        return $qb;
81
    }
82
83 1
    private function apply_constraints(midcom_core_query $query, array $constraints)
84
    {
85 1
        ksort($constraints);
86 1
        foreach ($constraints as $key => $data) {
87 1
            if (   !array_key_exists('value', $data)
88 1
                || empty($data['field'])
89 1
                || empty($data['op'])) {
90
                debug_add("Constraint #{$key} is not correctly defined, skipping", MIDCOM_LOG_WARN);
91
                continue;
92
            }
93 1
            if ($data['field'] === 'username') {
94
                midcom_core_account::add_username_constraint($query, $data['op'], $data['value']);
95
            } else {
96 1
                $query->add_constraint($data['field'], $data['op'], $data['value']);
97
            }
98
        }
99 1
    }
100
101 1
    private function get_search_constraints() : array
102
    {
103 1
        $constraints = [];
104 1
        $query = $this->request["term"];
105 1
        if (preg_match('/^%+$/', $query)) {
106
            debug_add('query is all wildcards, don\'t waste time in adding LIKE constraints');
107
            return $constraints;
108
        }
109
110 1
        $reflector = new midcom_helper_reflector($this->request['class']);
111
112 1
        foreach ($this->request['searchfields'] as $field) {
113 1
            $field_type = $reflector->get_midgard_type($field);
114 1
            $operator = 'LIKE';
115 1
            if (str_contains($field, '.')) {
116
                //TODO: This should be resolved properly
117
                $field_type = MGD_TYPE_STRING;
118
            }
119
            switch ($field_type) {
120 1
                case MGD_TYPE_GUID:
121
                case MGD_TYPE_STRING:
122
                case MGD_TYPE_LONGTEXT:
123 1
                    $query = $this->get_querystring();
124 1
                    break;
125
                case MGD_TYPE_INT:
126
                case MGD_TYPE_UINT:
127
                case MGD_TYPE_FLOAT:
128
                    $operator = '=';
129
                    break;
130
                default:
131
                    debug_add("can't handle field type " . $field_type, MIDCOM_LOG_WARN);
132
                    continue 2;
133
            }
134 1
            debug_add("adding search (ORed) constraint: {$field} {$operator} '{$query}'");
135 1
            $constraints[] = [
136 1
                'field' => $field,
137 1
                'op' => $operator,
138 1
                'value' => $query
139
            ];
140
        }
141 1
        return $constraints;
142
    }
143
144 2
    public function get_querystring() : string
145
    {
146 2
        $query = $this->request["term"];
147 2
        $wildcard_query = $query;
148 2
        if (   isset($this->request['auto_wildcards'])
149 2
            && !str_contains($query, '%')) {
150 1
            switch ($this->request['auto_wildcards']) {
151 1
                case 'start':
152
                    $wildcard_query = '%' . $query;
153
                    break;
154 1
                case 'end':
155 1
                    $wildcard_query = $query . '%';
156 1
                    break;
157
                case 'both':
158
                    $wildcard_query = '%' . $query . '%';
159
                    break;
160
                default:
161
                    debug_add("Don't know how to handle auto_wildcards value '" . $this->request['auto_wildcards'] . "'", MIDCOM_LOG_WARN);
162
                    break;
163
            }
164
        }
165 2
        $wildcard_query = str_replace("*", "%", $wildcard_query);
166 2
        return preg_replace('/%+/', '%', $wildcard_query);
167
    }
168
169 1
    public function get_objects() : array
170
    {
171 1
        return $this->prepare_qb()->execute();
172
    }
173
174 1
    public function get_results() : array
175
    {
176 1
        if (empty($this->request["id_field"])) {
177
            throw new midcom_error("Empty ID field.");
178
        }
179
180 1
        $results = $this->get_objects();
181 1
        $items = [];
182
183 1
        foreach ($results as $object) {
184
            $item = [
185 1
                'id' => $object->{$this->request['id_field']},
186 1
                'label' => self::create_item_label($object, $this->request['result_headers'], $this->request['titlefield']),
187
            ];
188 1
            if (!empty($this->request['result_headers'])) {
189 1
                $item['description'] = self::build_label($object, array_column($this->request['result_headers'], 'name'));
190
            }
191 1
            if (!empty($this->request['categorize_by_parent_label'])) {
192
                $item['category'] = '';
193
                if ($parent = $object->get_parent()) {
194
                    $item['category'] = midcom_helper_reflector::get($parent)->get_object_label($parent);
195
                }
196
            }
197 1
            $item['value'] = $item['label'];
198
199 1
            $items[] = $item;
200
        }
201 1
        usort($items, [$this, 'sort_items']);
202
203 1
        return $items;
204
    }
205
206
    public static function sort_items($a, $b)
207
    {
208
        if (isset($a['category'])) {
209
            $cmp = strnatcasecmp($a['category'], $b['category']);
210
            if ($cmp != 0) {
211
                return $cmp;
212
            }
213
        }
214
        return strnatcasecmp($a['label'], $b['label']);
215
    }
216
217 74
    public static function add_head_elements(bool $creation_mode_enabled = false, bool $sortable = false)
218
    {
219 74
        $head = midcom::get()->head;
220 74
        $head->add_stylesheet(MIDCOM_STATIC_URL . "/stock-icons/font-awesome-4.7.0/css/font-awesome.min.css");
221 74
        $head->add_stylesheet(MIDCOM_STATIC_URL . '/midcom.datamanager/autocomplete.css');
222
223 74
        $components = ['menu', 'autocomplete'];
224 74
        if ($sortable) {
225 2
            $components[] = 'mouse';
226 2
            $components[] = 'sortable';
227
        }
228 74
        if ($creation_mode_enabled) {
229 23
            $components = array_merge($components, ['mouse', 'draggable', 'resizable', 'button', 'dialog']);
230 23
            $head->add_jsfile(MIDCOM_STATIC_URL . '/midcom.workflow/workflow.js');
231
        }
232 74
        $head->enable_jquery_ui($components);
233 74
        $head->add_jsfile(MIDCOM_STATIC_URL . '/midcom.datamanager/autocomplete.js');
234 74
    }
235
236 7
    public static function get_widget_config(string $type) : array
237
    {
238 7
        $handler_url = midcom_connection::get_url('self') . 'midcom-exec-midcom.datamanager/autocomplete.php';
239
240 7
        $widget_config = midcom_baseclasses_components_configuration::get('midcom.datamanager', 'config')->get('clever_classes');
241 7
        $config = $widget_config[$type];
242 7
        $config['handler_url'] = $handler_url;
243 7
        return $config;
244
    }
245
246 37
    public static function create_item_label($object, array $result_headers, ?string $titlefield) : string
247
    {
248 37
        if (!empty($titlefield)) {
249 30
            if (is_callable($titlefield)) {
250
                return $titlefield($object);
251
            }
252 30
            if ($label = self::build_label($object, (array) $titlefield)) {
253 24
                return $label;
254
            }
255
        }
256 17
        return midcom_helper_reflector::get($object)->get_object_label($object) ?:
257 9
            self::build_label($object, array_column($result_headers, 'name')) ?:
258 17
            get_class($object) . ' #' . $object->id;
259
    }
260
261 34
    private static function build_label($object, array $fields) : string
262
    {
263 34
        $label = [];
264 34
        foreach ($fields as $field) {
265 34
            if ($value = self::get_property_string($object, $field)) {
266 32
                $label[] = $value;
267
            }
268
        }
269 34
        return implode(', ', $label);
270
    }
271
272 34
    private static function get_property_string($object, string $field) : string
273
    {
274 34
        if (preg_match('/^metadata\.(.+)$/', $field, $regs)) {
275
            $date_fields = ['created', 'revised', 'published', 'schedulestart', 'scheduleend', 'imported', 'exported', 'approved'];
276
            $person_fields = ['creator', 'revisor', 'approver', 'locker'];
277
            $metadata_property = $regs[1];
278
            $value = $object->metadata->$metadata_property;
279
280
            if (in_array($metadata_property, $date_fields)) {
281
                $formatter = midcom::get()->i18n->get_l10n()->get_formatter();
282
                return $value ? $formatter->datetime($value, \IntlDateFormatter::SHORT, \IntlDateFormatter::MEDIUM) : '';
283
            }
284
            if (in_array($metadata_property, $person_fields)) {
285
                if ($value) {
286
                    $person = new midcom_db_person($value);
287
                    return self::sanitize_label($person->name);
288
                }
289
                return '';
290
            }
291
            return self::sanitize_label($value);
292
        }
293 34
        if (   $field == 'username'
294 34
            && $object instanceof midcom_db_person) {
295
            $account = new midcom_core_account($object);
296
            return self::sanitize_label($account->get_username());
297
        }
298 34
        return self::sanitize_label($object->$field);
299
    }
300
301 34
    private static function sanitize_label($input) : string
302
    {
303 34
        return trim(strip_tags((string) $input));
304
    }
305
}