Passed
Push — master ( e62da3...60022f )
by Alex
02:34
created

src/Hydrator/EntityHydrator.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\LaminasDoctrine\Hydrator;
6
7
use Doctrine\Laminas\Hydrator\DoctrineObject;
8
use Doctrine\Laminas\Hydrator\Strategy\AbstractCollectionStrategy;
9
use Laminas\Hydrator\Exception\InvalidArgumentException;
10
use Laminas\Hydrator\Exception\RuntimeException;
11
use Laminas\Hydrator\Filter\FilterProviderInterface;
12
13
/**
14
 * When using hydrators and PHP 7.4+ type hinted properties, there will be times where our entity classes will be
15
 * instantiated via reflection (due to the Doctrine/Laminas hydration processes). This instantiation will bypass the
16
 * entity's __construct() and therefore not initialise the default class property values. This will lead to
17
 * "Typed property must not be accessed before initialization" fatal errors, despite using the hydrators in their
18
 * intended way. This class extends the existing DoctrineObject in order to first check if the requested property has
19
 * been instantiated before attempting to use it as part of the extractByValue() method.
20
 *
21
 * @author  Alex Patterson <[email protected]>
22
 * @package Arp\LaminasDoctrine\Hydrator
23
 */
24
final class EntityHydrator extends DoctrineObject
25
{
26
    /**
27
     * @var \ReflectionClass|null
28
     */
29
    private ?\ReflectionClass $reflectionClass = null;
30
31
    /**
32
     * @noinspection PhpMissingParamTypeInspection
33
     * @noinspection ReturnTypeCanBeDeclaredInspection
34
     *
35
     * @param object $object
36
     * @param mixed  $collectionName
37
     * @param string $target
38
     * @param mixed  $values
39
     *
40
     * @throws RuntimeException
41
     * @throws InvalidArgumentException
42
     * @throws \InvalidArgumentException
43
     */
44
    protected function toMany($object, $collectionName, $target, $values)
45
    {
46
        $metadata = $this->objectManager->getClassMetadata(ltrim($target, '\\'));
47
        $identifier = $metadata->getIdentifier();
48
49
        if (! is_array($values) && ! $values instanceof \Traversable) {
50
            $values = (array) $values;
51
        }
52
53
        $collection = [];
54
55
        // If the collection contains identifiers, fetch the objects from database
56
        foreach ($values as $value) {
57
            if ($value instanceof $target) {
58
                // assumes modifications have already taken place in object
59
                $collection[] = $value;
60
                continue;
61
            }
62
63
            if (empty($value)) {
64
                // assumes no id and retrieves new $target
65
                $collection[] = $this->find($value, $target);
66
                continue;
67
            }
68
69
            $find = $this->getFindCriteria($identifier, $value);
70
71
            if (! empty($find) && $found = $this->find($find, $target)) {
72
                $collection[] = is_array($value) ? $this->hydrate($value, $found) : $found;
73
            } else {
74
                $newTarget = $this->createTargetEntity($target);
75
76
                $collection[] = is_array($value) ? $this->hydrate($value, $newTarget) : $newTarget;
77
            }
78
        }
79
80
        $collection = array_filter(
81
            $collection,
82
            static fn($item) => null !== $item
83
        );
84
85
        /** @var AbstractCollectionStrategy $collectionStrategy */
86
        $collectionStrategy = $this->getStrategy($collectionName);
87
        $collectionStrategy->setObject($object);
88
89
        $this->hydrateValue($collectionName, $collection, $values);
0 ignored issues
show
It seems like $values can also be of type Traversable; however, parameter $data of Doctrine\Laminas\Hydrato...eObject::hydrateValue() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
        $this->hydrateValue($collectionName, $collection, /** @scrutinizer ignore-type */ $values);
Loading history...
90
    }
91
92
    /**
93
     * @param string $className
94
     *
95
     * @return object
96
     *
97
     * @throws RuntimeException
98
     */
99
    private function createTargetEntity(string $className): object
100
    {
101
        return $this->getReflectionClass($className)->newInstanceWithoutConstructor();
102
    }
103
104
    /**
105
     * Copied from parent to check for isInitialisedFieldName()
106
     *
107
     * @noinspection PhpMissingParamTypeInspection
108
     *
109
     * @param object $object
110
     *
111
     * @return array
112
     *
113
     * @throws RuntimeException
114
     */
115
    public function extractByValue($object): array
116
    {
117
        $fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
118
        $methods = get_class_methods($object);
119
        $filter = $object instanceof FilterProviderInterface
120
            ? $object->getFilter()
121
            : $this->filterComposite;
122
123
        $data = [];
124
        foreach ($fieldNames as $fieldName) {
125
            if (!$this->isInitialisedFieldName($object, $fieldName)) {
126
                continue;
127
            }
128
            if ($filter && !$filter->filter($fieldName)) {
129
                continue;
130
            }
131
132
            $getter = 'get' . ucfirst($fieldName);
133
            $isser = 'is' . ucfirst($fieldName);
134
135
            $dataFieldName = $this->computeExtractFieldName($fieldName);
136
            if (in_array($getter, $methods, true)) {
137
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$getter(), $object);
138
            } elseif (in_array($isser, $methods, true)) {
139
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$isser(), $object);
140
            } elseif (
141
                0 === strpos($fieldName, 'is')
142
                && in_array($fieldName, $methods, true)
143
                && ctype_upper(substr($fieldName, 2, 1))
144
            ) {
145
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$fieldName(), $object);
146
            }
