Passed
Branch master (018ba4)
by Mathieu
06:43
created

AbstractModel::__construct()   D

Complexity

Conditions 8
Paths 128

Size

Total Lines 32
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 15
nc 128
nop 1
dl 0
loc 32
rs 4.6666
c 0
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
     */
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])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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().'`
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

313
               `'.$this->source()->/** @scrutinizer ignore-call */ table().'`
Loading history...
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);
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

323
        $sth = $this->source()->/** @scrutinizer ignore-call */ dbQuery($q, $binds);
Loading history...
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)
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

358
    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...
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();
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

439
        /** @scrutinizer ignore-call */ 
440
        $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...
440
        $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

440
        /** @scrutinizer ignore-call */ 
441
        $sourceConfig  = $metadata->source($defaultSource);
Loading history...
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);
0 ignored issues
show
Bug introduced by
The method setData() does not exist on Charcoal\Model\ModelValidator. ( Ignorable by Annotation )

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

468
            $validator->/** @scrutinizer ignore-call */ 
469
                        setData($data);

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...
469
        }
470
        return $validator;
471
    }
472
}
473