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