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
|
|
|
*/ |
216
|
|
|
private function setBaseColumns(Mapper $mapper) |
217
|
|
|
{ |
218
|
|
|
$calledClass = get_called_class(); |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* @var array $fieldMapping are public properties of Mapper |
222
|
|
|
* where KEY is database origin column name and VALUE is Entity class field declaration |
223
|
|
|
* Structure look like this: |
224
|
|
|
* { |
225
|
|
|
* "person_email": "email", |
226
|
|
|
* "person_id": "id", |
227
|
|
|
* "person_is_active": "isActive", |
228
|
|
|
* "person_name": "name", |
229
|
|
|
* "person_registration_date": "registrationDate" |
230
|
|
|
* } |
231
|
|
|
* EntityCache declaration happens in out constructor only once for time savings |
232
|
|
|
*/ |
233
|
|
|
$fieldMapping = EntityCache::$mapCache[$calledClass][EntityCache::ARRAY_REVERSE_MAP]; |
234
|
|
|
|
235
|
|
|
/** If it is FullEntity or StrictlyFilledEntity, we must ensure all database columns are provided */ |
236
|
|
|
if ($this instanceof FullEntity or $this instanceof StrictlyFilledEntity) { |
237
|
|
|
$intersection = array_intersect_key($fieldMapping, $this->rawData); |
238
|
|
|
if ($intersection != $fieldMapping) { |
239
|
|
|
throw new EntityException(sprintf("Missing columns for FullEntity or StrictlyFilledEntity '%s': %s", |
240
|
|
|
get_class($this), |
241
|
|
|
json_encode(array_keys(array_diff_key($fieldMapping, $intersection))) |
242
|
|
|
) |
243
|
|
|
); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* @var string $originColumnName database origin column name |
249
|
|
|
* @var mixed $columnValue value of this columns |
250
|
|
|
*/ |
251
|
|
|
foreach ($this->rawData as $originColumnName => &$columnValue) { |
252
|
|
|
|
253
|
|
|
/** process only if Entity class has such field declaration */ |
254
|
|
|
if (!isset($fieldMapping[$originColumnName])) { |
255
|
|
|
continue; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** @var string $property name of field declaration in Entity class */ |
259
|
|
|
$property = $fieldMapping[$originColumnName]; |
260
|
|
|
|
261
|
|
|
if (!property_exists($this, $property)) { |
262
|
|
|
continue; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** Note: Function names are case-insensitive, though it is usually good form to call functions as they appear in their declaration. */ |
266
|
|
|
$setterMethod = "set{$property}"; |
267
|
|
|
|
268
|
|
|
/** @var Column $fieldDefinition */ |
269
|
|
|
$fieldDefinition = $mapper->$property; |
270
|
|
|
|
271
|
|
|
if (is_null($columnValue) and $fieldDefinition->nullable === false) { |
272
|
|
|
throw new EntityException(sprintf("Column %s of %s shouldn't accept null values according Mapper definition", $originColumnName, $calledClass)); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** We can define setter method for field definition in Entity class, so let's check it first */ |
276
|
|
|
if (method_exists($this, $setterMethod)) { |
277
|
|
|
$this->$setterMethod($columnValue); |
278
|
|
|
} else { |
279
|
|
|
/** If initially column type is json, then let's parse it as JSON */ |
280
|
|
|
if (!is_null($columnValue) && !is_null($fieldDefinition->originType) && stripos($fieldDefinition->originType, "json") !== false) { |
281
|
|
|
$this->$property = json_decode($columnValue, true); |
282
|
|
|
} else { |
283
|
|
|
/** |
284
|
|
|
* Entity public variables should not have default values. |
285
|
|
|
* But some times we need to have default value for column in case of $rowData has null value |
286
|
|
|
* In this case we should not override default value if $columnValue is null |
287
|
|
|
*/ |
288
|
|
|
if (!isset($this->$property) and isset($columnValue)) { |
289
|
|
|
$this->$property = &$columnValue; |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* @param Mapper $map |
298
|
|
|
* @param int $maxLevels |
299
|
|
|
* @param int $currentLevel |
300
|
|
|
* |
301
|
|
|
* @throws EntityException |
302
|
|
|
*/ |
303
|
|
|
private function setEmbedded(Mapper $map, int $maxLevels, int $currentLevel) |
304
|
|
|
{ |
305
|
|
|
if ($this instanceof FullEntity or $this instanceof StrictlyFilledEntity) { |
306
|
|
|
/** @var Embedded[] $embeddings */ |
307
|
|
|
$embeddings = MapperCache::me()->embedded[$map->name()]; |
308
|
|
|
$missingColumns = []; |
309
|
|
|
foreach ($embeddings as $embedding) { |
310
|
|
|
if ($embedding->name !== false and !array_key_exists($embedding->name, $this->rawData)) { |
311
|
|
|
$missingColumns[] = $embedding->name; |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
if (count($missingColumns) > 0) { |
315
|
|
|
throw new EntityException(sprintf("Seems you forgot to select columns for FullEntity or StrictlyFilledEntity '%s': %s", |
316
|
|
|
get_class($this), |
317
|
|
|
json_encode($missingColumns) |
318
|
|
|
) |
319
|
|
|
); |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
foreach ($map->getEmbedded() as $embeddedName => $embeddedValue) { |
324
|
|
|
if ($embeddedValue->name === false) { |
325
|
|
|
continue; |
326
|
|
|
} |
327
|
|
|
if ($currentLevel <= $maxLevels) { |
328
|
|
|
$setterMethod = "set" . ucfirst($embeddedName); |
329
|
|
|
|
330
|
|
|
if (method_exists($this, $setterMethod)) { |
331
|
|
|
$this->$setterMethod($this->rawData[$embeddedValue->name]); |
332
|
|
|
continue; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
if (isset($embeddedValue->dbType) and $embeddedValue->dbType == Type::Json) { |
336
|
|
|
if (isset($this->rawData[$embeddedValue->name]) and is_string($this->rawData[$embeddedValue->name])) { |
337
|
|
|
$this->rawData[$embeddedValue->name] = json_decode($this->rawData[$embeddedValue->name], true); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
if (isset($embeddedValue->entityClass)) { |
341
|
|
|
if ($embeddedValue->isIterable) { |
342
|
|
|
$iterables = []; |
343
|
|
|
if (isset($this->rawData[$embeddedValue->name]) and !is_null($this->rawData[$embeddedValue->name])) { |
344
|
|
|
foreach ($this->rawData[$embeddedValue->name] as $value) { |
345
|
|
|
$iterables[] = new $embeddedValue->entityClass($value, $maxLevels, $currentLevel); |
346
|
|
|
} |
347
|
|
|
$this->$embeddedName = $iterables; |
348
|
|
|
} |
349
|
|
|
} else { |
350
|
|
|
$this->$embeddedName = new $embeddedValue->entityClass($this->rawData[$embeddedValue->name], $maxLevels, $currentLevel); |
351
|
|
|
} |
352
|
|
|
} else { |
353
|
|
|
$this->$embeddedName = &$this->rawData[$embeddedValue->name]; |
354
|
|
|
} |
355
|
|
|
} else { |
356
|
|
|
unset($this->$embeddedName); |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* @param Mapper $map |
363
|
|
|
* @param int $maxLevels |
364
|
|
|
* @param int $currentLevel |
365
|
|
|
*/ |
366
|
|
|
private function setComplex(Mapper $map, int $maxLevels, int $currentLevel) |
367
|
|
|
{ |
368
|
|
|
foreach ($map->getComplex() as $complexName => $complexValue) { |
369
|
|
|
//if (!property_exists($this, $complexName) or isset(EntityCache::$mapCache[get_called_class()][EntityCache::UNSET_PROPERTIES][$complexName])) |
370
|
|
|
// continue; |
371
|
|
|
|
372
|
|
|
if ($currentLevel <= $maxLevels) { |
373
|
|
|
$this->$complexName = new $complexValue->complexClass($this->rawData, $maxLevels, $currentLevel); |
374
|
|
|
} else { |
375
|
|
|
unset($this->$complexName); |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* If entity data should be modified after setModelData, create same function in Entity. |
382
|
|
|
* For example it is heavy cost to aggregate some data in SQL side, any more cost efficient will do that with PHP |
383
|
|
|
* |
384
|
|
|
* @see Embedded::$name |
385
|
|
|
* @see setModelData() |
386
|
|
|
*/ |
387
|
|
|
protected function postProcessing(): void |
388
|
|
|
{ |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* get Entity table name |
393
|
|
|
* |
394
|
|
|
* @return string |
395
|
|
|
*/ |
396
|
|
|
public static function table(): string |
397
|
|
|
{ |
398
|
|
|
$calledClass = get_called_class(); |
399
|
|
|
|
400
|
|
|
return $calledClass::SCHEME . "." . $calledClass::TABLE; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* @return array|null |
405
|
|
|
*/ |
406
|
|
|
public function raw(): ?array |
407
|
|
|
{ |
408
|
|
|
return $this->rawData; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* Special getter to access properties with getters |
413
|
|
|
* For example, having method getName you can access $name property declared with (@)property annotation |
414
|
|
|
* @param string $methodName |
415
|
|
|
* @return mixed |
416
|
|
|
* @throws EntityException |
417
|
|
|
*/ |
418
|
|
|
public function __get(string $methodName) |
419
|
|
|
{ |
420
|
|
|
$lookupMethod = $methodName; |
421
|
|
|
|
422
|
|
|
if (ctype_lower($methodName{0})) { |
423
|
|
|
$lookupMethod = ucfirst($methodName); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** @noinspection PhpUnnecessaryCurlyVarSyntaxInspection */ |
427
|
|
|
if (!method_exists($this, "get{$lookupMethod}")) { |
428
|
|
|
throw new EntityException(sprintf("Can't find property or getter method for '\$%s' of '%s'", $methodName, get_class($this))); |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
return $this->$methodName(); |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|