Issues (6)

src/Configurator.php (1 issue)

Severity
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Annotated;
13
14
use Cycle\Annotated\Annotation\Column;
15
use Cycle\Annotated\Annotation\Embeddable;
16
use Cycle\Annotated\Annotation\Entity;
17
use Cycle\Annotated\Annotation\Relation as RelationAnnotation;
18
use Cycle\Annotated\Exception\AnnotationException;
19
use Cycle\Schema\Definition\Entity as EntitySchema;
20
use Cycle\Schema\Definition\Field;
21
use Cycle\Schema\Definition\Relation;
22
use Cycle\Schema\Generator\SyncTables;
23
use Doctrine\Common\Annotations\AnnotationException as DoctrineException;
24
use Doctrine\Common\Annotations\AnnotationReader;
25
26
final class Configurator
27
{
28
    /** @var AnnotationReader */
29
    private $reader;
30
31
    /** @var \Doctrine\Inflector\Inflector */
32
    private $inflector;
33
34
    /**
35
     * @param AnnotationReader $reader
36
     */
37
    public function __construct(AnnotationReader $reader)
38
    {
39
        $this->reader = $reader;
40
        $this->inflector = (new \Doctrine\Inflector\Rules\English\InflectorFactory())->build();
41
    }
42
43
    /**
44
     * @param Entity           $ann
45
     * @param \ReflectionClass $class
46
     * @return EntitySchema
47
     */
48
    public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema
49
    {
50
        $e = new EntitySchema();
51
        $e->setClass($class->getName());
52
53
        $e->setRole($ann->getRole() ?? $this->inflector->camelize($class->getShortName()));
54
55
        // representing classes
56
        $e->setMapper($this->resolveName($ann->getMapper(), $class));
57
        $e->setRepository($this->resolveName($ann->getRepository(), $class));
58
        $e->setSource($this->resolveName($ann->getSource(), $class));
59
        $e->setConstrain($this->resolveName($ann->getConstrain(), $class));
60
61
        if ($ann->isReadonlySchema()) {
62
            $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true);
63
        }
64
65
        return $e;
66
    }
67
68
    /**
69
     * @param Embeddable       $emb
70
     * @param \ReflectionClass $class
71
     * @return EntitySchema
72
     */
73
    public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema
74
    {
75
        $e = new EntitySchema();
76
        $e->setClass($class->getName());
77
78
        $e->setRole($emb->getRole() ?? $this->inflector->camelize($class->getShortName()));
79
80
        // representing classes
81
        $e->setMapper($this->resolveName($emb->getMapper(), $class));
82
83
        return $e;
84
    }
85
86
    /**
87
     * @param EntitySchema     $entity
88
     * @param \ReflectionClass $class
89
     * @param string           $columnPrefix
90
     */
91
    public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = ''): void
92
    {
93
        foreach ($class->getProperties() as $property) {
94
            try {
95
                /** @var Column $column */
96
                $column = $this->reader->getPropertyAnnotation($property, Column::class);
97
            } catch (DoctrineException $e) {
98
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
99
            }
100
101
            if ($column === null) {
102
                continue;
103
            }
104
105
            $entity->getFields()->set(
106
                $property->getName(),
107
                $this->initField($property->getName(), $column, $class, $columnPrefix)
108
            );
109
        }
110
    }
111
112
    /**
113
     * @param EntitySchema     $entity
114
     * @param \ReflectionClass $class
115
     */
116
    public function initRelations(EntitySchema $entity, \ReflectionClass $class): void
