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()); |
||
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()); |
||
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(), |
||
181 | $this->initField($column->getColumn() ?? $name, $column, $class) |
||
0 ignored issues
–
show
|
|||
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 | $field->setTypecast($column->getTypecast()); |
||
207 | |||
208 | if ($column->isNullable()) { |
||
209 | $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true); |
||
210 | } |
||
211 | |||
212 | if ($column->hasDefault()) { |
||
213 | $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault()); |
||
214 | } |
||
215 | |||
216 | if ($column->isCastedDefault()) { |
||
217 | $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_CAST_DEFAULT, true); |
||
218 | } |
||
219 | |||
220 | return $field; |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Resolve class or role name relative to the current class. |
||
225 | * |
||
226 | * @param string $name |
||
227 | * @param \ReflectionClass $class |
||
228 | * @return string |
||
229 | */ |
||
230 | public function resolveName(?string $name, \ReflectionClass $class): ?string |
||
231 | { |
||
232 | if (is_null($name) || class_exists($name, true) || interface_exists($name, true)) { |
||
233 | return $name; |
||
234 | } |
||
235 | |||
236 | $resolved = sprintf( |
||
237 | "%s\\%s", |
||
238 | $class->getNamespaceName(), |
||
239 | ltrim(str_replace('/', '\\', $name), '\\') |
||
240 | ); |
||
241 | |||
242 | if (class_exists($resolved, true) || interface_exists($resolved, true)) { |
||
243 | return $resolved; |
||
244 | } |
||
245 | |||
246 | return $name; |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * @return Parser |
||
251 | */ |
||
252 | public static function getDefaultParser(): Parser |
||
253 | { |
||
254 | $p = new Parser(); |
||
255 | $p->register(new Entity()); |
||
256 | $p->register(new Embeddable()); |
||
257 | $p->register(new Column()); |
||
258 | $p->register(new Table()); |
||
259 | $p->register(new Table\Index()); |
||
260 | |||
261 | // embedded relations |
||
262 | $p->register(new RelationAnnotation\Embedded()); |
||
263 | $p->register(new RelationAnnotation\BelongsTo()); |
||
264 | $p->register(new RelationAnnotation\HasOne()); |
||
265 | $p->register(new RelationAnnotation\HasMany()); |
||
266 | $p->register(new RelationAnnotation\RefersTo()); |
||
267 | $p->register(new RelationAnnotation\ManyToMany()); |
||
268 | $p->register(new RelationAnnotation\Morphed\BelongsToMorphed()); |
||
269 | $p->register(new RelationAnnotation\Morphed\MorphedHasOne()); |
||
270 | $p->register(new RelationAnnotation\Morphed\MorphedHasMany()); |
||
271 | |||
272 | return $p; |
||
273 | } |
||
274 | } |
This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.