PropertyHelper::control()   B
last analyzed

Complexity

Conditions 10
Paths 32

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 15
nc 32
nop 4
dl 0
loc 22
rs 7.6666
c 0
b 0
f 0

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