Entity::setModelData()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
dl 0
loc 12
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
/********************************************************************************
3
 *   Apache License, Version 2.0                                                *
4
 *                                                                              *
5
 *   Copyright [2020] [Nurlan Mukhanov <[email protected]>]                      *
6
 *                                                                              *
7
 *   Licensed under the Apache License, Version 2.0 (the "License");            *
8
 *   you may not use this file except in compliance with the License.           *
9
 *   You may obtain a copy of the License at                                    *
10
 *                                                                              *
11
 *       http://www.apache.org/licenses/LICENSE-2.0                             *
12
 *                                                                              *
13
 *   Unless required by applicable law or agreed to in writing, software        *
14
 *   distributed under the License is distributed on an "AS IS" BASIS,          *
15
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
16
 *   See the License for the specific language governing permissions and        *
17
 *   limitations under the License.                                             *
18
 *                                                                              *
19
 ********************************************************************************/
20
21
declare(strict_types=1);
22
23
namespace DBD\Entity;
24
25
use DBD\Common\Singleton;
26
use DBD\Entity\Common\Enforcer;
27
use DBD\Entity\Common\EntityException;
28
use DBD\Entity\Interfaces\FullEntity;
29
use DBD\Entity\Interfaces\OnlyDeclaredPropertiesEntity;
30
use DBD\Entity\Interfaces\StrictlyFilledEntity;
31
use DBD\Entity\Interfaces\SyntheticEntity;
32
use Exception;
33
use ReflectionClass;
34
use ReflectionObject;
35
36
/**
37
 * Class Entity
38
 *
39
 * @package DBD\Entity
40
 */
41
abstract class Entity
42
{
43
    const SCHEME = "abstract";
44
    const TABLE = "abstract";
45
46
    /** @var array */
47
    private $rawData;
48
49
    /**
50
     * Конструктор модели
51
     *
52
     * @param array|null $data
53
     * @param int $maxLevels
54
     * @param int $currentLevel
55
     *
56
     * @throws EntityException
57
     */
58
    public function __construct(array $data = null, int $maxLevels = 2, int $currentLevel = 0)
59
    {
60
        $this->rawData = $data;
61
62
        $calledClass = get_class($this);
63
64
        if (!$this instanceof SyntheticEntity) {
65
            Enforcer::__add(__CLASS__, $calledClass);
66
        }
67
68
        try {
69
            /** @var Mapper $map */
70
            $map = self::map();
71
        } catch (Exception $e) {
72
            throw new EntityException(sprintf("Construction of %s failed, %s", $calledClass, $e->getMessage()));
73
        }
74
75
        if (!isset(EntityCache::$mapCache[$calledClass])) {
76
            /** @scrutinizer ignore-call */
77
            $columnsDefinition = $map->getOriginFieldNames();
78
79
            EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_MAP] = $columnsDefinition;
80
            EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_REVERSE_MAP] = array_flip($columnsDefinition);
81
82
            /*            if ($this instanceof FullEntity or $this instanceof StrictlyFilledEntity) {
83
                            foreach (get_object_vars($this) as $propertyName => $propertyDefaultValue) {
84
                                if (!array_key_exists($propertyName, $columnsDefinition))
85
                                    throw new EntityException(sprintf("FullEntity or StrictlyFilledEntity %s has unmapped property '%s'", $calledClass, $propertyName));
86
                            }
87
                        }*/
88
89
            // У нас может быть цепочка классов, где какой-то конечный уже не имеет интерфейса OnlyDeclaredPropertiesEntity
90
            // соответственно нам надо собрать все переменные всех дочерних классов, даже если они расширяют друг друга
91
            if ($this instanceof OnlyDeclaredPropertiesEntity) {
92
                $this->collectDeclarationsOnly(new ReflectionObject($this), $calledClass);
93
            }
94
        }
95
96
        if ($this instanceof OnlyDeclaredPropertiesEntity) {
97
            foreach (get_object_vars($this) as $varName => $varValue) {
98
                if (!isset(EntityCache::$mapCache[$calledClass][EntityCache::DECLARED_PROPERTIES][$varName]) && $varName != 'rawData') {
99
                    unset($this->$varName);
100
                    EntityCache::$mapCache[$calledClass][EntityCache::UNSET_PROPERTIES][$varName] = true;
101
                }
102
            }
103
        }
