Test Setup Failed
Push — master ( 6592af...c06444 )
by Chauncey
08:19
created

StorablePropertyTrait::isValidFieldKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Charcoal\Property;
4
5
use InvalidArgumentException;
6
7
// From 'charcoal-translator'
8
use Charcoal\Translator\Translation;
9
10
// From 'charcoal-property'
11
use Charcoal\Property\PropertyField;
12
13
/**
14
 *
15
 */
16
trait StorablePropertyTrait
17
{
18
    /**
19
     * An empty value implies that the property will inherit the table's encoding.
20
     *
21
     * @var string|null
22
     */
23
    private $sqlEncoding;
24
25
    /**
26
     * The property's identifier formatted for field names.
27
     *
28
     * @var string
29
     */
30
    private $fieldIdent;
31
32
    /**
33
     * Store of the property's storage fields.
34
     *
35
     * @var PropertyField[]
36
     */
37
    protected $fields;
38
39
    /**
40
     * Store of the property's storage field identifiers.
41
     *
42
     * @var string[]
43
     */
44
    protected $fieldNames;
45
46
    /**
47
     * Holds a list of all snake_case strings.
48
     *
49
     * @var string[]
50
     */
51
    protected static $snakeCache = [];
52
53
    /**
54
     * Retrieve the property's storage fields.
55
     *
56
     * @param  mixed $val The value to set as field value.
57
     * @return PropertyField[]
58
     */
59
    public function fields($val = null)
60
    {
61
        if (empty($this->fields)) {
62
            $this->fields = $this->generateFields($val);
63
        } else {
64
            $this->fields = $this->updatedFields($this->fields, $val);
65
        }
66
67
        return $this->fields;
68
    }
69
70
    /**
71
     * Retrieve the property's identifier formatted for field names.
72
     *
73
     * @param  string|null $key The field key to suffix to the property identifier.
74
     * @return string|null Returns the property's field name.
75
     *     If $key is provided, returns the namespaced field name otherwise NULL.
76
     */
77
    public function fieldIdent($key = null)
78
    {
79
        if ($this->fieldIdent === null) {
80
            $this->fieldIdent = $this->snakeize($this['ident']);
81
        }
82
83
        if ($key === null || $key === '') {
84
            return $this->fieldIdent;
85
        }
86
87
        if ($this->isValidFieldKey($key)) {
88
            return $this->fieldIdent.'_'.$this->snakeize($key);
89
        }
90
91
        return null;
92
    }
93
94
    /**
95
     * Retrieve the property's namespaced storage field names.
96
     *
97
     * Examples:
98
     * 1. `name`: `name_en`, `name_fr`, `name_de`
99
     * 2. `obj`: `obj_id`, `obj_type`
100
     * 3. `file`: `file`, `file_type`
101
     * 4. `opt`: `opt_0`, `opt_1`, `opt_2`
102
     *
103
     * @return string[]
104
     */
105
    public function fieldNames()
106
    {
107
        if ($this->fieldNames === null) {
108
            $names = [];
109
            if ($this['l10n']) {
110
                $keys = $this->translator()->availableLocales();
111
                foreach ($keys as $key) {
112
                    $names[$key] = $this->fieldIdent($key);
113
                }
114
            } else {
115
                $names[''] = $this->fieldIdent();
116
            }
117
118
            $this->fieldNames = $names;
119
        }
120
121
        return $this->fieldNames;
122
    }
123
124
    /**
125
     * Retrieve the property's value in a format suitable for the given field key.
126
     *
127
     * @param  string $key The property field key.
128
     * @param  mixed  $val The value to set as field value.
129
     * @return mixed
130
     */
131
    protected function fieldValue($key, $val)
132
    {
133
        if ($val === null) {
134
            return null;
135
        }
136
137
        if (is_scalar($val)) {
138
            return $this->storageVal($val);
139
        }
140
141
        if (!$this->isValidFieldKey($key)) {
142
            return $this->storageVal($val);
143
        }
144
145
        if (isset($val[$key])) {
146
            return $this->storageVal($val[$key]);
147
        }
148
149
        return null;
150
    }
151
152
    /**
153
     * Retrieve the property's value in a format suitable for storage.
154
     *
155
     * @param  mixed $val The value to convert for storage.
156
     * @return mixed
157
     */
158
    public function storageVal($val)
159
    {
160
        if ($val === null) {
161
            // Do not json_encode NULL values
162
            return null;
163
        }
164
165
        if ($this['allowNull'] && $val === '') {
166
            return null;
167
        }
168
169
        if (!$this['l10n'] && $val instanceof Translation) {
170
            $val = (string)$val;
171
        }
172
173
        if ($this['multiple']) {
174
            if (is_array($val)) {
175
                $val = implode($this->multipleSeparator(), $val);
176
            }
177
        }
178
179
        if (!is_scalar($val)) {
180
            return json_encode($val, JSON_UNESCAPED_UNICODE);
181
        }
182
183
        return $val;
184
    }
185
186
    /**
187
     * Parse the property's value (from a flattened structure)
188
     * in a format suitable for models.
189
     *
190
     * This method takes a one-dimensional array and, depending on
191
     * the property's {@see self::fieldNames() field structure},
192
     * returns a complex array.
193
     *
194
     * @param  array $flatData The model data subset.
195
     * @return mixed
196
     */
197
    public function parseFromFlatData(array $flatData)
198
    {
199
        $value = null;
200
201
        $fieldNames = $this->fieldNames();
202
        foreach ($fieldNames as $fieldKey => $fieldName) {
203
            if ($this->isValidFieldKey($fieldKey)) {
204
                if (isset($flatData[$fieldName])) {
205
                    $value[$fieldKey] = $flatData[$fieldName];
206
                }
207
            } elseif (isset($flatData[$fieldName])) {
208
                $value = $flatData[$fieldName];
209
            }
210
        }
211
212
        return $value;
213
    }
214
215
    /**
216
     * Update the property's storage fields.
217
     *
218
     * @param  PropertyField[] $fields The storage fields to update.
219
     * @param  mixed           $val    The value to set as field value.
220
     * @return PropertyField[]
221
     */
222
    protected function updatedFields(array $fields, $val)
223
    {
224
        if (empty($fields)) {
225
            $fields = $this->generateFields($val);
226
        }
227
228
        foreach ($fields as $fieldKey => $field) {
229
            $fields[$fieldKey]->setVal($this->fieldValue($fieldKey, $val));
230
        }
231
232
        return $fields;
233
    }
234
235
    /**
236
     * Reset the property's storage fields.
237
     *
238
     * @param  mixed $val The value to set as field value.
239
     * @return PropertyField[]
240
     */
241
    protected function generateFields($val = null)
242
    {
243
        $fields = [];
244
245
        $fieldNames = $this->fieldNames();
246
        foreach ($fieldNames as $fieldKey => $fieldName) {
247
            $field = $this->createPropertyField([
248
                'ident'       => $fieldName,
249
                'sqlType'     => $this->sqlType(),
250
                'sqlPdoType'  => $this->sqlPdoType(),
251
                'sqlEncoding' => $this->sqlEncoding(),
252
                'extra'       => $this->sqlExtra(),
253
                'val'         => $this->fieldValue($fieldKey, $val),
254
                'defaultVal'  => $this->sqlDefaultVal(),
255
                'allowNull'   => $this['allowNull'],
256
            ]);
257
258
            $fields[$fieldKey] = $field;
259
        }
260
261
        return $fields;
262
    }
263
264
    /**
265
     * @param  array $data Optional. Field data.
266
     * @return PropertyField
267
     */
268
    protected function createPropertyField(array $data = null)
269
    {
270
        $field = new PropertyField();
271
272
        if ($data !== null) {
273
            $field->setData($data);
274
        }
275
276
        return $field;
277
    }
278
279
    /**
280
     * Determine if the given value is a valid field key suffix.
281
     *
282
     * @param  mixed $key The key to test.
283
     * @return boolean
284
     */
285
    protected function isValidFieldKey($key)
286
    {
287
        return (!empty($key) || is_numeric($key));
288
    }
289
290
    /**
291
     * Transform a string from "camelCase" to "snake_case".
292
     *
293
     * @param  string $value The string to snakeize.
294
     * @return string The snake_case string.
295
     */
296 View Code Duplication
    protected function snakeize($value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
297
    {
298
        $key = $value;
299
300
        if (isset(static::$snakeCache[$key])) {
301
            return static::$snakeCache[$key];
302
        }
303
304
        $value = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $value));
305
306
        static::$snakeCache[$key] = $value;
307
308
        return static::$snakeCache[$key];
309
    }
