Completed
Push — master ( b1bbae...a2502b )
by Dante
23s queued 23s
created

PropertyHelper::translationsMap()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 51
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 7
eloc 38
c 1
b 1
f 0
nc 3
nop 0
dl 0
loc 51
rs 8.3786

How to fix   Long Method   

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 (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 = (string)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
                    $properties = (array)Configure::read(sprintf('Properties'));
214
                    $removeKeys = ['_element', '_hide', '_keep'];
215
                    foreach ($properties as $name => $prop) {
216
                        $keys[] = $name;
217
                        $keys = array_merge($keys, (array)Hash::get($prop, 'fastCreate.all', []));
218
                        $groups = array_keys((array)Hash::get($prop, 'view', []));
219
                        $addKeys = array_reduce($groups, function ($carry, $group) use ($prop, $removeKeys) {
220
                            $carry[] = $group;
221
                            $groupKeys = (array)Hash::get($prop, sprintf('view.%s', $group), []);
222
                            $groupKeys = array_filter(
223
                                $groupKeys,
224
                                function ($val, $key) use ($removeKeys) {
225
                                    return is_string($val) && !in_array($key, $removeKeys);
226
                                },
227
                                ARRAY_FILTER_USE_BOTH
228
                            );
229
230
                            return array_merge($carry, $groupKeys);
231
                        }, []);
232
                        $keys = array_merge($keys, $addKeys);
233
                    }
234
                    $keys = array_map(function ($key) {
235
                        return is_array($key) ? array_key_first($key) : $key;
236
                    }, $keys);
237
                    $keys = array_diff($keys, $removeKeys);
238
                    $keys = array_unique($keys);
239
                    $keys = array_map(function ($key) {
240
                        return strpos($key, '/') !== false ? substr($key, strrpos($key, '/') + 1) : $key;
241
                    }, $keys);
242
                    sort($keys);
243
                    foreach ($keys as $key) {
244
                        $map[$key] = (string)Translate::get($key);
245
                    }
246
247
                    return $map;
248
                }
249
            );
250
        } catch (\Throwable $e) {
251
            $map = [];
252
        }
253
254
        return $map;
255
    }
256
257
    /**
258
     * Return fast create fields per module map.
259
     *
260
     * @return array
261
     */
262
    public function fastCreateFieldsMap(): array
263
    {
264
        $defaultTitleType = Configure::read('UI.richeditor.title', []) ? 'textarea' : 'string';
265
        $map = [];
266
        $properties = (array)Configure::read(sprintf('Properties'));
267
        $uploadable = $this->getView()->get('uploadable', []);
268
        $defaults = [
269
            'objects' => [
270
                'all' => [['title' => $defaultTitleType], 'status', 'description'],
271
                'required' => ['status', 'title'],
272
            ],
273
            'media' => [
274
                'all' => [['title' => $defaultTitleType], 'status', 'name'],
275
                'required' => ['name', 'status', 'title'],
276
            ],
277
        ];
278
        foreach ($properties as $name => $prop) {
279
            $cfg = (array)Hash::get($prop, 'fastCreate', []);
280
            $defaultFieldMap = in_array($name, $uploadable) ? $defaults['media'] : $defaults['objects'];
281
            $fields = (array)Hash::get($cfg, 'all', $defaultFieldMap['all']);
282
            $required = (array)Hash::get($cfg, 'required', $defaultFieldMap['required']);
283
            $map[$name] = compact('fields', 'required');
284
        }
285
286
        return $map;
287
    }
288
289
    /**
290
     * Return html for fast create form fields.
291
     *
292
     * @param string $type The object type
293
     * @param string $prefix The prefix
294
     * @return string The html for form fields
295
     */
296
    public function fastCreateFields(string $type, string $prefix): string
