Passed
Pull Request — master (#1128)
by Dante
01:27
created

SchemaHelper   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
eloc 95
c 1
b 0
f 0
dl 0
loc 293
rs 9.36

12 Methods

Rating   Name   Duplication   Size   Complexity  
A rightTypes() 0 5 1
A customControl() 0 10 2
A formatByte() 0 3 1
A translatableFields() 0 15 2
A format() 0 13 4
A formatDateTime() 0 3 1
B typeFromSchema() 0 18 9
A formatDate() 0 7 2
A formatBoolean() 0 5 2
A sortable() 0 19 3
A translatableType() 0 20 4
B controlOptions() 0 42 7
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
            return array_merge($options, [
70
                'label' => Hash::get($ctrlOptions, 'label'),
71
                'readonly' => Hash::get($ctrlOptions, 'readonly', false),
72
                'disabled' => Hash::get($ctrlOptions, 'readonly', false),
73
            ]);
74
        }
75
        if (empty($ctrlOptions['type'])) {
76
            $ctrlOptionsType = ControlType::fromSchema($schema);
77
            $ctrlOptions['type'] = $ctrlOptionsType;
78
            if (in_array($ctrlOptionsType, ['integer', 'number'])) {
79
                $ctrlOptions['step'] = $ctrlOptionsType === 'number' ? 'any' : '1';
80
                $ctrlOptions['type'] = 'number';
81
            }
82
        }
83
        // verify if there's a custom control handler for $type and $name
84
        $custom = $this->customControl($name, $value, $ctrlOptions);
85
        if (!empty($custom)) {
86
            return $custom;
87
        }
88
        $opts = [
89
            'objectType' => $objectType,
90
            'property' => $name,
91
            'value' => $value,
92
            'schema' => (array)$schema,
93
            'propertyType' => (string)$ctrlOptions['type'],
94
            'label' => Hash::get($ctrlOptions, 'label'),
95
            'readonly' => Hash::get($ctrlOptions, 'readonly', false),
96
            'disabled' => Hash::get($ctrlOptions, 'readonly', false),
97
        ];
98
        if (!empty($ctrlOptions['step'])) {
99
            $opts['step'] = $ctrlOptions['step'];
100
        }
101
102
        return Control::control($opts);
103
    }
104
105
    /**
106
     * Return custom control array if a custom handler has been defined or null otherwise.
107
     *
108
     * @param string $name Property name
109
     * @param mixed $value Property value.
110
     * @param array $options Control options.
111
     * @return array|null
112
     */
113
    protected function customControl($name, $value, array $options): ?array
114
    {
115
        $handlerClass = Hash::get($options, 'handler');
116
        if (empty($handlerClass)) {
117
            return null;
118
        }
119
        /** @var \App\Form\CustomHandlerInterface $handler */
120
        $handler = new $handlerClass();
121
122
        return $handler->control($name, $value, $options);
123
    }
124
125
    /**
126
     * Display a formatted property value using schema.
127
     *
128
     * @param mixed $value Property value.
129
     * @param array $schema Property schema array.
130
     * @return string
131
     */
132
    public function format($value, $schema = []): string
133
    {
134
        $type = static::typeFromSchema((array)$schema);
135
        $type = Inflector::variable(str_replace('-', '_', $type));
136
        $methodName = sprintf('format%s', ucfirst($type));
137
        if (method_exists($this, $methodName) && $value !== null) {
138
            return call_user_func_array([$this, $methodName], [$value]);
139
        }
140
        if (is_array($value)) {
141
            return json_encode($value);
142
        }
143
144
        return (string)$value;
145
    }
146
147
    /**
148
     * Format byte value
149
     *
150
     * @param mixed $value Property value.
151
     * @return string
152
     */
153
    protected function formatByte($value): string
154
    {
155
        return Number::toReadableSize((int)$value);
156
    }
157
158
    /**
159
     * Format boolean value
160
     *
161
     * @param mixed $value Property value.
162
     * @return string
163
     */
164
    protected function formatBoolean($value): string
165
    {
166
        $res = filter_var($value, FILTER_VALIDATE_BOOLEAN);
167
168
        return $res ? __('Yes') : __('No');
169
    }
170
171
    /**
172
     * Format date value
173
     *
174
     * @param mixed $value Property value.
175
     * @return string
176
     */
177
    protected function formatDate($value): string
178
    {
179
        if (empty($value)) {
180
            return '';
181
        }
182
183
        return (string)$this->Time->format($value);
184
    }
