Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.41% covered (warning)
72.41%
63 / 87
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
OneToManyAttributeConverter
72.41% covered (warning)
72.41%
63 / 87
33.33% covered (danger)
33.33%
1 / 3
48.89
0.00% covered (danger)
0.00%
0 / 1
 applyToDomain
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
11.04
 toReflClass
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 applyToStorage
60.00% covered (warning)
60.00%
33 / 55
0.00% covered (danger)
0.00%
0 / 1
32.38
1<?php
2namespace Apie\StorageMetadata\PropertyConverters;
3
4use Apie\Core\ValueObjects\Utils;
5use Apie\StorageMetadata\Attributes\OneToManyAttribute;
6use Apie\StorageMetadata\Interfaces\PropertyConverterInterface;
7use Apie\StorageMetadata\Interfaces\StorageDtoInterface;
8use Apie\StorageMetadata\Mediators\DomainToStorageContext;
9use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
10use Apie\TypeConverter\ReflectionTypeFactory;
11use Doctrine\Common\Collections\ArrayCollection;
12use Doctrine\ORM\PersistentCollection;
13use ReflectionClass;
14use Throwable;
15
16class OneToManyAttributeConverter implements PropertyConverterInterface
17{
18    public function applyToDomain(
19        DomainToStorageContext $context
20    ): void {
21        foreach ($context->storageProperty->getAttributes(OneToManyAttribute::class) as $oneToManyAttribute) {
22            $oneToManyInfo = $oneToManyAttribute->newInstance();
23            $domainProperty = $oneToManyInfo->getReflectionProperty($context->domainClass, $context->domainObject);
24            if ($domainProperty) {
25                $storagePropertyValue = $context->getStoragePropertyValue();
26                if ($oneToManyInfo->nullableField) {
27                    $fieldName = $oneToManyInfo->nullableField;
28                    if ($context->storageObject->{$fieldName}) {
29                        $domainProperty->setValue($context->domainObject, null);
30                        return;
31                    }
32                }
33                $storagePropertyValue = Utils::toArray($storagePropertyValue);
34                $domainPropertyType = $domainProperty->getType();
35                $domainProperties = $domainProperty->isInitialized($context->domainObject)
36                    ? Utils::toArray($domainProperty->getValue($context->domainObject) ?? [])
37                    : [];
38                foreach ($storagePropertyValue as $arrayKey => $arrayValue) {
39                    if ($arrayValue instanceof MixedStorageInterface) {
40                        $domainProperties[$arrayKey] = $arrayValue->toOriginalObject();
41                    } elseif ($arrayValue instanceof StorageDtoInterface && isset($domainProperties[$arrayKey])) {
42                        $context->domainToStorageConverter->injectExistingDomainObject(
43                            $domainProperties[$arrayKey],
44                            $arrayValue,
45                            $context
46                        );
47                    } else {
48                        $domainProperties[$arrayKey] = $arrayValue instanceof StorageDtoInterface
49                            ? $context->domainToStorageConverter->createDomainObject($arrayValue, $context)
50                            : $context->dynamicCast($arrayValue, ReflectionTypeFactory::createReflectionType($oneToManyAttribute->newInstance()->declaredClass));
51                    }
52                }
53                $domainProperty->setValue($context->domainObject, $context->dynamicCast($domainProperties, $domainPropertyType));
54            }
55        }
56    }
57
58    /**
59     * @template T of object
60     * @param class-string<T> $className
61     * @return ReflectionClass<T>
62     */
63    private function toReflClass(string $className, mixed $contextStorageObject): ReflectionClass
64    {
65        if (str_starts_with($className, 'apie_') && is_object($contextStorageObject)) {
66            $refl = new ReflectionClass($contextStorageObject);
67            // @phpstan-ignore-next-line return.type
68            return new ReflectionClass($refl->getNamespaceName() . '\\' . $className);
69        }
70        return new ReflectionClass($className);
71    }
72
73    public function applyToStorage(
74        DomainToStorageContext $context
75    ): void {
76        foreach ($context->storageProperty->getAttributes(OneToManyAttribute::class) as $oneToManyAttribute) {
77            $oneToManyInfo = $oneToManyAttribute->newInstance();
78            $domainProperty = $oneToManyInfo->getReflectionProperty($context->domainClass, $context->domainObject);
79            if ($domainProperty) {
80                $domainPropertyValue = $domainProperty->getValue($context->domainObject);
81                if ($oneToManyInfo->nullableField) {
82                    $fieldName = $oneToManyInfo->nullableField;
83                    $context->storageObject->{$fieldName} = $domainPropertyValue === null;
84                }
85                $domainPropertyValue = Utils::toArray($domainPropertyValue ?? []);
86                $storageShouldBeReplaced = !$context->storageProperty->isInitialized($context->storageObject);
87                $storageProperties = $storageShouldBeReplaced
88                    ? []
89                    : $context->storageProperty->getValue($context->storageObject);
90                $keysToRemove = array_diff(
91                    array_keys(Utils::toArray($storageProperties)),
92                    array_keys($domainPropertyValue),
93                );
94                try {
95                    foreach ($keysToRemove as $keyToRemove) {
96                        unset($storageProperties[$keyToRemove]);
97                        // this is an edge case where we have some immutable item list object.
98                        if (isset($storageProperties[$keyToRemove])) {
99                            $storageProperties = $context->dynamicCast([], $context->storageProperty->getType());
100                            $storageShouldBeReplaced = true;
101                            break;
102                        }
103                    }
104                } catch (Throwable) {
105                    // another edge case where an array object class throws an exception.
106                    $storageProperties = $context->dynamicCast([], $context->storageProperty->getType());
107                    $storageShouldBeReplaced = true;
108                }
109                foreach ($domainPropertyValue as $arrayKey => $arrayValue) {
110                    $arrayContext = $context->withArrayKey($arrayKey);
111                    $storageClassRefl = $this->toReflClass($oneToManyAttribute->newInstance()->storageClass, $context->storageObject);
112                    if (is_object($arrayValue) && in_array(StorageDtoInterface::class, $storageClassRefl->getInterfaceNames())) {
113                        if (isset($storageProperties[$arrayKey]) && $storageProperties[$arrayKey] instanceof StorageDtoInterface && !$storageShouldBeReplaced) {
114                            $arrayContext->domainToStorageConverter->injectExistingStorageObject(
115                                $arrayValue,
116                                $storageProperties[$arrayKey],
117                                $arrayContext
118                            );
119                        } else {
120                            $storageProperties[$arrayKey] = $arrayContext->domainToStorageConverter->createStorageObject(
121                                $arrayValue,
122                                $storageClassRefl,
123                                $arrayContext
124                            );
125                        }
126                    } else {
127                        //  if we do not do this hack we get cloned objects.
128                        if ($storageProperties instanceof PersistentCollection) {
129                            $storageProperties = new ArrayCollection(
130                                $storageProperties
131                                    ->map(function ($t) {
132                                        return clone $t;
133                                    })
134                                    ->toArray()
135                            );
136                            $storageShouldBeReplaced = true;
137                        }
138                        $storageProperties[$arrayKey] = $storageClassRefl->newInstance(Utils::toString($arrayValue));
139                        // @phpstan-ignore-next-line
140                        $storageProperties[$arrayKey]->listOrder = $context->dynamicCast($arrayKey, $storageClassRefl->getProperty('listOrder')->getType());
141                        // @phpstan-ignore-next-line
142                        $storageProperties[$arrayKey]->parent = $context->storageObject;
143                    }
144                }
145                if ($storageShouldBeReplaced) {
146                    $context->storageProperty->setValue($context->storageObject, $context->dynamicCast($storageProperties, $context->storageProperty->getType()));
147                }
148            }
149        }
150    }
151}