AbstractModel::createSource()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 12
nc 3
nop 0
dl 0
loc 20
rs 9.8666
c 3
b 0
f 0
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
     * @see AbstractEntity::setData()
115
     */
116
    public function setData(array $data)
117
    {
118
        $data = $this->setIdFromData($data);
119
120
        parent::setData($data);
121
        return $this;
122
    }
123
124
    /**
125
     * Retrieve the model data as a structure (serialize to array).
126
     *
127
     * @param  array $properties Optional. List of property identifiers
128
     *     for retrieving a subset of data.
129
     * @return array
130
     */
131
    public function data(array $properties = null)
132
    {
133
        $data = [];
134
        $properties = $this->properties($properties);
135
        foreach ($properties as $propertyIdent => $property) {
136
            // Ensure objects are properly encoded.
137
            $val = $this->propertyValue($propertyIdent);
138
            $val = $this->serializedValue($val);
139
            $data[$propertyIdent] = $val;
140
        }
141
142
        return $data;
143
    }
144
145
    /**
146
     * Merge data on the model.
147
     *
148
     * Overrides `\Charcoal\Config\AbstractEntity::setData()`
149
     * to take properties into consideration.
150
     *
151
     * Also add a special case, to merge values for l10n properties.
152
     *
153
     * @param  array $data The data to merge.
154
     * @return self
155
     */
156
    public function mergeData(array $data)
157
    {
158
        $data = $this->setIdFromData($data);
159
160
        foreach ($data as $propIdent => $val) {
161
            if (!$this->hasProperty($propIdent)) {
162
                $this->logger->warning(sprintf(
163
                    'Cannot set property "%s" on object; not defined in metadata.',
164
                    $propIdent
165
                ));
166
                continue;
167
            }
168
169
            $property = $this->p($propIdent);
170
            if ($property['l10n'] && is_array($val)) {
171
                $currentValue = json_decode(json_encode($this[$propIdent]), true);
172
                if (is_array($currentValue)) {
173
                    $this[$propIdent] = array_merge($currentValue, $val);
174
                } else {
175
                    $this[$propIdent] = $val;
176
                }
177
            } else {
178
                $this[$propIdent] = $val;
179
            }
180
        }
181
182
        return $this;
183
    }
184
185
    /**
186
     * Retrieve the default values, from the model's metadata.
187
     *
188
     * @return array
189
     */
190
    public function defaultData()
191
    {
192
        $metadata = $this->metadata();
193
        return $metadata->defaultData();
194
    }
195
196
    /**
197
     * Set the model data (from a flattened structure).
198
     *
199
     * This method takes a 1-dimensional array and fills the object with its values.
200
     *
201
     * @param  array $flatData The model data.
202
     * @return self
203
     */
204
    public function setFlatData(array $flatData)
205
    {
206
        $flatData = $this->setIdFromData($flatData);
207
208
        $data = [];
209
        $properties = $this->properties();
210
        foreach ($properties as $propertyIdent => $property) {
211
            $fields = $property->fields(null);
212
            foreach ($fields as $k => $f) {
213
                if (is_string($k)) {
214
                    $fid = $f->ident();
215
                    $snake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $propertyIdent));
216
                    $key = str_replace($snake.'_', '', $fid);
217
                    if (isset($flatData[$fid])) {
218
                        $data[$propertyIdent][$key] = $flatData[$fid];
219
                        unset($flatData[$fid]);
220
                    }
221
                } else {
222
                    $fid = $f->ident();
223
                    if (isset($flatData[$fid])) {
224
                        $data[$propertyIdent] = $flatData[$fid];
225
                        unset($flatData[$fid]);
226
                    }
227
                }
228
            }
229
        }
230
231
        $this->setData($data);
232
233
        // Set remaining (non-property) data.
234
        if (!empty($flatData)) {
235
            $this->setData($flatData);
236
        }
237
238
        return $this;
239
    }
240
241
    /**
242
     * Retrieve the model data as a flattened structure.
243
     *
244
     * This method returns a 1-dimensional array of the object's values.
245
     *
246
     * @todo   Implementation required.
247
     * @return array
248
     */
