Configurator::initRelations()   B
last analyzed

Complexity

Conditions 8
Paths 11

Size

Total Lines 51
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 8.1867

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 8
eloc 30
c 2
b 2
f 0
nc 11
nop 2
dl 0
loc 51
ccs 24
cts 28
cp 0.8571
crap 8.1867
rs 8.1954

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\ForeignKey;
11
use Cycle\Annotated\Annotation\GeneratedValue;
12
use Cycle\Annotated\Annotation\Relation as RelationAnnotation;
13
use Cycle\Annotated\Exception\AnnotationException;
14
use Cycle\Annotated\Exception\AnnotationRequiredArgumentsException;
15
use Cycle\Annotated\Exception\AnnotationWrongTypeArgumentException;
16
use Cycle\Annotated\Utils\EntityUtils;
17
use Cycle\ORM\Schema\GeneratedField;
18
use Cycle\Schema\Definition\Entity as EntitySchema;
19
use Cycle\Schema\Definition\ForeignKey as ForeignKeySchema;
20
use Cycle\Schema\Definition\Field;
21
use Cycle\Schema\Definition\Relation;
22
use Cycle\Schema\Generator\SyncTables;
23
use Cycle\Schema\SchemaModifierInterface;
24
use Doctrine\Common\Annotations\Reader as DoctrineReader;
25
use Doctrine\Inflector\Inflector;
26
use Doctrine\Inflector\Rules\English\InflectorFactory;
27
use Spiral\Attributes\ReaderInterface;
28
29
final class Configurator
30
{
31
    private ReaderInterface $reader;
32 1056
    private Inflector $inflector;
33
    private EntityUtils $utils;
34
35
    public function __construct(
36 1056
        DoctrineReader|ReaderInterface $reader,
37 1056
        private int $tableNamingStrategy = Entities::TABLE_NAMING_PLURAL,
38 1056
    ) {
39 1056
        $this->reader = ReaderFactory::create($reader);
40
        $this->inflector = (new InflectorFactory())->build();
41 1056
        $this->utils = new EntityUtils($this->reader);
42
    }
43 1056
44 1056
    public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema
45
    {
46 1056
        $e = new EntitySchema();
47
        $e->setClass($class->getName());
48
49 1056
        $e->setRole($ann->getRole() ?? $this->inflector->camelize($class->getShortName()));
50 1056
51 1056
        // representing classes
52 1056
        $e->setMapper($this->resolveName($ann->getMapper(), $class));
53 1056
        $e->setRepository($this->resolveName($ann->getRepository(), $class));
54 1056
        $e->setSource($this->resolveName($ann->getSource(), $class));
55 1056
        $e->setScope($this->resolveName($ann->getScope(), $class));
56
        $e->setDatabase($ann->getDatabase());
57
        $e->setTableName(
58 1056
            $ann->getTable() ?? $this->utils->tableName($e->getRole(), $this->tableNamingStrategy)
59 1056
        );
60 496
61
        $typecast = $ann->getTypecast();
62 1056
        if (is_array($typecast)) {
63
            $typecast = array_map(fn (string $value): string => $this->resolveName($value, $class), $typecast);
64
        } else {
65 1056
            $typecast = $this->resolveName($typecast, $class);
66
        }
67 1056
68
        $e->setTypecast($typecast);
69
70
        if ($ann->isReadonlySchema()) {
71 1056
            $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true);
72
        }
73
74 72
        return $e;
75
    }
76 72
77 72
    public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema
78
    {
79 72
        $e = new EntitySchema();
80
        $e->setClass($class->getName());
81
82 72
        $e->setRole($emb->getRole() ?? $this->inflector->camelize($class->getShortName()));
83
84 72
        // representing classes
85
        $e->setMapper($this->resolveName($emb->getMapper(), $class));
86
87 1056
        return $e;
88
    }
89 1056
90
    public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = ''): void
91 1056
    {
92 24
        foreach ($class->getProperties() as $property) {
93
            try {
94 24
                $column = $this->reader->firstPropertyMetadata($property, Column::class);
95 24
            } catch (\Exception $e) {
96
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
97
            } catch (\ArgumentCountError $e) {
98
                throw AnnotationRequiredArgumentsException::createFor($property, Column::class, $e);
99
            } catch (\TypeError $e) {
100 1032
                throw AnnotationWrongTypeArgumentException::createFor($property, $e);
101 976
            }
102
103
            if ($column === null) {
104 1008
                continue;
105 1008
            }
106 1008
107
            $field = $this->initField($property->getName(), $column, $class, $columnPrefix);
108 1032
            $field->setEntityClass($property->getDeclaringClass()->getName());
109
            $entity->getFields()->set($property->getName(), $field);
110 1032
        }
111
    }
112 1032
113
    public function initRelations(EntitySchema $entity, \ReflectionClass $class): void
