Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.61% covered (success)
94.61%
193 / 204
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddDoctrineFields
94.61% covered (success)
94.61%
193 / 204
50.00% covered (danger)
50.00%
2 / 4
55.47
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.83% covered (success)
97.83%
135 / 138
0.00% covered (danger)
0.00%
0 / 1
29
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\DiscriminatorMappingAttribute;
14use Apie\StorageMetadata\Attributes\GetMethodAttribute;
15use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute;
16use Apie\StorageMetadata\Attributes\ManyToOneAttribute;
17use Apie\StorageMetadata\Attributes\OneToManyAttribute;
18use Apie\StorageMetadata\Attributes\OneToOneAttribute;
19use Apie\StorageMetadata\Attributes\OrderAttribute;
20use Apie\StorageMetadata\Attributes\ParentAttribute;
21use Apie\StorageMetadata\Attributes\PropertyAttribute;
22use Apie\StorageMetadata\Interfaces\AutoIncrementTableInterface;
23use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
24use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface;
25use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
26use Apie\TypeConverter\ReflectionTypeFactory;
27use Doctrine\Common\Collections\Collection;
28use Doctrine\ORM\Mapping\Column;
29use Doctrine\ORM\Mapping\Entity;
30use Doctrine\ORM\Mapping\GeneratedValue;
31use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
32use Doctrine\ORM\Mapping\Id;
33use Doctrine\ORM\Mapping\JoinColumn;
34use Doctrine\ORM\Mapping\ManyToMany;
35use Doctrine\ORM\Mapping\ManyToOne;
36use Doctrine\ORM\Mapping\OneToMany;
37use Doctrine\ORM\Mapping\OneToOne;
38use Doctrine\ORM\Mapping\OrderBy;
39use Generator;
40use Nette\PhpGenerator\Attribute;
41use Nette\PhpGenerator\ClassType;
42use Nette\PhpGenerator\PromotedParameter;
43use Nette\PhpGenerator\Property;
44use ReflectionClass;
45use ReflectionProperty;
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            foreach ($property->getAttributes() as $attribute) {
173                switch ($attribute->getName()) {
174                    case GetMethodAttribute::class:
175                    case PropertyAttribute::class:
176                        $added = true;
177                        if (in_array($property->getType(), ['DateTimeImmutable', '?DateTimeImmutable'])) {
178                            $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'datetimetz_immutable']);
179                        } else {
180                            $arguments = $attribute->getArguments();
181                            if ($arguments[2] ?? false) {
182                                $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'text']);
183                            } else {
184                                $property->addAttribute(Column::class, ['nullable' => true]);
185                            }
186                        }
187                        break;
188                    case DiscriminatorMappingAttribute::class:
189                        $added = true;
190                        $property->addAttribute(Column::class, ['type' => 'json']);
191                        break;
192                    case ManyToOneAttribute::class:
193                        $added = true;
194                        $targetEntity = $property->getType();
195                        $property->addAttribute(
196                            ManyToOne::class,
197                            [
198                                'targetEntity' => $targetEntity,
199                                'inversedBy' => $attribute->getArguments()[0],
200                            ]
201                        );
202                        $property->addAttribute(
203                            JoinColumn::class,
204                            [
205                                'nullable' => true,
206                                'onDelete' => 'CASCADE',
207                            ]
208                        );
209                        break;
210                    case OneToManyAttribute::class:
211                    case AclLinkAttribute::class:
212                        $added = true;
213                        $property->setType(Collection::class);
214                        if ($attribute->getName() === OneToManyAttribute::class) {
215                            $targetEntity = $attribute->getArguments()[1];
216                            $mappedByProperty = $generatedCodeContext->findParentProperty($targetEntity);
217                            $mappedByProperty ??= $attribute->getArguments()[0];
218                            $mappedByProperty ??= 'ref_' . $classType->getName();
219                        } else {
220                            $targetEntity = $attribute->getArguments()[0];
221                            $mappedByProperty = 'ref_' . $classType->getName();
222                        }
223                        $indexByProperty = $generatedCodeContext->findIndexProperty($targetEntity);
224                        if ($indexByProperty) {
225                            $property->addAttribute(OrderBy::class, [[$indexByProperty => 'ASC']]);
226                        }
227                        $property->addAttribute(
228                            OneToMany::class,
229                            [
230                                'cascade' => ['all'],
231                                'targetEntity' => $targetEntity,
232                                'mappedBy' => $mappedByProperty,
233                                'fetch' => 'EAGER',
234                                'indexBy' => $indexByProperty,
235                                'orphanRemoval' => true,
236                            ]
237                        );
238
239                        break;
240                    case OneToOneAttribute::class:
241                        $added = true;
242                        $targetEntity = $property->getType();
243                        // look for @ParentAttribute for inversedBy?
244                        $property->addAttribute(
245                            OneToOne::class,
246                            [
247                                'cascade' => ['all'],
248                                'targetEntity' => $targetEntity,
249                                'fetch' => 'EAGER',
250                                'orphanRemoval' => true,
251                            ]
252                        );
253                        break;
254                    case GetSearchIndexAttribute::class:
255                        $added = true;
256                        $property->setType(Collection::class);
257                        $searchTableName = strpos($classType->getName(), 'apie_resource__') === 0
258                            ? preg_replace('/^apie_resource__/', 'apie_index__', $classType->getName())
259                            : 'apie_index__' . $classType->getName();
260                        $searchTableName .= '_' . $property->getName();
261                        $searchTable = SearchIndex::createFor(
262                            $searchTableName,
263                            $classType->getName(),
264                            $property->getName(),
265                        );
266                        $generatedCodeContext->generatedCode->generatedCodeHashmap[$searchTableName] = $searchTable;
267                        $property->addAttribute(
268                            OneToMany::class,
269                            [
270                                'cascade' => ['all'],
271                                'targetEntity' => $searchTableName,
272                                'mappedBy' => 'parent',
273                                'orphanRemoval' => true,
274                            ]
275                        );
276                        $args = $attribute->getArguments();
277                        $args['arrayValueType'] = $searchTableName;
278                        // there is no good method in nette/php-generator
279                        (new ReflectionProperty(Attribute::class, 'args'))->setValue($attribute, $args);
280                        $type = $property->getType();
281                        break;
282                    case OrderAttribute::class:
283                        $added = true;
284                        $type = 'text';
285                        if ($property->getType() === 'int') {
286                            $type = 'integer';
287                        }
288                        $property->addAttribute(Column::class, ['type' => $type]);
289                        break;
290                    case ParentAttribute::class:
291                        $added = true;
292                        $inversedBy = $generatedCodeContext->findInverseProperty($property->getType(), $classType->getName());
293                        $property->addAttribute(
294                            ManyToOne::class,
295                            ['targetEntity' => $property->getType(), 'inversedBy' => $inversedBy]
296                        );
297                        $property->addAttribute(
298                            JoinColumn::class,
299                            [
300                                'onDelete' => 'CASCADE',
301                            ]
302                        );
303                        break;
304                }
305            }
306            if (!$added) {
307                $type = $property->getType();
308                switch ((string) $type) {
309                    case 'string':
310                        $property->addAttribute(Column::class, ['type' => 'text', 'nullable' => $property->isNullable()]);
311                        break;
312                    case 'float':
313                        $property->addAttribute(Column::class, ['type' => 'float', 'nullable' => $property->isNullable()]);
314                        break;
315                    case 'int':
316                    case '?int':
317                        $property->addAttribute(Column::class, ['type' => 'integer', 'nullable' => $property->isNullable()]);
318                        break;
319                    case 'array':
320                    case '?array':
321                        $property->addAttribute(Column::class, ['type' => 'json', 'nullable' => $property->isNullable()]);
322                        break;
323                }
324            }
325        }
326
327        $this->applyId($classType);
328    }
329}