Configurator   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 366
Duplicated Lines 0 %

Test Coverage

Coverage 85.51%

Importance

Changes 3
Bugs 3 Features 0
Metric Value
eloc 174
c 3
b 3
f 0
dl 0
loc 366
ccs 118
cts 138
cp 0.8551
rs 3.2
wmc 65

16 Methods

Rating   Name   Duplication   Size   Complexity  
B initColumns() 0 32 8
B initForeignKeys() 0 35 8
A __construct() 0 7 1
A getPropertyMetadata() 0 6 2
A isOnInsertGeneratedField() 0 5 1
B initField() 0 35 7
A exists() 0 3 2
A resolveTypecast() 0 18 6
A initGeneratedFields() 0 10 4
A resolveName() 0 17 4
A initFields() 0 20 6
B initRelations() 0 51 8
A initModifiers() 0 9 2
A initEmbedding() 0 11 1
A initEntity() 0 31 3
A getClassMetadata() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like Configurator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Configurator, and based on these observations, apply Extract Interface, too.

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