Passed
Pull Request — 1.x (#321)
by Akihito
02:24
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\ParameterException;
9
use InvalidArgumentException;
10
use Ray\Di\InjectorInterface;
11
use ReflectionClass;
12
use ReflectionMethod;
13
use ReflectionParameter;
14
use Throwable;
15
16
use function assert;
17
use function class_exists;
18
use function count;
19
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...
20
use function is_array;
21
22
final class InputParamFactory
23
{
24
    public function __construct(
25
        private readonly InputParamEnumHandler $enumHandler,
26
        private readonly InputParamObjectHandler $objectHandler,
27
    ) {
28
    }
29
30
    /** @param array<string, mixed> $query */
31
    public function create(
32
        string $type,
33
        string $varName,
34
        array $query,
35
        InjectorInterface $injector,
36
        ReflectionParameter $parameter,
37
        bool $isDefaultAvailable,
38
        mixed $defaultValue,
39
    ): mixed {
40
        /** @var class-string $type */
41
        /** @psalm-suppress MixedArgument, ArgumentTypeCoercion */
42
        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

42
        assert(class_exists($type) || /** @scrutinizer ignore-call */ enum_exists($type));
Loading history...
43
        /** @psalm-suppress ArgumentTypeCoercion */
44
        $refClass = new ReflectionClass($type);
45
46
        // Handle enums
47
        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

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