Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.11% covered (success)
90.11%
82 / 91
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
SimplePropertiesCodeGenerator
90.11% covered (success)
90.11%
82 / 91
33.33% covered (danger)
33.33%
1 / 3
31.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
90.70% covered (success)
90.70%
39 / 43
0.00% covered (danger)
0.00%
0 / 1
16.21
 allowsLargeStrings
82.14% covered (warning)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
15.12
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 = 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 = 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        }
98        $declaredProperty = $table->addProperty($propertyName)
99            ->setType($nullable . $scalar->value);
100        if ($scalar === ScalarType::STRING && in_array((string) $property->getType(), [DateTimeInterface::class, DateTimeImmutable::class, DateTime::class])) {
101            $declaredProperty->setType($nullable . DateTimeImmutable::class);
102        }
103        if (in_array((string) $property->getType(), [UploadedFileInterface::class])) {
104            $declaredProperty->setType($nullable . 'string');
105            $scalar = ScalarType::STRING;
106        }
107        switch ($scalar) {
108            case ScalarType::ARRAY:
109            case ScalarType::STDCLASS:
110            case ScalarType::MIXED:
111                $declaredProperty->setType($nullable . 'apie_mixed_data')->addAttribute(OneToOneAttribute::class, [$property->name, $property->getDeclaringClass()->name]);
112                break;
113            case ScalarType::NULLVALUE:
114                $declaredProperty->setType(null)
115                    ->setValue(null); // fallthrough
116                // no break
117            default:
118                $declaredProperty->addAttribute(
119                    PropertyAttribute::class,
120                    [
121                        $property->name,
122                        $property->getDeclaringClass()->name,
123                        $this->allowsLargeStrings($property)
124                    ]
125                );
126        }
127    }
128
129    private function allowsLargeStrings(ReflectionProperty $property): bool
130    {
131        $propertyTypeAsString = ltrim((string) $property->getType(), '?');
132        if ('string' === $propertyTypeAsString) {
133            return true;
134        }
135        if (in_array($propertyTypeAsString, [UploadedFileInterface::class])) {
136            return true;
137        }
138        $class = ConverterUtils::toReflectionClass($property);
139        if (!$class) {
140            return false;
141        }
142        $interfaceNames = $class->getInterfaceNames();
143        
144        if (in_array(AllowsLargeStringsInterface::class, $interfaceNames)
145            || in_array(UploadedFileInterface::class, $interfaceNames)) {
146            return true;
147        }
148        if (in_array(LengthConstraintStringValueObjectInterface::class, $interfaceNames)) {
149            $maxLength = $class->getMethod('maxStringLength')->invoke(null);
150            return $maxLength > 127 || $maxLength === null;
151        }
152        if (in_array(IsPasswordValueObject::class, $class->getTraitNames())) {
153            $maxLength = $class->getMethod('getMaxLength')->invoke(null);
154            return $maxLength > 127;
155        }
156        if (in_array(HasRegexValueObjectInterface::class, $interfaceNames)) {
157            $regex = $class->getMethod('getRegularExpression')->invoke(null);
158            $maxLength = RegexUtils::getMaximumAcceptedStringLengthOfRegularExpression($regex, true);
159            return $maxLength > 127 || $maxLength === null;
160        }
161        if (in_array(LimitedOptionsInterface::class, $interfaceNames)) {
162            $options = $class->getMethod('getOptions')->invoke(null);
163            foreach ($options as $option) {
164                if (strlen($option) > 127) {
165                    return true;
166                }
167            }
168        }
169        return false;
170    }
171}