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