Entities::run()   B
last analyzed

Complexity

Conditions 7
Paths 11

Size

Total Lines 57
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 7.0222

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 7
eloc 26
c 4
b 0
f 0
nc 11
nop 1
dl 0
loc 57
ccs 24
cts 26
cp 0.9231
crap 7.0222
rs 8.5706

How to fix   Long Method   

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\Entity;
8
use Cycle\Annotated\Exception\AnnotationException;
9
use Cycle\Annotated\Utils\EntityUtils;
10
use Cycle\Schema\Definition\Entity as EntitySchema;
11
use Cycle\Schema\Exception\RegistryException;
12
use Cycle\Schema\Exception\RelationException;
13
use Cycle\Schema\GeneratorInterface;
14
use Cycle\Schema\Registry;
15
use Doctrine\Common\Annotations\Reader as DoctrineReader;
16
use Spiral\Attributes\ReaderInterface;
17
use Spiral\Tokenizer\ClassesInterface;
18
19
/**
20
 * Generates ORM schema based on annotated classes.
21
 */
22
final class Entities implements GeneratorInterface
23
{
24
    // table name generation
25
    public const TABLE_NAMING_PLURAL = 1;
26
    public const TABLE_NAMING_SINGULAR = 2;
27
    public const TABLE_NAMING_NONE = 3;
28
29
    private ReaderInterface $reader;
30
    private Configurator $generator;
31
    private EntityUtils $utils;
32
33 1056
    public function __construct(
34
        private ClassesInterface $locator,
35
        DoctrineReader|ReaderInterface $reader = null,
36
        int $tableNamingStrategy = self::TABLE_NAMING_PLURAL,
37
    ) {
38 1056
        $this->reader = ReaderFactory::create($reader);
39 1056
        $this->utils = new EntityUtils($this->reader);
40 1056
        $this->generator = new Configurator($this->reader, $tableNamingStrategy);
41 1056
    }
42
43 1056
    public function run(Registry $registry): Registry
44
    {
45
        /** @var EntitySchema[] $children */
46 1056
        $children = [];
47 1056
        foreach ($this->locator->getClasses() as $class) {
48
            try {
49
                /** @var Entity $ann */
50 1056
                $ann = $this->reader->firstClassMetadata($class, Entity::class);
51
            } catch (\Exception $e) {
52
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
53
            }
54
55 1056
            if ($ann === null) {
56 736
                continue;
57
            }
58
59 1056
            $e = $this->generator->initEntity($ann, $class);
60
61
            // columns
62 1056
            $this->generator->initFields($e, $class);
63
64
            // relations
65 1032
            $this->generator->initRelations($e, $class);
66
67
            // schema modifiers
68 1032
            $this->generator->initModifiers($e, $class);
69
70
            // foreign keys
71 1032
            $this->generator->initForeignKeys($ann, $e, $class);
72
73 1032
            // additional columns (mapped to local fields automatically)
74 688
            $this->generator->initColumns($e, $ann->getColumns(), $class);
75 688
76
            if ($this->utils->hasParent($e->getClass())) {
0 ignored issues
show
Bug introduced by
It seems like $e->getClass() can also be of type null; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::hasParent() 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

76
            if ($this->utils->hasParent(/** @scrutinizer ignore-type */ $e->getClass())) {
Loading history...
77
                foreach ($this->utils->findParents($e->getClass()) as $parent) {
0 ignored issues
show
Bug introduced by
It seems like $e->getClass() can also be of type null; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParents() 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

77
                foreach ($this->utils->findParents(/** @scrutinizer ignore-type */ $e->getClass()) as $parent) {
Loading history...
78
                    // additional columns from parent class
79 1032
                    $ann = $this->reader->firstClassMetadata($parent, Entity::class);
80 1032
                    $this->generator->initColumns($e, $ann->getColumns(), $parent);
81
                }
82
83 1032
                $children[] = $e;
84 688
                continue;
85
            }
86
87 1032
            // generated fields
88
            $this->generator->initGeneratedFields($e, $class);
89
90 1032
            // register entity (OR find parent)
91
            $registry->register($e);
92
            $registry->linkTable($e, $e->getDatabase(), $e->getTableName());
0 ignored issues
show
Bug introduced by
It seems like $e->getTableName() can also be of type null; however, parameter $table of Cycle\Schema\Registry::linkTable() 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

92
            $registry->linkTable($e, $e->getDatabase(), /** @scrutinizer ignore-type */ $e->getTableName());
Loading history...
93 1032
        }
94 1032
95 712
        foreach ($children as $e) {
96
            $registry->registerChildWithoutMerge($registry->getEntity($this->utils->findParent($e->getClass())), $e);
0 ignored issues
show
Bug introduced by
It seems like $this->utils->findParent($e->getClass()) can also be of type null; however, parameter $role of Cycle\Schema\Registry::getEntity() 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

96
            $registry->registerChildWithoutMerge($registry->getEntity(/** @scrutinizer ignore-type */ $this->utils->findParent($e->getClass())), $e);
Loading history...
97
        }
98 1032
99
        return $this->normalizeNames($registry);
100
    }
101 1032
102
    private function normalizeNames(Registry $registry): Registry
103 808
    {
104
        foreach ($this->locator->getClasses() as $class) {
105 784
            if (!$registry->hasEntity($class->getName())) {
106 568
                continue;
107 568
            }
108 496
109 496
            $e = $registry->getEntity($class->getName());
110 496
111
            // resolve all the relation target names into roles
112
            foreach ($e->getRelations() as $name => $r) {
113
                try {
114
                    $r->setTarget($this->resolveTarget($registry, $r->getTarget()));
115 784
116 568
                    if ($r->getOptions()->has('though')) {
117 568
                        $though = $r->getOptions()->get('though');
118 568
                        if ($though !== null) {
119 568
                            $r->getOptions()->set(
120 568
                                'though',
121
                                $this->resolveTarget($registry, $though)
122
                            );
123
                        }
124
                    }
125 784
126 568
                    if ($r->getOptions()->has('through')) {
127 568
                        $through = $r->getOptions()->get('through');
128
                        if ($through !== null) {
129
                            $r->getOptions()->set(
130
                                'through',
131 784
                                $this->resolveTarget($registry, $through)
132 568
                            );
133 784
                        }
134
                    }
135
136 24
                    if ($r->getOptions()->has('throughInnerKey')) {
137 24
                        if ($throughInnerKey = (array)$r->getOptions()->get('throughInnerKey')) {
138 24
                            $r->getOptions()->set('throughInnerKey', $throughInnerKey);
139 24
                        }
140 24
                    }
141
142
                    if ($r->getOptions()->has('throughOuterKey')) {
143 24
                        if ($throughOuterKey = (array)$r->getOptions()->get('throughOuterKey')) {
144
                            $r->getOptions()->set('throughOuterKey', $throughOuterKey);
145
                        }
146
                    }
147
                } catch (RegistryException $ex) {
148
                    throw new RelationException(
149
                        sprintf(
150 1008
                            'Unable to resolve `%s`.`%s` relation target (not found or invalid)',
151
                            $e->getRole(),
152
                            $name
153 808
                        ),
154
                        $ex->getCode(),
155 808
                        $ex
156
                    );
157 592
                }
158
            }
159
160 808
            // resolve foreign key target and column names
161
            foreach ($e->getForeignKeys() as $foreignKey) {
162 72
                $target = $this->resolveTarget($registry, $foreignKey->getTarget());
163 72
                \assert(!empty($target), 'Unable to resolve foreign key target entity.');
164 48
                $targetEntity = $registry->getEntity($target);
165 48
166
                $foreignKey->setTarget($target);
167
                $foreignKey->setInnerColumns($this->getColumnNames($e, $foreignKey->getInnerColumns()));
168
169
                $foreignKey->setOuterColumns(empty($foreignKey->getOuterColumns())
170
                    ? $targetEntity->getPrimaryFields()->getColumnNames()
171 784
                    : $this->getColumnNames($targetEntity, $foreignKey->getOuterColumns()));
172
            }
173
        }
174
175
        return $registry;
176
    }
177
178
    private function resolveTarget(Registry $registry, string $name): ?string
179
    {
180
        if (\interface_exists($name, true)) {
181
            // do not resolve interfaces
182
            return $name;
183
        }
184
185
        if (!$registry->hasEntity($name)) {
186
            // point all relations to the parent
187
            foreach ($registry as $entity) {
188
                foreach ($registry->getChildren($entity) as $child) {
189
                    if ($child->getClass() === $name || $child->getRole() === $name) {
190
                        return $entity->getRole();
191
                    }
192
                }
193
            }
194
        }
195
196
        return $registry->getEntity($name)->getRole();
197
    }
198
199
    /**
200
     * @param array<non-empty-string> $columns
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string>.
Loading history...
201
     *
202
     * @throws AnnotationException
203
     *
204
     * @return array<non-empty-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string>.
Loading history...
205
     */
206
    private function getColumnNames(EntitySchema $entity, array $columns): array
207
    {
208
        $names = [];
209
        foreach ($columns as $name) {
210
            $names[] = match (true) {
211
                $entity->getFields()->has($name) => $entity->getFields()->get($name)->getColumn(),
212
                $entity->getFields()->hasColumn($name) => $name,
213
                default => throw new AnnotationException('Unable to resolve column name.'),
214
            };
215
        }
216
217
        return $names;
218
    }
219
}
220