310
311
    /**
312
     * Set the property's SQL encoding & collation.
313
     *
314
     * @param  string|null $encoding The encoding identifier or SQL encoding and collation.
315
     * @throws InvalidArgumentException  If the identifier is not a string.
316
     * @return self
317
     */
318
    public function setSqlEncoding($encoding)
319
    {
320
        if (!is_string($encoding) && $encoding !== null) {
321
            throw new InvalidArgumentException(
322
                'Encoding ident needs to be string.'
323
            );
324
        }
325
326
        if ($encoding === 'utf8mb4') {
327
            $encoding = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
328
        }
329
330
        $this->sqlEncoding = $encoding;
331
        return $this;
332
    }
333
334
    /**
335
     * Retrieve the property's SQL encoding & collation.
336
     *
337
     * @return string|null
338
     */
339
    public function sqlEncoding()
340
    {
341
        return $this->sqlEncoding;
342
    }
343
344
    /**
345
     * @return string|null
346
     */
347
    public function sqlExtra()
348
    {
349
        return null;
350
    }
351
352
    /**
353
     * @return string|null
354
     */
355
    public function sqlDefaultVal()
356
    {
357
        return null;
358
    }
359
360
    /**
361
     * @return string|null
362
     */
363
    abstract public function sqlType();
364
365
    /**
366
     * @return integer
367
     */
368
    abstract public function sqlPdoType();
369
370
    /**
371
     * @return string
372
     */
373
    abstract public function getIdent();
374
375
    /**
376
     * @return boolean
377
     */
378
    abstract public function getL10n();
379
380
    /**
381
     * @return boolean
382
     */
383
    abstract public function getMultiple();
384
385
    /**
386
     * @return string
387
     */
388
    abstract public function multipleSeparator();
389
390
    /**
391
     * @return boolean
392
     */
393
    abstract public function getAllowNull();
394
395
    /**
396
     * @return \Charcoal\Translator\Translator
397
     */
398
    abstract protected function translator();
399
}
400