EntityHydrator::extractByValue()   B
last analyzed

Complexity

Conditions 11
Paths 14

Size

Total Lines 35
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
c 1
b 0
f 0
dl 0
loc 35
rs 7.3166
cc 11
nc 14
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Doctrine\ORM\Proxy\Proxy;
10
use Laminas\Hydrator\Exception\InvalidArgumentException;
11
use Laminas\Hydrator\Exception\RuntimeException;
12
use Laminas\Hydrator\Filter\FilterProviderInterface;
13
14
/**
15
 * When using hydrators and PHP 7.4+ type hinted properties, there will be times when our entity classes will be
16
 * instantiated via reflection (due to the Doctrine/Laminas hydration processes). This instantiation will bypass the
17
 * entity's __construct() and therefore not initialise the default class property values. This will lead to
18
 * "Typed property must not be accessed before initialization" fatal errors, despite using the hydrators in their
19
 * intended way. This class extends the existing DoctrineObject in order to first check if the requested property has
20
 * been instantiated before attempting to use it as part of the extractByValue() method.
21
 */
22
final class EntityHydrator extends DoctrineObject
23
{
24
    /**
25
     * @var \ReflectionClass<object>|null
26
     */
27
    private ?\ReflectionClass $reflectionClass = null;
28
29
    /**
30
     * @param object               $object
31
     * @param mixed                $collectionName
32
     * @param class-string<object> $target
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<object> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<object>.
Loading history...
33
     * @param array<mixed>|null    $values
34
     *
35
     * @return void
36
     *
37
     * @throws RuntimeException
38
     * @throws InvalidArgumentException
39
     * @throws \InvalidArgumentException
40
     * @throws \ReflectionException
41
     */
42
    protected function toMany($object, $collectionName, $target, $values): void
43
    {
44
        if (!is_iterable($values)) {
45
            $values = (array)$values;
46
        }
47
48
        $metadata = $this->objectManager->getClassMetadata($target);
49
        $identifier = $metadata->getIdentifier();
50
        $collection = [];
51
52
        // If the collection contains identifiers, fetch the objects from database
53
        foreach ($values as $value) {
54
            if ($value instanceof $target) {
55
                // Assumes modifications have already taken place in object
56
                $collection[] = $value;
57
                continue;
58
            }
59
60
            if (empty($value)) {
61
                // Assumes no id and retrieves new $target
62
                $collection[] = $this->find($value, $target);
63
                continue;
64
            }
65
66
            $find = $this->getFindCriteria($identifier, $value);
67
68
            if (!empty($find) && $found = $this->find($find, $target)) {
69
                $collection[] = is_array($value) ? $this->hydrate($value, $found) : $found;
70
                continue;
71
            }
72
73
            $newTarget = $this->createTargetEntity($target);
74
            $collection[] = is_array($value) ? $this->hydrate($value, $newTarget) : $newTarget;
75
        }
76
77
        $collection = array_filter(
78
            $collection,
79
            static fn ($item) => null !== $item
80
        );
81
82
        /** @var AbstractCollectionStrategy $collectionStrategy */
83
        $collectionStrategy = $this->getStrategy($collectionName);
84
        $collectionStrategy->setObject($object);
85
86
        $this->hydrateValue($collectionName, $collection, $values);
87
    }
88
89
    /**
90
     * @param string $className
91
     *
92
     * @return object
93
     *
94
     * @throws RuntimeException
95
     * @throws \ReflectionException
96
     */
97
    private function createTargetEntity(string $className): object
98
    {
99
        return $this->createReflectionClass($className)->newInstanceWithoutConstructor();
100
    }
101
102
    /**
103
     * Copied from parent to check for isInitialisedFieldName()
104
     *
105
     * @param object $object
106
     *
107
     * @return array<string, mixed>
108
     *
109
     * @throws RuntimeException
110
     * @throws \ReflectionException
111
     */
112
    public function extractByValue($object): array
113
    {
114
        $fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
0 ignored issues
show
Bug introduced by
The method getFieldNames() does not exist on null. ( Ignorable by Annotation )

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

114
        $fieldNames = array_merge($this->metadata->/** @scrutinizer ignore-call */ getFieldNames(), $this->metadata->getAssociationNames());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
115
        $methods = get_class_methods($object);
116
        $filter = $object instanceof FilterProviderInterface
117
            ? $object->getFilter()
118
            : $this->filterComposite;
119
120
        $data = [];
121
        foreach ($fieldNames as $fieldName) {
122
            if (!$this->isInitialisedFieldName($object, $fieldName)) {
123
                continue;
124
            }
125
            if ($filter && !$filter->filter($fieldName)) {
126
                continue;
127
            }
128
129
            $getter = 'get' . ucfirst($fieldName);
130
            $isser = 'is' . ucfirst($fieldName);
131
132
            $dataFieldName = $this->computeExtractFieldName($fieldName);
133
            if (in_array($getter, $methods, true)) {
134
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$getter(), $object);
135
            } elseif (in_array($isser, $methods, true)) {
136
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$isser(), $object);
137
            } elseif (
138
                str_starts_with($fieldName, 'is')
139
                && in_array($fieldName, $methods, true)
140
                && ctype_upper($fieldName[2])
141
            ) {
142
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$fieldName(), $object);
143
            }
