Completed
Push — master ( c66481...ebbf5e )
by Anton
20s queued 11s
created

Entities   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 28
eloc 72
c 3
b 1
f 0
dl 0
loc 189
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B run() 0 47 6
A hasParent() 0 3 1
A findParent() 0 22 5
A tableName() 0 3 1
B resolveTarget() 0 19 8
B normalizeNames() 0 36 6
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\Annotated;
11
12
use Cycle\Annotated\Annotation\Entity;
13
use Cycle\Annotated\Exception\AnnotationException;
14
use Cycle\Schema\Definition\Entity as EntitySchema;
15
use Cycle\Schema\Exception\RegistryException;
16
use Cycle\Schema\Exception\RelationException;
17
use Cycle\Schema\GeneratorInterface;
18
use Cycle\Schema\Registry;
19
use Doctrine\Common\Annotations\AnnotationException as DoctrineException;
20
use Doctrine\Common\Annotations\AnnotationReader;
21
use Doctrine\Common\Inflector\Inflector;
22
use Spiral\Tokenizer\ClassesInterface;
23
24
/**
25
 * Generates ORM schema based on annotated classes.
26
 */
27
final class Entities implements GeneratorInterface
28
{
29
    /** @var ClassesInterface */
30
    private $locator;
31
32
    /** @var AnnotationReader */
33
    private $reader;
34
35
    /** @var Configurator */
36
    private $generator;
37
38
    /**
39
     * @param ClassesInterface      $locator
40
     * @param AnnotationReader|null $reader
41
     */
42
    public function __construct(ClassesInterface $locator, AnnotationReader $reader = null)
43
    {
44
        $this->locator = $locator;
45
        $this->reader = $reader ?? new AnnotationReader();
46
        $this->generator = new Configurator($this->reader);
47
    }
48
49
    /**
50
     * @param Registry $registry
51
     * @return Registry
52
     */
53
    public function run(Registry $registry): Registry
54
    {
55
        /** @var EntitySchema[] $children */
56
        $children = [];
57
        foreach ($this->locator->getClasses() as $class) {
58
            try {
59
                /** @var Entity $ann */
60
                $ann = $this->reader->getClassAnnotation($class, Entity::class);
61
            } catch (DoctrineException $e) {
62
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
63
            }
64
65
            if ($ann === null) {
66
                continue;
67
            }
68
69
            $e = $this->generator->initEntity($ann, $class);
70
71
            // columns
72
            $this->generator->initFields($e, $class);
73
74
            // relations
75
            $this->generator->initRelations($e, $class);
76
77
            // additional columns (mapped to local fields automatically)
78
            $this->generator->initColumns($e, $ann->getColumns(), $class);
79
80
            if ($this->hasParent($registry, $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\Entities::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

80
            if ($this->hasParent($registry, /** @scrutinizer ignore-type */ $e->getClass())) {
Loading history...
81
                $children[] = $e;
82
                continue;
83
            }
84
85
            // register entity (OR find parent)
86
            $registry->register($e);
87
88
            $registry->linkTable(
89
                $e,
90
                $ann->getDatabase(),
91
                $ann->getTable() ?? $this->tableName($e->getRole())
92
            );
93
        }
94
95
        foreach ($children as $e) {
96
            $registry->registerChild($registry->getEntity($this->findParent($registry, $e->getClass())), $e);
0 ignored issues
show
Bug introduced by
It seems like $this->findParent($registry, $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->registerChild($registry->getEntity(/** @scrutinizer ignore-type */ $this->findParent($registry, $e->getClass())), $e);
Loading history...
97
        }
98
99
        return $this->normalizeNames($registry);
100
    }
101
102
    /**
103
     * @param Registry $registry
104
     * @return Registry
105
     */
106
    protected function normalizeNames(Registry $registry): Registry
107
    {
108
        // resolve all the relation target names into roles
109
        foreach ($this->locator->getClasses() as $class) {
110
            if (!$registry->hasEntity($class->getName())) {
111
                continue;
112
            }
113
114
            $e = $registry->getEntity($class->getName());
115
116
            // relations
117
            foreach ($e->getRelations() as $name => $r) {
118
                try {
119
                    $r->setTarget($this->resolveTarget($registry, $r->getTarget()));
120
121
                    if ($r->getOptions()->has('though')) {
122
                        $r->getOptions()->set(
123
                            'though',
124
                            $this->resolveTarget($registry, $r->getOptions()->get('though'))
125
                        );
126
                    }
127
                } catch (RegistryException $ex) {
128
                    throw new RelationException(
129
                        sprintf(
130
                            "Unable to resolve `%s`.`%s` relation target (not found or invalid)",
131
                            $e->getRole(),
132
                            $name
133
                        ),
134
                        $ex->getCode(),
135
                        $ex
136
                    );
137
                }
138
            }
139
        }
140
141
        return $registry;
142
    }
143
144
    /**
145
     * @param Registry $registry
146
     * @param string   $name
147
     * @return string|null
148
     */
149
    protected function resolveTarget(Registry $registry, string $name): ?string
150
    {
151
        if (is_null($name) || interface_exists($name, true)) {
152
            // do not resolve interfaces
153
            return $name;
154
        }
155
156
        if (!$registry->hasEntity($name)) {
157
            // point all relations to the parent
158
            foreach ($registry as $entity) {
159
                foreach ($registry->getChildren($entity) as $child) {
160
                    if ($child->getClass() === $name || $child->getRole() === $name) {
161
                        return $entity->getRole();
162
                    }
163
                }
164
            }
165
        }
166
167
        return $registry->getEntity($name)->getRole();
168
    }
169
170
    /**
171
     * @param string $role
172
     * @return string
173
     */
174
    protected function tableName(string $role): string
175
    {
176
        return Inflector::pluralize(Inflector::tableize($role));
177
    }
178
179
    /**
180
     * @param Registry $registry
181
     * @param string   $class
182
     * @return bool
183
     */
184
    protected function hasParent(Registry $registry, string $class): bool
185
    {
186
        return $this->findParent($registry, $class) !== null;
187
    }
188
189
    /**
190
     * @param Registry $registry
191
     * @param string   $class
192
     * @return string|null
193
     */
194
    protected function findParent(Registry $registry, string $class): ?string
0 ignored issues
show
Unused Code introduced by
The parameter $registry is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

194
    protected function findParent(/** @scrutinizer ignore-unused */ Registry $registry, string $class): ?string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
195
    {
196
        $parents = class_parents($class);
197
198
        foreach (array_reverse($parents) as $parent) {
199
            try {
200
                $class = new \ReflectionClass($parent);
201
            } catch (\ReflectionException $e) {
202
                continue;
203
            }
204
205
            if ($class->getDocComment() === false) {
206
                continue;
207
            }
208
209
            $ann = $this->reader->getClassAnnotation($class, Entity::class);
210
            if ($ann !== null) {
211
                return $parent;
212
            }
213
        }
214
215
        return null;
216
    }
217
}
218