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) |
|
|
|
|
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
|
|
|
|
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.