Passed
Pull Request — 3.x (#43)
by Aleksei
14:43
created

TableInheritance::getOuterFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 3
nop 3
dl 0
loc 16
ccs 0
cts 0
cp 0
crap 12
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 72
    private EntityUtils $utils;
24
25
    public function __construct(
26 72
        DoctrineReader|ReaderInterface $reader = null
27 72
    ) {
28 72
        $this->reader = ReaderFactory::create($reader);
29
        $this->utils = new EntityUtils($this->reader);
30 72
    }
31
32
    public function run(Registry $registry): Registry
33 72
    {
34
        /** @var EntitySchema[] $found */
35 72
        $found = [];
36
37 72
        foreach ($registry as $entity) {
38
            // Only child entities can have table inheritance annotation
39 72
            $children = $registry->getChildren($entity);
40
41 72
            foreach ($children as $child) {
42 72
                /** @var Inheritance $annotation */
43
                if ($annotation = $this->parseMetadata($child, Inheritance::class)) {
44
                    $childClass = $child->getClass();
45
46 72
                    // Child entities always have parent entity
47
                    do {
48 72
                        $parent = $this->findParent(
49
                            $registry,
50
                            $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 72
                        );
52 72
53
                        if ($annotation instanceof Inheritance\JoinedTable) {
54
                            break;
55 72
                        }
56 72
57
                        $childClass = $parent->getClass();
58 72
                    } 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 72
60
                    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
                        $found[] = $entity;
62
                    }
63
                }
64
65 72
                // All child should be presented in a schema as separated entity
66 72
                // Every child will be handled according its table inheritance type
67
                if (!$registry->hasEntity($child->getRole())) {
68 72
                    $registry->register($child);
69
70 72
                    $registry->linkTable(
71 72
                        $child,
72
                        $child->getDatabase(),
73
                        $child->getTableName(),
74
                    );
75
                }
76
            }
77 72
        }
78 72
79 72
        foreach ($found as $entity) {
80 72
            if ($entity->getInheritance() instanceof SingleTableInheritanceSchema) {
81 72
                $allowedEntities = \array_map(
82
                    static fn (string $role) => $registry->getEntity($role)->getClass(),
83 72
                    $entity->getInheritance()->getChildren()
84 72
                );
85 72
                $this->removeStiExtraFields($entity, $allowedEntities);
86
            } elseif ($entity->getInheritance() instanceof JoinedTableInheritanceSchema) {
87
                $this->removeJtiExtraFields($entity);
88
                $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 72
            }
90
        }
91
92 72
        return $registry;
93
    }
94 72
95 72
    private function findParent(Registry $registry, string $role): ?EntitySchema
96 72
    {
97
        foreach ($registry as $entity) {
98
            if ($entity->getRole() === $role || $entity->getClass() === $role) {
99 72
                return $entity;
100 72
            }
101 72
102 72
            $children = $registry->getChildren($entity);
103
            foreach ($children as $child) {
104
                if ($child->getRole() === $role || $child->getClass() === $role) {
105
                    return $child;
106
                }
107
            }
108
        }
109
110 72
        return null;
111
    }
