1 | <?php declare(strict_types=1); |
||||
2 | /** |
||||
3 | * Spiral Framework. |
||||
4 | * |
||||
5 | * @license MIT |
||||
6 | * @author Anton Titov (Wolfy-J) |
||||
7 | */ |
||||
8 | |||||
9 | namespace Cycle\Annotated; |
||||
10 | |||||
11 | use Cycle\Annotated\Annotation\Column; |
||||
12 | use Cycle\Annotated\Annotation\Embeddable; |
||||
13 | use Cycle\Annotated\Annotation\Entity; |
||||
14 | use Cycle\Annotated\Annotation\Relation as RelationAnnotation; |
||||
15 | use Cycle\Annotated\Annotation\Table; |
||||
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\Inflector\Inflector; |
||||
22 | use Spiral\Annotations\Parser; |
||||
23 | |||||
24 | final class Generator |
||||
25 | { |
||||
26 | /** @var Parser */ |
||||
27 | private $parser; |
||||
28 | |||||
29 | /** |
||||
30 | * @param Parser $parser |
||||
31 | */ |
||||
32 | public function __construct(Parser $parser) |
||||
33 | { |
||||
34 | $this->parser = $parser; |
||||
35 | } |
||||
36 | |||||
37 | /** |
||||
38 | * @param Entity $ann |
||||
39 | * @param \ReflectionClass $class |
||||
40 | * @return EntitySchema |
||||
41 | */ |
||||
42 | public function initEntity(Entity $ann, \ReflectionClass $class): EntitySchema |
||||
43 | { |
||||
44 | $e = new EntitySchema(); |
||||
45 | $e->setClass($class->getName()); |
||||
46 | |||||
47 | $e->setRole($ann->getRole() ?? Inflector::camelize($class->getShortName())); |
||||
48 | |||||
49 | // representing classes |
||||
50 | $e->setMapper($this->resolveName($ann->getMapper(), $class)); |
||||
51 | $e->setRepository($this->resolveName($ann->getRepository(), $class)); |
||||
52 | $e->setSource($this->resolveName($ann->getSource(), $class)); |
||||
53 | $e->setConstrain($this->resolveName($ann->getConstrain(), $class)); |
||||
54 | |||||
55 | if ($ann->isReadonlySchema()) { |
||||
56 | $e->getOptions()->set(SyncTables::READONLY_SCHEMA, true); |
||||
57 | } |
||||
58 | |||||
59 | return $e; |
||||
60 | } |
||||
61 | |||||
62 | /** |
||||
63 | * @param Embeddable $emb |
||||
64 | * @param \ReflectionClass $class |
||||
65 | * @return EntitySchema |
||||
66 | */ |
||||
67 | public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntitySchema |
||||
68 | { |
||||
69 | $e = new EntitySchema(); |
||||
70 | $e->setClass($class->getName()); |
||||
71 | |||||
72 | $e->setRole($emb->getRole() ?? Inflector::camelize($class->getShortName())); |
||||
73 | |||||
74 | // representing classes |
||||
75 | $e->setMapper($this->resolveName($emb->getMapper(), $class)); |
||||
76 | |||||
77 | return $e; |
||||
78 | } |
||||
79 | |||||
80 | /** |
||||
81 | * @param EntitySchema $entity |
||||
82 | * @param \ReflectionClass $class |
||||
83 | * @param string $columnPrefix |
||||
84 | */ |
||||
85 | public function initFields(EntitySchema $entity, \ReflectionClass $class, string $columnPrefix = '') |
||||
86 | { |
||||
87 | foreach ($class->getProperties() as $property) { |
||||
88 | if ($property->getDocComment() === false) { |
||||
89 | continue; |
||||
90 | } |
||||
91 | |||||
92 | $ann = $this->parser->parse($property->getDocComment()); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
93 | if (!isset($ann[Column::NAME])) { |
||||
94 | continue; |
||||
95 | } |
||||
96 | |||||
97 | $entity->getFields()->set( |
||||
98 | $property->getName(), |
||||
99 | $this->initField( |
||||
100 | $property->getName(), |
||||
101 | $ann[Column::NAME], |
||||
102 | $class, |
||||
103 | $columnPrefix |
||||
104 | ) |
||||
105 | ); |
||||
106 | } |
||||
107 | } |
||||
108 | |||||
109 | /** |
||||
110 | * @param EntitySchema $entity |
||||
111 | * @param \ReflectionClass $class |
||||
112 | */ |
||||
113 | public function initRelations(EntitySchema $entity, \ReflectionClass $class) |
||||
114 | { |
||||
115 | foreach ($class->getProperties() as $property) { |
||||
116 | if ($property->getDocComment() === false) { |
||||
117 | continue; |
||||
118 | } |
||||
119 | |||||
120 | $ann = $this->parser->parse($property->getDocComment()); |
||||
0 ignored issues
–
show
It seems like
$property->getDocComment() can also be of type true ; however, parameter $body of Spiral\Annotations\Parser::parse() 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
Loading history...
|
|||||
121 | |||||
122 | foreach ($ann as $ra) { |
||||
123 | if (!$ra instanceof RelationAnnotation\RelationInterface) { |
||||
124 | continue; |
||||
125 | } |
||||
126 | |||||
127 | if ($ra->getTarget() === null) { |
||||
128 | throw new AnnotationException( |
||||
129 | "Relation target definition is required on `{$entity->getClass()}`" |
||||
130 | ); |
||||
131 | } |
||||
132 | |||||
133 | $relation = new Relation(); |
||||
134 | $relation->setTarget($this->resolveName($ra->getTarget(), $class)); |
||||
135 | $relation->setType($ra->getName()); |
||||
136 | |||||
137 | if ($ra->isInversed()) { |
||||
138 | $relation->setInverse( |
||||
139 | $ra->getInverseName(), |
||||
140 | $ra->getInverseType(), |
||||
141 | $ra->getInverseLoadMethod() |
||||
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(), |
||||
0 ignored issues
–
show
It seems like
$name ?? $column->getColumn() 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
Loading history...
|
|||||
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) { |
||||
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->isCastedDefault()) { |
||||
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 $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 | |||||
275 | /** |
||||
276 | * @return Parser |
||||
277 | */ |
||||
278 | public static function getDefaultParser(): Parser |
||||
279 | { |
||||
280 | $p = new Parser(); |
||||
281 | $p->register(new Entity()); |
||||
282 | $p->register(new Embeddable()); |
||||
283 | $p->register(new Column()); |
||||
284 | $p->register(new Table()); |
||||
285 | $p->register(new Table\Index()); |
||||
286 | |||||
287 | // embedded relations |
||||
288 | $p->register(new RelationAnnotation\Embedded()); |
||||
289 | $p->register(new RelationAnnotation\BelongsTo()); |
||||
290 | $p->register(new RelationAnnotation\HasOne()); |
||||
291 | $p->register(new RelationAnnotation\HasMany()); |
||||
292 | $p->register(new RelationAnnotation\RefersTo()); |
||||
293 | $p->register(new RelationAnnotation\ManyToMany()); |
||||
294 | $p->register(new RelationAnnotation\Morphed\BelongsToMorphed()); |
||||
295 | $p->register(new RelationAnnotation\Morphed\MorphedHasOne()); |
||||
296 | $p->register(new RelationAnnotation\Morphed\MorphedHasMany()); |
||||
297 | |||||
298 | return $p; |
||||
299 | } |
||||
300 | } |