Completed
Push — master ( 92301f...6fa9f1 )
by Kirill
16s queued 13s
created

Configurator::initColumns()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
c 0
b 0
f 0
dl 0
loc 32
rs 8.4444
cc 8
nc 8
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Annotated;
6
7
use Cycle\Annotated\Annotation\Column;
8
use Cycle\Annotated\Annotation\Embeddable;
9
use Cycle\Annotated\Annotation\Entity;
10
use Cycle\Annotated\Annotation\Relation as RelationAnnotation;
11
use Cycle\Annotated\Exception\AnnotationException;
12
use Cycle\Schema\Definition\Entity as EntitySchema;
13
use Cycle\Schema\Definition\Field;
14
use Cycle\Schema\Definition\Relation;
15
use Cycle\Schema\Generator\SyncTables;
16
use Doctrine\Common\Annotations\Reader as DoctrineReader;
17
use Doctrine\Inflector\Inflector;
18
use Doctrine\Inflector\Rules\English\InflectorFactory;
19
use Exception;
20
use Spiral\Attributes\ReaderInterface;
21
22
final class Configurator
23
{
24
    /** @var ReaderInterface */
25
    private $reader;
26
27
    /** @var Inflector */
28
    private $inflector;
29
30
    /**
31
     * @param object<ReaderInterface|DoctrineReader> $reader
32
     */
33
    public function __construct(object $reader)
34
    {
35
        $this->reader = ReaderFactory::create($reader);
36
        $this->inflector = (new InflectorFactory())->build();
37
    }
38
39
    /**
40
     * @param Entity           $ann
41
     * @param \ReflectionClass $class
42
     * @return EntitySchema
43
     */
44
    public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema
45
    {
46
        $e = new EntitySchema();
47
        $e->setClass($class->getName());
48
49
        $e->setRole($ann->getRole() ?? $this->inflector->camelize($class->getShortName()));
50
51
        // representing classes
52
        $e->setMapper($this->resolveName($ann->getMapper(), $class));
53
        $e->setRepository($this->resolveName($ann->getRepository(), $class));
54
        $e->setSource($this->resolveName($ann->getSource(), $class));
55
        $e->setConstrain($this->resolveName($ann->getConstrain(), $class));
56
57
        if ($ann->isReadonlySchema()) {
58
            $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true);
59
        }
60
61
        return $e;
62
    }
63
64
    /**
65
     * @param Embeddable       $emb
66
     * @param \ReflectionClass $class
67
     * @return EntitySchema
68
     */
69
    public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema
70
    {
71
        $e = new EntitySchema();
72
        $e->setClass($class->getName());
73
74
        $e->setRole($emb->getRole() ?? $this->inflector->camelize($class->getShortName()));
75
76
        // representing classes
77
        $e->setMapper($this->resolveName($emb->getMapper(), $class));
78
79
        return $e;
80
    }
81
82
    /**
83
     * @param EntitySchema     $entity
84
     * @param \ReflectionClass $class
85
     * @param string           $columnPrefix
86
     */
87
    public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = ''): void
88
    {
89
        foreach ($class->getProperties() as $property) {
90
            try {
91
                /** @var Column $column */
92
                $column = $this->reader->firstPropertyMetadata($property, Column::class);
93
            } catch (Exception $e) {
94
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
95
            }
96
97
            if ($column === null) {
98
                continue;
99
            }
100
101
            $entity->getFields()->set(
102
                $property->getName(),
103
                $this->initField($property->getName(), $column, $class, $columnPrefix)
104
            );
105
        }
106
    }
107
108
    /**
109
     * @param EntitySchema     $entity
110
     * @param \ReflectionClass $class
111
     */
112
    public function initRelations(EntitySchema $entity, \ReflectionClass $class): void
113
    {
114
        foreach ($class->getProperties() as $property) {
115
            try {
116
                $metadata = $this->reader->getPropertyMetadata($property);
117
            } catch (Exception $e) {
118
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
119
            }
120
121
            foreach ($metadata as $meta) {
122
                if (!$meta instanceof RelationAnnotation\RelationInterface) {
123
                    continue;
124
                }
125
126
                if ($meta->getTarget() === null) {
127
                    throw new AnnotationException(
128
                        "Relation target definition is required on `{$entity->getClass()}`.`{$property->getName()}`"
129
                    );
130
                }
131
132
                $relation = new Relation();
133
                $relation->setTarget($this->resolveName($meta->getTarget(), $class));
134
                $relation->setType($meta->getType());
135
136
                $inverse = $meta->getInverse() ?? $this->reader->firstPropertyMetadata(
137
                        $property,
138
                        RelationAnnotation\Inverse::class
139
                    );
140
                if ($inverse !== null) {
141
                    $relation->setInverse(
142
                        $inverse->getName(),
143
                        $inverse->getType(),
144
                        $inverse->getLoadMethod()
145
                    );
146
                }
147
148
                foreach ($meta->getOptions() as $option => $value) {
149
                    if ($option === 'though') {
150
                        $value = $this->resolveName($value, $class);
151
                    }
152
153
                    $relation->getOptions()->set($option, $value);
154
                }
155
156
                // need relation definition
157
                $entity->getRelations()->set($property->getName(), $relation);
158
            }
159
        }
160
    }
