Passed
Push — 3.x ( 6901e5...b6dc6a )
by Aleksei
12:10
created

Configurator::initRelations()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 55
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9.1738

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
eloc 33
nc 12
nop 2
dl 0
loc 55
ccs 27
cts 31
cp 0.871
crap 9.1738
rs 8.0555
c 1
b 1
f 0

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

219
            $entity->getFields()->set(/** @scrutinizer ignore-type */ $propertyName, $field);
Loading history...
220
        }
221 1032
    }
222
223 1032
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
224
    {
225 1032
        $field = new Field();
226
227 1032
        $field->setType($column->getType());
228 1032
        $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
229 1032
        $field->setPrimary($column->isPrimary());
230
231 1032
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
232
233 1032
        if ($column->isNullable()) {
234 48
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
235 48
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null);
236
        }
237
238 1032
        if ($column->hasDefault()) {
239 496
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
240
        }
241
242 1032
        if ($column->castDefault()) {
243
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
244
        }
245
246 1032
        return $field;
247
    }
248
249
    /**
250
     * Resolve class or role name relative to the current class.
251
     */
252 1056
    public function resolveName(?string $name, \ReflectionClass $class): ?string
253
    {
254 1056
        if ($name === null || $this->exists($name)) {
255 1056
            return $name;
256
        }
257
258 680
        $resolved = \sprintf(
259 680
            '%s\\%s',
260 680
            $class->getNamespaceName(),
261 680
            \ltrim(\str_replace('/', '\\', $name), '\\')
262
        );
263
264 680
        if ($this->exists($resolved)) {
265 656
            return \ltrim($resolved, '\\');
266
        }
267
268 520
        return $name;
269
    }
270
271 1032
    private function exists(string $name): bool
272
    {
273 1032
        return \class_exists($name, true) || \interface_exists($name, true);
274
    }
275
276
    private function resolveTypecast(mixed $typecast, \ReflectionClass $class): mixed
277
    {
278
        if (\is_string($typecast) && \str_contains($typecast, '::')) {
279
            // short definition
280
            $typecast = \explode('::', $typecast);
281 1032
282
            // resolve class name
283
            $typecast[0] = $this->resolveName($typecast[0], $class);
284
        }
285
286
        if (\is_string($typecast)) {
287
            $typecast = $this->resolveName($typecast, $class);
288 1032
            if (\class_exists($typecast) && \method_exists($typecast, 'typecast')) {
289
                $typecast = [$typecast, 'typecast'];
290
            }
291
        }
292
293
        return $typecast;
294
    }
295
}
296