Passed
Push — 3.x ( 4e5cb3...f6084f )
by Aleksei
05:06
created

Configurator::initRelations()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 55
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 9.2124

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
eloc 33
c 1
b 1
f 0
nc 12
nop 2
dl 0
loc 55
ccs 25
cts 29
cp 0.8621
crap 9.2124
rs 8.0555

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 492
    public function __construct(
33
        DoctrineReader|ReaderInterface $reader,
34
        private int $tableNamingStrategy = Entities::TABLE_NAMING_PLURAL,
35
    ) {
36 492
        $this->reader = ReaderFactory::create($reader);
37 492
        $this->inflector = (new InflectorFactory())->build();
38 492
        $this->utils = new EntityUtils($this->reader);
39 492
    }
40
41 492
    public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema
42
    {
43 492
        $e = new EntitySchema();
44 492
        $e->setClass($class->getName());
45
46 492
        $e->setRole($ann->getRole() ?? $this->inflector->camelize($class->getShortName()));
47
48
        // representing classes
49 492
        $e->setMapper($this->resolveName($ann->getMapper(), $class));
50 492
        $e->setRepository($this->resolveName($ann->getRepository(), $class));
51 492
        $e->setSource($this->resolveName($ann->getSource(), $class));
52 492
        $e->setScope($this->resolveName($ann->getScope(), $class));
53 492
        $e->setDatabase($ann->getDatabase());
54 492
        $e->setTableName(
55 492
            $ann->getTable() ?? $this->utils->tableName($e->getRole(), $this->tableNamingStrategy)
56
        );
57
58 492
        $typecast = $ann->getTypecast();
59 492
        if (is_array($typecast)) {
60 248
            $typecast = array_map(fn (string $value): string => $this->resolveName($value, $class), $typecast);
61
        } else {
62 492
            $typecast = $this->resolveName($typecast, $class);
63
        }
64
65 492
        $e->setTypecast($typecast);
66
67 492
        if ($ann->isReadonlySchema()) {
68
            $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true);
69
        }
70
71 492
        return $e;
72
    }
73
74 36
    public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema
75
    {
76 36
        $e = new EntitySchema();
77 36
        $e->setClass($class->getName());
78
79 36
        $e->setRole($emb->getRole() ?? $this->inflector->camelize($class->getShortName()));
80
81
        // representing classes
82 36
        $e->setMapper($this->resolveName($emb->getMapper(), $class));
83
84 36
        return $e;
85
    }
86
87 492
    public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = ''): void
88
    {
89 492
        foreach ($class->getProperties() as $property) {
90
            try {
91 492
                $column = $this->reader->firstPropertyMetadata($property, Column::class);
92 12
            } catch (Exception $e) {
93
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
94 12
            } catch (\ArgumentCountError $e) {
95 12
                throw AnnotationRequiredArgumentsException::createFor($property, Column::class, $e);
96
            } catch (\TypeError $e) {
97
                throw AnnotationWrongTypeArgumentException::createFor($property, $e);
98
            }
99
100 480
            if ($column === null) {
101 452
                continue;
102
            }
103
104 468
            $field = $this->initField($property->getName(), $column, $class, $columnPrefix);
105 468
            $field->setEntityClass($property->getDeclaringClass()->getName());
106 468
            $entity->getFields()->set($property->getName(), $field);
107
        }
108 480
    }
109
110 480
    public function initRelations(EntitySchema $entity, \ReflectionClass $class): void
111
    {
112 480
        foreach ($class->getProperties() as $property) {
113
            try {
114 480
                $metadata = $this->reader->getPropertyMetadata($property, RelationAnnotation\RelationInterface::class);
115
            } catch (Exception $e) {
116
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
117
            }
118
119 480
            foreach ($metadata as $meta) {
120
                assert($meta instanceof RelationAnnotation\RelationInterface);
121
122 404
                if ($meta->getTarget() === null) {
123
                    throw new AnnotationException(
124
                        "Relation target definition is required on `{$entity->getClass()}`.`{$property->getName()}`"
125
                    );
126
                }
127
128 404
                $relation = new Relation();
129 404
                $relation->setTarget($this->resolveName($meta->getTarget(), $class));
130 404
                $relation->setType($meta->getType());
131
132 404
                $inverse = $meta->getInverse() ?? $this->reader->firstPropertyMetadata(
133
                    $property,
134 384
                    RelationAnnotation\Inverse::class
135
                );
136 404
                if ($inverse !== null) {
137 72
                    $relation->setInverse(
138 72
                        $inverse->getName(),
139 72
                        $inverse->getType(),
140 72
                        $inverse->getLoadMethod()
141
                    );
142
                }
143
144 404
                if ($meta instanceof RelationAnnotation\Embedded && $meta->getPrefix() === null) {
145
                    /** @var Embeddable|null $embeddable */
146 36
                    $embeddable = $this->reader->firstClassMetadata(
147 36
                        new \ReflectionClass($relation->getTarget()),
148 36
                        Embeddable::class
149
                    );
150 36
                    $meta->setPrefix($embeddable->getColumnPrefix());
151
                }
152
153 404
                foreach ($meta->getOptions() as $option => $value) {
154 404
                    $value = match ($option) {
155 284
                        'collection' => $this->resolveName($value, $class),
156
                        'though', 'through' => $this->resolveName($value, $class),
157
                        default => $value
158
                    };
159
160 404
                    $relation->getOptions()->set($option, $value);
161
                }
162
163
                // need relation definition
164 404
                $entity->getRelations()->set($property->getName(), $relation);
165
            }
166
        }
167 480
    }
