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

Configurator::resolveName()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 9
nc 3
nop 2
dl 0
loc 17
ccs 10
cts 10
cp 1
crap 6
rs 9.2222
c 0
b 0
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 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