144
        }
145
146
        return $data;
147
    }
148
149
    /**
150
     * Check if the provided $fieldName is initialised for the given $object
151
     *
152
     * @throws RuntimeException
153
     */
154
    protected function isInitialisedFieldName(object $object, string $fieldName): bool
155
    {
156
        if ($object instanceof Proxy) {
157
            return true;
158
        }
159
        return $this->getReflectionProperty($object, $fieldName)->isInitialized($object);
160
    }
161
162
    /**
163
     * @param object $object
164
     * @param string $fieldName
165
     *
166
     * @return \ReflectionProperty
167
     *
168
     * @throws RuntimeException
169
     */
170
    private function getReflectionProperty(object $object, string $fieldName): \ReflectionProperty
171
    {
172
        $className = get_class($object);
173
        $reflectionClass = $this->getReflectionClass($className);
174
175
        if (!$reflectionClass->hasProperty($fieldName)) {
176
            throw new RuntimeException(
177
                sprintf(
178
                    'The hydration property \'%s\' could not be found for class \'%s\'',
179
                    $fieldName,
180
                    $className
181
                )
182
            );
183
        }
184
185
        try {
186
            $property = $reflectionClass->getProperty($fieldName);
187
        } catch (\Throwable $e) {
188
            throw new RuntimeException(
189
                sprintf(
190
                    'The hydration property \'%s\' could not be loaded for class \'%s\': %s',
191
                    $fieldName,
192
                    $className,
193
                    $e->getMessage()
194
                ),
195
                $e->getCode(),
196
                $e
197
            );
198
        }
199
200
        return $property;
201
    }
202
203
    /**
204
     * @param string $className
205
     *
206
     * @return \ReflectionClass<object>
207
     *
208
     * @throws RuntimeException
209
     */
210
    private function getReflectionClass(string $className): \ReflectionClass
211
    {
212
        if (null !== $this->reflectionClass && $this->reflectionClass->getName() === $className) {
213
            return $this->reflectionClass;
214
        }
215
        $this->reflectionClass = $this->createReflectionClass($className);
216
        return $this->reflectionClass;
217
    }
218
219
    /**
220
     * @param array<string|object|array|mixed> $identifier
221
     * @param mixed                            $value
222
     *
223
     * @return array<string|int, mixed>
224
     */
225
    protected function getFindCriteria(array $identifier, mixed $value): array
226
    {
227
        $find = [];
228
        foreach ($identifier as $field) {
229
            if (is_object($value)) {
230
                $getter = 'get' . ucfirst($field);
231
232
                if (is_callable([$value, $getter])) {
233
                    $find[$field] = $value->$getter();
234
                } elseif (property_exists($value, $field)) {
235
                    $find[$field] = $value->{$field};
236
                }
237
                continue;
238
            }
239
240
            if (is_array($value)) {
241
                if (isset($value[$field])) {
242
                    $find[$field] = $value[$field];
243
                    unset($value[$field]);
244
                }
245
                continue;
246
            }
247
248
            $find[$field] = $value;
249
        }
250
251
        return $find;
252
    }
253
254
    /**
255
     * @param string $className
256
     *
257
     * @return \ReflectionClass<object>
258
     *
259
     * @throws RuntimeException
260
     */
261
    private function createReflectionClass(string $className): \ReflectionClass
262
    {
263
        if (!class_exists($className)) {
264
            throw new RuntimeException(
265
                sprintf(
266
                    'The hydrator was unable to create a reflection instance for class \'%s\': %s',
267
                    'The class could not be found',
268
                    $className,
269
                )
270
            );
271
        }
272
273
        return new \ReflectionClass($className);
274
    }
275
276
    /**
277
     * @param mixed $value
278
     * @param string $typeOfField
279
     */
280
    protected function handleTypeConversions(mixed $value, $typeOfField): mixed
281
    {
282
        if ($typeOfField === 'string' && $value instanceof \BackedEnum) {
0 ignored issues
show
Bug introduced by
The type BackedEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
283
            return $value;
284
        }
285
286
        if ($value !== null && $typeOfField === 'bigint') {
287
            return (int)$value;
288
        }
289
290
        return parent::handleTypeConversions($value, $typeOfField);
291
    }
292
}
293