HydratorCollectionStrategy   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 111
c 2
b 0
f 0
dl 0
loc 262
rs 9.84
wmc 32

12 Methods

Rating   Name   Duplication   Size   Complexity  
B prepareCollectionValues() 0 28 7
A getById() 0 28 3
A resolveEntityCollection() 0 19 4
A createArrayCollection() 0 3 1
A compareEntities() 0 3 1
B hydrate() 0 49 7
A isInitialized() 0 18 2
A __construct() 0 6 1
A createInstance() 0 19 3
A setObject() 0 3 1
A extract() 0 3 1
A getObject() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\LaminasDoctrine\Hydrator\Strategy;
6
7
use Arp\Entity\EntityInterface;
8
use Arp\LaminasDoctrine\Hydrator\Strategy\Exception\RuntimeException;
9
use Arp\LaminasDoctrine\Repository\EntityRepositoryInterface;
10
use Doctrine\Common\Collections\ArrayCollection;
11
use Doctrine\Common\Collections\Collection;
12
use Laminas\Hydrator\HydratorInterface;
13
use Laminas\Hydrator\Strategy\Exception\InvalidArgumentException;
14
15
class HydratorCollectionStrategy extends AbstractHydratorStrategy implements HydrationObjectAwareInterface
16
{
17
    private string $name;
18
19
    private HydratorInterface $hydrator;
20
21
    private ?object $object;
22
23
    /**
24
     * @param string $name
25
     * @param EntityRepositoryInterface<EntityInterface> $repository
26
     * @param HydratorInterface $hydrator
27
     */
28
    public function __construct(string $name, EntityRepositoryInterface $repository, HydratorInterface $hydrator)
29
    {
30
        parent::__construct($repository);
31
32
        $this->name = $name;
33
        $this->hydrator = $hydrator;
34
    }
35
36
    /**
37
     * @param mixed $value
38
     * @param array<string, mixed>|null $data
39
     *
40
     * @return iterable<int, EntityInterface>
41
     *
42
     * @throws InvalidArgumentException
43
     * @throws RuntimeException
44
     */
45
    public function hydrate($value, ?array $data): iterable
46
    {
47
        $entityName = $this->repository->getClassName();
48
        $object = $this->getObject();
49
50
        if (null === $object) {
51
            throw new InvalidArgumentException(
52
                sprintf('The hydration object has not been set for strategy \'%s\'', static::class)
53
            );
54
        }
55
56
        if (!is_iterable($value)) {
57
            throw new InvalidArgumentException(
58
                sprintf(
59
                    'The \'value\' argument must be of type \'iterable\'; \'%s\' provided for entity \'%s\'',
60
                    gettype($value),
61
                    $entityName
62
                )
63
            );
64
        }
65
66
        $addMethodName = 'add' . ucfirst($this->name);
67
        if (!is_callable([$object, $addMethodName])) {
68
            throw new InvalidArgumentException(
69
                sprintf('The \'%s\' method is not callable for entity \'%s\'', $addMethodName, $entityName)
70
            );
71
        }
72
73
        $removeMethodName = 'remove' . ucfirst($this->name);
74
        if (!is_callable([$object, $addMethodName])) {
75
            throw new InvalidArgumentException(
76
                sprintf('The \'%s\' method is not callable for entity \'%s\'', $removeMethodName, $entityName)
77
            );
78
        }
79
80
        $collection = $this->resolveEntityCollection($object);
81
        $values = $this->prepareCollectionValues($entityName, $value);
82
83
        $toAdd = $this->createArrayCollection(array_udiff($values, $collection, [$this, 'compareEntities']));
84
        if (!$toAdd->isEmpty()) {
85
            $object->$addMethodName($toAdd);
86
        }
87
88
        $toRemove = $this->createArrayCollection(array_udiff($collection, $values, [$this, 'compareEntities']));
89
        if (!$toRemove->isEmpty()) {
90
            $object->$removeMethodName($toRemove);
91
        }
92
93
        return $this->resolveEntityCollection($object);
94
    }
95
96
    /**
97
     * @return EntityInterface[]
98
     *
99
     * @throws InvalidArgumentException
100
     */
101
    private function resolveEntityCollection(object $object): array
102
    {
103
        $methodName = 'get' . ucfirst($this->name);
104
        if (!is_callable([$object, $methodName])) {
105
            throw new InvalidArgumentException(
106
                sprintf('The method \'%s\' is not callable for entity \'%s\'', $methodName, get_class($object))
107
            );
108
        }
109
110
        if (!$this->isInitialized($object)) {
111
            return [];
112
        }
113
114
        $collection = $object->$methodName();
115
        if ($collection instanceof Collection) {
116
            $collection = $collection->toArray();
117
        }
118
119
        return $collection;
120
    }
121
122
    /**
123
     * @throws InvalidArgumentException
124
     */
125
    private function isInitialized(object $object): bool
126
    {
127
        try {
128
            $reflectionProperty = new \ReflectionProperty(get_class($object), $this->name);
129
        } catch (\Throwable $e) {
130
            throw new InvalidArgumentException(
131
                sprintf(
132
                    'Failed to create reflection property \'%s::%s\': %s',
133
                    get_class($object),
134
                    $this->name,
135
                    $e->getMessage()
136
                ),
137
                $e->getCode(),
138
                $e
139
            );
140
        }
141
142
        return $reflectionProperty->isInitialized($object);
143
    }
144
145
    /**
146
     * @param iterable<int, EntityInterface|int|string|array<mixed>> $value
147
     *
148
     * @return array<int, EntityInterface>
149
     *
150
     * @throws InvalidArgumentException
151
     * @throws RuntimeException
152
     */
153
    private function prepareCollectionValues(string $entityName, iterable $value): array
154
    {
155
        $collection = [];
156
        foreach ($value as $item) {
157
            if ($item instanceof EntityInterface) {
158
                $collection[] = $collection;
159
                continue;
160
            }
161
162
            if (empty($item)) {
163
                $collection[] = null;
164
                continue;
165
            }
166
167
            $id = $this->resolveId($item);
168
169
            $entity = empty($id)
170
                ? $this->createInstance($entityName)
171
                : $this->getById($entityName, $id);
172
173
            $collection[] = is_array($item)
174
                ? $this->hydrator->hydrate($item, $entity)
175
                : $entity;
176
        }
177
178
        return array_filter(
179
            $collection,
180
            static fn ($item) => (isset($item) && $item instanceof EntityInterface)
181
        );
182
    }
183
184
    /**
185
     * @throws RuntimeException
186
     */
187
    private function createInstance(string $entityName): object
188
    {
189
        if (!class_exists($entityName, true)) {
190
            throw new RuntimeException(
191
                sprintf(
192
                    'The hydrator was unable to create a reflection instance for class \'%s\': %s',
193
                    'The class could not be found',
194
                    $entityName,
195
                )
196
            );
197
        }
198
199
        try {
200
            return (new \ReflectionClass($entityName))->newInstanceWithoutConstructor();
201
        } catch (\ReflectionException $e) {
202
            throw new RuntimeException(
203
                sprintf('The reflection class \'%s\' could not be created: %s', $entityName, $e->getMessage()),
204
                $e->getCode(),
205
                $e
206
            );
207
        }
208
    }
209
210
    /**
211
     * @throws InvalidArgumentException
212
     * @throws RuntimeException
213
     */
214
    private function getById(string $entityName, int|string $id): object
215
    {
216
        try {
217
            $entity = $this->repository->find($id);
218
        } catch (\Exception $e) {
219
            throw new RuntimeException(
220
                sprintf(
221
                    'Collection item of type \'%s\', with id \'%d\' could not be found: %s',
222
                    $entityName,
223
                    $id,
224
                    $e->getMessage()
225
                ),
226
                $e->getCode(),
227
                $e
228
            );
229
        }
230
231
        if (null === $entity) {
232
            throw new InvalidArgumentException(
233
                sprintf(
234
                    'Collection item of type \'%s\' with id \'%d\' could not be found',
235
                    $entityName,
236
                    $id
237
                )
238
            );
239
        }
240
241
        return $entity;
242
    }
243
244
    /**
245
     * @param array<int, EntityInterface> $items
246
     *
247
     * @return ArrayCollection<int, EntityInterface>
248
     */
249
    private function createArrayCollection(array $items): ArrayCollection
250
    {
251
        return new ArrayCollection($items);
252
    }
253
254
    private function compareEntities(EntityInterface $a, EntityInterface $b): int
255
    {
256
        return strcmp(spl_object_hash($a), spl_object_hash($b));
257
    }
258
259
    /**
260
     * @param mixed $value
261
     *
262
     * @return iterable<EntityInterface>
263
     */
264
    public function extract($value, ?object $object = null): iterable
265
    {
266
        return $value;
267
    }
268
269
    public function setObject(?object $object): void
270
    {
271
        $this->object = $object;
272
    }
273
274
    public function getObject(): ?object
275
    {
276
        return $this->object;
277
    }
278
}
279