114 1032
    {
115
        foreach ($class->getProperties() as $property) {
116
            $metadata = $this->getPropertyMetadata($property, RelationAnnotation\RelationInterface::class);
117
118
            foreach ($metadata as $meta) {
119 1032
                assert($meta instanceof RelationAnnotation\RelationInterface);
120
121
                if ($meta->getTarget() === null) {
122 808
                    throw new AnnotationException(
123
                        "Relation target definition is required on `{$entity->getClass()}`.`{$property->getName()}`"
124
                    );
125
                }
126
127
                $relation = new Relation();
128 808
                $relation->setTarget($this->resolveName($meta->getTarget(), $class));
129 808
                $relation->setType($meta->getType());
130 808
131
                $inverse = $meta->getInverse() ?? $this->reader->firstPropertyMetadata(
132 808
                    $property,
133
                    RelationAnnotation\Inverse::class
134 768
                );
135
                if ($inverse !== null) {
136 808
                    $relation->setInverse(
137 144
                        $inverse->getName(),
138 144
                        $inverse->getType(),
139 144
                        $inverse->getLoadMethod()
140 144
                    );
141
                }
142
143
                if ($meta instanceof RelationAnnotation\Embedded && $meta->getPrefix() === null) {
144 808
                    /** @var Embeddable|null $embeddable */
145
                    $embeddable = $this->reader->firstClassMetadata(
146 72
                        new \ReflectionClass($relation->getTarget()),
147 72
                        Embeddable::class
148 72
                    );
149
                    $meta->setPrefix($embeddable->getColumnPrefix());
150 72
                }
151
152
                foreach ($meta->getOptions() as $option => $value) {
153 808
                    $value = match ($option) {
154 808
                        'collection' => $this->resolveName($value, $class),
155 568
                        'though', 'through' => $this->resolveName($value, $class),
156 284
                        default => $value
157 404
                    };
158
159
                    $relation->getOptions()->set($option, $value);
160 808
                }
161
162
                // need relation definition
163
                $entity->getRelations()->set($property->getName(), $relation);
164 808
            }
165
        }
166
    }
167 1032
168
    public function initModifiers(EntitySchema $entity, \ReflectionClass $class): void
169 1032
    {
170
        $metadata = $this->getClassMetadata($class, SchemaModifierInterface::class);
171
172 1032
        foreach ($metadata as $meta) {
173
            assert($meta instanceof SchemaModifierInterface);
174
175
            // need relation definition
176
            $entity->addSchemaModifier($meta);
177 1032
        }
178
    }
179
180
    /**
181 24
     * @param Column[] $columns
182
     */
183 1032
    public function initColumns(EntitySchema $entity, array $columns, \ReflectionClass $class): void
184
    {
185
        foreach ($columns as $key => $column) {
186
            $isNumericKey = is_numeric($key);
187
            $propertyName = $column->getProperty();
188 1032
189
            if (!$isNumericKey && $propertyName !== null && $key !== $propertyName) {
190 1032
                throw new AnnotationException(
191 472
                    "Can not use name \"{$key}\" for Column of the `{$entity->getRole()}` role, because the "
192 472
                    . "\"property\" field of the metadata class has already been set to \"{$propertyName}\"."
193
                );
194 472
            }
195 6
196 6
            $propertyName = $propertyName ?? ($isNumericKey ? null : $key);
197 6
            $columnName = $column->getColumn() ?? $propertyName;
198
            $propertyName = $propertyName ?? $columnName;
199
200
            if ($columnName === null) {
201 466
                throw new AnnotationException(
202 466
                    "Column name definition is required on `{$entity->getClass()}`"
203 466
                );
204
            }
205 466
206
            if ($column->getType() === null) {
207
                throw new AnnotationException(
208
                    "Column type definition is required on `{$entity->getClass()}`.`{$columnName}`"
209
                );
210
            }
211 466
212
            $field = $this->initField($columnName, $column, $class, '');
213
            $field->setEntityClass($entity->getClass());
214
            $entity->getFields()->set($propertyName, $field);
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

214
            $entity->getFields()->set(/** @scrutinizer ignore-type */ $propertyName, $field);
Loading history...
215
        }
216
    }
217 466
218 466
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
219 466
    {
220
        $field = new Field();
221 1032
222
        $field->setType($column->getType());
223 1032
        $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
224
        $field->setPrimary($column->isPrimary());
225 1032
        if ($this->isOnInsertGeneratedField($field)) {
226
            $field->setGenerated(GeneratedField::ON_INSERT);
227 1032
        }
228 1032
229 1032
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
230
231 1032
        if ($column->isNullable()) {
232
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
233 1032
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null);
234 48
        }
235 48
236
        if ($column->hasDefault()) {
237
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
238 1032
        }
239 496
240
        if ($column->castDefault()) {
241
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
242 1032
        }
243
244
        if ($column->isReadonlySchema()) {
245
            $field->getAttributes()->set('readonlySchema', true);
246 1032
        }
247
248
        foreach ($column->getAttributes() as $k => $v) {
249
            $field->getAttributes()->set($k, $v);
250
        }
