Passed
Push — master ( c93c78...d9ab06 )
by Anton
04:33 queued 02:57
created

src/Entities.php (3 issues)

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
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
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("Unable to resolve `%s`.`%s` relation target (not found or invalid)",
130
                            $e->getRole(),
131
                            $name
132
                        ),
133
                        $ex->getCode(),
134
                        $ex
135
                    );
136
                }
137
            }
138
        }
139
140
        return $registry;
141
    }
142
143
    /**
144
     * @param Registry $registry
145
     * @param string   $name
146
     * @return string|null
147
     */
148
    protected function resolveTarget(Registry $registry, string $name): ?string
149
    {
150
        if (is_null($name) || interface_exists($name, true)) {
151
            // do not resolve interfaces
152
            return $name;
153
        }
154
155
        return $registry->getEntity($name)->getRole();
156
    }
157
158
    /**
159
     * @param string $role
160
     * @return string
161
     */
162
    protected function tableName(string $role): string
163
    {
164
        return Inflector::pluralize(Inflector::tableize($role));
165
    }
166
167
    /**
168
     * @param Registry $registry
169
     * @param string   $class
170
     * @return bool
171
     */
172
    protected function hasParent(Registry $registry, string $class): bool
173
    {
174
        return $this->findParent($registry, $class) !== null;
175
    }
176
177
    /**
178
     * @param Registry $registry
179
     * @param string   $class
180
     * @return string|null
181
     */
182
    protected function findParent(Registry $registry, string $class): ?string
0 ignored issues
show
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

182
    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...
183
    {
184
        $parents = class_parents($class);
185
186
        foreach (array_reverse($parents) as $parent) {
187
            try {
188
                $class = new \ReflectionClass($parent);
189
            } catch (\ReflectionException $e) {
190
                continue;
191
            }
192
193
            if ($class->getDocComment() === false) {
194
                continue;
195
            }
196
197
            $ann = $this->reader->getClassAnnotation($class, Entity::class);
198
            if ($ann !== null) {
199
                return $parent;
200
            }
201
        }
202
203
        return null;
204
    }
205
}