Passed
Push — 3.x ( 080893...4e5cb3 )
by Aleksei
09:48
created

Entities::run()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 45
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.031

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
nc 9
nop 1
dl 0
loc 45
ccs 19
cts 21
cp 0.9048
crap 6.031
rs 8.9617
c 3
b 0
f 0
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 480
    public function __construct(
34
        private ClassesInterface $locator,
35
        DoctrineReader|ReaderInterface $reader = null,
36
        int $tableNamingStrategy = self::TABLE_NAMING_PLURAL
37
    ) {
38 480
        $this->reader = ReaderFactory::create($reader);
39 480
        $this->utils = new EntityUtils($this->reader);
40 480
        $this->generator = new Configurator($this->reader, $tableNamingStrategy);
41 480
    }
42
43 480
    public function run(Registry $registry): Registry
44
    {
45
        /** @var EntitySchema[] $children */
46 480
        $children = [];
47 480
        foreach ($this->locator->getClasses() as $class) {
48
            try {
49
                /** @var Entity $ann */
50 480
                $ann = $this->reader->firstClassMetadata($class, Entity::class);
51
            } catch (\Exception $e) {
52
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
53
            }
54
55 480
            if ($ann === null) {
56 320
                continue;
57
            }
58
59 480
            $e = $this->generator->initEntity($ann, $class);
60
61
            // columns
62 480
            $this->generator->initFields($e, $class);
63
64
            // relations
65 468
            $this->generator->initRelations($e, $class);
66
67
            // schema modifiers
68 468
            $this->generator->initModifiers($e, $class);
69
70
            // additional columns (mapped to local fields automatically)
71 468
            $this->generator->initColumns($e, $ann->getColumns(), $class);
72
73 468
            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

73
            if ($this->utils->hasParent(/** @scrutinizer ignore-type */ $e->getClass())) {
Loading history...
74 308
                $children[] = $e;
75 308
                continue;
76
            }
77
78
            // register entity (OR find parent)
79 468
            $registry->register($e);
80 468
            $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

80
            $registry->linkTable($e, $e->getDatabase(), /** @scrutinizer ignore-type */ $e->getTableName());
Loading history...
81
        }
82
83 468
        foreach ($children as $e) {
84 308
            $registry->registerChild($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

84
            $registry->registerChild($registry->getEntity(/** @scrutinizer ignore-type */ $this->utils->findParent($e->getClass())), $e);
Loading history...
85
        }
86
87 468
        return $this->normalizeNames($registry);
88
    }
89
90 468
    private function normalizeNames(Registry $registry): Registry
91
    {
92
        // resolve all the relation target names into roles
93 468
        foreach ($this->locator->getClasses() as $class) {
94 468
            if (! $registry->hasEntity($class->getName())) {
95 320
                continue;
96
            }
97
98 468
            $e = $registry->getEntity($class->getName());
99
100
            // relations
101 468
            foreach ($e->getRelations() as $name => $r) {
102
                try {
103 392
                    $r->setTarget($this->resolveTarget($registry, $r->getTarget()));
104
105 380
                    if ($r->getOptions()->has('though')) {
106 284
                        $though = $r->getOptions()->get('though');
107 284
                        if ($though !== null) {
108 248
                            $r->getOptions()->set(
109 248
                                'though',
110 248
                                $this->resolveTarget($registry, $though)
111
                            );
112
                        }
113
                    }
114
115 380
                    if ($r->getOptions()->has('through')) {
116 284
                        $through = $r->getOptions()->get('through');
117 284
                        if ($through !== null) {
118 284
                            $r->getOptions()->set(
119 284
                                'through',
120 284
                                $this->resolveTarget($registry, $through)
121
                            );
122
                        }
123
                    }
124
125 380
                    if ($r->getOptions()->has('throughInnerKey')) {
126 284
                        if ($throughInnerKey = (array)$r->getOptions()->get('throughInnerKey')) {
127 284
                            $r->getOptions()->set('throughInnerKey', $throughInnerKey);
128
                        }
129
                    }
130
131 380
                    if ($r->getOptions()->has('throughOuterKey')) {
132 284
                        if ($throughOuterKey = (array)$r->getOptions()->get('throughOuterKey')) {
133 380
                            $r->getOptions()->set('throughOuterKey', $throughOuterKey);
134
                        }
135
                    }
136 12
                } catch (RegistryException $ex) {
137 12
                    throw new RelationException(
138 12
                        sprintf(
139 12
                            'Unable to resolve `%s`.`%s` relation target (not found or invalid)',
140 12
                            $e->getRole(),
141
                            $name
142
                        ),
143 12
                        $ex->getCode(),
144
                        $ex
145
                    );
146
                }
147
            }
148
        }
149
150 456
        return $registry;
151
    }
152
153 392
    private function resolveTarget(Registry $registry, string $name): ?string
154
    {
155 392
        if (interface_exists($name, true)) {
156
            // do not resolve interfaces
157 296
            return $name;
158
        }
159
160 392
        if (! $registry->hasEntity($name)) {
161
            // point all relations to the parent
162 36
            foreach ($registry as $entity) {
163 36
                foreach ($registry->getChildren($entity) as $child) {
164 24
                    if ($child->getClass() === $name || $child->getRole() === $name) {
165 24
                        return $entity->getRole();
166
                    }
167
                }
168
            }
169
        }
170
171 380
        return $registry->getEntity($name)->getRole();
172
    }
173
}
174