MethodCallDenormalizer::supportsDenormalization()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 3
c 1
b 0
f 1
nc 2
nop 4
dl 0
loc 6
rs 10
1
<?php
2
3
4
namespace W2w\Lib\ApieObjectAccessNormalizer\Normalizers;
5
6
use ReflectionClass;
7
use ReflectionMethod;
8
use stdClass;
9
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
10
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
11
use Throwable;
12
use W2w\Lib\ApieObjectAccessNormalizer\Errors\ErrorBag;
13
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\ValidationException;
14
use W2w\Lib\ApieObjectAccessNormalizer\NameConverters\NullNameConverter;
15
use W2w\Lib\ApieObjectAccessNormalizer\ObjectAccess\ObjectAccessInterface;
16
17
/**
18
 * Special denormalizer to call a reflection method and return the value back.
19
 *
20
 * Usage:
21
 * - type: ReflectionMethod::ClassWithNameSpace::method
22
 * - context['object-instance'] object to run method on
23
 * - context['initial-arguments'] already hydrated arguments with key = variable name.
24
 * - context['object-access'] used object access instance.
25
 */
26
class MethodCallDenormalizer implements ContextAwareDenormalizerInterface
27
{
28
    /**
29
     * @var ObjectAccessInterface
30
     */
31
    private $objectAccess;
32
33
    /**
34
     * @var ApieObjectAccessNormalizer
35
     */
36
    private $normalizer;
37
38
    /**
39
     * @var NameConverterInterface
40
     */
41
    private $nameConverter;
42
43
    public function __construct(
44
        ObjectAccessInterface $objectAccess,
45
        ApieObjectAccessNormalizer $normalizer,
46
        NameConverterInterface $nameConverter = null
47
    ) {
48
        $this->objectAccess = $objectAccess;
49
        $this->normalizer = $normalizer;
50
        $this->nameConverter = $nameConverter ?? new NullNameConverter();
51
    }
52
53
    /**
54
     * {@inheritDoc}
55
     */
56
    public function supportsDenormalization($data, $type, $format = null, array $context = [])
57
    {
58
        if (strpos($type, 'ReflectionMethod::') !== 0) {
59
            return false;
60
        }
61
        return isset($context['object-instance']);
62
    }
63
64
    /**
65
     * {@inheritDoc}
66
     */
67
    public function denormalize($data, $type, $format = null, array $context = [])
68
    {
69
        if ($data instanceof stdClass) {
70
            $data = json_decode(json_encode($data), true);
71
        }
72
        if (!isset($context['key_prefix'])) {
73
            $context['key_prefix'] = '';
74
        }
75
        $method = new ReflectionMethod(substr($type, strlen('ReflectionMethod::')));
0 ignored issues
show
Bug introduced by
The call to ReflectionMethod::__construct() has too few arguments starting with name. ( Ignorable by Annotation )

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

75
        $method = /** @scrutinizer ignore-call */ new ReflectionMethod(substr($type, strlen('ReflectionMethod::')));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
76
        $objectAccess = $context['object_access'] ?? $this->objectAccess;
77
        $arguments = $objectAccess->getMethodArguments($method, new ReflectionClass($context['object-instance']));
78
        $initialArguments = $context['initial-arguments'] ?? [];
79
        $returnObject = $initialArguments;
80
        $errorBag = new ErrorBag('');
81
        foreach ($arguments as $denormalizedFieldName => $typeHint) {
82
            $fieldName = $this->nameConverter->normalize($denormalizedFieldName, $type, $format, $context);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $type. ( Ignorable by Annotation )

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

82
            /** @scrutinizer ignore-call */ 
83
            $fieldName = $this->nameConverter->normalize($denormalizedFieldName, $type, $format, $context);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
83
            if (isset($initialArguments[$fieldName])) {
84
                continue;
85
            }
86
            if (!isset($data[$fieldName])) {
87
                $errorBag->addThrowable($fieldName, new ValidationException([$fieldName => ['required']]));
88
                continue;
89
            }
90
            try {
91
                $returnObject[$fieldName] = $this->normalizer->denormalizeType(
92
                    $data, $denormalizedFieldName, $fieldName, $typeHint, $format, $context
93
                );
94
            } catch (Throwable $throwable) {
95
                $errorBag->addThrowable($fieldName, $throwable);
96
            }
97
        }
98
        if ($errorBag->hasErrors()) {
99
            throw new ValidationException($errorBag);
100
        }
101
        return $method->invokeArgs($context['object-instance'], $returnObject);
102
    }
103
}
104