Passed
Push — 3.x ( 44cdb4...47c35b )
by Aleksei
13:33
created

TableInheritance::run()   C

Complexity

Conditions 12
Paths 68

Size

Total Lines 68
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 37
c 1
b 0
f 0
nc 68
nop 1
dl 0
loc 68
ccs 34
cts 34
cp 1
crap 12
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 ($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
                    $database = $child->getDatabase();
71
                    $tableName = $child->getTableName();
72 144
                    if ($entity->getInheritance() instanceof SingleTableInheritanceSchema) {
73 144
                        $database = $parent->getDatabase();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parent does not seem to be defined for all execution paths leading up to this point.
Loading history...
74
                        $tableName = $parent->getTableName();
75
                    }
76
77
                    $registry->linkTable(
78
                        $child,
79 144
                        $database,
80 144
                        $tableName,
81 144
                    );
82 144
                }
83 144
            }
84
        }
85 144
86 144
        foreach ($found as $entity) {
87 144
            if ($entity->getInheritance() instanceof SingleTableInheritanceSchema) {
88 144
                $allowedEntities = \array_map(
89
                    static fn (string $role) => $registry->getEntity($role)->getClass(),
90
                    $entity->getInheritance()->getChildren()
91
                );
92 144
                $this->removeStiExtraFields($entity, $allowedEntities);
93
            } elseif ($entity->getInheritance() instanceof JoinedTableInheritanceSchema) {
94
                $this->removeJtiExtraFields($entity);
95 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

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

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

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

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

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

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

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