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

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

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