Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.12% covered (warning)
74.12%
63 / 85
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
OneToManyAttributeConverter
74.12% covered (warning)
74.12%
63 / 85
33.33% covered (danger)
33.33%
1 / 3
45.60
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
62.26% covered (warning)
62.26%
33 / 53
0.00% covered (danger)
0.00%
0 / 1
29.76
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            return new ReflectionClass($refl->getNamespaceName() . '\\' . $className);
68        }
69        return new ReflectionClass($className);
70    }
71
72    public function applyToStorage(
73        DomainToStorageContext $context
74    ): void {
75        foreach ($context->storageProperty->getAttributes(OneToManyAttribute::class) as $oneToManyAttribute) {
76            $oneToManyInfo = $oneToManyAttribute->newInstance();
77            $domainProperty = $oneToManyInfo->getReflectionProperty($context->domainClass, $context->domainObject);
78            if ($domainProperty) {
79                $domainPropertyValue = $domainProperty->getValue($context->domainObject);
80                if ($oneToManyInfo->nullableField) {
81                    $fieldName = $oneToManyInfo->nullableField;
82                    $context->storageObject->{$fieldName} = $domainPropertyValue === null;
83                }
84                $domainPropertyValue = Utils::toArray($domainPropertyValue ?? []);
85                $storageShouldBeReplaced = !$context->storageProperty->isInitialized($context->storageObject);
86                $storageProperties = $storageShouldBeReplaced
87                    ? []
88                    : $context->storageProperty->getValue($context->storageObject);
89                $keysToRemove = array_diff(
90                    array_keys(Utils::toArray($storageProperties)),
91                    array_keys($domainPropertyValue),
92                );
93                try {
94                    foreach ($keysToRemove as $keyToRemove) {
95                        unset($storageProperties[$keyToRemove]);
96                        // this is an edge case where we have some immutable item list object.
97                        if (isset($storageProperties[$keyToRemove])) {
98                            $storageProperties = $context->dynamicCast([], $context->storageProperty->getType());
99                            $storageShouldBeReplaced = true;
100                            break;
101                        }
102                    }
103                } catch (Throwable) {
104                    // another edge case where an array object class throws an exception.
105                    $storageProperties = $context->dynamicCast([], $context->storageProperty->getType());
106                    $storageShouldBeReplaced = true;
107                }
108                foreach ($domainPropertyValue as $arrayKey => $arrayValue) {
109                    $arrayContext = $context->withArrayKey($arrayKey);
110                    $storageClassRefl = $this->toReflClass($oneToManyAttribute->newInstance()->storageClass, $context->storageObject);
111                    if (is_object($arrayValue) && in_array(StorageDtoInterface::class, $storageClassRefl->getInterfaceNames())) {
112                        if (isset($storageProperties[$arrayKey]) && $storageProperties[$arrayKey] instanceof StorageDtoInterface && !$storageShouldBeReplaced) {
113                            $arrayContext->domainToStorageConverter->injectExistingStorageObject(
114                                $arrayValue,
115                                $storageProperties[$arrayKey],
116                                $arrayContext
117                            );
118                        } else {
119                            $storageProperties[$arrayKey] = $arrayContext->domainToStorageConverter->createStorageObject(
120                                $arrayValue,
121                                $storageClassRefl,
122                                $arrayContext
123                            );
124                        }
125                    } else {
126                        //  if we do not do this hack we get cloned objects.
127                        if ($storageProperties instanceof PersistentCollection) {
128                            $storageProperties = new ArrayCollection(
129                                $storageProperties
130                                    ->map(function ($t) { return clone $t; })
131                                    ->toArray()
132                            );
133                            $storageShouldBeReplaced = true;
134                        }
135                        $storageProperties[$arrayKey] = $storageClassRefl->newInstance(Utils::toString($arrayValue));
136                        // @phpstan-ignore-next-line
137                        $storageProperties[$arrayKey]->listOrder = $context->dynamicCast($arrayKey, $storageClassRefl->getProperty('listOrder')->getType());
138                        // @phpstan-ignore-next-line
139                        $storageProperties[$arrayKey]->parent = $context->storageObject;
140                    }
141                }
142                if ($storageShouldBeReplaced) {
143                    $context->storageProperty->setValue($context->storageObject, $context->dynamicCast($storageProperties, $context->storageProperty->getType()));
144                }
145            }
146        }
147    }
148}