1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Charcoal\Model; |
4
|
|
|
|
5
|
|
|
use PDO; |
6
|
|
|
use PDOException; |
7
|
|
|
use DateTimeInterface; |
8
|
|
|
use UnexpectedValueException; |
9
|
|
|
|
10
|
|
|
// From PSR-3 |
11
|
|
|
use Psr\Log\LoggerAwareInterface; |
12
|
|
|
use Psr\Log\LoggerAwareTrait; |
13
|
|
|
use Psr\Log\NullLogger; |
14
|
|
|
|
15
|
|
|
// From Pimple |
16
|
|
|
use Pimple\Container; |
17
|
|
|
|
18
|
|
|
// From 'charcoal-config' |
19
|
|
|
use Charcoal\Config\AbstractEntity; |
20
|
|
|
|
21
|
|
|
// From 'charcoal-view' |
22
|
|
|
use Charcoal\View\ViewableInterface; |
23
|
|
|
use Charcoal\View\ViewableTrait; |
24
|
|
|
|
25
|
|
|
// From 'charcoal-property' |
26
|
|
|
use Charcoal\Property\DescribablePropertyInterface; |
27
|
|
|
use Charcoal\Property\DescribablePropertyTrait; |
28
|
|
|
use Charcoal\Property\PropertyInterface; |
29
|
|
|
|
30
|
|
|
// From 'charcoal-core' |
31
|
|
|
use Charcoal\Model\DescribableInterface; |
32
|
|
|
use Charcoal\Model\DescribableTrait; |
33
|
|
|
use Charcoal\Model\ModelInterface; |
34
|
|
|
use Charcoal\Model\ModelMetadata; |
35
|
|
|
use Charcoal\Model\ModelValidator; |
36
|
|
|
use Charcoal\Source\StorableTrait; |
37
|
|
|
use Charcoal\Validator\ValidatableInterface; |
38
|
|
|
use Charcoal\Validator\ValidatableTrait; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* An abstract class that implements most of `ModelInterface`. |
42
|
|
|
* |
43
|
|
|
* In addition to `ModelInterface`, the abstract model implements the following interfaces: |
44
|
|
|
* |
45
|
|
|
* - `DescribableInterface` |
46
|
|
|
* - `StorableInterface |
47
|
|
|
* - `ValidatableInterface` |
48
|
|
|
* - `ViewableInterface`. |
49
|
|
|
* |
50
|
|
|
* Those interfaces are implemented (in parts, at least) with |
51
|
|
|
* `DescribableTrait`, `StorableTrait`, `ValidatableTrait`, and `ViewableTrait`. |
52
|
|
|
* |
53
|
|
|
* The `JsonSerializable` interface is fully provided by the `DescribableTrait`. |
54
|
|
|
*/ |
55
|
|
|
abstract class AbstractModel extends AbstractEntity implements |
56
|
|
|
ModelInterface, |
57
|
|
|
DescribablePropertyInterface, |
58
|
|
|
LoggerAwareInterface, |
59
|
|
|
ValidatableInterface, |
60
|
|
|
ViewableInterface |
61
|
|
|
{ |
62
|
|
|
use LoggerAwareTrait; |
63
|
|
|
use DescribableTrait; |
64
|
|
|
use DescribablePropertyTrait; |
65
|
|
|
use StorableTrait; |
66
|
|
|
use ValidatableTrait; |
67
|
|
|
use ViewableTrait; |
68
|
|
|
|
69
|
|
|
const DEFAULT_SOURCE_TYPE = 'database'; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @param array $data Dependencies. |
73
|
|
|
*/ |
74
|
|
|
public function __construct(array $data = null) |
75
|
|
|
{ |
76
|
|
|
// LoggerAwareInterface dependencies |
77
|
|
|
$this->setLogger($data['logger']); |
78
|
|
|
|
79
|
|
|
// Optional DescribableInterface dependencies |
80
|
|
|
if (isset($data['property_factory'])) { |
81
|
|
|
$this->setPropertyFactory($data['property_factory']); |
82
|
|
|
} |
83
|
|
|
if (isset($data['metadata'])) { |
84
|
|
|
$this->setMetadata($data['metadata']); |
85
|
|
|
} |
86
|
|
|
if (isset($data['metadata_loader'])) { |
87
|
|
|
$this->setMetadataLoader($data['metadata_loader']); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
// Optional StorableInterface dependencies |
91
|
|
|
if (isset($data['source'])) { |
92
|
|
|
$this->setSource($data['source']); |
93
|
|
|
} |
94
|
|
|
if (isset($data['source_factory'])) { |
95
|
|
|
$this->setSourceFactory($data['source_factory']); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
// Optional ViewableInterface dependencies |
99
|
|
|
if (isset($data['view'])) { |
100
|
|
|
$this->setView($data['view']); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
// Optional dependencies injection via Pimple Container |
104
|
|
|
if (isset($data['container'])) { |
105
|
|
|
$this->setDependencies($data['container']); |
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Sets the object data, from an associative array map (or any other Traversable). |
111
|
|
|
* |
112
|
|
|
* @param array $data The entity data. Will call setters. |
113
|
|
|
* @return self |
114
|
|
|
*/ |
115
|
|
|
public function setData(array $data) |
116
|
|
|
{ |
117
|
|
|
$data = $this->setIdFromData($data); |
118
|
|
|
|
119
|
|
|
parent::setData($data); |
120
|
|
|
return $this; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Retrieve the model data as a structure (serialize to array). |
125
|
|
|
* |
126
|
|
|
* @param array $properties Optional. List of property identifiers |
127
|
|
|
* for retrieving a subset of data. |
128
|
|
|
* @return array |
129
|
|
|
*/ |
130
|
|
|
public function data(array $properties = null) |
131
|
|
|
{ |
132
|
|
|
$data = []; |
133
|
|
|
$properties = $this->properties($properties); |
134
|
|
|
foreach ($properties as $propertyIdent => $property) { |
135
|
|
|
// Ensure objects are properly encoded. |
136
|
|
|
$val = $this->propertyValue($propertyIdent); |
137
|
|
|
$val = $this->serializedValue($val); |
138
|
|
|
$data[$propertyIdent] = $val; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return $data; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Merge data on the model. |
146
|
|
|
* |
147
|
|
|
* Overrides `\Charcoal\Config\AbstractEntity::setData()` |
148
|
|
|
* to take properties into consideration. |
149
|
|
|
* |
150
|
|
|
* Also add a special case, to merge values for l10n properties. |
151
|
|
|
* |
152
|
|
|
* @param array $data The data to merge. |
153
|
|
|
* @return self |
154
|
|
|
*/ |
155
|
|
|
public function mergeData(array $data) |
156
|
|
|
{ |
157
|
|
|
$data = $this->setIdFromData($data); |
158
|
|
|
|
159
|
|
|
foreach ($data as $propIdent => $val) { |
160
|
|
|
if (!$this->hasProperty($propIdent)) { |
161
|
|
|
$this->logger->warning(sprintf( |
162
|
|
|
'Cannot set property "%s" on object; not defined in metadata.', |
163
|
|
|
$propIdent |
164
|
|
|
)); |
165
|
|
|
continue; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$property = $this->p($propIdent); |
169
|
|
|
if ($property->l10n() && is_array($val)) { |
170
|
|
|
$currentValue = json_decode(json_encode($this[$propIdent]), true); |
171
|
|
|
if (is_array($currentValue)) { |
172
|
|
|
$this[$propIdent] = array_merge($currentValue, $val); |
173
|
|
|
} else { |
174
|
|
|
$this[$propIdent] = $val; |
175
|
|
|
} |
176
|
|
|
} else { |
177
|
|
|
$this[$propIdent] = $val; |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
return $this; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Retrieve the default values, from the model's metadata. |
186
|
|
|
* |
187
|
|
|
* @return array |
188
|
|
|
*/ |
189
|
|
|
public function defaultData() |
190
|
|
|
{ |
191
|
|
|
$metadata = $this->metadata(); |
192
|
|
|
return $metadata->defaultData(); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Set the model data (from a flattened structure). |
197
|
|
|
* |
198
|
|
|
* This method takes a 1-dimensional array and fills the object with its values. |
199
|
|
|
* |
200
|
|
|
* @param array $flatData The model data. |
201
|
|
|
* @return self |
202
|
|
|
*/ |
203
|
|
|
public function setFlatData(array $flatData) |
204
|
|
|
{ |
205
|
|
|
$flatData = $this->setIdFromData($flatData); |
206
|
|
|
|
207
|
|
|
$data = []; |
208
|
|
|
$properties = $this->properties(); |
209
|
|
|
foreach ($properties as $propertyIdent => $property) { |
210
|
|
|
$fields = $property->fields(null); |
211
|
|
|
foreach ($fields as $k => $f) { |
212
|
|
|
if (is_string($k)) { |
213
|
|
|
$fid = $f->ident(); |
214
|
|
|
$key = str_replace($propertyIdent.'_', '', $fid); |
215
|
|
View Code Duplication |
if (isset($flatData[$fid])) { |
|
|
|
|
216
|
|
|
$data[$propertyIdent][$key] = $flatData[$fid]; |
217
|
|
|
unset($flatData[$fid]); |
218
|
|
|
} |
219
|
|
|
} else { |
220
|
|
|
$fid = $f->ident(); |
221
|
|
View Code Duplication |
if (isset($flatData[$fid])) { |
|
|
|
|
222
|
|
|
$data[$propertyIdent] = $flatData[$fid]; |
223
|
|
|
unset($flatData[$fid]); |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
$this->setData($data); |
230
|
|
|
|
231
|
|
|
// Set remaining (non-property) data. |
232
|
|
|
if (!empty($flatData)) { |
233
|
|
|
$this->setData($flatData); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
return $this; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Retrieve the model data as a flattened structure. |
241
|
|
|
* |
242
|
|
|
* This method returns a 1-dimensional array of the object's values. |
243
|
|
|
* |
244
|
|
|
* @todo Implementation required. |
245
|
|
|
* @return array |
246
|
|
|
*/ |
247
|
|
|
public function flatData() |
248
|
|
|
{ |
249
|
|
|
return []; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Retrieve the value for the given property. |
254
|
|
|
* |
255
|
|
|
* @param string $propertyIdent The property identifier to fetch. |
256
|
|
|
* @return mixed |
257
|
|
|
*/ |
258
|
|
|
public function propertyValue($propertyIdent) |
259
|
|
|
{ |
260
|
|
|
return $this[$propertyIdent]; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* @param array $properties Optional array of properties to save. If null, use all object's properties. |
265
|
|
|
* @return boolean |
266
|
|
|
*/ |
267
|
|
|
public function saveProperties(array $properties = null) |
268
|
|
|
{ |
269
|
|
|
if ($properties === null) { |
270
|
|
|
$properties = array_keys($this->metadata()->properties()); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
foreach ($properties as $propertyIdent) { |
274
|
|
|
$p = $this->p($propertyIdent); |
275
|
|
|
$v = $p->save($this->propertyValue($propertyIdent)); |
276
|
|
|
|
277
|
|
|
if ($v === null) { |
278
|
|
|
continue; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
$this[$propertyIdent] = $v; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
return true; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Load an object from the database from its l10n key $key. |
289
|
|
|
* Also retrieve and return the actual language that matched. |
290
|
|
|
* |
291
|
|
|
* @param string $key Key pointing a column's l10n base ident. |
292
|
|
|
* @param mixed $value Value to search in all languages. |
293
|
|
|
* @param array $langs List of languages (code, ex: "en") to check into. |
294
|
|
|
* @throws PDOException If the PDO query fails. |
295
|
|
|
* @return string The matching language. |
296
|
|
|
*/ |
297
|
|
|
public function loadFromL10n($key, $value, array $langs) |
298
|
|
|
{ |
299
|
|
|
$switch = []; |
300
|
|
|
$where = []; |
301
|
|
|
foreach ($langs as $lang) { |
302
|
|
|
$switch[] = 'when `'.$key.'_'.$lang.'` = :ident then \''.$lang.'\''; |
303
|
|
|
$where[] = '`'.$key.'_'.$lang.'` = :ident'; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
$q = ' |
307
|
|
|
SELECT |
308
|
|
|
*, |
309
|
|
|
(case |
310
|
|
|
'.implode("\n", $switch).' |
311
|
|
|
end) as _lang |
312
|
|
|
FROM |
313
|
|
|
`'.$this->source()->table().'` |
|
|
|
|
314
|
|
|
WHERE |
315
|
|
|
('.implode(' OR ', $where).') |
316
|
|
|
LIMIT |
317
|
|
|
1'; |
318
|
|
|
|
319
|
|
|
$binds = [ |
320
|
|
|
'ident' => $value |
321
|
|
|
]; |
322
|
|
|
|
323
|
|
|
$sth = $this->source()->dbQuery($q, $binds); |
|
|
|
|
324
|
|
|
if ($sth === false) { |
325
|
|
|
throw new PDOException('Could not load item.'); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
$data = $sth->fetch(PDO::FETCH_ASSOC); |
329
|
|
|
$lang = $data['_lang']; |
330
|
|
|
unset($data['_lang']); |
331
|
|
|
|
332
|
|
|
if ($data) { |
333
|
|
|
$this->setFlatData($data); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
return $lang; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Convert the current class name in "type-ident" format. |
341
|
|
|
* |
342
|
|
|
* @return string |
343
|
|
|
*/ |
344
|
|
|
public function objType() |
345
|
|
|
{ |
346
|
|
|
$ident = preg_replace('/([a-z])([A-Z])/', '$1-$2', get_class($this)); |
347
|
|
|
$objType = strtolower(str_replace('\\', '/', $ident)); |
348
|
|
|
return $objType; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* Inject dependencies from a DI Container. |
354
|
|
|
* |
355
|
|
|
* @param Container $container A Pimple DI service container. |
356
|
|
|
* @return void |
357
|
|
|
*/ |
358
|
|
|
protected function setDependencies(Container $container) |
|
|
|
|
359
|
|
|
{ |
360
|
|
|
// This method is a stub. Reimplement in children method to inject dependencies in your class from a container. |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Set the object's ID from an associative array map (or any other Traversable). |
365
|
|
|
* |
366
|
|
|
* Useful for setting the object ID before the rest of the object's data. |
367
|
|
|
* |
368
|
|
|
* @param array $data The object data. |
369
|
|
|
* @return array The object data without the pre-set ID. |
370
|
|
|
*/ |
371
|
|
|
protected function setIdFromData(array $data) |
372
|
|
|
{ |
373
|
|
|
$key = $this->key(); |
374
|
|
|
if (isset($data[$key])) { |
375
|
|
|
$this->setId($data[$key]); |
376
|
|
|
unset($data[$key]); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
return $data; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Serialize the given value. |
384
|
|
|
* |
385
|
|
|
* @param mixed $val The value to serialize. |
386
|
|
|
* @return mixed |
387
|
|
|
*/ |
388
|
|
|
protected function serializedValue($val) |
389
|
|
|
{ |
390
|
|
|
if (is_scalar($val)) { |
391
|
|
|
return $val; |
392
|
|
|
} elseif ($val instanceof DateTimeInterface) { |
393
|
|
|
return $val->format('Y-m-d H:i:s'); |
394
|
|
|
} else { |
395
|
|
|
return json_decode(json_encode($val), true); |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* StorableTrait > preSave(). Save hook called before saving the model. |
401
|
|
|
* |
402
|
|
|
* @return boolean |
403
|
|
|
*/ |
404
|
|
|
protected function preSave() |
405
|
|
|
{ |
406
|
|
|
return $this->saveProperties(); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* StorableTrait > preUpdate(). Update hook called before updating the model. |
411
|
|
|
* |
412
|
|
|
* @param string[] $properties Optional. The properties to update. |
413
|
|
|
* @return boolean |
414
|
|
|
*/ |
415
|
|
|
protected function preUpdate(array $properties = null) |
416
|
|
|
{ |
417
|
|
|
return $this->saveProperties($properties); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* DescribableTrait > createMetadata(). |
422
|
|
|
* |
423
|
|
|
* @return MetadataInterface |
424
|
|
|
*/ |
425
|
|
|
protected function createMetadata() |
426
|
|
|
{ |
427
|
|
|
return new ModelMetadata(); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* StorableInterface > createSource() |
432
|
|
|
* |
433
|
|
|
* @throws UnexpectedValueException If the metadata source can not be found. |
434
|
|
|
* @return \Charcoal\Source\SourceInterface |
435
|
|
|
*/ |
436
|
|
|
protected function createSource() |
437
|
|
|
{ |
438
|
|
|
$metadata = $this->metadata(); |
439
|
|
|
$defaultSource = $metadata->defaultSource(); |
|
|
|
|
440
|
|
|
$sourceConfig = $metadata->source($defaultSource); |
|
|
|
|
441
|
|
|
|
442
|
|
|
if (!$sourceConfig) { |
443
|
|
|
throw new UnexpectedValueException(sprintf( |
444
|
|
|
'Can not create source for [%s]: invalid metadata.', |
445
|
|
|
get_class($this) |
446
|
|
|
)); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
$type = isset($sourceConfig['type']) ? $sourceConfig['type'] : self::DEFAULT_SOURCE_TYPE; |
450
|
|
|
$source = $this->sourceFactory()->create($type); |
451
|
|
|
$source->setModel($this); |
452
|
|
|
|
453
|
|
|
$source->setData($sourceConfig); |
454
|
|
|
|
455
|
|
|
return $source; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* ValidatableInterface > create_validator(). |
460
|
|
|
* |
461
|
|
|
* @param array $data Optional. |
462
|
|
|
* @return \Charcoal\Validator\ValidatorInterface |
463
|
|
|
*/ |
464
|
|
|
protected function createValidator(array $data = null) |
465
|
|
|
{ |
466
|
|
|
$validator = new ModelValidator($this); |
467
|
|
|
if ($data !== null) { |
468
|
|
|
$validator->setData($data); |
|
|
|
|
469
|
|
|
} |
470
|
|
|
return $validator; |
471
|
|
|
} |
472
|
|
|
} |
473
|
|
|
|
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.