251
252 1056
        return $field;
253
    }
254 1056
255 1056
    public function initForeignKeys(Entity $ann, EntitySchema $entity, \ReflectionClass $class): void
256
    {
257
        $foreignKeys = [];
258 680
        foreach ($ann->getForeignKeys() as $foreignKey) {
259 680
            $foreignKeys[] = $foreignKey;
260 680
        }
261 680
262
        foreach ($this->getClassMetadata($class, ForeignKey::class) as $foreignKey) {
263
            $foreignKeys[] = $foreignKey;
264 680
        }
265 656
266
        foreach ($class->getProperties() as $property) {
267
            foreach ($this->getPropertyMetadata($property, ForeignKey::class) as $foreignKey) {
268 520
                if ($foreignKey->innerKey === null) {
269
                    $foreignKey->innerKey = [$property->getName()];
270
                }
271 1032
                $foreignKeys[] = $foreignKey;
272
            }
273 1032
        }
274
275
        foreach ($foreignKeys as $foreignKey) {
276
            if ($foreignKey->innerKey === null) {
277
                throw new AnnotationException(
278
                    "Inner column definition for the foreign key is required on `{$entity->getClass()}`"
279
                );
280
            }
281 1032
282
            $fk = new ForeignKeySchema();
283
            $fk->setTarget($foreignKey->target);
284
            $fk->setInnerColumns((array) $foreignKey->innerKey);
285
            $fk->setOuterColumns((array) $foreignKey->outerKey);
286
            $fk->createIndex($foreignKey->indexCreate);
287
            $fk->setAction($foreignKey->action);
288 1032
289
            $entity->getForeignKeys()->set($fk);
290
        }
291
    }
292
293
    public function initGeneratedFields(EntitySchema $entity, \ReflectionClass $class): void
294
    {
295
        foreach ($class->getProperties() as $property) {
296
            try {
297
                $generated = $this->reader->firstPropertyMetadata($property, GeneratedValue::class);
298
                if ($generated !== null) {
299
                    $entity->getFields()->get($property->getName())->setGenerated($generated->getFlags());
300
                }
301
            } catch (\Throwable $e) {
302
                throw new AnnotationException($e->getMessage(), (int) $e->getCode(), $e);
303
            }
304
        }
305
    }
306
307
    /**
308
     * Resolve class or role name relative to the current class.
309
     */
310
    public function resolveName(?string $name, \ReflectionClass $class): ?string
311
    {
312
        if ($name === null || $this->exists($name)) {
313
            return $name;
314
        }
315
316
        $resolved = \sprintf(
317
            '%s\\%s',
318
            $class->getNamespaceName(),
319
            \ltrim(\str_replace('/', '\\', $name), '\\')
320
        );
321
322
        if ($this->exists($resolved)) {
323
            return \ltrim($resolved, '\\');
324
        }
325
326
        return $name;
327
    }
328
329
    private function exists(string $name): bool
330
    {
331
        return \class_exists($name, true) || \interface_exists($name, true);
332
    }
333
334
    private function resolveTypecast(mixed $typecast, \ReflectionClass $class): mixed
335
    {
336
        if (\is_string($typecast) && \str_contains($typecast, '::')) {
337
            // short definition
338
            $typecast = \explode('::', $typecast);
339
340
            // resolve class name
341
            $typecast[0] = $this->resolveName($typecast[0], $class);
342
        }
343
344
        if (\is_string($typecast)) {
345
            $typecast = $this->resolveName($typecast, $class);
346
            if (\class_exists($typecast) && \method_exists($typecast, 'typecast')) {
347
                $typecast = [$typecast, 'typecast'];
348
            }
349
        }
350
351
        return $typecast;
352
    }
353
354
    /**
355
     * @template T
356
     *
357
     * @param class-string<T>|null
358
     *
359
     * @throws AnnotationException
360
     *
361
     * @return iterable<T>
362
     */
363
    private function getClassMetadata(\ReflectionClass $class, string $name): iterable
364
    {
365
        try {
366
            return $this->reader->getClassMetadata($class, $name);
367
        } catch (\Exception $e) {
368
            throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
369
        }
370
    }
371
372
    /**
373
     * @template T
374
     *
375
     * @param class-string<T>|null $name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T>|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>|null.
Loading history...
376
     *
377
     * @throws AnnotationException
378
     *
379
     * @return iterable<T>
380
     */
381
    private function getPropertyMetadata(\ReflectionProperty $property, string $name): iterable
382
    {
383
        try {
384
            return $this->reader->getPropertyMetadata($property, $name);
385
        } catch (\Exception $e) {
386
            throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
387
        }
388
    }
389
390
    private function isOnInsertGeneratedField(Field $field): bool
391
    {
392
        return match ($field->getType()) {
393
            'serial', 'bigserial', 'smallserial' => true,
394
            default => $field->isPrimary()
395
        };
396
    }
397
}
398