Passed
Pull Request — 1.x (#321)
by Akihito
02:49
created

InputParamFactory::createFromStructuredData()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 8
dl 0
loc 25
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\Input;
6
7
use BEAR\Resource\Annotation\Input;
8
use BEAR\Resource\Exception\InputClassCreateException;
9
use BEAR\Resource\Exception\ParameterException;
10
use InvalidArgumentException;
11
use Ray\Di\InjectorInterface;
12
use ReflectionClass;
13
use ReflectionMethod;
14
use ReflectionParameter;
15
use Throwable;
16
17
use function assert;
18
use function class_exists;
19
use function count;
20
use function enum_exists;
0 ignored issues
show
introduced by
The function enum_exists was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
21
use function is_array;
22
23
final class InputParamFactory
24
{
25
    public function __construct(
26
        private readonly InputParamEnumHandler $enumHandler,
27
        private readonly InputParamObjectHandler $objectHandler,
28
    ) {
29
    }
30
31
    /** @param array<string, mixed> $query */
32
    public function create(
33
        string $type,
34
        string $varName,
35
        array $query,
36
        InjectorInterface $injector,
37
        ReflectionParameter $parameter,
38
        bool $isDefaultAvailable,
39
        mixed $defaultValue,
40
    ): mixed {
41
        /** @var class-string $type */
42
        /** @psalm-suppress MixedArgument, ArgumentTypeCoercion */
43
        assert(class_exists($type) || enum_exists($type));
0 ignored issues
show
Bug introduced by
The function enum_exists was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

43
        assert(class_exists($type) || /** @scrutinizer ignore-call */ enum_exists($type));
Loading history...
44
        /** @psalm-suppress ArgumentTypeCoercion */
45
        $refClass = new ReflectionClass($type);
46
47
        // Handle enums
48
        if ($refClass->isEnum()) {
0 ignored issues
show
Bug introduced by
The method isEnum() does not exist on ReflectionClass. ( Ignorable by Annotation )

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

48
        if ($refClass->/** @scrutinizer ignore-call */ isEnum()) {

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...
49
            return $this->enumHandler->createEnum($type, $varName, $query, $isDefaultAvailable, $defaultValue);
50
        }
51
52
        $constructor = $refClass->getConstructor();
53
        if ($constructor === null) {
54
            return $this->objectHandler->createWithoutConstructor($type, $varName, $query, $isDefaultAvailable, $defaultValue);
55
        }
56
57
        // Check if this parameter has #[Input] attribute
58
        $inputAttr = $this->getInputAttribute($parameter);
59
60
        // If no #[Input] attribute, use ClassParam behavior
61
        if ($inputAttr === null) {
62
            return $this->createFromStructuredData($refClass, $constructor, $varName, $query, $injector, $type, $isDefaultAvailable, $defaultValue);
63
        }
64
65
        // If #[Input] has key specified, use structured data approach
66
        if ($inputAttr->key !== null) {
67
            return $this->createFromStructuredData($refClass, $constructor, $inputAttr->key, $query, $injector, $type, $isDefaultAvailable, $defaultValue);
68
        }
69
70
        try {
71
            $constructorArgs = $this->objectHandler->getConstructorArgs($constructor, $query, $injector, $type);
72
73
            /** @psalm-suppress MixedArgumentTypeCoercion */
74
            return $refClass->newInstanceArgs($constructorArgs);
75
        } catch (Throwable $e) {
76
            if ($e instanceof InvalidArgumentException) {
77
                throw $e;
78
            }
79
80
            throw new InputClassCreateException($type, 0, $e);
81
        }
82
    }
83
84
    private function getInputAttribute(ReflectionParameter $param): Input|null
85
    {
86
        $attributes = $param->getAttributes(Input::class);
87
        if (count($attributes) === 0) {
88
            return null;
89
        }
90
91
        return $attributes[0]->newInstance();
92
    }
93
94
    /**
95
     * @param ReflectionClass<object> $refClass
96
     * @param array<string, mixed>    $query
97
     */
98
    private function createFromStructuredData(
99
        ReflectionClass $refClass,
100
        ReflectionMethod $constructor,
101
        string $key,
102
        array $query,
103
        InjectorInterface $injector,
104
        string $type,
105
        bool $isDefaultAvailable,
106
        mixed $defaultValue,
107
    ): mixed {
108
        if (! isset($query[$key])) {
109
            if ($isDefaultAvailable) {
110
                return $defaultValue;
111
            }
112
113
            throw new ParameterException("Required key '{$key}' not found for {$type}");
114
        }
115
116
        $data = $query[$key];
117
        if (! is_array($data)) {
118
            throw new ParameterException("Data under key '{$key}' must be an array for {$type}");
119
        }
120
121
        /** @var array<string, mixed> $data */
122
        return $this->objectHandler->newInstance($constructor, $data, $injector, $key, $refClass, $type);
123
    }
124
}
125