Passed
Push — master ( 1e6bc1...7a1c1c )
by Anton
04:14
created

Generator::initEmbedding()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php declare(strict_types=1);
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Cycle\Annotated;
10
11
use Cycle\Annotated\Annotation\Column;
12
use Cycle\Annotated\Annotation\Embeddable;
13
use Cycle\Annotated\Annotation\Entity;
14
use Cycle\Annotated\Annotation\Relation as RelationAnnotation;
15
use Cycle\Annotated\Annotation\Table;
16
use Cycle\Annotated\Exception\AnnotationException;
17
use Cycle\Schema\Definition\Entity as EntitySchema;
18
use Cycle\Schema\Definition\Field;
19
use Cycle\Schema\Definition\Relation;
20
use Cycle\Schema\Generator\SyncTables;
21
use Doctrine\Common\Inflector\Inflector;
22
use Spiral\Annotations\Parser;
23
24
final class Generator
25
{
26
    /** @var Parser */
27
    private $parser;
28
29
    /**
30
     * @param Parser $parser
31
     */
32
    public function __construct(Parser $parser)
33
    {
34
        $this->parser = $parser;
35
    }
36
37
    /**
38
     * @param Entity           $ann
39
     * @param \ReflectionClass $class
40
     * @return EntitySchema
41
     */
42
    public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema
43
    {
44
        $e = new EntitySchema();
45
        $e->setClass($class->getName());
46
47
        $e->setRole($ann->getRole() ?? Inflector::camelize($class->getShortName()));
48
49
        // representing classes
50
        $e->setMapper($this->resolveName($ann->getMapper(), $class));
51
        $e->setRepository($this->resolveName($ann->getRepository(), $class));
52
        $e->setSource($this->resolveName($ann->getSource(), $class));
53
        $e->setConstrain($this->resolveName($ann->getConstrain(), $class));
54
55
        if ($ann->isReadonlySchema()) {
56
            $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true);
57
        }
58
59
        return $e;
60
    }
61
62
    /**
63
     * @param Embeddable       $emb
64
     * @param \ReflectionClass $class
65
     * @return EntitySchema
66
     */
67
    public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema
68
    {
69
        $e = new EntitySchema();
70
        $e->setClass($class->getName());
71
72
        $e->setRole($emb->getRole() ?? Inflector::camelize($class->getShortName()));
73
74
        // representing classes
75
        $e->setMapper($this->resolveName($emb->getMapper(), $class));
76
77
        return $e;
78
    }
79
80
    /**
81
     * @param EntitySchema     $entity
82
     * @param \ReflectionClass $class
83
     * @param string           $columnPrefix
84
     */
85
    public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = '')
86
    {
87
        foreach ($class->getProperties() as $property) {
88
            if ($property->getDocComment() === false) {
89
                continue;
90
            }
91
92
            $ann = $this->parser->parse($property->getDocComment());
0 ignored issues
show
Bug introduced by Wolfy-J
It seems like $property->getDocComment() can also be of type true; however, parameter $body of Spiral\Annotations\Parser::parse() 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

92
            $ann = $this->parser->parse(/** @scrutinizer ignore-type */ $property->getDocComment());
Loading history...
93
            if (!isset($ann[Column::NAME])) {
94
                continue;
95
            }
96
97
            $entity->getFields()->set(
98
                $property->getName(),
99
                $this->initField(
100
                    $property->getName(),
101
                    $ann[Column::NAME],
102
                    $class,
103
                    $columnPrefix
104
                )
105
            );
106
        }
107
    }
108
109
    /**
110
     * @param EntitySchema     $entity
111
     * @param \ReflectionClass $class
112
     */
113
    public function initRelations(EntitySchema $entity, \ReflectionClass $class)
114
    {
115
        foreach ($class->getProperties() as $property) {
116
            if ($property->getDocComment() === false) {
117
                continue;
118
            }
119
120
            $ann = $this->parser->parse($property->getDocComment());
0 ignored issues
show
Bug introduced by Wolfy-J
It seems like $property->getDocComment() can also be of type true; however, parameter $body of Spiral\Annotations\Parser::parse() 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

120
            $ann = $this->parser->parse(/** @scrutinizer ignore-type */ $property->getDocComment());
Loading history...
121
122
            foreach ($ann as $ra) {
123
                if (!$ra instanceof RelationAnnotation\RelationInterface) {
124
                    continue;
125
                }
126
127
                if ($ra->getTarget() === null) {
128
                    throw new AnnotationException(
129
                        "Relation target definition is required on `{$entity->getClass()}`"
130
                    );
131
                }
132
133
                $relation = new Relation();
134
                $relation->setTarget($this->resolveName($ra->getTarget(), $class));
135
                $relation->setType($ra->getName());
136
137
                if ($ra->isInversed()) {
138
                    $relation->setInverse(
139
                        $ra->getInverseName(),
140
                        $ra->getInverseType(),
141
                        $ra->getInverseLoadMethod()
142
                    );
143
                }
144
145
                foreach ($ra->getOptions() as $option => $value) {
146
                    if ($option === "though") {
147
                        $value = $this->resolveName($value, $class);
148
                    }
149
150
                    $relation->getOptions()->set($option, $value);
151
                }
152
153
                // need relation definition
154
                $entity->getRelations()->set($property->getName(), $relation);
155
            }
156
        }
157
    }
