Passed
Push — master ( abb18c...83efca )
by Divine Niiquaye
02:29
created

Entities::hasParent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\Cycle\Annotated;
19
20
use Cycle\Annotated\Annotation\Entity;
21
use Cycle\Annotated\Configurator;
22
use Cycle\Annotated\Exception\AnnotationException;
23
use Cycle\Schema\Definition\Entity as EntitySchema;
24
use Cycle\Schema\Exception\RegistryException;
25
use Cycle\Schema\Exception\RelationException;
26
use Cycle\Schema\GeneratorInterface;
27
use Cycle\Schema\Registry;
28
use Doctrine\Common\Annotations\AnnotationException as DoctrineException;
29
use Doctrine\Common\Annotations\AnnotationReader;
30
use ReflectionClass;
31
use ReflectionException;
32
33
/**
34
 * Generates ORM schema based on annotated classes.
35
 */
36
final class Entities implements GeneratorInterface
37
{
38
    // table name generation
39
    public const TABLE_NAMING_PLURAL   = 1;
40
41
    public const TABLE_NAMING_SINGULAR = 2;
42
43
    public const TABLE_NAMING_NONE     = 3;
44
45
    /** @var class-string[] */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string[] at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string[].
Loading history...
46
    private $locator;
47
48
    /** @var AnnotationReader */
49
    private $reader;
50
51
    /** @var Configurator */
52
    private $generator;
53
54
    /** @var int */
55
    private $tableNaming;
56
57
    /** @var \Doctrine\Inflector\Inflector */
58
    private $inflector;
59
60
    /**
61
     * @param class-string[]        $locator
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string[] at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string[].
Loading history...
62
     * @param null|AnnotationReader $reader
63
     * @param int                   $tableNaming
64
     */
65
    public function __construct(
66
        array $locator,
67
        AnnotationReader $reader = null,
68
        int $tableNaming = self::TABLE_NAMING_PLURAL
69
    ) {
70
        $this->locator     = $locator;
71
        $this->reader      = $reader ?? new AnnotationReader();
72
        $this->generator   = new Configurator($this->reader);
73
        $this->tableNaming = $tableNaming;
74
        $this->inflector   = (new \Doctrine\Inflector\Rules\English\InflectorFactory())->build();
75
    }
76
77
    /**
78
     * @param Registry $registry
79
     *
80
     * @return Registry
81
     */
82
    public function run(Registry $registry): Registry
83
    {
84
        /** @var EntitySchema[] $children */
85
        $children = [];
86
87
        foreach ($this->locator as $class) {
88
            try {
89
                $class = new ReflectionClass($class);
90
91
                /** @var Entity $ann */
92
                $ann = $this->reader->getClassAnnotation($class, Entity::class);
93
            } catch (DoctrineException $e) {
94
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
95
            }
96
97
            if ($ann === null) {
98
                continue;
99
            }
100
101
            $e = $this->generator->initEntity($ann, $class);
102
103
            // columns
104
            $this->generator->initFields($e, $class);
105
106
            // relations
107
            $this->generator->initRelations($e, $class);
108
109
            // additional columns (mapped to local fields automatically)
110
            $this->generator->initColumns($e, $ann->getColumns(), $class);
111
112
            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 Biurad\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

112
            if ($this->hasParent($registry, /** @scrutinizer ignore-type */ $e->getClass())) {
Loading history...
113
                $children[] = $e;
114
115
                continue;
116
            }
117
118
            // register entity (OR find parent)
119
            $registry->register($e);
120
121
            $registry->linkTable($e, $ann->getDatabase(), $ann->getTable() ?? $this->tableName($e->getRole()));
122
        }
123
124
        foreach ($children as $e) {
125
            $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

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

241
    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...
242
    {
243
        $parents = \class_parents($class);
244
245
        foreach (\array_reverse($parents) as $parent) {
246
            try {
247
                $class = new ReflectionClass($parent);
248
            } catch (ReflectionException $e) {
249
                continue;
250
            }
251
252
            if ($class->getDocComment() === false) {
253
                continue;
254
            }
255
256
            $ann = $this->reader->getClassAnnotation($class, Entity::class);
257
258
            if ($ann !== null) {
259
                return $parent;
260
            }
261
        }
262
263
        return null;
264
    }
265
}
266