Passed
Push — master ( 283dec...2a8c04 )
by Anton
02:21 queued 13s
created

Generator::resolveTypecast()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
rs 9.6111
c 0
b 0
f 0
cc 5
nc 6
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
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
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
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, '')
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
207
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
208
209
        if ($column->isNullable()) {
210
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
211
        }
212
213
        if ($column->hasDefault()) {
214
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
215
        }
216
217
        if ($column->isCastedDefault()) {
218
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
219
        }
220
221
        return $field;
222
    }
223
224
    /**
225
     * Resolve class or role name relative to the current class.
226
     *
227
     * @param string           $name
228
     * @param \ReflectionClass $class
229
     * @return string
230
     */
231
    public function resolveName(?string $name, \ReflectionClass $class): ?string
232
    {
233
        if (is_null($name) || class_exists($name, true) || interface_exists($name, true)) {
234
            return $name;
235
        }
236
237
        $resolved = sprintf(
238
            "%s\\%s",
239
            $class->getNamespaceName(),
240
            ltrim(str_replace('/', '\\', $name), '\\')
241
        );
242
243
        if (class_exists($resolved, true) || interface_exists($resolved, true)) {
244
            return $resolved;
245
        }
246
247
        return $name;
248
    }
249
250
    /**
251
     * @param mixed            $typecast
252
     * @param \ReflectionClass $class
253
     * @return mixed
254
     */
255
    protected function resolveTypecast($typecast, \ReflectionClass $class)
256
    {
257
        if (is_string($typecast) && strpos($typecast, '::') !== false) {
258
            // short definition
259
            $typecast = explode('::', $typecast);
260
261
            // rsolve class name
262
            $typecast[0] = $this->resolveName($typecast[0], $class);
263
        }
264
265
        if (is_string($typecast)) {
266
            $typecast = $this->resolveName($typecast, $class);
267
            if (class_exists($typecast)) {
268
                $typecast = [$typecast, 'typecast'];
269
            }
270
        }
271
272
        return $typecast;
273
    }
274
275
    /**
276
     * @return Parser
277
     */
278
    public static function getDefaultParser(): Parser
279
    {
280
        $p = new Parser();
281
        $p->register(new Entity());
282
        $p->register(new Embeddable());
283
        $p->register(new Column());
284
        $p->register(new Table());
285
        $p->register(new Table\Index());
286
287
        // embedded relations
288
        $p->register(new RelationAnnotation\Embedded());
289
        $p->register(new RelationAnnotation\BelongsTo());
290
        $p->register(new RelationAnnotation\HasOne());
291
        $p->register(new RelationAnnotation\HasMany());
292
        $p->register(new RelationAnnotation\RefersTo());
293
        $p->register(new RelationAnnotation\ManyToMany());
294
        $p->register(new RelationAnnotation\Morphed\BelongsToMorphed());
295
        $p->register(new RelationAnnotation\Morphed\MorphedHasOne());
296
        $p->register(new RelationAnnotation\Morphed\MorphedHasMany());
297
298
        return $p;
299
    }
300
}