Passed
Pull Request — master (#1214)
by Stefano
12:58 queued 11:24
created

SchemaHelper   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 121
dl 0
loc 347
rs 6.96
c 3
b 2
f 0
wmc 53

14 Methods

Rating   Name   Duplication   Size   Complexity  
A customControl() 0 10 2
B controlOptions() 0 45 7
A formatByte() 0 3 1
A translatableFields() 0 15 2
A format() 0 13 4
B typeFromSchema() 0 18 9
A formatDateTime() 0 3 1
A updateRicheditorOptions() 0 9 3
A formatDate() 0 7 2
A formatBoolean() 0 5 2
A translatableType() 0 20 4
A filterList() 0 15 6
A filterListByType() 0 12 6
A sortable() 0 19 4

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2019 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
14
namespace App\View\Helper;
15
16
use App\Form\Control;
17
use App\Form\ControlType;
18
use App\Form\Options;
19
use Cake\Core\Configure;
20
use Cake\I18n\Number;
21
use Cake\Utility\Hash;
22
use Cake\Utility\Inflector;
23
use Cake\View\Helper;
24
25
/**
26
 * Schema helper
27
 *
28
 * @property \Cake\View\Helper\TimeHelper $Time
29
 */
30
class SchemaHelper extends Helper
31
{
32
    /**
33
     * {@inheritDoc}
34
     *
35
     * @var array
36
     */
37
    public $helpers = ['Time'];
38
39
    /**
40
     * Default translatable fields to be prepended in translations
41
     *
42
     * @var array
43
     */
44
    public const DEFAULT_TRANSLATABLE = ['title', 'description', 'body'];
45
46
    /**
47
     * Translatable media types
48
     *
49
     * @var array
50
     */
51
    public const TRANSLATABLE_MEDIATYPES = ['text/html', 'text/plain'];
52
53
    /**
54
     * Get control options for a property schema.
55
     *
56
     * @param string $name Property name.
57
     * @param mixed $value Property value.
58
     * @param array|null $schema Property schema.
59
     * @return array
60
     */
61
    public function controlOptions(string $name, $value, ?array $schema = null): array
62
    {
63
        $options = Options::customControl($name, $value);
64
        $objectType = (string)$this->_View->get('objectType');
65
        $ctrlOptionsPath = sprintf('Properties.%s.options.%s', $objectType, $name);
66
        $ctrlOptions = (array)Configure::read($ctrlOptionsPath);
67
68
        if (!empty($options)) {
69
            $this->updateRicheditorOptions($name, !empty($schema['placeholders']), $options);
70
71
            return array_merge($options, [
72
                'label' => Hash::get($ctrlOptions, 'label'),
73
                'readonly' => Hash::get($ctrlOptions, 'readonly', false),
74
                'disabled' => Hash::get($ctrlOptions, 'readonly', false),
75
            ]);
76
        }
77
        if (empty($ctrlOptions['type'])) {
78
            $ctrlOptionsType = ControlType::fromSchema($schema);
79
            $ctrlOptions['type'] = $ctrlOptionsType;
80
            if (in_array($ctrlOptionsType, ['integer', 'number'])) {
81
                $ctrlOptions['step'] = $ctrlOptionsType === 'number' ? 'any' : '1';
82
                $ctrlOptions['type'] = 'number';
83
            }
84
        }
85
        // verify if there's a custom control handler for $type and $name
86
        $custom = $this->customControl($name, $value, $ctrlOptions);
87
        if (!empty($custom)) {
88
            return $custom;
89
        }
90
        $opts = [
91
            'objectType' => $objectType,
92
            'property' => $name,
93
            'value' => $value,
94
            'schema' => (array)$schema,
95
            'propertyType' => (string)$ctrlOptions['type'],
96
            'label' => Hash::get($ctrlOptions, 'label'),
97
            'readonly' => Hash::get($ctrlOptions, 'readonly', false),
98
            'disabled' => Hash::get($ctrlOptions, 'readonly', false),
99
        ];
100
        if (!empty($ctrlOptions['step'])) {
101
            $opts['step'] = $ctrlOptions['step'];
102
        }
103
        $this->updateRicheditorOptions($name, !empty($schema['placeholders']), $opts);
104
105
        return Control::control($opts);
106
    }
107
108
    /**
109
     * Update richeditor options if a toolbar config is defined in UI.richeditor for the property.
110
     *
111
     * @param string $name Property name
112
     * @param bool $placeholders True if property has placeholders in schema
113
     * @param array $options Control options
114
     * @return void
115
     */
116
    protected function updateRicheditorOptions(string $name, bool $placeholders, array &$options)
117
    {
118
        $uiRichtext = (array)Configure::read(sprintf('UI.richeditor.%s.toolbar', $name));
119
        if (empty($uiRichtext)) {
120
            return;
121
        }
122
        $options['type'] = 'textarea';
123
        $richeditorKey = $placeholders ? 'v-richeditor.placeholders' : 'v-richeditor';
124
        $options[$richeditorKey] = json_encode($uiRichtext);
125
    }
126
127
    /**
128
     * Return custom control array if a custom handler has been defined or null otherwise.
129
     *
130
     * @param string $name Property name
131
     * @param mixed $value Property value.
132
     * @param array $options Control options.
133
     * @return array|null
134
     */
135
    protected function customControl($name, $value, array $options): ?array
136
    {
137
        $handlerClass = Hash::get($options, 'handler');
138
        if (empty($handlerClass)) {
139
            return null;
140
        }
141
        /** @var \App\Form\CustomHandlerInterface $handler */
142
        $handler = new $handlerClass();
143
144
        return $handler->control($name, $value, $options);
145
    }
146
147
    /**
148
     * Display a formatted property value using schema.
149
     *
150
     * @param mixed $value Property value.
151
     * @param array $schema Property schema array.
152
     * @return string
153
     */
154
    public function format($value, $schema = []): string
155
    {
156
        $type = static::typeFromSchema((array)$schema);
157
        $type = Inflector::variable(str_replace('-', '_', $type));
158
        $methodName = sprintf('format%s', ucfirst($type));
159
        if (method_exists($this, $methodName) && $value !== null) {
160
            return call_user_func_array([$this, $methodName], [$value]);
161
        }
162
        if (is_array($value)) {
163
            return json_encode($value);
164
        }
165
166
        return (string)$value;
167
    }
168
169
    /**
170
     * Format byte value
171
     *
172
     * @param mixed $value Property value.
173
     * @return string
174
     */
175
    protected function formatByte($value): string
176
    {
177
        return Number::toReadableSize((int)$value);
178
    }
179
180
    /**
181
     * Format boolean value
182
     *
183
     * @param mixed $value Property value.
184
     * @return string
185
     */
186
    protected function formatBoolean($value): string
187
    {
188
        $res = filter_var($value, FILTER_VALIDATE_BOOLEAN);
189
190
        return $res ? __('Yes') : __('No');
191
    }
192
193
    /**
194
     * Format date value
195
     *
196
     * @param mixed $value Property value.
197
     * @return string
198
     */
199
    protected function formatDate($value): string
200
    {
201
        if (empty($value)) {
202
            return '';
203
        }
204
205
        return (string)$this->Time->format($value);
206
    }
207
208
    /**
209
     * Format date-time value
210
     *
211
     * @param mixed $value Property value.
212
     * @return string
213
     */
214
    protected function formatDateTime($value): string
215
    {
216
        return $this->formatDate($value);
217
    }
218
219
    /**
220
     * Infer type from property schema in JSON-SCHEMA format
221
     * Possible return values:
222
     *
223
     *   'string'
224
     *   'number'
225
     *   'integer'
226
     *   'boolean'
227
     *   'array'
228
     *   'object'
229
     *   'date-time'
230
     *   'date'
231
     *
232
     * @param array $schema The property schema
233
     * @return string
234
     */
235
    public static function typeFromSchema(array $schema): string
236
    {
237
        if (!empty($schema['oneOf'])) {
238
            foreach ($schema['oneOf'] as $subSchema) {
239
                if (!empty($subSchema['type']) && $subSchema['type'] === 'null') {
240
                    continue;
241
                }
242
243
                return static::typeFromSchema($subSchema);
244
            }
245
        }
246
        if (empty($schema['type']) || !in_array($schema['type'], ControlType::SCHEMA_PROPERTY_TYPES)) {
247
            return 'string';
248
        }
249
        $format = Hash::get($schema, 'format');
250
        $isStringDate = $schema['type'] === 'string' && in_array($format, ['date', 'date-time']);
251
252
        return $isStringDate ? $format : $schema['type'];
253
    }
254
255
    /**
256
     * Provides list of translatable fields.
257
     * If set Properties.<objectType>.translatable, it will be added to translatable fields.
258
     *
259
     * @param array $schema The object type schema
260
     * @return array
261
     */
262
    public function translatableFields(array $schema): array
263
    {
264
        if (isset($schema['translatable'])) {
265
            $priorityFields = array_intersect(static::DEFAULT_TRANSLATABLE, (array)$schema['translatable']);
266
            $otherFields = array_diff((array)$schema['translatable'], $priorityFields);
267
        } else {
268
            $properties = (array)Hash::get($schema, 'properties');
269
            $priorityFields = array_intersect(static::DEFAULT_TRANSLATABLE, array_keys($properties));
270
            $otherFields = array_keys(array_filter(
271
                array_diff_key($properties, array_flip($priorityFields)),
272
                [$this, 'translatableType']
273
            ));
274
        }
275
276
        return array_unique(array_values(array_merge($priorityFields, $otherFields)));
277
    }
278
279
    /**
280
     * Helper recursive method to check if a property is translatable checking its JSON SCHEMA
281
     *
282
     * @param array $schema Property schema
283
     * @return bool
284
     */
285
    protected function translatableType(array $schema): bool
286
    {
287
        if (!empty($schema['oneOf'])) {
288
            return array_reduce(
289
                (array)$schema['oneOf'],
290
                function ($carry, $item) {
291
                    if ($carry) {
292
                        return true;
293
                    }
294
295
                    return $this->translatableType((array)$item);
296
                }
297
            );
298
        }
299
        // accept as translatable 'string' type having text/html or tex/plain 'contentMediaType'
300
        $type = (string)Hash::get($schema, 'type');
301
        $contentMediaType = Hash::get($schema, 'contentMediaType');
302
303
        return $type === 'string' &&
304
            in_array($contentMediaType, static::TRANSLATABLE_MEDIATYPES);
305
    }
306
307
    /**
308
     * Verify field's schema, return true if field is sortable.
309
     *
310
     * @param string $field The field to check
311
     * @return bool
312
     */
313
    public function sortable(string $field): bool
314
    {
315
        // default sortable fields
316
        if (in_array($field, ['date_ranges', 'modified', 'id', 'title'])) {
317
            return true;
318
        }
319
        $schema = (array)$this->_View->get('schema');
320
        $customProps = (array)$this->_View->get('customProps');
321
        $schema = Hash::get($schema, sprintf('properties.%s', $field), []);
322
        // empty schema or field is a custom prop, then not sortable
323
        if (empty($schema) || in_array($field, $customProps)) {
324
            return false;
325
        }
326
        $type = self::typeFromSchema($schema);
327
328
        // not sortable: 'array', 'object'
329
        // other types are sortable: 'string', 'number', 'integer', 'boolean', 'date-time', 'date'
330
331
        return !in_array($type, ['array', 'object']);
332
    }
333
334
    /**
335
     * Get filter list from filters and schema properties
336
     *
337
     * @param array $filters Filters list
338
     * @param array|null $schemaProperties Schema properties
339
     * @return array
340
     */
341
    public function filterList(array $filters, ?array $schemaProperties): array
342
    {
343
        $list = [];
344
        foreach ($filters as $f) {
345
            $fname = is_array($f) ? (string)Hash::get($f, 'name', __('untitled')) : $f;
346
            $flabel = is_array($f) ? (string)Hash::get($f, 'label', $fname) : Inflector::humanize($f);
347
            $noProperties = empty($schemaProperties) || !array_key_exists($fname, $schemaProperties);
348
            $schema = $noProperties ? null : (array)Hash::get($schemaProperties, $fname);
349
            $item = self::controlOptions($fname, null, $schema);
350
            $item['name'] = $fname;
351
            $item['label'] = $flabel;
352
            $list[] = $item;
353
        }
354
355
        return $list;
356
    }
357
358
    /**
359
     * Get filter list by type
360
     *
361
     * @param array $filtersByType Filters list by type
362
     * @param array|null $schemasByType Schema properties by type
363
     * @return array
364
     */
365
    public function filterListByType(array $filtersByType, ?array $schemasByType): array
366
    {
367
        $list = [];
368
        foreach ($filtersByType as $type => $filters) {
369
            $noSchema = empty($schemasByType) || !array_key_exists($type, $schemasByType);
370
            $schemaProperties = $noSchema ? null : Hash::get($schemasByType, $type);
371
            $schemaProperties = array_key_exists('properties', (array)$schemaProperties) ? $schemaProperties['properties'] : $schemaProperties;
372
            $schemaProperties = $schemaProperties !== false ? $schemaProperties : null;
373
            $list[$type] = self::filterList($filters, $schemaProperties);
374
        }
375
376
        return $list;
377
    }
378
}
379