Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.71% |
197 / 208 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
AddDoctrineFields | |
94.71% |
197 / 208 |
|
50.00% |
2 / 4 |
56.46 | |
0.00% |
0 / 1 |
postRun | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
applyId | |
86.21% |
50 / 58 |
|
0.00% |
0 / 1 |
19.95 | |||
iterateProperties | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
patch | |
97.89% |
139 / 142 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | namespace Apie\DoctrineEntityConverter\CodeGenerators; |
3 | |
4 | use Apie\Core\Context\ApieContext; |
5 | use Apie\Core\Entities\RequiresRecalculatingInterface; |
6 | use Apie\Core\Identifiers\AutoIncrementInteger; |
7 | use Apie\Core\Metadata\MetadataFactory; |
8 | use Apie\Core\Utils\ConverterUtils; |
9 | use Apie\DoctrineEntityConverter\Concerns\HasGeneralDoctrineFields; |
10 | use Apie\DoctrineEntityConverter\Concerns\RequiresDomainUpdate; |
11 | use Apie\DoctrineEntityConverter\Entities\SearchIndex; |
12 | use Apie\StorageMetadata\Attributes\AclLinkAttribute; |
13 | use Apie\StorageMetadata\Attributes\DecimalPropertyAttribute; |
14 | use Apie\StorageMetadata\Attributes\DiscriminatorMappingAttribute; |
15 | use Apie\StorageMetadata\Attributes\GetMethodAttribute; |
16 | use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute; |
17 | use Apie\StorageMetadata\Attributes\ManyToOneAttribute; |
18 | use Apie\StorageMetadata\Attributes\OneToManyAttribute; |
19 | use Apie\StorageMetadata\Attributes\OneToOneAttribute; |
20 | use Apie\StorageMetadata\Attributes\OrderAttribute; |
21 | use Apie\StorageMetadata\Attributes\ParentAttribute; |
22 | use Apie\StorageMetadata\Attributes\PropertyAttribute; |
23 | use Apie\StorageMetadata\Interfaces\AutoIncrementTableInterface; |
24 | use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface; |
25 | use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface; |
26 | use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext; |
27 | use Apie\TypeConverter\ReflectionTypeFactory; |
28 | use Doctrine\Common\Collections\Collection; |
29 | use Doctrine\ORM\Mapping\Column; |
30 | use Doctrine\ORM\Mapping\Entity; |
31 | use Doctrine\ORM\Mapping\GeneratedValue; |
32 | use Doctrine\ORM\Mapping\HasLifecycleCallbacks; |
33 | use Doctrine\ORM\Mapping\Id; |
34 | use Doctrine\ORM\Mapping\JoinColumn; |
35 | use Doctrine\ORM\Mapping\ManyToMany; |
36 | use Doctrine\ORM\Mapping\ManyToOne; |
37 | use Doctrine\ORM\Mapping\OneToMany; |
38 | use Doctrine\ORM\Mapping\OneToOne; |
39 | use Doctrine\ORM\Mapping\OrderBy; |
40 | use Generator; |
41 | use Nette\PhpGenerator\Attribute; |
42 | use Nette\PhpGenerator\ClassType; |
43 | use Nette\PhpGenerator\PromotedParameter; |
44 | use Nette\PhpGenerator\Property; |
45 | use ReflectionClass; |
46 | use ReflectionProperty; |
47 | |
48 | /** |
49 | * Adds created_at and updated_at and Doctrine attributes |
50 | */ |
51 | class AddDoctrineFields implements PostRunGeneratedCodeContextInterface |
52 | { |
53 | public function postRun(GeneratedCodeContext $generatedCodeContext): void |
54 | { |
55 | foreach ($generatedCodeContext->generatedCode->generatedCodeHashmap as $code) { |
56 | $this->patch($generatedCodeContext, $code); |
57 | } |
58 | } |
59 | |
60 | private function applyId(ClassType $classType): void |
61 | { |
62 | $property = null; |
63 | $doctrineType = null; |
64 | $nullable = false; |
65 | $generatedValue = false; |
66 | if ($classType->hasProperty('id')) { |
67 | $property = $classType->getProperty('id'); |
68 | } elseif ($classType->hasProperty('search_id')) { |
69 | $property = $classType->getProperty('search_id')->cloneWithName('id'); |
70 | $classType->addMember($property); |
71 | } |
72 | if ($property === null) { |
73 | $property = $classType->addProperty('id')->setType('?int'); |
74 | $generatedValue = true; |
75 | $doctrineType = 'integer'; |
76 | } else { |
77 | // @see ClassTypeFactory |
78 | $originalClass = $classType->getComment(); |
79 | if ($originalClass && class_exists($originalClass)) { |
80 | $metadata = MetadataFactory::getResultMetadata( |
81 | new ReflectionClass($originalClass), |
82 | new ApieContext() |
83 | ); |
84 | $hashmap = $metadata->getHashmap(); |
85 | if (isset($hashmap['id'])) { |
86 | $type = $hashmap['id']->getTypehint(); |
87 | $nullable = $hashmap['id']->allowsNull(); |
88 | $class = ConverterUtils::toReflectionClass($type); |
89 | if ($class && $class->isSubclassOf(AutoIncrementInteger::class)) { |
90 | $generatedValue = true; |
91 | $nullable = false; |
92 | $property->setInitialized(true); |
93 | } |
94 | $scalarType = MetadataFactory::getScalarForType($hashmap['id']->getTypehint(), true); |
95 | $property->setType( |
96 | $scalarType->value |
97 | ); |
98 | $doctrineType = $scalarType->toDoctrineType(); |
99 | } |
100 | } |
101 | } |
102 | |
103 | if (in_array(AutoIncrementTableInterface::class, $classType->getImplements()) |
104 | || in_array(MixedStorageInterface::class, $classType->getImplements())) { |
105 | $generatedValue = true; |
106 | $nullable = false; |
107 | } |
108 | |
109 | $hasIdAttribute = false; |
110 | $hasColumnAttribute = false; |
111 | foreach ($property->getAttributes() as $attribute) { |
112 | if (in_array($attribute->getName(), [Column::class, ManyToOne::class, OneToMany::class, ManyToMany::class])) { |
113 | $hasColumnAttribute = true; |
114 | break; |
115 | } |
116 | if ($attribute->getName() === GeneratedValue::class) { |
117 | $generatedValue = false; |
118 | } |
119 | if ($attribute->getName() === Id::class) { |
120 | $hasIdAttribute = true; |
121 | } |
122 | } |
123 | if (!$hasIdAttribute) { |
124 | $property->addAttribute(Id::class); |
125 | } |
126 | if (!$hasColumnAttribute) { |
127 | if ($doctrineType === null) { |
128 | $doctrineType = MetadataFactory::getScalarForType( |
129 | ReflectionTypeFactory::createReflectionType($property->getType()), |
130 | true |
131 | )->toDoctrineType(); |
132 | } |
133 | $property->addAttribute(Column::class, ['type' => $doctrineType, 'nullable' => $nullable]); |
134 | } |
135 | if ($generatedValue) { |
136 | $property->addAttribute(GeneratedValue::class); |
137 | } |
138 | } |
139 | |
140 | /** |
141 | * @return Generator<int, PromotedParameter|Property> |
142 | */ |
143 | private function iterateProperties(ClassType $classType): Generator |
144 | { |
145 | foreach ($classType->getProperties() as $property) { |
146 | yield $property; |
147 | } |
148 | if ($classType->hasMethod('__construct')) { |
149 | foreach ($classType->getMethod('__construct')->getParameters() as $parameter) { |
150 | if ($parameter instanceof PromotedParameter) { |
151 | yield $parameter; |
152 | } |
153 | } |
154 | } |
155 | } |
156 | |
157 | private function patch(GeneratedCodeContext $generatedCodeContext, ClassType $classType): void |
158 | { |
159 | $classType->addAttribute(Entity::class); |
160 | $classType->addAttribute(HasLifecycleCallbacks::class); |
161 | $classType->addTrait('\\' . HasGeneralDoctrineFields::class); |
162 | |
163 | // @see ClassTypeFactory |
164 | $originalClass = $classType->getComment(); |
165 | if ($originalClass && class_exists($originalClass)) { |
166 | if (is_a($originalClass, RequiresRecalculatingInterface::class, true)) { |
167 | $classType->addTrait('\\' . RequiresDomainUpdate::class); |
168 | } |
169 | } |
170 | |
171 | foreach ($this->iterateProperties($classType) as $property) { |
172 | $added = false; |
173 | foreach ($property->getAttributes() as $attribute) { |
174 | switch ($attribute->getName()) { |
175 | case DecimalPropertyAttribute::class: |
176 | $added = true; |
177 | $arguments = $attribute->getArguments(); |
178 | $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'decimal', 'precision' => $arguments[2] ?? 2]); |
179 | break; |
180 | case GetMethodAttribute::class: |
181 | case PropertyAttribute::class: |
182 | $added = true; |
183 | if (in_array($property->getType(), ['DateTimeImmutable', '?DateTimeImmutable'])) { |
184 | $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'datetimetz_immutable']); |
185 | } else { |
186 | $arguments = $attribute->getArguments(); |
187 | if ($arguments[2] ?? false) { |
188 | $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'text']); |
189 | } else { |
190 | $property->addAttribute(Column::class, ['nullable' => true]); |
191 | } |
192 | } |
193 | break; |
194 | case DiscriminatorMappingAttribute::class: |
195 | $added = true; |
196 | $property->addAttribute(Column::class, ['type' => 'json']); |
197 | break; |
198 | case ManyToOneAttribute::class: |
199 | $added = true; |
200 | $targetEntity = $property->getType(); |
201 | $property->addAttribute( |
202 | ManyToOne::class, |
203 | [ |
204 | 'targetEntity' => $targetEntity, |
205 | 'inversedBy' => $attribute->getArguments()[0], |
206 | ] |
207 | ); |
208 | $property->addAttribute( |
209 | JoinColumn::class, |
210 | [ |
211 | 'nullable' => true, |
212 | 'onDelete' => 'CASCADE', |
213 | ] |
214 | ); |
215 | break; |
216 | case OneToManyAttribute::class: |
217 | case AclLinkAttribute::class: |
218 | $added = true; |
219 | $property->setType(Collection::class); |
220 | if ($attribute->getName() === OneToManyAttribute::class) { |
221 | $targetEntity = $attribute->getArguments()[1]; |
222 | $mappedByProperty = $generatedCodeContext->findParentProperty($targetEntity); |
223 | $mappedByProperty ??= $attribute->getArguments()[0]; |
224 | $mappedByProperty ??= 'ref_' . $classType->getName(); |
225 | } else { |
226 | $targetEntity = $attribute->getArguments()[0]; |
227 | $mappedByProperty = 'ref_' . $classType->getName(); |
228 | } |
229 | $indexByProperty = $generatedCodeContext->findIndexProperty($targetEntity); |
230 | if ($indexByProperty) { |
231 | $property->addAttribute(OrderBy::class, [[$indexByProperty => 'ASC']]); |
232 | } |
233 | $property->addAttribute( |
234 | OneToMany::class, |
235 | [ |
236 | 'cascade' => ['all'], |
237 | 'targetEntity' => $targetEntity, |
238 | 'mappedBy' => $mappedByProperty, |
239 | 'fetch' => 'EAGER', |
240 | 'indexBy' => $indexByProperty, |
241 | 'orphanRemoval' => true, |
242 | ] |
243 | ); |
244 | |
245 | break; |
246 | case OneToOneAttribute::class: |
247 | $added = true; |
248 | $targetEntity = $property->getType(); |
249 | // look for @ParentAttribute for inversedBy? |
250 | $property->addAttribute( |
251 | OneToOne::class, |
252 | [ |
253 | 'cascade' => ['all'], |
254 | 'targetEntity' => $targetEntity, |
255 | 'fetch' => 'EAGER', |
256 | 'orphanRemoval' => true, |
257 | ] |
258 | ); |
259 | break; |
260 | case GetSearchIndexAttribute::class: |
261 | $added = true; |
262 | $property->setType(Collection::class); |
263 | $searchTableName = strpos($classType->getName(), 'apie_resource__') === 0 |
264 | ? preg_replace('/^apie_resource__/', 'apie_index__', $classType->getName()) |
265 | : 'apie_index__' . $classType->getName(); |
266 | $searchTableName .= '_' . $property->getName(); |
267 | $searchTable = SearchIndex::createFor( |
268 | $searchTableName, |
269 | $classType->getName(), |
270 | $property->getName(), |
271 | ); |
272 | $generatedCodeContext->generatedCode->generatedCodeHashmap[$searchTableName] = $searchTable; |
273 | $property->addAttribute( |
274 | OneToMany::class, |
275 | [ |
276 | 'cascade' => ['all'], |
277 | 'targetEntity' => $searchTableName, |
278 | 'mappedBy' => 'parent', |
279 | 'orphanRemoval' => true, |
280 | ] |
281 | ); |
282 | $args = $attribute->getArguments(); |
283 | $args['arrayValueType'] = $searchTableName; |
284 | // there is no good method in nette/php-generator |
285 | (new ReflectionProperty(Attribute::class, 'args'))->setValue($attribute, $args); |
286 | $type = $property->getType(); |
287 | break; |
288 | case OrderAttribute::class: |
289 | $added = true; |
290 | $type = 'text'; |
291 | if ($property->getType() === 'int') { |
292 | $type = 'integer'; |
293 | } |
294 | $property->addAttribute(Column::class, ['type' => $type]); |
295 | break; |
296 | case ParentAttribute::class: |
297 | $added = true; |
298 | $inversedBy = $generatedCodeContext->findInverseProperty($property->getType(), $classType->getName()); |
299 | $property->addAttribute( |
300 | ManyToOne::class, |
301 | ['targetEntity' => $property->getType(), 'inversedBy' => $inversedBy] |
302 | ); |
303 | $property->addAttribute( |
304 | JoinColumn::class, |
305 | [ |
306 | 'onDelete' => 'CASCADE', |
307 | ] |
308 | ); |
309 | break; |
310 | } |
311 | } |
312 | if (!$added) { |
313 | $type = $property->getType(); |
314 | switch ((string) $type) { |
315 | case 'string': |
316 | $property->addAttribute(Column::class, ['type' => 'text', 'nullable' => $property->isNullable()]); |
317 | break; |
318 | case 'float': |
319 | $property->addAttribute(Column::class, ['type' => 'float', 'nullable' => $property->isNullable()]); |
320 | break; |
321 | case 'int': |
322 | case '?int': |
323 | $property->addAttribute(Column::class, ['type' => 'integer', 'nullable' => $property->isNullable()]); |
324 | break; |
325 | case 'array': |
326 | case '?array': |
327 | $property->addAttribute(Column::class, ['type' => 'json', 'nullable' => $property->isNullable()]); |
328 | break; |
329 | } |
330 | } |
331 | } |
332 | |
333 | $this->applyId($classType); |
334 | } |
335 | } |