Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.00% covered (success)
98.00%
98 / 100
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ItemListCodeGenerator
98.00% covered (success)
98.00%
98 / 100
66.67% covered (warning)
66.67%
4 / 6
43
0.00% covered (danger)
0.00%
0 / 1
 isValidList
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 isNotHashmap
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 isSet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getEntityClass
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 run
98.59% covered (success)
98.59%
70 / 71
0.00% covered (danger)
0.00%
0 / 1
22
 finetune
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2namespace Apie\StorageMetadataBuilder\CodeGenerators;
3
4use Apie\Core\Attributes\StoreOptions;
5use Apie\Core\Context\ApieContext;
6use Apie\Core\Entities\EntityInterface;
7use Apie\Core\Enums\ScalarType;
8use Apie\Core\Identifiers\KebabCaseSlug;
9use Apie\Core\Lists\ItemSet;
10use Apie\Core\Metadata\ItemHashmapMetadata;
11use Apie\Core\Metadata\ItemListMetadata;
12use Apie\Core\Metadata\MetadataFactory;
13use Apie\Core\Metadata\MetadataInterface;
14use Apie\Core\Metadata\UnionTypeMetadata;
15use Apie\Core\Utils\ConverterUtils;
16use Apie\StorageMetadata\Attributes\ManyToManyAttribute;
17use Apie\StorageMetadata\Attributes\OneToManyAttribute;
18use Apie\StorageMetadata\Attributes\OrderAttribute;
19use Apie\StorageMetadata\Attributes\ParentAttribute;
20use Apie\StorageMetadata\Interfaces\OwnerLocalEntityStorageInterface;
21use Apie\StorageMetadataBuilder\Factories\ClassTypeFactory;
22use Apie\StorageMetadataBuilder\Interfaces\RunGeneratedCodeContextInterface;
23use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
24use ReflectionClass;
25use ReflectionType;
26
27/**
28 * Creates storage mappings for typed lists and hashmaps.
29 * - primitive and value-object items use a one-to-many child table
30 * - entity items in lists and hashmaps use owner-local many-to-many entities
31 * - an 'order' property stores list positions and hashmap keys.
32 */
33final class ItemListCodeGenerator implements RunGeneratedCodeContextInterface
34{
35    private function isValidList(MetadataInterface $metadata): bool
36    {
37        if ($metadata instanceof ItemListMetadata || $metadata instanceof ItemHashmapMetadata) {
38            return true;
39        }
40        if ($metadata instanceof UnionTypeMetadata) {
41            $sub = $metadata->toSkipNull();
42            if ($sub instanceof ItemListMetadata || $sub instanceof ItemHashmapMetadata) {
43                return true;
44            }
45        }
46
47        return false;
48    }
49
50    private function isNotHashmap(MetadataInterface $metadata): bool
51    {
52        if ($metadata instanceof ItemListMetadata) {
53            return true;
54        }
55        if ($metadata instanceof ItemHashmapMetadata) {
56            return false;
57        }
58        if ($metadata instanceof UnionTypeMetadata) {
59            $sub = $metadata->toSkipNull();
60            if ($sub instanceof ItemHashmapMetadata) {
61                return false;
62            }
63        }
64
65        return true;
66    }
67
68    private function isSet(MetadataInterface $metadata): bool
69    {
70        if ($metadata instanceof ItemListMetadata) {
71            return $metadata->toClass()->isSubclassOf(ItemSet::class);
72        }
73        if ($metadata instanceof UnionTypeMetadata) {
74            return $this->isSet($metadata->toSkipNull());
75        }
76        return false;
77    }
78
79    private function getEntityClass(ReflectionType $type): ?ReflectionClass
80    {
81        $class = ConverterUtils::toReflectionClass($type);
82        return $class && $class->implementsInterface(EntityInterface::class) ? $class : null;
83    }
84
85    public function run(GeneratedCodeContext $generatedCodeContext): void
86    {
87        $property = $generatedCodeContext->getCurrentProperty();
88        $class = $property ? ConverterUtils::toReflectionClass($property) : null;
89        $currentTable = $generatedCodeContext->getCurrentTable();
90        if (null === $class || null === $currentTable) {
91            return;
92        }
93        $metadata = MetadataFactory::getMetadataStrategyForType($property->getType())
94            ->getResultMetadata(new ApieContext());
95        $propertyName = 'apie_'
96            . str_replace('-', '_', (string) KebabCaseSlug::fromClass($property->getDeclaringClass()))
97            . '_'
98            . str_replace('-', '_', (string) KebabCaseSlug::fromClass($property));
99        if ($currentTable->hasProperty($propertyName)) {
100            return;
101        }
102        if ($this->isValidList($metadata)) {
103            $nullableFieldName = null;
104            $mutableListFieldName = null;
105            foreach ($property->getAttributes(StoreOptions::class) as $attribute) {
106                $options = $attribute->newInstance();
107                if ($options->mutableListField) {
108                    $mutableListFieldName = preg_replace('/^apie_/', 'mute_', $propertyName);
109                }
110            }
111            if ($property->getType() === null || $property->getType()->allowsNull()) {
112                $nullableFieldName = preg_replace('/^apie_/', 'null_', $propertyName);
113            }
114            $tableName = $generatedCodeContext->getPrefix('apie_resource_');
115            $arrayType = $class->getMethod('offsetGet')->getReturnType();
116            $scalar = MetadataFactory::getScalarForType($arrayType, $arrayType->allowsNull());
117            if (!$arrayType || 'mixed' === (string) $arrayType) {
118                return;
119            }
120            $arrayClass = $arrayType ? ConverterUtils::toReflectionClass($arrayType) : null;
121            $entityClass = $arrayType ? $this->getEntityClass($arrayType) : null;
122            $useManyToMany = $entityClass !== null && !$this->isSet($metadata);
123            $table = in_array($scalar, ScalarType::PRIMITIVES)
124                ? ClassTypeFactory::createPrimitiveTable($tableName, $scalar->toReflectionType())
125                : ClassTypeFactory::createStorageTable($tableName, $arrayClass);
126            if ($useManyToMany) {
127                $domainId = $table->getProperty('id')->cloneWithName('domainId');
128                $table->removeProperty('id');
129                $table->addMember($domainId);
130                $table->addImplement(OwnerLocalEntityStorageInterface::class);
131                $table->addMethod('getKey')
132                    ->setReturnType('?int')
133                    ->setBody('return $this->id;');
134            } else {
135                $table->addProperty('parent')
136                    ->setType($currentTable->getName())
137                    ->addAttribute(ParentAttribute::class);
138            }
139            $table->addProperty('listOrder')
140                ->setType($this->isNotHashmap($metadata) ? 'int' : 'string')
141                ->addAttribute(OrderAttribute::class);
142            if (in_array($scalar, ScalarType::PRIMITIVES)) {
143                $generatedCodeContext->generatedCode->generatedCodeHashmap[$tableName] = $table;
144            } else {
145                $generatedCodeContext->withCurrentObject($arrayClass)->iterateOverTable($table);
146            }
147            if ($nullableFieldName) {
148                $currentTable->addProperty($nullableFieldName)
149                    ->setType('bool')
150                    ->setValue(true);
151            }
152            if ($mutableListFieldName) {
153                $currentTable->addProperty($mutableListFieldName)
154                    ->setType('bool')
155                    ->setValue(true);
156            }
157            $currentTable->addProperty($propertyName)
158                ->addAttribute(
159                    $useManyToMany ? ManyToManyAttribute::class : OneToManyAttribute::class,
160                    $this->finetune(
161                        [
162                            $property->name,
163                            $tableName,
164                            $property->getDeclaringClass()->name,
165                            $nullableFieldName,
166                            $mutableListFieldName
167                        ]
168                    )
169                );
170        }
171    }
172
173    /**
174     * @param array<int, string> $args
175     * @return array<int, string>
176     */
177    private function finetune(array $args): array
178    {
179        while (true) {
180            if (count($args) === 0) {
181                return [];
182            }
183            if ($args[count($args) - 1] !== null) {
184                return $args;
185            };
186            array_pop($args);
187        }
188    }
189}