158
159
    /**
160
     * @param EntitySchema     $entity
161
     * @param Column[]         $columns
162
     * @param \ReflectionClass $class
163
     */
164
    public function initColumns(EntitySchema $entity, array $columns, \ReflectionClass $class)
165
    {
166
        foreach ($columns as $name => $column) {
167
            if ($column->getColumn() === null && is_numeric($name)) {
168
                throw new AnnotationException(
169
                    "Column name definition is required on `{$entity->getClass()}`"
170
                );
171
            }
172
173
            if ($column->getType() === null) {
174
                throw new AnnotationException(
175
                    "Column type definition is required on `{$entity->getClass()}`"
176
                );
177
            }
178
179
            $entity->getFields()->set(
180
                $name ?? $column->getColumn(),
0 ignored issues
show
Bug introduced by Wolfy-J
It seems like $name ?? $column->getColumn() can also be of type null; however, parameter $name of Cycle\Schema\Definition\Map\FieldMap::set() 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

180
                /** @scrutinizer ignore-type */ $name ?? $column->getColumn(),
Loading history...
181
                $this->initField($column->getColumn() ?? $name, $column, $class)
0 ignored issues
show
Bug introduced by Wolfy-J
The call to Cycle\Annotated\Generator::initField() has too few arguments starting with columnPrefix. ( Ignorable by Annotation )

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

181
                $this->/** @scrutinizer ignore-call */ 
182
                       initField($column->getColumn() ?? $name, $column, $class)

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
182
            );
183
        }
184
    }
185
186
    /**
187
     * @param string           $name
188
     * @param Column           $column
189
     * @param \ReflectionClass $class
190
     * @param string           $columnPrefix
191
     * @return Field
192
     */
193
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
194
    {
195
        if ($column->getType() === null) {
196
            throw new AnnotationException(
197
                "Column type definition is required on `{$class->getName()}`.`{$name}`"
198
            );
199
        }
200
201
        $field = new Field();
202
203
        $field->setType($column->getType());
204
        $field->setColumn($columnPrefix . ($column->getColumn() ?? Inflector::tableize($name)));
205
        $field->setPrimary($column->isPrimary());
206
        $field->setTypecast($column->getTypecast());
207
208
        if ($column->isNullable()) {
209
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
210
        }
211
212
        if ($column->hasDefault()) {
213
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
214
        }
215
216
        if ($column->isCastedDefault()) {
217
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
218
        }
219
220
        return $field;
221
    }
222
223
    /**
224
     * Resolve class or role name relative to the current class.
225
     *
226
     * @param string           $name
227
     * @param \ReflectionClass $class
228
     * @return string
229
     */
230
    public function resolveName(?string $name, \ReflectionClass $class): ?string
231
    {
232
        if (is_null($name) || class_exists($name, true) || interface_exists($name, true)) {
233
            return $name;
234
        }
235
236
        $resolved = sprintf(
237
            "%s\\%s",
238
            $class->getNamespaceName(),
239
            ltrim(str_replace('/', '\\', $name), '\\')
240
        );
241
242
        if (class_exists($resolved, true) || interface_exists($resolved, true)) {
243
            return $resolved;
244
        }
245
246
        return $name;
247
    }
248
249
    /**
250
     * @return Parser
251
     */
252
    public static function getDefaultParser(): Parser
253
    {
254
        $p = new Parser();
255
        $p->register(new Entity());
256
        $p->register(new Embeddable());
257
        $p->register(new Column());
258
        $p->register(new Table());
259
        $p->register(new Table\Index());
260
261
        // embedded relations
262
        $p->register(new RelationAnnotation\Embedded());
263
        $p->register(new RelationAnnotation\BelongsTo());
264
        $p->register(new RelationAnnotation\HasOne());
265
        $p->register(new RelationAnnotation\HasMany());
266
        $p->register(new RelationAnnotation\RefersTo());
267
        $p->register(new RelationAnnotation\ManyToMany());
268
        $p->register(new RelationAnnotation\Morphed\BelongsToMorphed());
269
        $p->register(new RelationAnnotation\Morphed\MorphedHasOne());
270
        $p->register(new RelationAnnotation\Morphed\MorphedHasMany());
271
272
        return $p;
273
    }
274
}