TableInheritance::parseMetadata()   A
last analyzed

Complexity

Conditions 2
Paths 4

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 8
ccs 2
cts 4
cp 0.5
crap 2.5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Annotated;
6
7
use Cycle\Annotated\Annotation\Inheritance;
8
use Cycle\Annotated\Exception\AnnotationException;
9
use Cycle\Annotated\Utils\EntityUtils;
10
use Cycle\Schema\Definition\Entity as EntitySchema;
11
use Cycle\Schema\Definition\Inheritance\JoinedTable as JoinedTableInheritanceSchema;
12
use Cycle\Schema\Definition\Inheritance\SingleTable as SingleTableInheritanceSchema;
13
use Cycle\Schema\Definition\Map\FieldMap;
14
use Cycle\Schema\Exception\TableInheritance\WrongParentKeyColumnException;
15
use Cycle\Schema\GeneratorInterface;
16
use Cycle\Schema\Registry;
17
use Doctrine\Common\Annotations\Reader as DoctrineReader;
18
use Spiral\Attributes\ReaderInterface;
19
20
class TableInheritance implements GeneratorInterface
21
{
22
    private ReaderInterface $reader;
23
    private EntityUtils $utils;
24
25 144
    public function __construct(
26
        DoctrineReader|ReaderInterface $reader = null
27
    ) {
28 144
        $this->reader = ReaderFactory::create($reader);
29 144
        $this->utils = new EntityUtils($this->reader);
30 144
    }
31
32 144
    public function run(Registry $registry): Registry
33
    {
34
        /** @var EntitySchema[] $found */
35 144
        $found = [];
36
37 144
        foreach ($registry as $entity) {
38
            // Only child entities can have table inheritance annotation
39 144
            $children = $registry->getChildren($entity);
40
41 144
            foreach ($children as $child) {
42
                /** @var Inheritance $annotation */
43 144
                if ($annotation = $this->parseMetadata($child, Inheritance::class)) {
44 144
                    $childClass = $child->getClass();
45
46
                    // Child entities always have parent entity
47
                    do {
48 144
                        $parent = $this->findParent(
49
                            $registry,
50 144
                            $this->utils->findParent($childClass, false)
51
                        );
52
53 144
                        if ($annotation instanceof Inheritance\JoinedTable) {
54 144
                            break;
55
                        }
56
57 144
                        $childClass = $parent->getClass();
58 144
                    } while ($this->parseMetadata($parent, Inheritance::class) !== null);
0 ignored issues
show
Bug introduced by
It seems like $parent can also be of type null; however, parameter $entity of Cycle\Annotated\TableInheritance::parseMetadata() does only seem to accept Cycle\Schema\Definition\Entity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
                    } while ($this->parseMetadata(/** @scrutinizer ignore-type */ $parent, Inheritance::class) !== null);
Loading history...
59
60 144
                    if ($inheritanceEntity = $this->initInheritance($annotation, $child, $parent)) {
0 ignored issues
show
Bug introduced by
It seems like $parent can also be of type null; however, parameter $parent of Cycle\Annotated\TableInh...ance::initInheritance() does only seem to accept Cycle\Schema\Definition\Entity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

60
                    if ($inheritanceEntity = $this->initInheritance($annotation, $child, /** @scrutinizer ignore-type */ $parent)) {
Loading history...
61 144
                        $found[] = $inheritanceEntity;
62
                    }
63
                }
64
65
                // All child should be presented in a schema as separated entity
66
                // Every child will be handled according its table inheritance type
67 144
                if (!$registry->hasEntity($child->getRole())) {
68 144
                    $registry->register($child);
69
70 144
                    $registry->linkTable(
71
                        $child,
72 144
                        $this->getDatabase($child, $registry),
73 144
                        $this->getTableName($child, $registry)
74
                    );
75
                }
76
            }
77
        }
78
79 144
        foreach ($found as $entity) {
80 144
            if ($entity->getInheritance() instanceof SingleTableInheritanceSchema) {
81 144
                $allowedEntities = \array_map(
82 144
                    static fn (string $role) => $registry->getEntity($role)->getClass(),
83 144
                    $entity->getInheritance()->getChildren()
84
                );
85 144
                $this->removeStiExtraFields($entity, $allowedEntities);
86 144
            } elseif ($entity->getInheritance() instanceof JoinedTableInheritanceSchema) {
87 144
                $this->removeJtiExtraFields($entity);
88 144
                $this->addForeignKey($this->parseMetadata($entity, Inheritance::class), $entity, $registry);
0 ignored issues
show
Bug introduced by
It seems like $this->parseMetadata($en...ion\Inheritance::class) can also be of type null; however, parameter $annotation of Cycle\Annotated\TableInheritance::addForeignKey() does only seem to accept Cycle\Annotated\Annotation\Inheritance\JoinedTable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
                $this->addForeignKey(/** @scrutinizer ignore-type */ $this->parseMetadata($entity, Inheritance::class), $entity, $registry);
Loading history...
89
            }
90
        }
91
92 144
        return $registry;
93
    }
94
95 144
    private function findParent(Registry $registry, string $role): ?EntitySchema
96
    {
97 144
        foreach ($registry as $entity) {
98 144
            if ($entity->getRole() === $role || $entity->getClass() === $role) {
99 144
                return $entity;
100
            }
101
102 144
            $children = $registry->getChildren($entity);
103 144
            foreach ($children as $child) {
104 144
                if ($child->getRole() === $role || $child->getClass() === $role) {
105 144
                    return $child;
106
                }
107
            }
108
        }
109
110
        return null;
111
    }
112
113 144
    private function initInheritance(
114
        Inheritance $inheritance,
115
        EntitySchema $entity,
116
        EntitySchema $parent
117
    ): ?EntitySchema {
118 144
        if ($inheritance instanceof Inheritance\SingleTable) {
119 144
            if (!$parent->getInheritance() instanceof SingleTableInheritanceSchema) {
120 144
                $parent->setInheritance(new SingleTableInheritanceSchema());
121
            }
122
123 144
            $parent->getInheritance()->addChild(
0 ignored issues
show
Bug introduced by
The method addChild() does not exist on Cycle\Schema\Definition\Inheritance. It seems like you code against a sub-type of Cycle\Schema\Definition\Inheritance such as Cycle\Schema\Definition\Inheritance\SingleTable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
            $parent->getInheritance()->/** @scrutinizer ignore-call */ addChild(
Loading history...
124 144
                $inheritance->getValue() ?? $entity->getRole(),
0 ignored issues
show
Bug introduced by
It seems like $inheritance->getValue() ?? $entity->getRole() can also be of type null; however, parameter $discriminatorValue of Cycle\Schema\Definition\...SingleTable::addChild() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

124
                /** @scrutinizer ignore-type */ $inheritance->getValue() ?? $entity->getRole(),
Loading history...
125 144
                $entity->getClass()
0 ignored issues
show
Bug introduced by
It seems like $entity->getClass() can also be of type null; however, parameter $class of Cycle\Schema\Definition\...SingleTable::addChild() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

125
                /** @scrutinizer ignore-type */ $entity->getClass()
Loading history...
126
            );
127
128 144
            $entity->markAsChildOfSingleTableInheritance($parent->getClass());
129
130
            // Root STI may have a discriminator annotation
131
            /** @var Inheritance\DiscriminatorColumn $annotation */
132 144
            if ($annotation = $this->parseMetadata($parent, Inheritance\DiscriminatorColumn::class)) {
133 144
                $parent->getInheritance()->setDiscriminator($annotation->getName());
0 ignored issues
show
Bug introduced by
The method setDiscriminator() does not exist on Cycle\Schema\Definition\Inheritance. It seems like you code against a sub-type of Cycle\Schema\Definition\Inheritance such as Cycle\Schema\Definition\Inheritance\SingleTable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

133
                $parent->getInheritance()->/** @scrutinizer ignore-call */ setDiscriminator($annotation->getName());
Loading history...
134
            }
135
136 144
            $parent->merge($entity);
137
138 144
            return $parent;
139 144
        }
140 144
        if ($inheritance instanceof Inheritance\JoinedTable) {
141
            $entity->setInheritance(
142 144
                new JoinedTableInheritanceSchema(
143
                    $parent,
144
                    $inheritance->getOuterKey()
145
                )
146 144
            );
147
148
            return $entity;
149
        }
150
151
        // Custom table inheritance types developers can handle in their own generators
152
        return null;
153
    }
154
155
    /**
156
     * @template T
157
     *
158
     * @param class-string<T> $name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
159
     *
160 144
     * @return T|null
161
     */
162
    private function parseMetadata(EntitySchema $entity, string $name): ?object
163 144
    {
164
        try {
165 144
            $class = $entity->getClass();
166
            assert($class !== null);
167
            return $this->reader->firstClassMetadata(new \ReflectionClass($class), $name);
168
        } catch (\Exception $e) {
169
            throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
170
        }
171
    }
172
173
    /**
174 144
     * Removes parent entity fields from given entity except Primary key.
175
     */
176 144
    private function removeJtiExtraFields(EntitySchema $entity): void
177 144
    {
178 144
        foreach ($entity->getFields() as $name => $field) {
179
            if ($field->getEntityClass() === $entity->getClass()) {
180
                continue;
181 144
            }
182 144
183
            if (!$field->isPrimary()) {
184 144
                $entity->getFields()->remove($name);
185 144
            } else {
186
                if ($field->getType() === 'primary') {
187
                    $field->setType('integer')->setPrimary(true);
188
                } elseif ($field->getType() === 'bigPrimary') {
189
                    $field->setType('bigInteger')->setPrimary(true);
190
                }
191 144
            }
192
        }
193
    }
194
195
    /**
196 144
     * Removes non STI child entity fields from given entity.
197
     */
198 144
    private function removeStiExtraFields(EntitySchema $entity, array $allowedEntities): void
199
    {
200 144
        $allowedEntities[] = $entity->getClass();
201 144
202 144
        foreach ($entity->getFields() as $name => $field) {
203
            if (\in_array($field->getEntityClass(), $allowedEntities, true)) {
204
                continue;
205 144
            }
206
207 144
            $entity->getFields()->remove($name);
208
        }
209 144
    }
210
211
    private function addForeignKey(
212
        Inheritance\JoinedTable $annotation,
213
        EntitySchema $entity,
214 144
        Registry $registry
215 144
    ): void {
216
        if (!$annotation->isCreateFk()) {
217
            return;
218 144
        }
219 144
220
        $parent = $this->getParentForForeignKey($entity, $registry);
221 144
        $outerFields = $this->getOuterFields($entity, $parent, $annotation);
222 144
223 144
        foreach ($outerFields->getColumnNames() as $column) {
224
            if (!$registry->getTableSchema($parent)->hasColumn($column)) {
225
                return;
226
            }
227 120
        }
228 120
229 24
        foreach ($entity->getPrimaryFields()->getColumnNames() as $column) {
230
            if (!$registry->getTableSchema($entity)->hasColumn($column)) {
231
                return;
232
            }
233 96
        }
234
235 96
        $registry->getTableSchema($parent)->index($outerFields->getColumnNames())->unique();
236 96
237 96
        $registry->getTableSchema($entity)
238 96
            ->foreignKey($entity->getPrimaryFields()->getColumnNames())
239 96
            ->references($registry->getTable($parent), $outerFields->getColumnNames())
240 96
            ->onUpdate($annotation->getFkAction())
241
            ->onDelete($annotation->getFkAction());
242 144
    }
243
244 144
    private function getParentForForeignKey(EntitySchema $schema, Registry $registry): EntitySchema
245
    {
246 144
        $parentSchema = $schema->getInheritance();
247
248 144
        if ($parentSchema instanceof JoinedTableInheritanceSchema) {
249 144
            // entity is STI child
250 144
            $parent = $parentSchema->getParent();
251 144
            if ($parent->isChildOfSingleTableInheritance()) {
252 144
                return $this->getParentForForeignKey($this->findParent(
0 ignored issues
show
Bug introduced by
It seems like $this->findParent($regis...nt->getClass(), false)) can also be of type null; however, parameter $schema of Cycle\Annotated\TableInh...etParentForForeignKey() does only seem to accept Cycle\Schema\Definition\Entity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
                return $this->getParentForForeignKey(/** @scrutinizer ignore-type */ $this->findParent(
Loading history...
253 144
                    $registry,
254
                    $this->utils->findParent($parent->getClass(), false)
0 ignored issues
show
Bug introduced by
It seems like $parent->getClass() can also be of type null; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParent() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

254
                    $this->utils->findParent(/** @scrutinizer ignore-type */ $parent->getClass(), false)
Loading history...
255
                ), $registry);
256 144
            }
257
258
            return $parent;
259 144
        }
260
261
        return $schema;
262 144
    }
263
264
    private function getOuterFields(
265
        EntitySchema $entity,
266
        EntitySchema $parent,
267 144
        Inheritance\JoinedTable $annotation
268
    ): FieldMap {
269 144
        $outerKey = $annotation->getOuterKey();
270 144
271
        if ($outerKey) {
272
            if (!$parent->getFields()->has($outerKey)) {
273
                throw new WrongParentKeyColumnException($entity, $outerKey);
274 144
            }
275
276
            return (new FieldMap())->set($outerKey, $parent->getFields()->get($outerKey));
277 144
        }
278
279
        return $parent->getPrimaryFields();
280
    }
281
282
    private function getTableName(EntitySchema $child, Registry $registry): string
283
    {
284
        $parent = $this->findParent($registry, $this->utils->findParent($child->getClass(), false));
0 ignored issues
show
Bug introduced by
It seems like $child->getClass() can also be of type null; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParent() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

284
        $parent = $this->findParent($registry, $this->utils->findParent(/** @scrutinizer ignore-type */ $child->getClass(), false));
Loading history...
285
286
        $inheritance = $parent->getInheritance();
287
        if (!$inheritance instanceof SingleTableInheritanceSchema) {
288
            return $child->getTableName();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $child->getTableName() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
289
        }
290
        $entities = \array_map(
291
            static fn (string $role) => $registry->getEntity($role)->getClass(),
292
            $inheritance->getChildren()
293
        );
294
295
        return \in_array($child->getClass(), $entities, true) ? $parent->getTableName() : $child->getTableName();
0 ignored issues
show
Bug Best Practice introduced by
The expression return in_array($child->... $child->getTableName() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
296
    }
297
298
    private function getDatabase(EntitySchema $child, Registry $registry): ?string
299
    {
300
        $parent = $this->findParent($registry, $this->utils->findParent($child->getClass(), false));
0 ignored issues
show
Bug introduced by
It seems like $child->getClass() can also be of type null; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParent() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

300
        $parent = $this->findParent($registry, $this->utils->findParent(/** @scrutinizer ignore-type */ $child->getClass(), false));
Loading history...
301
302
        $inheritance = $parent->getInheritance();
303
        if (!$inheritance instanceof SingleTableInheritanceSchema) {
304
            return $child->getDatabase();
305
        }
306
        $entities = \array_map(
307
            static fn (string $role) => $registry->getEntity($role)->getClass(),
308
            $inheritance->getChildren()
309
        );
310
311
        return \in_array($child->getClass(), $entities, true) ? $parent->getDatabase() : $child->getDatabase();
312
    }
313
}
314