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

Entities::getColumnNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 12
ccs 0
cts 0
cp 0
crap 6
rs 10
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