Entities   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 97.01%

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 94
c 6
b 1
f 0
dl 0
loc 196
ccs 65
cts 67
cp 0.9701
rs 9.84
wmc 32

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
B run() 0 57 7
B resolveTarget() 0 19 7
A getColumnNames() 0 12 2
F normalizeNames() 0 74 15
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