Total Complexity | 72 |
Total Lines | 410 |
Duplicated Lines | 0 % |
Changes | 5 | ||
Bugs | 1 | 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) |
||
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() |
||
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 |
||
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 |
||
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) |
||
451 | } |
||
452 | } |
||
453 |