Passed
Push — 3.x ( 42ab4a...bb376d )
by Aleksei
12:31 queued 16s
created

Entities::run()   B

Complexity

Conditions 7
Paths 11

Size

Total Lines 54
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 7.025

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 7
eloc 25
c 4
b 0
f 0
nc 11
nop 1
dl 0
loc 54
ccs 23
cts 25
cp 0.92
crap 7.025
rs 8.5866

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
            // register entity (OR find parent)
88
            $registry->register($e);
89
            $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

89
            $registry->linkTable($e, $e->getDatabase(), /** @scrutinizer ignore-type */ $e->getTableName());
Loading history...
90 1032
        }
91
92
        foreach ($children as $e) {
93 1032
            $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

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