Passed
Push — 3.x ( 080893...4e5cb3 )
by Aleksei
09:48
created

Configurator::initRelations()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.2269

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 7
eloc 28
nc 8
nop 2
dl 0
loc 46
ccs 20
cts 24
cp 0.8333
crap 7.2269
rs 8.5386
c 1
b 1
f 0
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 480
    public function __construct(
33
        DoctrineReader|ReaderInterface $reader,
34
        private int $tableNamingStrategy = Entities::TABLE_NAMING_PLURAL,
35
    ) {
36 480
        $this->reader = ReaderFactory::create($reader);
37 480
        $this->inflector = (new InflectorFactory())->build();
38 480
        $this->utils = new EntityUtils($this->reader);
39 480
    }
40
41 480
    public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema
42
    {
43 480
        $e = new EntitySchema();
44 480
        $e->setClass($class->getName());
45
46 480
        $e->setRole($ann->getRole() ?? $this->inflector->camelize($class->getShortName()));
47
48
        // representing classes
49 480
        $e->setMapper($this->resolveName($ann->getMapper(), $class));
50 480
        $e->setRepository($this->resolveName($ann->getRepository(), $class));
51 480
        $e->setSource($this->resolveName($ann->getSource(), $class));
52 480
        $e->setScope($this->resolveName($ann->getScope(), $class));
53 480
        $e->setDatabase($ann->getDatabase());
54 480
        $e->setTableName(
55 480
            $ann->getTable() ?? $this->utils->tableName($e->getRole(), $this->tableNamingStrategy)
56
        );
57
58 480
        $typecast = $ann->getTypecast();
59 480
        if (is_array($typecast)) {
60 248
            $typecast = array_map(fn (string $value): string => $this->resolveName($value, $class), $typecast);
61
        } else {
62 480
            $typecast = $this->resolveName($typecast, $class);
63
        }
64
65 480
        $e->setTypecast($typecast);
66
67 480
        if ($ann->isReadonlySchema()) {
68
            $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true);
69
        }
70
71 480
        return $e;
72
    }
73
74 24
    public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema
75
    {
76 24
        $e = new EntitySchema();
77 24
        $e->setClass($class->getName());
78
79 24
        $e->setRole($emb->getRole() ?? $this->inflector->camelize($class->getShortName()));
80
81
        // representing classes
82 24
        $e->setMapper($this->resolveName($emb->getMapper(), $class));
83
84 24
        return $e;
85
    }
86
87 480
    public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = ''): void
