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