Total Complexity | 72 |
Total Lines | 391 |
Duplicated Lines | 0 % |
Changes | 4 | ||
Bugs | 0 | Features | 0 |
Complex classes like Entity often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Entity, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 |
||
401 | } |
||
402 | |||
403 | /** |
||
404 | * @return array|null |
||
405 | */ |
||
406 | public function raw(): ?array |
||
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) |
||
434 |