104
105
        if ($this instanceof FullEntity or $this instanceof StrictlyFilledEntity) {
106
            $checkAgainst = array_merge($map->getColumns(), $map->getComplex(), $map->getEmbedded(), $map->getConstraints());
107
            foreach (get_object_vars($this) as $propertyName => $propertyDefaultValue) {
108
                if (!array_key_exists($propertyName, $checkAgainst) && $propertyName != 'rawData') {
109
                    throw new EntityException(sprintf("Strict Entity %s has unmapped property '%s'", $calledClass, $propertyName));
110
                }
111
            }
112
        }
113
114
        if (is_null($this->rawData)) {
115
            return;
116
        }
117
        // Если мы определяем класс с интерфейсом OnlyDeclaredPropertiesEntity и экстендим его
118
        // то по сути мы не можем знать какие переменные классам нам обязательны к обработке.
119
        // Ладно еще если это 2 класса, а если цепочка?
120
        //if($this instanceof OnlyDeclaredPropertiesEntity and !$reflectionObject->isFinal())
121
        //	throw new EntityException("Class " . $reflectionObject->getParentClass()->getShortName() . " which implements OnlyDeclaredPropertiesEntity interface must be final");
122
123
        if ($currentLevel <= $maxLevels) {
124
            $this->setModelData($map, $maxLevels, $currentLevel);
125
        }
126
    }
127
128
    /**
129
     * @return Singleton|Mapper|static
130
     * @throws EntityException
131
     * @noinspection PhpDocMissingThrowsInspection ReflectionClass will never throw exception because of get_called_class()
132
     */
133
    final public static function map()
134
    {
135
        $calledClass = get_called_class();
136
137
        $mapClass = $calledClass . Mapper::POSTFIX;
138
139
        if (!class_exists($mapClass, false)) {
140
            throw new EntityException(sprintf("Class %s does not have Map definition", $calledClass));
141
        }
142
143
        /** @noinspection PhpUnhandledExceptionInspection */
144
        $reflection = new ReflectionClass($calledClass);
145
        $interfaces = $reflection->getInterfaces();
146
147
        if (isset($interfaces[SyntheticEntity::class])) {
148
            return $mapClass::meWithoutEnforcer();
149
        } else {
150
            return $mapClass::me();
151
        }
152
    }
153
154
    /**
155
     * @param ReflectionClass $reflectionObject
156
     * @param string $calledClass
157
     * @param string|null $parentClass
158
     */
159
    private function collectDeclarationsOnly(ReflectionClass $reflectionObject, string $calledClass, string $parentClass = null): void
160
    {
161
        foreach ($reflectionObject->getProperties() as $property) {
162
163
            $declaringClass = $property->getDeclaringClass();
164
165
            if ($declaringClass->name == $calledClass || $declaringClass->name == $parentClass) {
166
                EntityCache::$mapCache[$calledClass][EntityCache::DECLARED_PROPERTIES][$property->name] = true;
167
            }
168
        }
169
170
        $parentClass = $reflectionObject->getParentClass();
171
        $parentInterfaces = $parentClass->getInterfaces();
172
173
        if (isset($parentInterfaces[OnlyDeclaredPropertiesEntity::class])) {
174
            $this->collectDeclarationsOnly($parentClass, $calledClass, $parentClass->name);
175
        }
176
177
        /** If we have defined declaredProperties key, we must exclude some keys from reverseMap and arrayMap */
178
        if (isset(EntityCache::$mapCache[$calledClass][EntityCache::DECLARED_PROPERTIES])) {
179
            foreach (EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_MAP] as $propertyName => $fieldName) {
180
                if (!array_key_exists($propertyName, EntityCache::$mapCache[$calledClass][EntityCache::DECLARED_PROPERTIES])) {
181
                    unset(EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_MAP][$propertyName]);
182
                    unset(EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_REVERSE_MAP][$fieldName]);
183
                }
184
            }
185
        }
186
    }
187
188
    /**
189
     * @param Mapper $map
190
     * @param int $maxLevels
191
     * @param int $currentLevel
192
     *
193
     * @throws EntityException
194
     */
195
    private function setModelData(Mapper $map, int $maxLevels, int $currentLevel): void
196
    {
197
        $currentLevel++;
198
199
        $this->setBaseColumns($map);
200
201
        // TODO: check if I declare Constraint in Mapper and use same property name in Entity
202
        $this->setEmbedded($map, $maxLevels, $currentLevel);
203
204
        $this->setComplex($map, $maxLevels, $currentLevel);
205
206
        $this->postProcessing();
207
    }
208
209
    /**
210
     * Reads public variables and set them to the self instance
211
     *
212
     * @param Mapper $mapper
213
     *
214
     * @throws EntityException
215
     * @throws \ReflectionException
216
     */
