Passed
Pull Request — master (#951)
by Stefano
01:09
created

SchemaHelper::customControl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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