147
        }
148
149
        return $data;
150
    }
151
152
    /**
153
     * Check if the provided $fieldName is initialised for the given $object
154
     *
155
     * @param object $object
156
     * @param string $fieldName
157
     *
158
     * @return bool
159
     *
160
     * @throws RuntimeException
161
     */
162
    protected function isInitialisedFieldName(object $object, string $fieldName): bool
163
    {
164
        $property = $this->getReflectionProperty($object, $fieldName);
165
166
        $isPublic = $property->isPublic();
167
        if (!$isPublic) {
168
            $property->setAccessible(true);
169
        }
170
171
        $initialized = $property->isInitialized($object);
172
173
        if (!$isPublic) {
174
            $property->setAccessible(false);
175
        }
176
177
        return $initialized;
178
    }
179
180
    /**
181
     * @param object $object
182
     * @param string $fieldName
183
     *
184
     * @return \ReflectionProperty
185
     *
186
     * @throws RuntimeException
187
     */
188
    private function getReflectionProperty(object $object, string $fieldName): \ReflectionProperty
189
    {
190
        $className = get_class($object);
191
        $reflectionClass = $this->getReflectionClass($className);
192
193
        if (!$reflectionClass->hasProperty($fieldName)) {
194
            throw new RuntimeException(
195
                sprintf(
196
                    'The hydration property \'%s\' could not be found for class \'%s\'',
197
                    $fieldName,
198
                    $className
199
                )
200
            );
201
        }
202
203
        try {
204
            $property = $reflectionClass->getProperty($fieldName);
205
        } catch (\Throwable $e) {
206
            throw new RuntimeException(
207
                sprintf(
208
                    'The hydration property \'%s\' could not be loaded for class \'%s\': %s',
209
                    $fieldName,
210
                    $className,
211
                    $e->getMessage()
212
                ),
213
                $e->getCode(),
214
                $e
215
            );
216
        }
217
218
        return $property;
219
    }
220
221
    /**
222
     * @param string $className
223
     *
224
     * @return \ReflectionClass
225
     *
226
     * @throws RuntimeException
227
     */
228
    private function getReflectionClass(string $className): \ReflectionClass
229
    {
230
        if (null !== $this->reflectionClass && $this->reflectionClass->getName() === $className) {
231
            return $this->reflectionClass;
232
        }
233
234
        try {
235
            $this->reflectionClass = new \ReflectionClass($className);
236
        } catch (\Throwable $e) {
237
            throw new RuntimeException(
238
                sprintf(
239
                    'The hydrator was unable to create a reflection instance for class \'%s\': %s',
240
                    $className,
241
                    $e->getMessage()
242
                ),
243
                $e->getCode(),
244
                $e
245
            );
246
        }
247
248
        return $this->reflectionClass;
249
    }
250
251
    /**
252
     * @param array $identifier
253
     * @param mixed  $value
254
     *
255
     * @return array
256
     */
257
    protected function getFindCriteria(array $identifier, $value): array
258
    {
259
        if (!is_array($identifier)) {
0 ignored issues
show
The condition is_array($identifier) is always true.
Loading history...
260
            return [];
261
        }
262
263
        $find = [];
264
        foreach ($identifier as $field) {
265
            if (is_object($value)) {
266
                $getter = 'get' . ucfirst($field);
267
268
                if (is_callable([$value, $getter])) {
269
                    $find[$field] = $value->$getter();
270
                } elseif (property_exists($value, $field)) {
271
                    $find[$field] = $value->{$field};
272
                }
273
                continue;
274
            }
275
276
            if (is_array($value)) {
277
                if (isset($value[$field])) {
278
                    $find[$field] = $value[$field];
279
                    unset($value[$field]);
280
                }
281
                continue;
282
            }
283
284
            $find[$field] = $value;
285
        }
286
287
        return $find;
288
    }
289
}
290