249
    public function flatData()
250
    {
251
        return [];
252
    }
253
254
    /**
255
     * Retrieve the value for the given property.
256
     * Force camelcase on the parameter.
257
     *
258
     * @param  string $propertyIdent The property identifier to fetch.
259
     * @return mixed
260
     */
261
    public function propertyValue($propertyIdent)
262
    {
263
        $propertyIdent = $this->camelize($propertyIdent);
264
        return $this[$propertyIdent];
265
    }
266
267
    /**
268
     * @param array $properties Optional array of properties to save. If null, use all object's properties.
269
     * @return boolean
270
     */
271
    public function saveProperties(array $properties = null)
272
    {
273
        if ($properties === null) {
274
            $properties = array_keys($this->metadata()->properties());
275
        }
276
277
        foreach ($properties as $propertyIdent) {
278
            $p = $this->p($propertyIdent);
279
            $v = $p->save($this->propertyValue($propertyIdent));
280
281
            if ($v === null) {
282
                continue;
283
            }
284
285
            $this[$propertyIdent] = $v;
286
        }
287
288
        return true;
289
    }
290
291
    /**
292
     * Load an object from the database from its l10n key $key.
293
     * Also retrieve and return the actual language that matched.
294
     *
295
     * @param  string $key   Key pointing a column's l10n base ident.
296
     * @param  mixed  $value Value to search in all languages.
297
     * @param  array  $langs List of languages (code, ex: "en") to check into.
298
     * @throws PDOException If the PDO query fails.
299
     * @return string The matching language.
300
     */
301
    public function loadFromL10n($key, $value, array $langs)
302
    {
303
        $switch = [];
304
        $where = [];
305
        foreach ($langs as $lang) {
306
            $switch[] = 'when `'.$key.'_'.$lang.'` = :ident then \''.$lang.'\'';
307
            $where[] = '`'.$key.'_'.$lang.'` = :ident';
308
        }
309
310
        $q = '
311
            SELECT
312
                *,
313
                (case
314
                    '.implode("\n", $switch).'
315
                end) as _lang
316
            FROM
317
               `'.$this->source()->table().'`
0 ignored issues
show
Bug introduced by
The method table() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

317
               `'.$this->source()->/** @scrutinizer ignore-call */ table().'`
Loading history...
318
            WHERE
319
                ('.implode(' OR ', $where).')
320
            LIMIT
321
               1';
322
323
        $binds = [
324
            'ident' => $value
325
        ];
326
327
        $sth = $this->source()->dbQuery($q, $binds);
0 ignored issues
show
Bug introduced by
The method dbQuery() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

327
        $sth = $this->source()->/** @scrutinizer ignore-call */ dbQuery($q, $binds);
Loading history...
328
        if ($sth === false) {
329
            throw new PDOException('Could not load item.');
330
        }
331
332
        $data = $sth->fetch(PDO::FETCH_ASSOC);
333
        $lang = $data['_lang'];
334
        unset($data['_lang']);
335
336
        if ($data) {
337
            $this->setFlatData($data);
338
        }
339
340
        return $lang;
341
    }
342
343
    /**
344
     * Generate a model type identifier from this object's class name.
345
     *
346
     * Based on {@see DescribableTrait::generateMetadataIdent()}.
347
     *
348
     * @return string
349
     */
350
    public static function objType()
351
    {
352
        $class = get_called_class();
353
        $ident = preg_replace('/([a-z])([A-Z])/', '$1-$2', $class);
354
        $ident = strtolower(str_replace('\\', '/', $ident));
355
        return $ident;
356
    }
357
358
    /**
359
     * Inject dependencies from a DI Container.
360
     *
361
     * @param  Container $container A Pimple DI service container.
362
     * @return void
363
     */
364
    protected function setDependencies(Container $container)
0 ignored issues
show
Unused Code introduced by
The parameter $container is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

364
    protected function setDependencies(/** @scrutinizer ignore-unused */ Container $container)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
365
    {
366
        // This method is a stub.
367
        // Reimplement in children method to inject dependencies in your class from a Pimple container.
368
    }