161
162
    /**
163
     * @param EntitySchema     $entity
164
     * @param Column[]         $columns
165
     * @param \ReflectionClass $class
166
     */
167
    public function initColumns(EntitySchema $entity, array $columns, \ReflectionClass $class): void
168
    {
169
        foreach ($columns as $key => $column) {
170
            $isNumericKey = is_numeric($key);
171
            $propertyName = $column->getProperty();
172
173
            if (!$isNumericKey && $propertyName !== null && $key !== $propertyName) {
174
                throw new AnnotationException(
175
                    "Can not use name \"{$key}\" for Column of the `{$entity->getRole()}` role, because the "
176
                    . "\"property\" field of the metadata class has already been set to \"{$propertyName}\"."
177
                );
178
            }
179
180
            $propertyName = $propertyName ?? ($isNumericKey ? null : $key);
181
            $columnName = $column->getColumn() ?? $propertyName;
182
            $propertyName = $propertyName ?? $columnName;
183
184
            if ($columnName === null) {
185
                throw new AnnotationException(
186
                    "Column name definition is required on `{$entity->getClass()}`"
187
                );
188
            }
189
190
            if ($column->getType() === null) {
191
                throw new AnnotationException(
192
                    "Column type definition is required on `{$entity->getClass()}`"
193
                );
194
            }
195
196
            $entity->getFields()->set(
197
                $propertyName,
0 ignored issues
show
Bug introduced by
It seems like $propertyName 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

197
                /** @scrutinizer ignore-type */ $propertyName,
Loading history...
198
                $this->initField($columnName, $column, $class, '')
199
            );
200
        }
201
    }
202
203
    /**
204
     * @param string           $name
205
     * @param Column           $column
206
     * @param \ReflectionClass $class
207
     * @param string           $columnPrefix
208
     * @return Field
209
     */
210
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
211
    {
212
        if ($column->getType() === null) {
0 ignored issues
show
introduced by
The condition $column->getType() === null is always false.
Loading history...
213
            throw new AnnotationException(
214
                "Column type definition is required on `{$class->getName()}`.`{$name}`"
215
            );
216
        }
217
218
        $field = new Field();
219
220
        $field->setType($column->getType());
221
        $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
222
        $field->setPrimary($column->isPrimary());
223
224
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
225
226
        if ($column->isNullable()) {
227
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
228
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null);
229
        }
230
231
        if ($column->hasDefault()) {
232
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
233
        }
234
235
        if ($column->castDefault()) {
236
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
237
        }
238
239
        return $field;
240
    }
241
242
    /**
243
     * Resolve class or role name relative to the current class.
244
     *
245
     * @param string           $name
246
     * @param \ReflectionClass $class
247
     * @return string
248
     */
249
    public function resolveName(?string $name, \ReflectionClass $class): ?string
250
    {
251
        if ($name === null || class_exists($name, true) || interface_exists($name, true)) {
252
            return $name;
253
        }
254
255
        $resolved = sprintf(
256
            '%s\\%s',
257
            $class->getNamespaceName(),
258
            ltrim(str_replace('/', '\\', $name), '\\')
259
        );
260
261
        if (class_exists($resolved, true) || interface_exists($resolved, true)) {
262
            return ltrim($resolved, '\\');
263
        }
264
265
        return $name;
266
    }
267
268
    /**
269
     * @param mixed            $typecast
270
     * @param \ReflectionClass $class
271
     * @return mixed
272
     */
273
    protected function resolveTypecast($typecast, \ReflectionClass $class)
274
    {
275
        if (is_string($typecast) && strpos($typecast, '::') !== false) {
276
            // short definition
277
            $typecast = explode('::', $typecast);
278
279
            // resolve class name
280
            $typecast[0] = $this->resolveName($typecast[0], $class);
281
        }
282
283
        if (is_string($typecast)) {
284
            $typecast = $this->resolveName($typecast, $class);
285
            if (class_exists($typecast)) {
286
                $typecast = [$typecast, 'typecast'];
287
            }
288
        }
289
290
        return $typecast;
291
    }
292
}
293