PropertyHelper   C
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 398
Duplicated Lines 0 %

Importance

Changes 3
Bugs 3 Features 0
Metric Value
eloc 210
dl 0
loc 398
rs 6.4799
c 3
b 3
f 0
wmc 54

10 Methods

Rating   Name   Duplication   Size   Complexity  
A schema() 0 16 4
A value() 0 17 3
B translationControl() 0 21 7
A fieldLabel() 0 10 3
B control() 0 22 10
A fastCreateFieldsMap() 0 25 4
A prepareFieldOptions() 0 14 4
A dateRange() 0 28 1
C translationsMap() 0 84 11
B fastCreateFields() 0 37 7

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2020 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace App\View\Helper;
14
15
use App\Form\Control;
16
use App\Form\Form;
17
use App\Utility\CacheTools;
18
use App\Utility\Translate;
19
use Cake\Cache\Cache;
20
use Cake\Core\Configure;
21
use Cake\Utility\Hash;
22
use Cake\View\Helper;
23
use Throwable;
24
25
/**
26
 * Helper class to generate properties html
27
 *
28
 * @property \App\View\Helper\SchemaHelper $Schema The schema helper
29
 * @property \Cake\View\Helper\FormHelper $Form The form helper
30
 */
31
class PropertyHelper extends Helper
32
{
33
    /**
34
     * List of helpers used by this helper
35
     *
36
     * @var array
37
     */
38
    public array $helpers = ['Form', 'Schema'];
39
40
    /**
41
     * Special paths to retrieve properties from related resources
42
     *
43
     * @var array
44
     */
45
    public const RELATED_PATHS = [
46
        'file_name' => 'relationships.streams.data.0.attributes.file_name',
47
        'mime_type' => 'relationships.streams.data.0.attributes.mime_type',
48
        'file_size' => 'relationships.streams.data.0.meta.file_size',
49
    ];
50
51
    /**
52
     * Special properties having their own custom schema type
53
     *
54
     * @var array
55
     */
56
    public const SPECIAL_PROPS_TYPE = [
57
        'categories' => 'categories',
58
        'relations' => 'relations',
59
        'file_size' => 'byte',
60
    ];
61
62
    /**
63
     * Generates a form control element for an object property by name, value and options.
64
     * Use SchemaHelper (@see \App\View\Helper\SchemaHelper) to get control options by schema model.
65
     * Use FormHelper (@see \Cake\View\Helper\FormHelper::control) to render control.
66
     *
67
     * @param string $name The property name
68
     * @param mixed|null $value The property value
69
     * @param array $options The form element options, if any
70
     * @param string|null $type The object or resource type, for others schemas
71
     * @return string
72
     */
73
    public function control(string $name, mixed $value, array $options = [], ?string $type = null): string
74
    {
75
        $forceReadonly = !empty(Hash::get($options, 'readonly'));
76
        $controlOptions = $this->Schema->controlOptions($name, $value, $this->schema($name, $type));
77
        $controlOptions['label'] = $this->fieldLabel($name, $type);
78
        $readonly = Hash::get($controlOptions, 'readonly') || $forceReadonly;
79
        if ($readonly === true && array_key_exists('html', $controlOptions)) {
80
            $controlOptions['html'] = str_replace('readonly="false"', 'readonly="true"', $controlOptions['html']);
81
            $controlOptions['html'] = str_replace(':readonly=false', ':readonly=true', $controlOptions['html']);
82
        }
83
        if ($readonly === true && array_key_exists('v-datepicker', $controlOptions)) {
84
            unset($controlOptions['v-datepicker']);
85
        }
86
        if (!$readonly && Hash::get($controlOptions, 'class') === 'json' || Hash::get($controlOptions, 'type') === 'json') {
87
            $jsonKeys = (array)Configure::read('_jsonKeys');
88
            Configure::write('_jsonKeys', array_merge($jsonKeys, [$name]));
89
        }
90
        if (Hash::check($controlOptions, 'html')) {
91
            return (string)Hash::get($controlOptions, 'html', '');
92
        }
93
94
        return $this->Form->control($name, array_merge($controlOptions, $options));
95
    }
96
97
    /**
98
     * Generates a form control for translation property
99
     *
100
     * @param string $name The property name
101
     * @param mixed|null $value The property value
102
     * @param array $options The form element options, if any
103
     * @return string
104
     */
105
    public function translationControl(string $name, mixed $value, array $options = []): string
106
    {
107
        $formControlName = sprintf('translated_fields[%s]', $name);
108
        $controlOptions = $this->Schema->controlOptions($name, $value, $this->schema($name, null));
109
        if (array_key_exists('html', $controlOptions)) {
110
            $controlOptions['html'] = str_replace(sprintf('name="%s"', $name), sprintf('name="%s"', $formControlName), $controlOptions['html']);
111
        }
112
        $controlOptions['label'] = $this->fieldLabel($name, null);
113
        $readonly = Hash::get($controlOptions, 'readonly');
114
        if ($readonly === true && array_key_exists('v-datepicker', $controlOptions)) {
115
            unset($controlOptions['v-datepicker']);
116
        }
117
        if (Hash::get($controlOptions, 'class') === 'json' || Hash::get($controlOptions, 'type') === 'json') {
118
            $jsonKeys = (array)Configure::read('_jsonKeys');
119
            Configure::write('_jsonKeys', array_merge($jsonKeys, [$formControlName]));
120
        }
121
        if (Hash::check($controlOptions, 'html')) {
122
            return (string)Hash::get($controlOptions, 'html', '');
123
        }
124
125
        return $this->Form->control($formControlName, array_merge($controlOptions, $options));
126
    }
127
128
    /**
129
     * Return label for field by name and type.
130
     * If there's a config for the field and type, return it.
131
     * Return translation of name, otherwise.
132
     *
133
     * @param string $name The field name
134
     * @param string|null $type The object type
135
     * @return string
136
     */
137
    public function fieldLabel(string $name, ?string $type = null): string
138
    {
139
        $defaultLabel = (string)Translate::get($name);
140
        $t = empty($type) ? $this->getView()->get('objectType') : $type;
141
        if (empty($t)) {
142
            return $defaultLabel;
143
        }
144
        $key = sprintf('Properties.%s.options.%s.label', $t, $name);
145
146
        return (string)Configure::read($key, $defaultLabel);
147
    }
148
149
    /**
150
     * JSON Schema array of property name
151
     *
152
     * @param string $name The property name
153
     * @param string|null $objectType The object or resource type to use as schema
154
     * @return array|null
155
     */
156
    public function schema(string $name, ?string $objectType = null): ?array
157
    {
158
        $schema = (array)$this->_View->get('schema');
159
        if (!empty($objectType)) {
160
            $schemas = (array)$this->_View->get('schemasByType');
161
            $schema = (array)Hash::get($schemas, $objectType);
162
        }
163
        if (Hash::check(self::SPECIAL_PROPS_TYPE, $name)) {
164
            return array_filter([
165
                'type' => Hash::get(self::SPECIAL_PROPS_TYPE, $name),
166
                $name => Hash::get($schema, sprintf('%s', $name)),
167
            ]);
168
        }
169
        $res = Hash::get($schema, sprintf('properties.%s', $name));
170
171
        return $res === null ? null : (array)$res;
172
    }
173
174
    /**
175
     * Get formatted property value of a resource or object.
176
     *
177
     * @param array $resource Resource or object data
178
     * @param string $property Property name
179
     * @return string
180
     */
181
    public function value(array $resource, string $property): string
182
    {
183
        $paths = array_filter([
184
            $property,
185
            sprintf('attributes.%s', $property),
186
            sprintf('meta.%s', $property),
187
            (string)Hash::get(self::RELATED_PATHS, $property),
188
        ]);
189
        $value = '';
190
        foreach ($paths as $path) {
191
            if (Hash::check($resource, $path)) {
192
                $value = Hash::get($resource, $path);
193
                break;
194
            }
195
        }
196
197
        return $this->Schema->format($value, $this->schema($property));
198
    }
199
200
    /**
201
     * Return translations for object fields and more.
202
     *
203
     * @return array
204
     */
205
    public function translationsMap(): array
206
    {
207
        try {
208
            $key = CacheTools::cacheKey('translationsMap');
209
            $map = Cache::remember(
210
                $key,
211
                function () {
212
                    $map = [];
213
                    $keys = [];
214
                    Configure::load('properties');
215
                    $properties = (array)Configure::read('DefaultProperties');
216
                    $removeKeys = ['_element', '_hide', '_keep'];
217
                    foreach ($properties as $name => $prop) {
218
                        $keys[] = trim($name);
219
                        $keys = array_merge($keys, (array)Hash::get($prop, 'fastCreate.all', []));
220
                        $keys = array_merge($keys, (array)Hash::get($prop, 'index', []));
221
                        $keys = array_merge($keys, (array)Hash::get($prop, 'filter', []));
222
                        $groups = array_keys((array)Hash::get($prop, 'view', []));
223
                        $addKeys = array_reduce($groups, function ($carry, $group) use ($prop, $removeKeys) {
224
                            $carry[] = $group;
225
                            $groupKeys = (array)Hash::get($prop, sprintf('view.%s', $group), []);
226
                            $groupKeys = array_filter(
227
                                $groupKeys,
228
                                function ($val, $key) use ($removeKeys) {
229
                                    return is_string($val) && !in_array($key, $removeKeys);
230
                                },
231
                                ARRAY_FILTER_USE_BOTH,
232
                            );
233
234
                            return array_merge($carry, $groupKeys);
235
                        }, []);
236
                        $keys = array_merge($keys, $addKeys);
237
                    }
238
                    $keys = array_map(function ($key) {
239
                        return is_array($key) ? array_key_first($key) : $key;
240
                    }, $keys);
241
                    $keys = array_diff($keys, $removeKeys);
242
                    $keys = array_unique($keys);
243
                    $keys = array_map(function ($key) {
244
                        return strpos($key, '/') !== false ? substr($key, strrpos($key, '/') + 1) : $key;
245
                    }, $keys);
246
                    $properties = (array)Configure::read(sprintf('Properties'));
247
                    foreach ($properties as $name => $prop) {
248
                        $keys[] = trim($name);
249
                        $keys = array_merge($keys, (array)Hash::get($prop, 'fastCreate.all', []));
250
                        $keys = array_merge($keys, (array)Hash::get($prop, 'index', []));
251
                        $keys = array_merge($keys, (array)Hash::get($prop, 'filter', []));
252
                        $groups = array_keys((array)Hash::get($prop, 'view', []));
253
                        $addKeys = array_reduce($groups, function ($carry, $group) use ($prop, $removeKeys) {
254
                            $carry[] = $group;
255
                            $groupKeys = (array)Hash::get($prop, sprintf('view.%s', $group), []);
256
                            $groupKeys = array_filter(
257
                                $groupKeys,
258
                                function ($val, $key) use ($removeKeys) {
259
                                    return is_string($val) && !in_array($key, $removeKeys);
260
                                },
261
                                ARRAY_FILTER_USE_BOTH,
262
                            );
263
264
                            return array_merge($carry, $groupKeys);
265
                        }, []);
266
                        $keys = array_merge($keys, $addKeys);
267
                    }
268
                    $keys = array_map(function ($key) {
269
                        return is_array($key) ? array_key_first($key) : $key;
270
                    }, $keys);
271
                    $keys = array_diff($keys, $removeKeys);
272
                    $keys = array_unique($keys);
273
                    $keys = array_map(function ($key) {
274
                        return strpos($key, '/') !== false ? substr($key, strrpos($key, '/') + 1) : $key;
275
                    }, $keys);
276
                    sort($keys);
277
                    foreach ($keys as $key) {
278
                        $map[$key] = (string)Translate::get($key);
279
                    }
280
281
                    return $map;
282
                },
283
            );
284
        } catch (Throwable $e) {
285
            $map = [];
286
        }
287
288
        return $map;
289
    }
290
291
    /**
292
     * Return fast create fields per module map.
293
     *
294
     * @return array
295
     */
296
    public function fastCreateFieldsMap(): array
297
    {
298
        $defaultTitleType = Configure::read('UI.richeditor.title', []) ? 'textarea' : 'string';
299
        $map = [];
300
        $properties = (array)Configure::read(sprintf('Properties'));
301
        $uploadable = $this->getView()->get('uploadable', []);
302
        $defaults = [
303
            'objects' => [
304
                'all' => [['title' => $defaultTitleType], 'status', 'description'],
305
                'required' => ['status', 'title'],
306
            ],
307
            'media' => [
308
                'all' => [['title' => $defaultTitleType], 'status', 'name'],
309
                'required' => ['name', 'status', 'title'],
310
            ],
311
        ];
312
        foreach ($properties as $name => $prop) {
313
            $cfg = (array)Hash::get($prop, 'fastCreate', []);
314
            $defaultFieldMap = in_array($name, $uploadable) ? $defaults['media'] : $defaults['objects'];
315
            $fields = (array)Hash::get($cfg, 'all', $defaultFieldMap['all']);
316
            $required = (array)Hash::get($cfg, 'required', $defaultFieldMap['required']);
317
            $map[$name] = compact('fields', 'required');
318
        }
319
320
        return $map;
321
    }
322
323
    /**
324
     * Return html for fast create form fields.
325
     *
326
     * @param string $type The object type
327
     * @param string $prefix The prefix
328
     * @return string The html for form fields
329
     */
330
    public function fastCreateFields(string $type, string $prefix): string
331
    {
332
        $cfg = (array)Configure::read(sprintf('Properties.%s.fastCreate', $type));
333
        $fields = (array)Hash::get($cfg, 'all', ['status', 'title', 'description']);
334
        $required = (array)Hash::get($cfg, 'required', ['status', 'title']);
335
        $html = '';
336
        $jsonKeys = [];
337
        $ff = [];
338
        foreach ($fields as $field => $fieldType) {
339
            $field = is_numeric($field) ? $fieldType : $field;
340
            $fieldClass = !in_array($field, $required) ? 'fastCreateField' : 'fastCreateField required';
341
            $fieldOptions = [
342
                'id' => sprintf('%s%s', $prefix, $field),
343
                'class' => $fieldClass,
344
                'data-name' => $field,
345
                'key' => sprintf('%s-%s', $type, $field),
346
            ];
347
            if ($field === 'date_ranges') {
348
                $html .= $this->dateRange($type, $fieldOptions);
349
                continue;
350
            }
351
            if ($fieldType === 'json') {
352
                $jsonKeys[] = $field;
353
            }
354
            $this->prepareFieldOptions($field, $fieldType, $fieldOptions);
355
356
            $html .= $this->control($field, '', $fieldOptions, $type);
357
            $ff[] = $field;
358
        }
359
        $jsonKeys = array_unique(array_merge($jsonKeys, (array)Configure::read('_jsonKeys')));
360
        $jsonKeys = array_intersect($jsonKeys, $ff);
361
362
        if (!empty($jsonKeys)) {
363
            $html .= $this->Form->control('_jsonKeys', ['type' => 'hidden', 'value' => implode(',', $jsonKeys)]);
364
        }
365
366
        return $html;
367
    }
368
369
    /**
370
     * Prepare field options for field.
371
     *
372
     * @param string $field The field name
373
     * @param string|null $fieldType The field type, if any
374
     * @param array $fieldOptions The field options
375
     * @return void
376
     */
377
    public function prepareFieldOptions(string $field, ?string $fieldType, array &$fieldOptions): void
378
    {
379
        $method = '';
380
        if (!empty($fieldType) && in_array($fieldType, Control::CONTROL_TYPES)) {
381
            $methodInfo = Form::getMethod(Control::class, $fieldType);
382
            $className = (string)Hash::get($methodInfo, 0);
383
            $method = (string)Hash::get($methodInfo, 1);
384
            $preserveClass = Hash::get($fieldOptions, 'class', '');
385
            $fieldOptions = array_merge($fieldOptions, $className::$method([]));
386
            $fieldOptions['class'] .= ' ' . $preserveClass;
387
            $fieldOptions['class'] = trim($fieldOptions['class']);
388
        }
389
        if ($field === 'status') {
390
            $fieldOptions['v-model'] = 'object.attributes.status';
391
        }
392
    }
393
394
    /**
395
     * Return html for date range fields.
396
     *
397
     * @param string $type The object type
398
     * @param array $options The options
399
     * @return string The html for date range fields
400
     */
401
    public function dateRange(string $type, array $options): string
402
    {
403
        $optionsFrom = array_merge($options, [
404
            'id' => 'start_date_0',
405
            'name' => 'date_ranges[0][start_date]',
406
            'v-datepicker' => 'true',
407
            'date' => 'true',
408
            'time' => 'true',
409
            'daterange' => 'true',
410
        ]);
411
        $optionsTo = array_merge($options, [
412
            'id' => 'end_date_0',
413
            'name' => 'date_ranges[0][end_date]',
414
            'v-datepicker' => 'true',
415
            'date' => 'true',
416
            'time' => 'true',
417
            'daterange' => 'true',
418
        ]);
419
        $optionsAllDay = array_merge($options, [
420
            'id' => 'all_day_0',
421
            'name' => 'date_ranges[0][params][all_day]',
422
            'type' => 'checkbox',
423
        ]);
424
        $from = $this->control(__('From'), '', $optionsFrom, $type);
425
        $to = $this->control(__('To'), '', $optionsTo, $type);
426
        $allDay = $this->control(__('All day'), '', $optionsAllDay, $type);
427
428
        return sprintf('<div class="date-ranges-item mb-1"><div>%s%s%s</div></div>', $from, $to, $allDay);
429
    }
430
}
431