Passed
Push — 3.x ( d3f62a...abd3c2 )
by Aleksei
15:20 queued 13s
created

TableInheritance::removeJtiExtraFields()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.5625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 10
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 14
ccs 6
cts 8
cp 0.75
crap 6.5625
rs 9.2222
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;
11
use Cycle\Schema\Definition\Entity as EntitySchema;
12
use Cycle\Schema\Definition\Inheritance\JoinedTable as JoinedTableInheritanceSchema;
13
use Cycle\Schema\Definition\Inheritance\SingleTable as SingleTableInheritanceSchema;
14
use Cycle\Schema\Definition\Map\FieldMap;
15
use Cycle\Schema\Exception\TableInheritance\WrongParentKeyColumnException;
16
use Cycle\Schema\GeneratorInterface;
17
use Cycle\Schema\Registry;
18
use Doctrine\Common\Annotations\Reader as DoctrineReader;
19
use Spiral\Attributes\ReaderInterface;
20
21
class TableInheritance implements GeneratorInterface
22
{
23
    private ReaderInterface $reader;
24
    private EntityUtils $utils;
25 144
26
    public function __construct(
27
        DoctrineReader|ReaderInterface $reader = null
28 144
    ) {
29 144
        $this->reader = ReaderFactory::create($reader);
30 144
        $this->utils = new EntityUtils($this->reader);
31
    }
32 144
33
    public function run(Registry $registry): Registry
34
    {
35 144
        /** @var EntitySchema[] $found */
36
        $found = [];
37 144
38
        foreach ($registry as $entity) {
39 144
            // Only child entities can have table inheritance annotation
40
            $children = $registry->getChildren($entity);
41 144
42
            foreach ($children as $child) {
43 144
                /** @var Inheritance $annotation */
44 144
                if ($annotation = $this->parseMetadata($child, Inheritance::class)) {
45
                    $childClass = $child->getClass();
46
47
                    // Child entities always have parent entity
48 144
                    do {
49
                        $parent = $this->findParent(
50 144
                            $registry,
51
                            $this->utils->findParent($childClass, false)
52
                        );
53 144
54 144
                        if ($annotation instanceof Inheritance\JoinedTable) {
55
                            break;
56
                        }
57 144
58 144
                        $childClass = $parent->getClass();
59
                    } 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

59
                    } while ($this->parseMetadata(/** @scrutinizer ignore-type */ $parent, Inheritance::class) !== null);
Loading history...
60 144
61 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

61
                    if ($inheritanceEntity = $this->initInheritance($annotation, $child, /** @scrutinizer ignore-type */ $parent)) {
Loading history...
62
                        $found[] = $inheritanceEntity;
63
                    }
64
                }
65
66
                // All child should be presented in a schema as separated entity
67 144
                // Every child will be handled according its table inheritance type
68 144
                if (!$registry->hasEntity($child->getRole())) {
69
                    $registry->register($child);
70 144
71
                    $registry->linkTable(
72 144
                        $child,
73 144
                        $this->getDatabase($child, $registry),
74
                        $this->getTableName($child, $registry)
75
                    );
76
                }
77
            }
78
        }
79 144
80 144
        foreach ($found as $entity) {
81 144
            if ($entity->getInheritance() instanceof SingleTableInheritanceSchema) {
82 144
                $allowedEntities = \array_map(
83 144
                    static fn (string $role) => $registry->getEntity($role)->getClass(),
84
                    $entity->getInheritance()->getChildren()
85 144
                );
86 144
                $this->removeStiExtraFields($entity, $allowedEntities);
87 144
            } elseif ($entity->getInheritance() instanceof JoinedTableInheritanceSchema) {
88 144
                $this->removeJtiExtraFields($entity);
89
                $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

89
                $this->addForeignKey(/** @scrutinizer ignore-type */ $this->parseMetadata($entity, Inheritance::class), $entity, $registry);
Loading history...
90
            }
91
        }
92 144
93
        return $registry;
94
    }
95 144
96
    private function findParent(Registry $registry, string $role): ?EntitySchema
97 144
    {
98 144
        foreach ($registry as $entity) {
99 144
            if ($entity->getRole() === $role || $entity->getClass() === $role) {
100
                return $entity;
101
            }
102 144
103 144
            $children = $registry->getChildren($entity);
104 144
            foreach ($children as $child) {
105 144
                if ($child->getRole() === $role || $child->getClass() === $role) {
106
                    return $child;
107
                }
108
            }
109
        }
110
111
        return null;
112
    }
113 144
114
    private function initInheritance(
115
        Inheritance $inheritance,
116
        EntitySchema $entity,
117
        EntitySchema $parent
118 144
    ): ?EntitySchema {
119 144
        if ($inheritance instanceof Inheritance\SingleTable) {
120 144
            if (!$parent->getInheritance() instanceof SingleTableInheritanceSchema) {
121
                $parent->setInheritance(new SingleTableInheritanceSchema());
122
            }
123 144
124 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

124
            $parent->getInheritance()->/** @scrutinizer ignore-call */ addChild(
Loading history...
125 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

125
                /** @scrutinizer ignore-type */ $inheritance->getValue() ?? $entity->getRole(),
Loading history...
126
                $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

126
                /** @scrutinizer ignore-type */ $entity->getClass()
Loading history...
127
            );
128 144
129
            $entity->markAsChildOfSingleTableInheritance($parent->getClass());
130
131
            // Root STI may have a discriminator annotation
132 144
            /** @var Inheritance\DiscriminatorColumn $annotation */
133 144
            if ($annotation = $this->parseMetadata($parent, Inheritance\DiscriminatorColumn::class)) {
134
                $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

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

253
                return $this->getParentForForeignKey(/** @scrutinizer ignore-type */ $this->findParent(
Loading history...
254
                    $registry,
255
                    $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

255
                    $this->utils->findParent(/** @scrutinizer ignore-type */ $parent->getClass(), false)
Loading history...
256 144
                ), $registry);
257
            }
258
259 144
            return $parent;
260
        }
261
262 144
        return $schema;
263
    }
264
265
    private function getOuterFields(
266
        EntitySchema $entity,
267 144
        EntitySchema $parent,
268
        Inheritance\JoinedTable $annotation
269 144
    ): FieldMap {
270 144
        $outerKey = $annotation->getOuterKey();
271
272
        if ($outerKey) {
273
            if (!$parent->getFields()->has($outerKey)) {
274 144
                throw new WrongParentKeyColumnException($entity, $outerKey);
275
            }
276
277 144
            return (new FieldMap())->set($outerKey, $parent->getFields()->get($outerKey));
278
        }
279
280
        return $parent->getPrimaryFields();
281
    }
282
283
    private function getTableName(Entity $child, Registry $registry): string
284
    {
285
        $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

285
        $parent = $this->findParent($registry, $this->utils->findParent(/** @scrutinizer ignore-type */ $child->getClass(), false));
Loading history...
286
287
        $inheritance = $parent->getInheritance();
288
        if (!$inheritance instanceof SingleTableInheritanceSchema) {
289
            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...
290
        }
291
        $entities = \array_map(
292
            static fn (string $role) => $registry->getEntity($role)->getClass(),
293
            $inheritance->getChildren()
294
        );
295
296
        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...
297
    }
298
299
    private function getDatabase(Entity $child, Registry $registry): ?string
300
    {
301
        $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

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