Passed
Push — 3.x ( 6aff79...9d7be0 )
by Aleksei
04:54
created

TableInheritance::addForeignKey()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 8
nop 3
dl 0
loc 31
ccs 17
cts 17
cp 1
crap 6
rs 9.1111
c 0
b 0
f 0
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)
0 ignored issues
show
Bug introduced by
It seems like $this->utils->findParent($childClass, false) can also be of type null; however, parameter $role of Cycle\Annotated\TableInheritance::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

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

250
                return $this->getParentForForeignKey(/** @scrutinizer ignore-type */ $this->findParent(
Loading history...
251 144
                    $registry,
252 144
                    $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

252
                    $this->utils->findParent(/** @scrutinizer ignore-type */ $parent->getClass(), false)
Loading history...
Bug introduced by
It seems like $this->utils->findParent...ent->getClass(), false) can also be of type null; however, parameter $role of Cycle\Annotated\TableInheritance::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

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