Passed
Push — master ( c93c78...d9ab06 )
by Anton
04:33 queued 02:57
created

Configurator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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