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