Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.84% covered (success)
93.84%
198 / 211
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddDoctrineFields
93.84% covered (success)
93.84%
198 / 211
50.00% covered (danger)
50.00%
2 / 4
56.73
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
82.76% covered (warning)
82.76%
48 / 58
0.00% covered (danger)
0.00%
0 / 1
20.85
 iterateProperties
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 patch
97.93% covered (success)
97.93%
142 / 145
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;
46
47/**
48 * Adds created_at and updated_at and Doctrine attributes
49 */
50class AddDoctrineFields implements PostRunGeneratedCodeContextInterface
51{
52    public function postRun(GeneratedCodeContext $generatedCodeContext): void
53    {
54        foreach ($generatedCodeContext->generatedCode->generatedCodeHashmap as $code) {
55            $this->patch($generatedCodeContext, $code);
56        }
57    }
58
59    private function applyId(ClassType $classType): void
60    {
61        $property = null;
62        $doctrineType = null;
63        $nullable = false;
64        $generatedValue = false;
65        if ($classType->hasProperty('id')) {
66            $property = $classType->getProperty('id');
67        } elseif ($classType->hasProperty('search_id')) {
68            $property = $classType->getProperty('search_id')->cloneWithName('id');
69            $classType->addMember($property);
70        }
71        if ($property === null) {
72            $property = $classType->addProperty('id')->setType('?int');
73            $generatedValue = true;
74            $doctrineType = 'integer';
75        } else {
76            // @see ClassTypeFactory
77            $originalClass = $classType->getComment();
78            if ($originalClass && class_exists($originalClass)) {
79                $metadata = MetadataFactory::getResultMetadata(
80                    new ReflectionClass($originalClass),
81                    new ApieContext()
82                );
83                $hashmap = $metadata->getHashmap();
84                if (isset($hashmap['id'])) {
85                    $type = $hashmap['id']->getTypehint();
86                    $nullable = $hashmap['id']->allowsNull();
87                    $class = ConverterUtils::toReflectionClass($type);
88                    if ($class && $class->isSubclassOf(AutoIncrementInteger::class)) {
89                        $generatedValue = true;
90                        $nullable = false;
91                        $property->setInitialized(true);
92                    }
93                    $scalarType = MetadataFactory::getScalarForType($hashmap['id']->getTypehint(), true);
94                    $property->setType(
95                        $scalarType->value
96                    );
97                    $doctrineType = $scalarType->toDoctrineType();
98                }
99            }
100        }
101
102        if (in_array(AutoIncrementTableInterface::class, $classType->getImplements())
103            || in_array(MixedStorageInterface::class, $classType->getImplements())) {
104            $generatedValue = true;
105            $nullable = false;
106        }
107
108        $hasIdAttribute = false;
109        $hasColumnAttribute = false;
110        foreach ($property->getAttributes() as $attribute) {
111            if (in_array($attribute->getName(), [Column::class, ManyToOne::class, OneToMany::class, ManyToMany::class])) {
112                $hasColumnAttribute = true;
113                break;
114            }
115            if ($attribute->getName() === GeneratedValue::class) {
116                $generatedValue = false;
117            }
118            if ($attribute->getName() === Id::class) {
119                $hasIdAttribute = true;
120            }
121        }
122        if (!$hasIdAttribute) {
123            $property->addAttribute(Id::class);
124        }
125        if (!$hasColumnAttribute) {
126            if ($doctrineType === null) {
127                $doctrineType = MetadataFactory::getScalarForType(
128                    ReflectionTypeFactory::createReflectionType($property->getType()),
129                    true
130                )->toDoctrineType();
131            }
132            $property->addAttribute(Column::class, ['type' => $doctrineType, 'nullable' => $nullable]);
133        }
134        if ($generatedValue) {
135            $property->addAttribute(GeneratedValue::class);
136        }
137    }
138
139    /**
140     * @return Generator<int, PromotedParameter|Property>
141     */
142    private function iterateProperties(ClassType $classType): Generator
143    {
144        foreach ($classType->getProperties() as $property) {
145            yield $property;
146        }
147        if ($classType->hasMethod('__construct')) {
148            foreach ($classType->getMethod('__construct')->getParameters() as $parameter) {
149                if ($parameter instanceof PromotedParameter) {
150                    yield $parameter;
151                }
152            }
153        }
154    }
155
156    private function patch(GeneratedCodeContext $generatedCodeContext, ClassType $classType): void
157    {
158        $classType->addAttribute(Entity::class);
159        $classType->addAttribute(HasLifecycleCallbacks::class);
160        $classType->addTrait('\\' . HasGeneralDoctrineFields::class);
161
162        // @see ClassTypeFactory
163        $originalClass = $classType->getComment();
164        if ($originalClass && class_exists($originalClass)) {
165            if (is_a($originalClass, RequiresRecalculatingInterface::class, true)) {
166                $classType->addTrait('\\' . RequiresDomainUpdate::class);
167            }
168        }
169
170        foreach ($this->iterateProperties($classType) as $property) {
171            $added = false;
172            $attributes = [];
173            foreach ($property->getAttributes() as $attribute) {
174                switch ($attribute->getName()) {
175                    case DecimalPropertyAttribute::class:
176                        $added = true;
177                        $arguments = $attribute->getArguments();
178                        $attributes[] = new Attribute(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                            $attributes[] = new Attribute(Column::class, ['nullable' => true, 'type' => 'datetimetz_immutable']);
185                        } else {
186                            $arguments = $attribute->getArguments();
187                            if ($arguments[2] ?? false) {
188                                $attributes[] = new Attribute(Column::class, ['nullable' => true, 'type' => 'text']);
189                            } else {
190                                $attributes[] = new Attribute(Column::class, ['nullable' => true]);
191                            }
192                        }
193                        break;
194                    case DiscriminatorMappingAttribute::class:
195                        $added = true;
196                        $attributes[] = new Attribute(Column::class, ['type' => 'json']);
197                        break;
198                    case ManyToOneAttribute::class:
199                        $added = true;
200                        $targetEntity = $property->getType();
201                        $attributes[] = new Attribute(
202                            ManyToOne::class,
203                            [
204                                'targetEntity' => $targetEntity,
205                                'inversedBy' => $attribute->getArguments()[0],
206                            ]
207                        );
208                        $attributes[] = new Attribute(
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                            $attributes[] = new Attribute(OrderBy::class, [[$indexByProperty => 'ASC']]);
232                        }
233                        $attributes[] = new Attribute(
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                        $attributes[] = new Attribute(
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                        $attributes[] = new Attribute(
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                        $attribute = new Attribute($attribute->getName(), $args);
285                        $type = $property->getType();
286                        break;
287                    case OrderAttribute::class:
288                        $added = true;
289                        $type = 'text';
290                        if ($property->getType() === 'int') {
291                            $type = 'integer';
292                        }
293                        $attributes[] = new Attribute(Column::class, ['type' => $type]);
294                        break;
295                    case ParentAttribute::class:
296                        $added = true;
297                        $inversedBy = $generatedCodeContext->findInverseProperty($property->getType(), $classType->getName());
298                        $attributes[] = new Attribute(
299                            ManyToOne::class,
300                            ['targetEntity' => $property->getType(), 'inversedBy' => $inversedBy]
301                        );
302                        $attributes[] = new Attribute(
303                            JoinColumn::class,
304                            [
305                                'onDelete' => 'CASCADE',
306                            ]
307                        );
308                        break;
309                }
310                $attributes[] = $attribute;
311            }
312            if (!$added) {
313                $type = $property->getType();
314                switch ((string) $type) {
315                    case 'string':
316                        $attributes[] = new Attribute(Column::class, ['type' => 'text', 'nullable' => $property->isNullable()]);
317                        break;
318                    case 'float':
319                        $attributes[] = new Attribute(Column::class, ['type' => 'float', 'nullable' => $property->isNullable()]);
320                        break;
321                    case 'int':
322                    case '?int':
323                        $attributes[] = new Attribute(Column::class, ['type' => 'integer', 'nullable' => $property->isNullable()]);
324                        break;
325                    case 'array':
326                    case '?array':
327                        $attributes[] = new Attribute(Column::class, ['type' => 'json', 'nullable' => $property->isNullable()]);
328                        break;
329                }
330            }
331            $property->setAttributes($attributes);
332        }
333
334        $this->applyId($classType);
335    }
336}