117
    {
118
        foreach ($class->getProperties() as $property) {
119
            try {
120
                $annotations = $this->reader->getPropertyAnnotations($property);
121
            } catch (DoctrineException $e) {
122
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
123
            }
124
125
            foreach ($annotations as $ra) {
126
                if (!$ra instanceof RelationAnnotation\RelationInterface) {
127
                    continue;
128
                }
129
130
                if ($ra->getTarget() === null) {
131
                    throw new AnnotationException(
132
                        "Relation target definition is required on `{$entity->getClass()}`"
133
                    );
134
                }
135
136
                $relation = new Relation();
137
                $relation->setTarget($this->resolveName($ra->getTarget(), $class));
138
                $relation->setType($ra->getType());
139
140
                $inverse = $ra->getInverse();
141
                if ($inverse !== null) {
142
                    $relation->setInverse(
143
                        $inverse->getName(),
144
                        $inverse->getType(),
145
                        $inverse->getLoadMethod()
146
                    );
147
                }
148
149
                foreach ($ra->getOptions() as $option => $value) {
150
                    if ($option === 'though') {
151
                        $value = $this->resolveName($value, $class);
152
                    }
153
154
                    $relation->getOptions()->set($option, $value);
155
                }
156
157
                // need relation definition
158
                $entity->getRelations()->set($property->getName(), $relation);
159
            }
160
        }
161
    }
162
163
    /**
164
     * @param EntitySchema     $entity
165
     * @param Column[]         $columns
166
     * @param \ReflectionClass $class
167
     */
168
    public function initColumns(EntitySchema $entity, array $columns, \ReflectionClass $class): void
169
    {
170
        foreach ($columns as $name => $column) {
171
            if ($column->getColumn() === null && is_numeric($name)) {
172
                throw new AnnotationException(
173
                    "Column name definition is required on `{$entity->getClass()}`"
174
                );
175
            }
176
177
            if ($column->getType() === null) {
178
                throw new AnnotationException(
179
                    "Column type definition is required on `{$entity->getClass()}`"
180
                );
181
            }
182
183
            $entity->getFields()->set(
184
                $name ?? $column->getColumn(),
185
                $this->initField($column->getColumn() ?? $name, $column, $class, '')
186
            );
187
        }
188
    }
189
190
    /**
191
     * @param string           $name
192
     * @param Column           $column
193
     * @param \ReflectionClass $class
194
     * @param string           $columnPrefix
195
     * @return Field
196
     */
197
    public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field
198
    {
199
        if ($column->getType() === null) {
0 ignored issues
show
The condition $column->getType() === null is always false.
Loading history...
200
            throw new AnnotationException(
201
                "Column type definition is required on `{$class->getName()}`.`{$name}`"
202
            );
203
        }
204
205
        $field = new Field();
206
207
        $field->setType($column->getType());
208
        $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
209
        $field->setPrimary($column->isPrimary());
210
211
        $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
212
213
        if ($column->isNullable()) {
214
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true);
215
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null);
216
        }
217
218
        if ($column->hasDefault()) {
219
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault());
220
        }
221
222
        if ($column->castDefault()) {
223
            $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true);
224
        }
225
226
        return $field;
227
    }
228
229
    /**
230
     * Resolve class or role name relative to the current class.
231
     *
232
     * @param string           $name
233
     * @param \ReflectionClass $class
234
     * @return string
235
     */
236
    public function resolveName(?string $name, \ReflectionClass $class): ?string
237
    {
238
        if ($name === null || class_exists($name, true) || interface_exists($name, true)) {
239
            return $name;
240
        }
241
242
        $resolved = sprintf(
243
            '%s\\%s',
244
            $class->getNamespaceName(),
245
            ltrim(str_replace('/', '\\', $name), '\\')
246
        );
247
248
        if (class_exists($resolved, true) || interface_exists($resolved, true)) {
249
            return ltrim($resolved, '\\');
250
        }
251
252
        return $name;
253
    }
254
255
    /**
256
     * @param mixed            $typecast
257
     * @param \ReflectionClass $class
258
     * @return mixed
259
     */
260
    protected function resolveTypecast($typecast, \ReflectionClass $class)
261
    {
262
        if (is_string($typecast) && strpos($typecast, '::') !== false) {
263
            // short definition
264
            $typecast = explode('::', $typecast);
265
266
            // resolve class name
267
            $typecast[0] = $this->resolveName($typecast[0], $class);
268
        }
269
270
        if (is_string($typecast)) {
271
            $typecast = $this->resolveName($typecast, $class);
272
            if (class_exists($typecast)) {
273
                $typecast = [$typecast, 'typecast'];
274
            }
275
        }
276
277
        return $typecast;
278
    }
279
}
280