88
    {
89 480
        foreach ($class->getProperties() as $property) {
90
            try {
91 480
                $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 468
            if ($column === null) {
101 440
                continue;
102
            }
103
104 456
            $field = $this->initField($property->getName(), $column, $class, $columnPrefix);
105 456
            $field->setEntityClass($property->getDeclaringClass()->getName());
106 456
            $entity->getFields()->set($property->getName(), $field);
107
        }
108 468
    }
109
110 468
    public function initRelations(EntitySchema $entity, \ReflectionClass $class): void
111
    {
112 468
        foreach ($class->getProperties() as $property) {
113
            try {
114 468
                $metadata = $this->reader->getPropertyMetadata($property, RelationAnnotation\RelationInterface::class);
115
            } catch (Exception $e) {
116
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
117
            }
118
119 468
            foreach ($metadata as $meta) {
120
                assert($meta instanceof RelationAnnotation\RelationInterface);
121
122 392
                if ($meta->getTarget() === null) {
123
                    throw new AnnotationException(
124
                        "Relation target definition is required on `{$entity->getClass()}`.`{$property->getName()}`"
125
                    );
126
                }
127
128 392
                $relation = new Relation();
129 392
                $relation->setTarget($this->resolveName($meta->getTarget(), $class));
130 392
                $relation->setType($meta->getType());
131
132 392
                $inverse = $meta->getInverse() ?? $this->reader->firstPropertyMetadata(
133
                    $property,
134 372
                    RelationAnnotation\Inverse::class
135
                );
136 392
                if ($inverse !== null) {
137 72
                    $relation->setInverse(
138 72
                        $inverse->getName(),
139 72
                        $inverse->getType(),
140 72
                        $inverse->getLoadMethod()
141
                    );
142
                }
143
144 392
                foreach ($meta->getOptions() as $option => $value) {
145 392
                    $value = match ($option) {
146 284
                        'collection' => $this->resolveName($value, $class),
147
                        'though', 'through' => $this->resolveName($value, $class),
148
                        default => $value
149
                    };
150
151 392
                    $relation->getOptions()->set($option, $value);
152
                }
153
154
                // need relation definition
155 392
                $entity->getRelations()->set($property->getName(), $relation);
156
            }
157
        }
158 468
    }
159
160 468
    public function initModifiers(EntitySchema $entity, \ReflectionClass $class): void
161
    {
162
        try {
163 468
            $metadata = $this->reader->getClassMetadata($class, SchemaModifierInterface::class);
164
        } catch (Exception $e) {
165
            throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
166
        }
167
168 468
        foreach ($metadata as $meta) {
169
            assert($meta instanceof SchemaModifierInterface);
170
171
            // need relation definition
172 12
            $entity->addSchemaModifier($meta);
173
        }
174 468
    }
175
176
    /**
177
     * @param Column[] $columns
178
     */
179 468
    public function initColumns(EntitySchema $entity, array $columns, \ReflectionClass $class): void
180
    {
181 468
        foreach ($columns as $key => $column) {
182 236
            $isNumericKey = is_numeric($key);
183 236
            $propertyName = $column->getProperty();
184
185 236
            if (!$isNumericKey && $propertyName !== null && $key !== $propertyName) {
186 3
                throw new AnnotationException(
187 3
                    "Can not use name \"{$key}\" for Column of the `{$entity->getRole()}` role, because the "
188 3
                    . "\"property\" field of the metadata class has already been set to \"{$propertyName}\"."
189
                );
190
            }
191
192 233
            $propertyName = $propertyName ?? ($isNumericKey ? null : $key);
193 233
            $columnName = $column->getColumn() ?? $propertyName;
194 233
            $propertyName = $propertyName ?? $columnName;
195
196 233
            if ($columnName === null) {
197
                throw new AnnotationException(
198
                    "Column name definition is required on `{$entity->getClass()}`"
199
                );
200
            }
201
202 233
            if ($column->getType() === null) {
203
                throw new AnnotationException(
204
                    "Column type definition is required on `{$entity->getClass()}`.`{$columnName}`"
205
                );
206
            }
207
208 233
            $field = $this->initField($columnName, $column, $class, '');
209 233
            $field->setEntityClass($entity->getClass());
210 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

210
            $entity->getFields()->set(/** @scrutinizer ignore-type */ $propertyName, $field);
Loading history...
211
        }
212 468
    }
213
214 468
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
215
    {
216 468
        $field = new Field();
217
218 468
        $field->setType($column->getType());
219 468
        $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
220 468
        $field->setPrimary($column->isPrimary());
221
222 468
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
223
224 468
        if ($column->isNullable()) {
225 24
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
226 24
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null);
227
        }
228
229 468
        if ($column->hasDefault()) {
230 248
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
231
        }
232
233 468
        if ($column->castDefault()) {
234
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
235
        }
236
237 468
        return $field;
238
    }
239
240
    /**
241
     * Resolve class or role name relative to the current class.
242
     */
243 480
    public function resolveName(?string $name, \ReflectionClass $class): ?string
244
    {
245 480
        if ($name === null || class_exists($name, true) || interface_exists($name, true)) {
246 480
            return $name;
247
        }
248
249 340
        $resolved = sprintf(
250 340
            '%s\\%s',
251 340
            $class->getNamespaceName(),
252 340
            ltrim(str_replace('/', '\\', $name), '\\')
253
        );
254
255 340
        if (class_exists($resolved, true) || interface_exists($resolved, true)) {
256 328
            return ltrim($resolved, '\\');
257
        }
258
259 260
        return $name;
260
    }
261
262 468
    private function resolveTypecast(mixed $typecast, \ReflectionClass $class): mixed
263
    {
264 468
        if (is_string($typecast) && strpos($typecast, '::') !== false) {
265
            // short definition
266
            $typecast = explode('::', $typecast);
267
268
            // resolve class name
269
            $typecast[0] = $this->resolveName($typecast[0], $class);
270
        }
271
272 468
        if (is_string($typecast)) {
273
            $typecast = $this->resolveName($typecast, $class);
274
            if (class_exists($typecast)) {
275
                $typecast = [$typecast, 'typecast'];
276
            }
277
        }
278
279 468
        return $typecast;
280
    }
281
}
282