168
169 480
    public function initModifiers(EntitySchema $entity, \ReflectionClass $class): void
170
    {
171
        try {
172 480
            $metadata = $this->reader->getClassMetadata($class, SchemaModifierInterface::class);
173
        } catch (Exception $e) {
174
            throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
175
        }
176
177 480
        foreach ($metadata as $meta) {
178
            assert($meta instanceof SchemaModifierInterface);
179
180
            // need relation definition
181 12
            $entity->addSchemaModifier($meta);
182
        }
183 480
    }
184
185
    /**
186
     * @param Column[] $columns
187
     */
188 480
    public function initColumns(EntitySchema $entity, array $columns, \ReflectionClass $class): void
189
    {
190 480
        foreach ($columns as $key => $column) {
191 236
            $isNumericKey = is_numeric($key);
192 236
            $propertyName = $column->getProperty();
193
194 236
            if (!$isNumericKey && $propertyName !== null && $key !== $propertyName) {
195 3
                throw new AnnotationException(
196 3
                    "Can not use name \"{$key}\" for Column of the `{$entity->getRole()}` role, because the "
197 3
                    . "\"property\" field of the metadata class has already been set to \"{$propertyName}\"."
198
                );
199
            }
200
201 233
            $propertyName = $propertyName ?? ($isNumericKey ? null : $key);
202 233
            $columnName = $column->getColumn() ?? $propertyName;
203 233
            $propertyName = $propertyName ?? $columnName;
204
205 233
            if ($columnName === null) {
206
                throw new AnnotationException(
207
                    "Column name definition is required on `{$entity->getClass()}`"
208
                );
209
            }
210
211 233
            if ($column->getType() === null) {
212
                throw new AnnotationException(
213
                    "Column type definition is required on `{$entity->getClass()}`.`{$columnName}`"
214
                );
215
            }
216
217 233
            $field = $this->initField($columnName, $column, $class, '');
218 233
            $field->setEntityClass($entity->getClass());
219 233
            $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 480
    }
222
223 480
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
224
    {
225 480
        $field = new Field();
226
227 480
        $field->setType($column->getType());
228 480
        $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
229 480
        $field->setPrimary($column->isPrimary());
230
231 480
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
232
233 480
        if ($column->isNullable()) {
234 24
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
235 24
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null);
236
        }
237
238 480
        if ($column->hasDefault()) {
239 248
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
240
        }
241
242 480
        if ($column->castDefault()) {
243
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
244
        }
245
246 480
        return $field;
247
    }
248
249
    /**
250
     * Resolve class or role name relative to the current class.
251
     */
252 492
    public function resolveName(?string $name, \ReflectionClass $class): ?string
253
    {
254 492
        if ($name === null || class_exists($name, true) || interface_exists($name, true)) {
255 492
            return $name;
256
        }
257
258 340
        $resolved = sprintf(
259 340
            '%s\\%s',
260 340
            $class->getNamespaceName(),
261 340
            ltrim(str_replace('/', '\\', $name), '\\')
262
        );
263
264 340
        if (class_exists($resolved, true) || interface_exists($resolved, true)) {
265 328
            return ltrim($resolved, '\\');
266
        }
267
268 260
        return $name;
269
    }
270
271 480
    private function resolveTypecast(mixed $typecast, \ReflectionClass $class): mixed
272
    {
273 480
        if (is_string($typecast) && strpos($typecast, '::') !== false) {
274
            // short definition
275
            $typecast = explode('::', $typecast);
276
277
            // resolve class name
278
            $typecast[0] = $this->resolveName($typecast[0], $class);
279
        }
280
281 480
        if (is_string($typecast)) {
282
            $typecast = $this->resolveName($typecast, $class);
283
            if (class_exists($typecast)) {
284
                $typecast = [$typecast, 'typecast'];
285
            }
286
        }
287
288 480
        return $typecast;
289
    }
290
}
291