185
186
    /**
187
     * Format date-time value
188
     *
189
     * @param mixed $value Property value.
190
     * @return string
191
     */
192
    protected function formatDateTime($value): string
193
    {
194
        return $this->formatDate($value);
195
    }
196
197
    /**
198
     * Infer type from property schema in JSON-SCHEMA format
199
     * Possible return values:
200
     *
201
     *   'string'
202
     *   'number'
203
     *   'integer'
204
     *   'boolean'
205
     *   'array'
206
     *   'object'
207
     *   'date-time'
208
     *   'date'
209
     *
210
     * @param array $schema The property schema
211
     * @return string
212
     */
213
    public static function typeFromSchema(array $schema): string
214
    {
215
        if (!empty($schema['oneOf'])) {
216
            foreach ($schema['oneOf'] as $subSchema) {
217
                if (!empty($subSchema['type']) && $subSchema['type'] === 'null') {
218
                    continue;
219
                }
220
221
                return static::typeFromSchema($subSchema);
222
            }
223
        }
224
        if (empty($schema['type']) || !in_array($schema['type'], ControlType::SCHEMA_PROPERTY_TYPES)) {
225
            return 'string';
226
        }
227
        $format = Hash::get($schema, 'format');
228
        $isStringDate = $schema['type'] === 'string' && in_array($format, ['date', 'date-time']);
229
230
        return $isStringDate ? $format : $schema['type'];
231
    }
232
233
    /**
234
     * Provides list of translatable fields.
235
     * If set Properties.<objectType>.translatable, it will be added to translatable fields.
236
     *
237
     * @param array $schema The object type schema
238
     * @return array
239
     */
240
    public function translatableFields(array $schema): array
241
    {
242
        if (isset($schema['translatable'])) {
243
            $priorityFields = array_intersect(static::DEFAULT_TRANSLATABLE, (array)$schema['translatable']);
244
            $otherFields = array_diff((array)$schema['translatable'], $priorityFields);
245
        } else {
246
            $properties = (array)Hash::get($schema, 'properties');
247
            $priorityFields = array_intersect(static::DEFAULT_TRANSLATABLE, array_keys($properties));
248
            $otherFields = array_keys(array_filter(
249
                array_diff_key($properties, array_flip($priorityFields)),
250
                [$this, 'translatableType']
251
            ));
252
        }
253
254
        return array_unique(array_values(array_merge($priorityFields, $otherFields)));
255
    }
256
257
    /**
258
     * Helper recursive method to check if a property is translatable checking its JSON SCHEMA
259
     *
260
     * @param array $schema Property schema
261
     * @return bool
262
     */
263
    protected function translatableType(array $schema): bool
264
    {
265
        if (!empty($schema['oneOf'])) {
266
            return array_reduce(
267
                (array)$schema['oneOf'],
268
                function ($carry, $item) {
269
                    if ($carry) {
270
                        return true;
271
                    }
272
273
                    return $this->translatableType((array)$item);
274
                }
275
            );
276
        }
277
        // accept as translatable 'string' type having text/html or tex/plain 'contentMediaType'
278
        $type = (string)Hash::get($schema, 'type');
279
        $contentMediaType = Hash::get($schema, 'contentMediaType');
280
281
        return $type === 'string' &&
282
            in_array($contentMediaType, static::TRANSLATABLE_MEDIATYPES);
283
    }
284
285
    /**
286
     * Verify field's schema, return true if field is sortable.
287
     *
288
     * @param string $field The field to check
289
     * @return bool
290
     */
291
    public function sortable(string $field): bool
292
    {
293
        // exception 'date_ranges' default sortable
294
        if ($field === 'date_ranges') {
295
            return true;
296
        }
297
        $schema = (array)$this->_View->get('schema');
298
        $schema = Hash::get($schema, sprintf('properties.%s', $field), []);
299
300
        // empty schema, then not sortable
301
        if (empty($schema)) {
302
            return false;
303
        }
304
        $type = self::typeFromSchema($schema);
305
306
        // not sortable: 'array', 'object'
307
        // other types are sortable: 'string', 'number', 'integer', 'boolean', 'date-time', 'date'
308
309
        return !in_array($type, ['array', 'object']);
310
    }
311
312
    /**
313
     * Return unique right types from schema "relationsSchema".
314
     *
315
     * @return array
316
     * @deprecated It will be removed in version 5.x.
317
     */
318
    public function rightTypes(): array
319
    {
320
        $relationsSchema = (array)$this->_View->get('relationsSchema');
321
322
        return \App\Utility\Schema::rightTypes($relationsSchema);
323
    }
324
}
325