Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.70% covered (success)
90.70%
78 / 86
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
SimplePropertiesCodeGenerator
90.70% covered (success)
90.70%
78 / 86
33.33% covered (danger)
33.33%
1 / 3
28.63
0.00% covered (danger)
0.00%
0 / 1
 boot
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 run
90.70% covered (success)
90.70%
39 / 43
0.00% covered (danger)
0.00%
0 / 1
16.21
 allowsLargeStrings
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
11.64
1<?php
2namespace Apie\StorageMetadataBuilder\CodeGenerators;
3
4use Apie\Core\Attributes\StoreOptions;
5use Apie\Core\Context\ApieContext;
6use Apie\Core\Enums\ScalarType;
7use Apie\Core\Identifiers\KebabCaseSlug;
8use Apie\Core\Metadata\Fields\FieldInterface;
9use Apie\Core\Metadata\MetadataFactory;
10use Apie\Core\RegexUtils;
11use Apie\Core\Utils\ConverterUtils;
12use Apie\Core\ValueObjects\Interfaces\AllowsLargeStringsInterface;
13use Apie\Core\ValueObjects\Interfaces\HasRegexValueObjectInterface;
14use Apie\Core\ValueObjects\Interfaces\LengthConstraintStringValueObjectInterface;
15use Apie\Core\ValueObjects\IsPasswordValueObject;
16use Apie\StorageMetadata\Attributes\OneToOneAttribute;
17use Apie\StorageMetadata\Attributes\PropertyAttribute;
18use Apie\StorageMetadataBuilder\Interfaces\BootGeneratedCodeInterface;
19use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
20use Apie\StorageMetadataBuilder\Interfaces\RunGeneratedCodeContextInterface;
21use Apie\StorageMetadataBuilder\Mediators\GeneratedCode;
22use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
23use Apie\TypeConverter\ReflectionTypeFactory;
24use DateTime;
25use DateTimeImmutable;
26use DateTimeInterface;
27use Nette\PhpGenerator\ClassType;
28use Nette\PhpGenerator\Parameter;
29use Psr\Http\Message\UploadedFileInterface;
30use ReflectionProperty;
31
32/**
33 * Maps simple properties that require no additional tables.
34 * - create apie_mixed_data table that can story any PHP object (which is used as a fallback)
35 * - create for a domain object property a property in the database table.
36 * - if this can be mapped to a scalar, it will be a regular property, else it will be mapped to apie_mixed_data as one to many
37 */
38final class SimplePropertiesCodeGenerator implements RunGeneratedCodeContextInterface, BootGeneratedCodeInterface
39{
40    public function boot(GeneratedCode $generatedCode): void
41    {
42        $mixedData = new ClassType('apie_mixed_data');
43        $mixedData->addImplement(MixedStorageInterface::class);
44        $mixedData->addProperty('serializedString')->setType('string')->setNullable(true);
45        $mixedData->addProperty('originalType')->setType('string')->setNullable(true);
46        $mixedData->addProperty('unserializedObject')->setType('mixed');
47        $mixedData->addMethod('__construct')->setParameters([new Parameter('input')])
48            ->setBody(
49                '$this->unserializedObject = $input;'
50                . PHP_EOL
51                . '$this->serializedString = serialize($input);'
52                . PHP_EOL
53                . '$this->originalType = get_debug_type($input);'
54            );
55        $mixedData->addMethod('toOriginalObject')
56            ->setReturnType('mixed')
57            ->setBody(
58                'if (!isset($this->unserializedObject)) {
59    $this->unserializedObject = unserialize($this->serializedString);
60    if (get_debug_type($this->unserializedObject) !== $this->originalType) {
61        throw new \LogicException("Could not unserialize object again");
62    }
63}
64return $this->unserializedObject;'
65            );
66
67        $generatedCode->generatedCodeHashmap['apie_mixed_data'] = $mixedData;
68    }
69
70    public function run(GeneratedCodeContext $generatedCodeContext): void
71    {
72        $property = $generatedCodeContext->getCurrentProperty();
73        $table = $generatedCodeContext->getCurrentTable();
74        if ($property === null || $table === null) {
75            return;
76        }
77        $propertyName = 'apie_'
78            . str_replace('-', '_', (string) KebabCaseSlug::fromClass($property->getDeclaringClass()))
79            . '_'
80            . str_replace('-', '_', (string) KebabCaseSlug::fromClass($property));
81        $metadata = MetadataFactory::getMetadataStrategyForType(
82            $property->getType() ?? ReflectionTypeFactory::createReflectionType('mixed')
83        )->getResultMetadata(new ApieContext());
84        $scalar = $metadata->toScalarType(true);
85    
86        if ($table->hasProperty($propertyName)) {
87            return;
88        }
89        $nullable = (($metadata instanceof FieldInterface ? $metadata->allowsNull() : false) ? '?' : '');
90        $nullable = '?';
91        foreach ($property->getAttributes(StoreOptions::class) as $attribute) {
92            $options = $attribute->newInstance();
93            if ($options->alwaysMixedData) {
94                $scalar = ScalarType::MIXED;
95            }
96        }
97        $declaredProperty = $table->addProperty($propertyName)
98            ->setType($nullable . $scalar->value);
99        if ($scalar === ScalarType::STRING && in_array((string) $property->getType(), [DateTimeInterface::class, DateTimeImmutable::class, DateTime::class])) {
100            $declaredProperty->setType($nullable . DateTimeImmutable::class);
101        }
102        if (in_array((string) $property->getType(), [UploadedFileInterface::class])) {
103            $declaredProperty->setType($nullable . 'string');
104            $scalar = ScalarType::STRING;
105        }
106        switch ($scalar) {
107            case ScalarType::ARRAY:
108            case ScalarType::STDCLASS:
109            case ScalarType::MIXED:
110                $declaredProperty->setType($nullable . 'apie_mixed_data')->addAttribute(OneToOneAttribute::class, [$property->name, $property->getDeclaringClass()->name]);
111                break;
112            case ScalarType::NULLVALUE:
113                $declaredProperty->setType(null)
114                    ->setValue(null); // fallthrough
115                // no break
116            default:
117                $declaredProperty->addAttribute(
118                    PropertyAttribute::class,
119                    [
120                        $property->name,
121                        $property->getDeclaringClass()->name,
122                        $this->allowsLargeStrings($property)
123                    ]
124                );
125        }
126    }
127
128    private function allowsLargeStrings(ReflectionProperty $property): bool
129    {
130        $propertyTypeAsString = ltrim((string) $property->getType(), '?');
131        if ('string' === $propertyTypeAsString) {
132            return true;
133        }
134        if (in_array($propertyTypeAsString, [UploadedFileInterface::class])) {
135            return true;
136        }
137        $class = ConverterUtils::toReflectionClass($property);
138        if (!$class) {
139            return false;
140        }
141        $interfaceNames = $class->getInterfaceNames();
142        
143        if (in_array(AllowsLargeStringsInterface::class, $interfaceNames)
144            || in_array(UploadedFileInterface::class, $interfaceNames)) {
145            return true;
146        }
147        if (in_array(LengthConstraintStringValueObjectInterface::class, $interfaceNames)) {
148            $maxLength = $class->getMethod('maxStringLength')->invoke(null);
149            return $maxLength > 127 || $maxLength === null;
150        }
151        if (in_array(IsPasswordValueObject::class, $class->getTraitNames())) {
152            $maxLength = $class->getMethod('getMaxLength')->invoke(null);
153            return $maxLength > 127;
154        }
155        if (in_array(HasRegexValueObjectInterface::class, $interfaceNames)) {
156            $regex = $class->getMethod('getRegularExpression')->invoke(null);
157            $maxLength = RegexUtils::getMaximumAcceptedStringLengthOfRegularExpression($regex, true);
158            return $maxLength > 127 || $maxLength === null;
159        }
160        return false;
161    }
162}