Issues (6)

src/Entities.php (3 issues)

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

99
            if ($this->hasParent($registry, /** @scrutinizer ignore-type */ $e->getClass())) {
Loading history...
100
                $children[] = $e;
101
                continue;
102
            }
103
104
            // register entity (OR find parent)
105
            $registry->register($e);
106
107
            $registry->linkTable(
108
                $e,
109
                $ann->getDatabase(),
110
                $ann->getTable() ?? $this->tableName($e->getRole())
111
            );
112
        }
113
114
        foreach ($children as $e) {
115
            $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

115
            $registry->registerChild($registry->getEntity(/** @scrutinizer ignore-type */ $this->findParent($registry, $e->getClass())), $e);
Loading history...
116
        }
117
118
        return $this->normalizeNames($registry);
119
    }
120
121
    /**
122
     * @param Registry $registry
123
     * @return Registry
124
     */
125
    protected function normalizeNames(Registry $registry): Registry
126
    {
127
        // resolve all the relation target names into roles
128
        foreach ($this->locator->getClasses() as $class) {
129
            if (!$registry->hasEntity($class->getName())) {
130
                continue;
131
            }
132
133
            $e = $registry->getEntity($class->getName());
134
135
            // relations
136
            foreach ($e->getRelations() as $name => $r) {
137
                try {
138
                    $r->setTarget($this->resolveTarget($registry, $r->getTarget()));
139
140
                    if ($r->getOptions()->has('though')) {
141
                        $r->getOptions()->set(
142
                            'though',
143
                            $this->resolveTarget($registry, $r->getOptions()->get('though'))
144
                        );
145
                    }
146
                } catch (RegistryException $ex) {
147
                    throw new RelationException(
148
                        sprintf(
149
                            'Unable to resolve `%s`.`%s` relation target (not found or invalid)',
150
                            $e->getRole(),
151
                            $name
152
                        ),
153
                        $ex->getCode(),
154
                        $ex
155
                    );
156
                }
157
            }
158
        }
159
160
        return $registry;
161
    }
162
163
    /**
164
     * @param Registry $registry
165
     * @param string   $name
166
     * @return string|null
167
     */
168
    protected function resolveTarget(Registry $registry, string $name): ?string
169
    {
170
        if (is_null($name) || interface_exists($name, true)) {
171
            // do not resolve interfaces
172
            return $name;
173
        }
174
175
        if (!$registry->hasEntity($name)) {
176
            // point all relations to the parent
177
            foreach ($registry as $entity) {
178
                foreach ($registry->getChildren($entity) as $child) {
179
                    if ($child->getClass() === $name || $child->getRole() === $name) {
180
                        return $entity->getRole();
181
                    }
182
                }
183
            }
184
        }
185
186
        return $registry->getEntity($name)->getRole();
187
    }
188
189
    /**
190
     * @param string $role
191
     * @return string
192
     */
193
    protected function tableName(string $role): string
194
    {
195
        $table = $this->inflector->tableize($role);
196
197
        switch ($this->tableNaming) {
198
            case self::TABLE_NAMING_PLURAL:
199
                return $this->inflector->pluralize($this->inflector->tableize($role));
200
201
            case self::TABLE_NAMING_SINGULAR:
202
                return $this->inflector->singularize($this->inflector->tableize($role));
203
204
            default:
205
                return $table;
206
        }
207
    }
208
209
    /**
210
     * @param Registry $registry
211
     * @param string   $class
212
     * @return bool
213
     */
214
    protected function hasParent(Registry $registry, string $class): bool
215
    {
216
        return $this->findParent($registry, $class) !== null;
217
    }
218
219
    /**
220
     * @param Registry $registry
221
     * @param string   $class
222
     * @return string|null
223
     */
224
    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

224
    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...
225
    {
226
        $parents = class_parents($class);
227
228
        foreach (array_reverse($parents) as $parent) {
229
            try {
230
                $class = new \ReflectionClass($parent);
231
            } catch (\ReflectionException $e) {
232
                continue;
233
            }
234
235
            if ($class->getDocComment() === false) {
236
                continue;
237
            }
238
239
            $ann = $this->reader->getClassAnnotation($class, Entity::class);
240
            if ($ann !== null) {
241
                return $parent;
242
            }
243
        }
244
245
        return null;
246
    }
247
}
248