297
    {
298
        $cfg = (array)Configure::read(sprintf('Properties.%s.fastCreate', $type));
299
        $fields = (array)Hash::get($cfg, 'all', ['status', 'title', 'description']);
300
        $required = (array)Hash::get($cfg, 'required', ['status', 'title']);
301
        $html = '';
302
        $jsonKeys = [];
303
        $ff = [];
304
        foreach ($fields as $field => $fieldType) {
305
            $field = is_numeric($field) ? $fieldType : $field;
306
            $fieldClass = !in_array($field, $required) ? 'fastCreateField' : 'fastCreateField required';
307
            $fieldOptions = [
308
                'id' => sprintf('%s%s', $prefix, $field),
309
                'class' => $fieldClass,
310
                'data-name' => $field,
311
                'key' => sprintf('%s-%s', $type, $field),
312
            ];
313
            if ($field === 'date_ranges') {
314
                $html .= $this->dateRange($type, $fieldOptions);
315
                continue;
316
            }
317
            if ($fieldType === 'json') {
318
                $jsonKeys[] = $field;
319
            }
320
            $this->prepareFieldOptions($field, $fieldType, $fieldOptions);
321
322
            $html .= $this->control($field, '', $fieldOptions, $type);
323
            $ff[] = $field;
324
        }
325
        $jsonKeys = array_unique(array_merge($jsonKeys, (array)Configure::read('_jsonKeys')));
326
        $jsonKeys = array_intersect($jsonKeys, $ff);
327
328
        if (!empty($jsonKeys)) {
329
            $html .= $this->Form->control('_jsonKeys', ['type' => 'hidden', 'value' => implode(',', $jsonKeys)]);
330
        }
331
332
        return $html;
333
    }
334
335
    /**
336
     * Prepare field options for field.
337
     *
338
     * @param string $field The field name
339
     * @param string|null $fieldType The field type, if any
340
     * @param array $fieldOptions The field options
341
     * @return void
342
     */
343
    public function prepareFieldOptions(string $field, ?string $fieldType, array &$fieldOptions): void
344
    {
345
        $method = '';
346
        if (!empty($fieldType) && in_array($fieldType, Control::CONTROL_TYPES)) {
347
            $methodInfo = Form::getMethod(Control::class, $fieldType);
348
            $className = (string)Hash::get($methodInfo, 0);
349
            $method = (string)Hash::get($methodInfo, 1);
350
            $preserveClass = Hash::get($fieldOptions, 'class', '');
351
            $fieldOptions = array_merge($fieldOptions, $className::$method([]));
352
            $fieldOptions['class'] .= ' ' . $preserveClass;
353
            $fieldOptions['class'] = trim($fieldOptions['class']);
354
        }
355
        if ($field === 'status') {
356
            $fieldOptions['v-model'] = 'object.attributes.status';
357
        }
358
    }
359
360
    /**
361
     * Return html for date range fields.
362
     *
363
     * @param string $type The object type
364
     * @param array $options The options
365
     * @return string The html for date range fields
366
     */
367
    public function dateRange(string $type, array $options): string
368
    {
369
        $optionsFrom = array_merge($options, [
370
            'id' => 'start_date_0',
371
            'name' => 'date_ranges[0][start_date]',
372
            'v-datepicker' => 'true',
373
            'date' => 'true',
374
            'time' => 'true',
375
            'daterange' => 'true',
376
        ]);
377
        $optionsTo = array_merge($options, [
378
            'id' => 'end_date_0',
379
            'name' => 'date_ranges[0][end_date]',
380
            'v-datepicker' => 'true',
381
            'date' => 'true',
382
            'time' => 'true',
383
            'daterange' => 'true',
384
        ]);
385
        $optionsAllDay = array_merge($options, [
386
            'id' => 'all_day_0',
387
            'name' => 'date_ranges[0][params][all_day]',
388
            'type' => 'checkbox',
389
        ]);
390
        $from = $this->control(__('From'), '', $optionsFrom, $type);
391
        $to = $this->control(__('To'), '', $optionsTo, $type);
392
        $allDay = $this->control(__('All day'), '', $optionsAllDay, $type);
393
394
        return sprintf('<div class="date-ranges-item mb-1"><div>%s%s%s</div></div>', $from, $to, $allDay);
395
    }
396
}
397