Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 254
0.00% covered (danger)
0.00%
0 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
Base
0.00% covered (danger)
0.00%
0 / 254
0.00% covered (danger)
0.00%
0 / 37
14042
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 entityNeedsUpdate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 setStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReferenceMapping
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 setupMapping
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 postSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preSetupFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 postSetupFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setupFields
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getReferenceFields
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setupReferences
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
182
 __set
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 addReference
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 __get
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 __isset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 __unset
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 offsetExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 offsetGet
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 offsetSet
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 offsetUnset
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 jsonSerialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 arrayAccessByDotPerpareKeys
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 save
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 saveEntitiy
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 saveReferences
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 delete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 deleteEntity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0
 deleteReferences
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
156
 deleteWith
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 clearEntity
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 clearEntityReferences
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 getTableName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BO\Zmsdldb\Importer\MySQL\Entity;
4
5use BO\Zmsdldb\PDOAccess;
6use BO\Zmsdldb\Importer\PDOTrait;
7use BO\Zmsdldb\Importer\ItemNeedsUpdateTrait;
8use BO\Zmsdldb\Importer\MySQL\Entity\Collection as EntityCollection
9;
10
11/**
12 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
13 * @SuppressWarnings(PHPMD.TooManyMethods)
14 * @SuppressWarnings(PHPMD.NumberOfChildren)
15 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
16 * @SuppressWarnings(PHPMD.NPathComplexity)
17 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18 * @implements \ArrayAccess<string|null, mixed>
19 */
20abstract class Base implements \Countable, \ArrayAccess, \JsonSerializable
21{
22    use ItemNeedsUpdateTrait;
23    use PDOTrait;
24
25    protected array $fieldMapping = [];
26
27    /** @var array<string, mixed> */
28    protected array $fields = [];
29
30    protected array $referanceMapping = [];
31
32    /** @var array<string, EntityCollection> */
33    protected array $references = [];
34
35    protected array $dataRaw = [];
36
37    protected bool $setupFields = true;
38    protected bool $setupReferences = true;
39
40    protected int $status = 1;
41
42    public const int STATUS_NEW = 1;
43    public const int STATUS_OLD = 0;
44
45    public function __construct(PDOAccess $mySqlAccess, array $dataRaw = [], bool $setup = true)
46    {
47        try {
48            $this->pdoAccess = $mySqlAccess;
49            $this->dataRaw = $dataRaw;
50
51            if (true === $setup) {
52                $this->setupMapping();
53
54                $this->preSetup();
55
56                $this->setupFields();
57
58                $this->setupReferences();
59
60                $this->postSetup();
61            }
62        } catch (\Exception $e) {
63            throw $e;
64        }
65    }
66
67    protected function entityNeedsUpdate(): bool
68    {
69        $fields = $this->get(['id', 'meta.locale', 'meta.hash']);
70        $hash = $fields['meta.hash'] ?? '';
71
72        return $this->itemNeedsUpdate(
73            (int) ($fields['id'] ?? 0),
74            (string) ($fields['meta.locale'] ?? ''),
75            is_scalar($hash) ? (string) $hash : '',
76            static::getTableName()
77        );
78    }
79
80    public function setStatus(int $status = Base::STATUS_NEW): void
81    {
82        $this->status = $status;
83    }
84
85    public function getStatus(): int
86    {
87        return $this->status;
88    }
89
90    public function getReferenceMapping(bool $setup = false): array
91    {
92        try {
93            if (true === $setup) {
94                $this->setupMapping();
95            }
96
97            return $this->referanceMapping;
98        } catch (\Exception $e) {
99            throw $e;
100        }
101    }
102
103    protected function setupMapping(): void
104    {
105    }
106
107    public function preSetup(): void
108    {
109    }
110
111    public function postSetup(): void
112    {
113    }
114
115    public function preSetupFields(): void
116    {
117    }
118
119    public function postSetupFields(): void
120    {
121    }
122
123    final public function setupFields(): void
124    {
125        try {
126            if (false === $this->setupFields) {
127                return;
128            }
129            $this->preSetupFields();
130
131            $values = $this->get(array_keys(array_filter($this->fieldMapping)));
132            foreach ($values as $key => $value) {
133                if (!is_string($key)) {
134                    continue;
135                }
136                $this->__set($key, $value);
137            }
138            $this->postSetupFields();
139            $this->setupFields = false;
140        } catch (\Exception $e) {
141            throw $e;
142        }
143    }
144
145    protected function getReferenceFields(): array
146    {
147        $referenceFields = array_flip(array_keys(array_filter($this->referanceMapping)));
148
149        foreach (array_keys($referenceFields) as $name) {
150            $referenceFields[$name] = $this->get($name);
151        }
152        return $referenceFields;
153    }
154
155    final public function setupReferences(): void
156    {
157        try {
158            if (false === $this->setupReferences) {
159                return;
160            }
161            $values = $this->getReferenceFields();
162
163            foreach ($values as $name => $references) {
164                $referenceEntityClass = $this->referanceMapping[$name]['class'];
165                if (!is_string($referenceEntityClass)) {
166                    throw new \InvalidArgumentException('Invalid reference entity class');
167                }
168                /** @var class-string<Base> $referenceEntityClass */
169                $addFields = [];
170
171                foreach (($this->referanceMapping[$name]['neededFields'] ?? []) as $sourceKey => $destinationKey) {
172                    $addFields[$destinationKey] = $this->get($sourceKey);
173                }
174                $isMultiple = $this->referanceMapping[$name]['multiple'] ?? true;
175
176                if (false === $isMultiple) {
177                    $references = [$references];
178                }
179
180                $position = 0;
181                foreach (($references ?? []) as $reference) {
182                    foreach ($this->referanceMapping[$name]['addFields'] as $key => $value) {
183                        if (is_callable($value)) {
184                            $addFields[$key] = call_user_func_array($value, [$position, $name, $reference]);
185                        } else {
186                            $addFields[$key] = $value;
187                        }
188                    }
189                    if (true === ($this->referanceMapping[$name]['selfAsArray'] ?? false)) {
190                        $reference = [
191                            $name => $reference
192                        ];
193                    }
194                    if (!is_array($reference)) {
195                        $reference = [];
196                    }
197                    /** @psalm-suppress UnsafeInstantiation */
198                    $referencesInstance = new $referenceEntityClass(
199                        $this->getPDOAccess(),
200                        array_merge(
201                            $reference,
202                            $addFields
203                        )
204                    );
205                    if (!$referencesInstance instanceof Base) {
206                        throw new \InvalidArgumentException($referenceEntityClass . ' must extend ' . Base::class);
207                    }
208
209                    $this->addReference($name, $referencesInstance);
210                    $position++;
211                }
212            }
213            $this->setupReferences = false;
214        } catch (\Exception $e) {
215            throw $e;
216        }
217    }
218
219    final public function __set(string $name, mixed $value): void
220    {
221        if (array_key_exists($name, $this->fieldMapping)) {
222            $name = $this->fieldMapping[$name];
223            if (is_bool($value)) {
224                $value = (int)$value;
225            } elseif (false !== stripos($name, '_json')) {
226                $value = json_encode($value);
227            }
228            $this->fields[$name] = $value;
229        } elseif (array_key_exists($name, $this->referanceMapping) && $value instanceof Base) {
230            $this->addReference($name, $value);
231        }
232    }
233
234    public function addReference(string $name, Base $reference): void
235    {
236        if (array_key_exists($name, $this->referanceMapping)) {
237            if (!isset($this->references[$name])) {
238                $this->references[$name] = new EntityCollection();
239            }
240            $this->references[$name][] = $reference;
241        }
242    }
243
244    final public function __get(string $name): mixed
245    {
246        if (array_key_exists($name, $this->fields)) {
247            return $this->fields[$name];
248        }
249        if (array_key_exists($name, $this->references)) {
250            return $this->references[$name];
251        }
252        throw new \InvalidArgumentException(__METHOD__ . " {$name} has not been set!");
253    }
254
255    final public function __isset(string $name): bool
256    {
257        return array_key_exists($name, $this->fields) || array_key_exists($name, $this->references);
258    }
259
260    final public function __unset(string $name): void
261    {
262        if (array_key_exists($name, $this->fields)) {
263            unset($this->fields[$name]);
264        }
265        if (array_key_exists($name, $this->references)) {
266            unset($this->references[$name]);
267        }
268    }
269
270    #[\Override]
271    final public function offsetExists($offset): bool
272    {
273        return is_string($offset) && $this->__isset($offset);
274    }
275
276    #[\Override]
277    final public function offsetGet($offset): mixed
278    {
279        if (!is_string($offset)) {
280            throw new \InvalidArgumentException(__METHOD__ . ' offset must be a string');
281        }
282        return $this->__get($offset);
283    }
284
285    #[\Override]
286    final public function offsetSet($offset, $value): Base
287    {
288        if (!is_string($offset)) {
289            throw new \InvalidArgumentException(__METHOD__ . ' offset must be a string');
290        }
291        $this->__set($offset, $value);
292        return $this;
293    }
294
295    #[\Override]
296    final public function offsetUnset($offset): Base
297    {
298        if (!is_string($offset)) {
299            throw new \InvalidArgumentException(__METHOD__ . ' offset must be a string');
300        }
301        $this->__unset($offset);
302        return $this;
303    }
304
305    #[\Override]
306    final public function count(): int
307    {
308        return count($this->fields);
309    }
310
311    #[\Override]
312    public function jsonSerialize(): mixed
313    {
314        return $this->fields;
315    }
316
317    public function getFields(): array
318    {
319        return $this->fields;
320    }
321
322    public function get(null|array|string $key = null, mixed $default = null): mixed
323    {
324        if (null === $key) {
325            return $this->dataRaw;
326        }
327        $keys = $key;
328        if (!is_array($keys)) {
329            $keys = [$keys];
330        }
331        $values = [];
332
333        foreach ($keys as $key) {
334            if ('__RAW__' == $key) {
335                $values[$key] = $this->dataRaw;
336                continue;
337            }
338            $levels = static::arrayAccessByDotPerpareKeys($key);
339
340            $value = $default;
341
342            $current = $this->dataRaw;
343            $levelsCount = count($levels);
344            for ($i = 0; $i < $levelsCount; ++$i) {
345                if (!is_array($current) || !array_key_exists($levels[$i], $current)) {
346                    continue;
347                }
348                $current = $current[$levels[$i]];
349                $value = $current;
350            }
351            $values[$key] = ($value);
352        }
353        return 1 == count($keys) ? $values[$keys[0]] : $values;
354    }
355
356    protected static function arrayAccessByDotPerpareKeys(?string $key = null): array
357    {
358        if (null === $key) {
359            return [];
360        }
361        $keys = explode('.', $key);
362        $keys = array_filter($keys, 'strlen');
363        $keys = array_map(static function (string $segment): int|string {
364            if (is_numeric($segment) && !str_contains($segment, '.')) {
365                return (int) $segment;
366            }
367            return $segment;
368        }, $keys);
369
370        return $keys;
371    }
372
373    public function save(): void
374    {
375        try {
376            if (static::STATUS_NEW !== $this->getStatus()) {
377                return;
378            }
379            $this->saveEntitiy();
380            $this->saveReferences();
381        } catch (\Exception $e) {
382            throw $e;
383        }
384    }
385
386    final public function saveEntitiy(): void
387    {
388        try {
389            if (static::STATUS_NEW !== $this->getStatus()) {
390                return;
391            }
392            if (!empty($this->fields)) {
393                $sql = 'REPLACE INTO ' . static::getTableName() . ' ';
394                $sql .= '(`' . implode('`, `', array_keys($this->fields)) . '`) ';
395
396                $questionMarks = array_fill(0, count($this->fields), '?');
397                $sql .= 'VALUES (' . implode(', ', $questionMarks) . ') ';
398
399                $stm = $this->getPDOAccess()->prepare($sql);
400
401                $stm->execute(array_values($this->fields));
402
403                if ($stm && 0 < $stm->rowCount()) {
404                    return;
405                }
406                throw new \Exception('Could not save entity');
407            }
408            throw new \Exception('Could not save entity, fields are empty');
409        } catch (\Exception $e) {
410            throw $e;
411        }
412    }
413
414    final public function saveReferences(): void
415    {
416        try {
417            if (static::STATUS_NEW !== $this->getStatus()) {
418                return;
419            }
420            foreach ($this->references as $referencesCollection) {
421                $referencesCollection->saveEntities();
422            }
423        } catch (\Exception $e) {
424            throw $e;
425        }
426    }
427
428    /** @psalm-api */
429    public function delete(): void
430    {
431        try {
432            $this->deleteEntity();
433            $this->deleteReferences();
434        } catch (\Exception $e) {
435            throw $e;
436        }
437    }
438
439    abstract public function deleteEntity(): void;
440
441    public function deleteReferences(): void
442    {
443        try {
444            foreach ($this->referanceMapping as $name => $mappingData) {
445                if (isset($mappingData['deleteFunction']) && is_callable($mappingData['deleteFunction'])) {
446                    call_user_func_array($mappingData['deleteFunction'], [$this, $name, $mappingData]);
447                    continue;
448                }
449                if (array_key_exists('delete', $mappingData) && false === $mappingData['delete']) {
450                    continue;
451                }
452                if (!array_key_exists('deleteFields', $mappingData) || empty($mappingData['deleteFields'])) {
453                    throw new \Exception(static::class . ' missing $deleteFields in reference mapping');
454                }
455                $addFields = [];
456                $referenceEntityClass = $mappingData['class'];
457                if (!is_string($referenceEntityClass)) {
458                    throw new \InvalidArgumentException('Invalid reference entity class');
459                }
460                /** @var class-string<Base> $referenceEntityClass */
461                foreach ($mappingData['deleteFields'] as $sourceKey => $val) {
462                    $addFields[$sourceKey] = $val;
463                }
464
465                /** @psalm-suppress UnsafeInstantiation */
466                $referencesInstance = new $referenceEntityClass(
467                    $this->getPDOAccess(),
468                    $addFields,
469                    false
470                );
471                if (!$referencesInstance instanceof Base) {
472                    throw new \InvalidArgumentException($referenceEntityClass . ' must extend ' . Base::class);
473                }
474                $referencesInstance->setupFields();
475
476                $referencesInstance->deleteWith(
477                    array_intersect_key($referencesInstance->getFields(), $addFields)
478                );
479            }
480        } catch (\Exception $e) {
481            throw $e;
482        }
483    }
484
485    final public function deleteWith(array $fields): void
486    {
487        try {
488            $sql = "DELETE FROM " . static::getTableName();
489            if (!empty($fields)) {
490                $where = array_map(function (int|string $field): string {
491                    return $field . ' = ?';
492                }, array_keys($fields));
493                $sql .= " WHERE " . implode(' AND ', $where);
494            }
495
496            $stm = $this->getPDOAccess()->prepare($sql);
497            $stm->execute(array_values($fields));
498        } catch (\Exception $e) {
499            throw $e;
500        }
501    }
502
503    public function clearEntity(array $addWhere = []): void
504    {
505        try {
506            $this->deleteWith($addWhere);
507        } catch (\Exception $e) {
508            throw $e;
509        }
510    }
511
512    public function clearEntityReferences(): void
513    {
514        try {
515            foreach ($this->getReferenceMapping(true) as $name => $mappingData) {
516                $referenceEntityClass = $mappingData['class'];
517                if (!is_string($referenceEntityClass)) {
518                    throw new \InvalidArgumentException('Invalid reference entity class');
519                }
520                /** @var class-string<Base> $referenceEntityClass */
521                $clearFields = [];
522                $position = 0;
523                foreach (($mappingData['clearFields'] ?? []) as $key => $value) {
524                    if (is_callable($value)) {
525                        $clearFields[$key] = call_user_func_array($value, [$position++, $name, null]);
526                    } else {
527                        $clearFields[$key] = $value;
528                    }
529                }
530
531                /** @psalm-suppress UnsafeInstantiation */
532                $referencesInstance = new $referenceEntityClass(
533                    $this->getPDOAccess(),
534                    [],
535                    false
536                );
537                if (!$referencesInstance instanceof Base) {
538                    throw new \InvalidArgumentException($referenceEntityClass . ' must extend ' . Base::class);
539                }
540                if (!empty($clearFields)) {
541                    $referencesInstance->deleteWith($clearFields);
542                } else {
543                    $referencesInstance->clearEntity();
544                }
545            }
546        } catch (\Exception $e) {
547            throw $e;
548        }
549    }
550
551    public static function getTableName(): string
552    {
553        $classNameWithNs = explode("\\", static::class);
554        $className = end($classNameWithNs);
555        $table = defined('static::TABLENAME')
556            ? constant('static::TABLENAME')
557            : preg_replace('/(?<!^)[A-Z]/', '_$0', $className);
558
559        return strtolower((string) $table);
560    }
561}