112
113
    private function initInheritance(
114
        Inheritance $inheritance,
115 72
        EntitySchema $entity,
116 72
        EntitySchema $parent
117 72
    ): ?EntitySchema {
118
        if ($inheritance instanceof Inheritance\SingleTable) {
119
            if (!$parent->getInheritance() instanceof SingleTableInheritanceSchema) {
120 72
                $parent->setInheritance(new SingleTableInheritanceSchema());
121 72
            }
122 72
123
            $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
                $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 72
                $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
            $entity->markAsChildOfSingleTableInheritance($parent->getClass());
129 72
130 72
            // Root STI may have a discriminator annotation
131
            /** @var Inheritance\DiscriminatorColumn $annotation */
132
            if ($annotation = $this->parseMetadata($parent, Inheritance\DiscriminatorColumn::class)) {
133 72
                $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 72
136 72
            return $parent;
137 72
        }
138
        if ($inheritance instanceof Inheritance\JoinedTable) {
139 72
            $entity->setInheritance(
140
                new JoinedTableInheritanceSchema(
141
                    $parent,
142
                    $inheritance->getOuterKey()
143 72
                )
144
            );
145
146
            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 72
     *
158
     * @return T|null
159
     */
160 72
    private function parseMetadata(EntitySchema $entity, string $name): ?object
161
    {
162 72
        try {
163
            $class = $entity->getClass();
164
            assert($class !== null);
165
            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 72
    /**
172
     * Removes parent entity fields from given entity except Primary key.
173 72
     */
174 72
    private function removeJtiExtraFields(EntitySchema $entity): void
175 72
    {
176
        foreach ($entity->getFields() as $name => $field) {
177
            if ($field->getEntityClass() === $entity->getClass()) {
178 72
                continue;
179 72
            }
180
181 72
            if (!$field->isPrimary()) {
182 72
                $entity->getFields()->remove($name);
183
            } else {
184
                if ($field->getType() === 'primary') {
185
                    $field->setType('integer')->setPrimary(true);
186
                } elseif ($field->getType() === 'bigPrimary') {
187
                    $field->setType('bigInteger')->setPrimary(true);
188 72
                }
189
            }
190
        }
191
    }
192
193 72
    /**
194
     * Removes non STI child entity fields from given entity.
195 72
     */
196
    private function removeStiExtraFields(EntitySchema $entity, array $allowedEntities): void
197 72
    {
198 72
        $allowedEntities[] = $entity->getClass();
199 72
200
        foreach ($entity->getFields() as $name => $field) {
201
            if (\in_array($field->getEntityClass(), $allowedEntities, true)) {
202 72
                continue;
203
            }
204 72
205
            $entity->getFields()->remove($name);
206
        }
207
    }
208
209
    private function addForeignKey(
210
        Inheritance\JoinedTable $annotation,
211
        EntitySchema $entity,
212
        Registry $registry
213
    ): void {
214
        if (!$annotation->isCreateFk()) {
215
            return;
216
        }
217
218
        $parent = $this->getParentForForeignKey($entity, $registry);
219
        $outerFields = $this->getOuterFields($entity, $parent, $annotation);
220
221
        foreach ($outerFields->getColumnNames() as $column) {
222
            if (!$registry->getTableSchema($parent)->hasColumn($column)) {
223
                return;
224
            }
225
        }
226
227
        foreach ($entity->getPrimaryFields()->getColumnNames() as $column) {
228
            if (!$registry->getTableSchema($entity)->hasColumn($column)) {
229
                return;
230
            }
231
        }
232
233
        $registry->getTableSchema($parent)->index($outerFields->getColumnNames())->unique();
234
235
        $registry->getTableSchema($entity)
236
            ->foreignKey($entity->getPrimaryFields()->getColumnNames())
237
            ->references($registry->getTable($parent), $outerFields->getColumnNames())
238
            ->onUpdate($annotation->getFkAction())
239
            ->onDelete($annotation->getFkAction());
240
    }
241
242
    private function getParentForForeignKey(EntitySchema $schema, Registry $registry): EntitySchema
243
    {
244
        $parentSchema = $schema->getInheritance();
245
246
        if ($parentSchema instanceof JoinedTableInheritanceSchema) {
247
            // entity is STI child
248
            $parent = $parentSchema->getParent();
249
            if ($parent->isChildOfSingleTableInheritance()) {
250
                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
                    $registry,
252
                    $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
                ), $registry);
254
            }
255
256
            return $parent;
257
        }
258
259
        return $schema;
260
    }
261
262
    private function getOuterFields(
263
        EntitySchema $entity,
264
        EntitySchema $parent,
265
        Inheritance\JoinedTable $annotation
266
    ): FieldMap {
267
        $outerKey = $annotation->getOuterKey();
268
269
        if ($outerKey) {
270
            if (!$parent->getFields()->has($outerKey)) {
271
                throw new WrongParentKeyColumnException($entity, $outerKey);
272
            }
273
274
            return (new FieldMap())->set($outerKey, $parent->getFields()->get($outerKey));
275
        }
276
277
        return $parent->getPrimaryFields();
278
    }
279
}
280