Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.71% covered (success)
94.71%
197 / 208
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddDoctrineFields
94.71% covered (success)
94.71%
197 / 208
50.00% covered (danger)
50.00%
2 / 4
56.46
0.00% covered (danger)
0.00%
0 / 1
 postRun
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 applyId
86.21% covered (warning)
86.21%
50 / 58
0.00% covered (danger)
0.00%
0 / 1
19.95
 iterateProperties
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 patch
97.89% covered (success)
97.89%
139 / 142
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2namespace Apie\DoctrineEntityConverter\CodeGenerators;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Entities\RequiresRecalculatingInterface;
6use Apie\Core\Identifiers\AutoIncrementInteger;
7use Apie\Core\Metadata\MetadataFactory;
8use Apie\Core\Utils\ConverterUtils;
9use Apie\DoctrineEntityConverter\Concerns\HasGeneralDoctrineFields;
10use Apie\DoctrineEntityConverter\Concerns\RequiresDomainUpdate;
11use Apie\DoctrineEntityConverter\Entities\SearchIndex;
12use Apie\StorageMetadata\Attributes\AclLinkAttribute;
13use Apie\StorageMetadata\Attributes\DecimalPropertyAttribute;
14use Apie\StorageMetadata\Attributes\DiscriminatorMappingAttribute;
15use Apie\StorageMetadata\Attributes\GetMethodAttribute;
16use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute;
17use Apie\StorageMetadata\Attributes\ManyToOneAttribute;
18use Apie\StorageMetadata\Attributes\OneToManyAttribute;
19use Apie\StorageMetadata\Attributes\OneToOneAttribute;
20use Apie\StorageMetadata\Attributes\OrderAttribute;
21use Apie\StorageMetadata\Attributes\ParentAttribute;
22use Apie\StorageMetadata\Attributes\PropertyAttribute;
23use Apie\StorageMetadata\Interfaces\AutoIncrementTableInterface;
24use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
25use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface;
26use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
27use Apie\TypeConverter\ReflectionTypeFactory;
28use Doctrine\Common\Collections\Collection;
29use Doctrine\ORM\Mapping\Column;
30use Doctrine\ORM\Mapping\Entity;
31use Doctrine\ORM\Mapping\GeneratedValue;
32use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
33use Doctrine\ORM\Mapping\Id;
34use Doctrine\ORM\Mapping\JoinColumn;
35use Doctrine\ORM\Mapping\ManyToMany;
36use Doctrine\ORM\Mapping\ManyToOne;
37use Doctrine\ORM\Mapping\OneToMany;
38use Doctrine\ORM\Mapping\OneToOne;
39use Doctrine\ORM\Mapping\OrderBy;
40use Generator;
41use Nette\PhpGenerator\Attribute;
42use Nette\PhpGenerator\ClassType;
43use Nette\PhpGenerator\PromotedParameter;
44use Nette\PhpGenerator\Property;
45use ReflectionClass;
46use ReflectionProperty;
47
48/**
49 * Adds created_at and updated_at and Doctrine attributes
50 */
51class 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}