217
    private function setBaseColumns(Mapper $mapper)
218
    {
219
        $calledClass = get_called_class();
220
221
        /**
222
         * @var array $fieldMapping are public properties of Mapper
223
         * where KEY is database origin column name and VALUE is Entity class field declaration
224
         * Structure look like this:
225
         * {
226
         *        "person_email":             "email",
227
         *        "person_id":                "id",
228
         *        "person_is_active":         "isActive",
229
         *        "person_name":              "name",
230
         *        "person_registration_date": "registrationDate"
231
         * }
232
         * EntityCache declaration happens in out constructor only once for time savings
233
         */
234
        $fieldMapping = EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_REVERSE_MAP];
235
236
        /** If it is FullEntity or StrictlyFilledEntity, we must ensure all database columns are provided */
237
        if ($this instanceof FullEntity or $this instanceof StrictlyFilledEntity) {
238
            $intersection = array_intersect_key($fieldMapping, $this->rawData);
239
            if ($intersection != $fieldMapping) {
240
                throw new EntityException(sprintf("Missing columns for FullEntity or StrictlyFilledEntity '%s': %s",
241
                        get_class($this),
242
                        json_encode(array_keys(array_diff_key($fieldMapping, $intersection)))
243
                    )
244
                );
245
            }
246
        }
247
248
        /**
249
         * @var string $originColumnName database origin column name
250
         * @var mixed $columnValue value of this columns
251
         */
252
        foreach ($this->rawData as $originColumnName => &$columnValue) {
253
254
            /** process only if Entity class has such field declaration */
255
            if (!isset($fieldMapping[$originColumnName])) {
256
                continue;
257
            }
258
259
            /** @var string $property name of field declaration in Entity class */
260
            $property = $fieldMapping[$originColumnName];
261
262
            if (!property_exists($this, $property)) {
263
                continue;
264
            }
265
266
            /** Note: Function names are case-insensitive, though it is usually good form to call functions as they appear in their declaration. */
267
            $setterMethod = sprintf("set%s", $property);
268
269
            /** @var Column $fieldDefinition */
270
            $fieldDefinition = $mapper->$property;
271
272
            if (is_null($columnValue) and $fieldDefinition->nullable === false) {
273
                throw new EntityException(sprintf("Column %s of %s shouldn't accept null values according Mapper definition", $originColumnName, $calledClass));
274
            }
275
276
            /** We can define setter method for field definition in Entity class, so let's check it first */
277
            if (method_exists($this, $setterMethod)) {
278
                $this->$setterMethod($columnValue);
279
            } else {
280
                /** If initially column type is json, then let's parse it as JSON */
281
                if (!is_null($columnValue) && !is_null($fieldDefinition->originType) && stripos($fieldDefinition->originType, "json") !== false) {
282
                    $this->$property = json_decode($columnValue, true);
283
                } else {
284
                    /**
285
                     * Entity public variables should not have default values.
286
                     * But sometimes we need to have default value for column in case of $rowData has null value
287
                     * In this case we should not override default value if $columnValue is null
288
                     * Иными словами нельзя переписывать дефолтное значение, если из базы пришло null
289
                     * но, если нет дефолтного значения, то мы должны его проинизиализировать null значением
290
                     */
291
                    $reflection = new ReflectionObject($this);
292
                    $reflectionProperty = $reflection->getProperty($property);
293
294
                    // Если мы еще не инциализировали переменную и у нас есть значение для этой переменной
295
                    //if (!isset($this->$property)) {
296
297
                        // Если у нас есть значение, то ставим его
298
                        if (isset($columnValue)) {
299
                            $this->$property = &$columnValue;
300
                        } else {
301
                            // У нас нет прицепленного значения
302
                            if (!$reflectionProperty->hasDefaultValue()) {
303
                                $this->$property = $columnValue; // this is NULL value
304
                            }
305
                        }
306
                    //}
307
                }
308
            }
309
        }
310
    }
311
312
    /**
313
     * @param Mapper $map
314
     * @param int $maxLevels
315
     * @param int $currentLevel
316
     *
317
     * @throws EntityException
318
     */
319
    private function setEmbedded(Mapper $map, int $maxLevels, int $currentLevel)