369
370
    /**
371
     * Set the object's ID from an associative array map (or any other Traversable).
372
     *
373
     * Useful for setting the object ID before the rest of the object's data.
374
     *
375
     * @param  array $data The object data.
376
     * @return array The object data without the pre-set ID.
377
     */
378
    protected function setIdFromData(array $data)
379
    {
380
        $key = $this->key();
381
        if (isset($data[$key])) {
382
            $this->setId($data[$key]);
383
            unset($data[$key]);
384
        }
385
386
        return $data;
387
    }
388
389
    /**
390
     * Serialize the given value.
391
     *
392
     * @param  mixed $val The value to serialize.
393
     * @return mixed
394
     */
395
    protected function serializedValue($val)
396
    {
397
        if (is_scalar($val)) {
398
            return $val;
399
        } elseif ($val instanceof DateTimeInterface) {
400
            return $val->format('Y-m-d H:i:s');
401
        } else {
402
            return json_decode(json_encode($val), true);
403
        }
404
    }
405
406
    /**
407
     * Save event called (in storable trait) before saving the model.
408
     *
409
     * @see StorableTrait::preSave()
410
     * @return boolean
411
     */
412
    protected function preSave()
413
    {
414
        return $this->saveProperties();
415
    }
416
417
    /**
418
     * StorableTrait > preUpdate(). Update hook called before updating the model.
419
     *
420
     * @param string[] $properties Optional. The properties to update.
421
     * @see StorableTrait::preUpdate()
422
     * @return boolean
423
     */
424
    protected function preUpdate(array $properties = null)
425
    {
426
        return $this->saveProperties($properties);
427
    }
428
429
    /**
430
     * Create a new metadata object.
431
     *
432
     * @see DescribablePropertyTrait::createMetadata()
433
     * @return ModelMetadata
434
     */
435
    protected function createMetadata()
436
    {
437
        $class = $this->metadataClass();
438
        return new $class();
439
    }
440
441
    /**
442
     * Retrieve the class name of the metadata object.
443
     *
444
     * @see DescribableTrait::metadataClass()
445
     * @return string
446
     */
447
    protected function metadataClass()
448
    {
449
        return ModelMetadata::class;
450
    }
451
452
    /**
453
     * @throws UnexpectedValueException If the metadata source can not be found.
454
     * @see StorableTrait::createSource()
455
     * @return \Charcoal\Source\SourceInterface
456
     */
457
    protected function createSource()
458
    {
459
        $metadata      = $this->metadata();
460
        $defaultSource = $metadata->defaultSource();
0 ignored issues
show
Bug introduced by
The method defaultSource() does not exist on Charcoal\Model\MetadataInterface. Did you maybe mean defaults()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

460
        /** @scrutinizer ignore-call */ 
461
        $defaultSource = $metadata->defaultSource();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
461
        $sourceConfig  = $metadata->source($defaultSource);
0 ignored issues
show
Bug introduced by
The method source() does not exist on Charcoal\Model\MetadataInterface. It seems like you code against a sub-type of Charcoal\Model\MetadataInterface such as Charcoal\Model\ModelMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

461
        /** @scrutinizer ignore-call */ 
462
        $sourceConfig  = $metadata->source($defaultSource);
Loading history...
462
463
        if (!$sourceConfig) {
464
            throw new UnexpectedValueException(sprintf(
465
                'Can not create source for [%s]: invalid metadata (can not load source\'s configuration).',
466
                get_class($this)
467
            ));
468
        }
469
470
        $type   = isset($sourceConfig['type']) ? $sourceConfig['type'] : self::DEFAULT_SOURCE_TYPE;
471
        $source = $this->sourceFactory()->create($type);
472
        $source->setModel($this);
473
474
        $source->setData($sourceConfig);
475
476
        return $source;
477
    }
478
479
    /**
480
     * ValidatableInterface > create_validator().
481
     *
482
     * @return \Charcoal\Validator\ValidatorInterface
483
     */
484
    protected function createValidator()
485
    {
486
        $validator = new ModelValidator($this);
487
        return $validator;
488
    }
489
}
490