Passed
Push — 3.x ( 42ab4a...bb376d )
by Aleksei
12:31 queued 16s
created

Configurator::initRelations()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 51
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 8.1678

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

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