320
    {
321
        if ($this instanceof FullEntity or $this instanceof StrictlyFilledEntity) {
322
            /** @var Embedded[] $embeddings */
323
            $embeddings = MapperCache::me()->embedded[$map->name()];
324
            $missingColumns = [];
325
            foreach ($embeddings as $embedding) {
326
                if ($embedding->name !== false and !array_key_exists($embedding->name, $this->rawData)) {
327
                    $missingColumns[] = $embedding->name;
328
                }
329
            }
330
            if (count($missingColumns) > 0) {
331
                throw new EntityException(sprintf("Seems you forgot to select columns for FullEntity or StrictlyFilledEntity '%s': %s",
332
                        get_class($this),
333
                        json_encode($missingColumns)
334
                    )
335
                );
336
            }
337
        }
338
339
        foreach ($map->getEmbedded() as $embeddedName => $embeddedValue) {
340
            if ($embeddedValue->name === false) {
341
                continue;
342
            }
343
            if ($currentLevel <= $maxLevels) {
344
                $setterMethod = "set" . ucfirst($embeddedName);
345
346
                if (method_exists($this, $setterMethod)) {
347
                    $this->$setterMethod($this->rawData[$embeddedValue->name]);
348
                    continue;
349
                }
350
351
                if (isset($embeddedValue->dbType) and $embeddedValue->dbType == Type::Json) {
352
                    if (isset($this->rawData[$embeddedValue->name]) and is_string($this->rawData[$embeddedValue->name])) {
353
                        $this->rawData[$embeddedValue->name] = json_decode($this->rawData[$embeddedValue->name], true);
354
                    }
355
                }
356
                if (isset($embeddedValue->entityClass)) {
357
                    if ($embeddedValue->isIterable) {
358
                        $iterables = [];
359
                        if (isset($this->rawData[$embeddedValue->name]) and !is_null($this->rawData[$embeddedValue->name])) {
360
                            foreach ($this->rawData[$embeddedValue->name] as $value) {
361
                                $iterables[] = new $embeddedValue->entityClass($value, $maxLevels, $currentLevel);
362
                            }
363
                            $this->$embeddedName = $iterables;
364
                        }
365
                    } else {
366
                        $this->$embeddedName = new $embeddedValue->entityClass($this->rawData[$embeddedValue->name], $maxLevels, $currentLevel);
367
                    }
368
                } else {
369
                    $this->$embeddedName = &$this->rawData[$embeddedValue->name];
370
                }
371
            } else {
372
                unset($this->$embeddedName);
373
            }
374
        }
375
    }
376
377
    /**
378
     * @param Mapper $map
379
     * @param int $maxLevels
380
     * @param int $currentLevel
381
     */
382
    private function setComplex(Mapper $map, int $maxLevels, int $currentLevel)
383
    {
384
        foreach ($map->getComplex() as $complexName => $complexValue) {
385
            //if (!property_exists($this, $complexName) or isset(EntityCache::$mapCache[get_called_class()][EntityCache::UNSET_PROPERTIES][$complexName]))
386
            //    continue;
387
388
            if ($currentLevel <= $maxLevels) {
389
                $this->$complexName = new $complexValue->complexClass($this->rawData, $maxLevels, $currentLevel);
390
            } else {
391
                unset($this->$complexName);
392
            }
393
        }
394
    }
395
396
    /**
397
     * If entity data should be modified after setModelData, create same function in Entity.
398
     * For example, it is heavy cost to aggregate some data in SQL side, any more cost-efficient will do that with PHP
399
     *
400
     * @see Embedded::$name
401
     * @see setModelData()
402
     */
403
    protected function postProcessing(): void
404
    {
405
    }
406
407
    /**
408
     * get Entity table name
409
     *
410
     * @return string
411
     */
412
    public static function table(): string
413
    {
414
        $calledClass = get_called_class();
415
416
        return $calledClass::SCHEME . "." . $calledClass::TABLE;
417
    }
418
419
    /**
420
     * @return array|null
421
     */
422
    public function raw(): ?array
423
    {
424
        return $this->rawData;
425
    }
426
427
    /**
428
     * Special getter to access properties with getters
429
     * For example, having method getName you can access $name property declared with (@)property annotation
430
     * @param string $methodName
431
     * @return mixed
432
     * @throws EntityException
433
     */
434
    public function __get(string $methodName)
435
    {
436
        $lookupMethod = $methodName;
437
438
        if (ctype_lower($methodName[0])) {
439
            $lookupMethod = ucfirst($methodName);
440
        }
441
442
        $lookupMethod = "get" . $lookupMethod;
443
444
        if (!method_exists($this, $lookupMethod)) {
445
            throw new EntityException(sprintf("Can't find property or getter method for '\$%s' of '%s'", $methodName, get_class($this)));
446
        }
447
448
        $this->$methodName = $this->$lookupMethod();
449
450
        return $this->$methodName;
451
    }
452
}
453