Passed
Push — 1.x ( e3781f...255b78 )
by Kevin
02:10
created

FunctionExecutor::replaceArgument()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 12
c 0
b 0
f 0
nc 6
nop 1
dl 0
loc 25
rs 8.8333
1
<?php
2
3
namespace Zenstruck\Browser\Util;
4
5
/**
6
 * Utility to manipulate and validate \Closure arguments.
7
 *
8
 * TODO extract to a library as zenstruck/foundry could benefit.
9
 *
10
 * @author Kevin Bond <[email protected]>
11
 *
12
 * @internal
13
 */
14
final class FunctionExecutor
15
{
16
    private \ReflectionFunction $function;
17
    private int $minArguments = 0;
18
    private array $typeReplace = [];
19
20
    public function __construct(\ReflectionFunction $function)
21
    {
22
        $this->function = $function;
23
    }
24
25
    /**
26
     * @param callable|\ReflectionFunction $value
27
     */
28
    public static function createFor($value): self
29
    {
30
        if (\is_callable($value)) {
31
            $value = new \ReflectionFunction(\Closure::fromCallable($value));
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type ReflectionFunction; however, parameter $callable of Closure::fromCallable() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

31
            $value = new \ReflectionFunction(\Closure::fromCallable(/** @scrutinizer ignore-type */ $value));
Loading history...
32
        }
33
34
        if (!$value instanceof \ReflectionFunction) {
35
            throw new \InvalidArgumentException('$value must be callable or \ReflectionFunction');
36
        }
37
38
        return new self($value);
39
    }
40
41
    public function minArguments(int $min): self
42
    {
43
        $this->minArguments = $min;
44
45
        return $this;
46
    }
47
48
    public function replaceTypedArgument(string $typehint, $value): self
49
    {
50
        $this->typeReplace[$typehint] = $value;
51
52
        return $this;
53
    }
54
55
    public function replaceUntypedArgument($value): self
56
    {
57
        $this->typeReplace[null] = $value;
58
59
        return $this;
60
    }
61
62
    public function execute()
63
    {
64
        $arguments = $this->function->getParameters();
65
66
        if (\count($arguments) < $this->minArguments) {
67
            throw new \ArgumentCountError("{$this->minArguments} argument(s) required.");
68
        }
69
70
        $arguments = \array_map([$this, 'replaceArgument'], $arguments);
71
72
        return $this->function->invoke(...$arguments);
0 ignored issues
show
Bug introduced by
$arguments is expanded, but the parameter $args of ReflectionFunction::invoke() does not expect variable arguments. ( Ignorable by Annotation )

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

72
        return $this->function->invoke(/** @scrutinizer ignore-type */ ...$arguments);
Loading history...
73
    }
74
75
    private function replaceArgument(\ReflectionParameter $argument)
76
    {
77
        $type = $argument->getType();
78
79
        if (!$type && \array_key_exists(null, $this->typeReplace)) {
80
            return $this->typeReplace[null];
81
        }
82
83
        if (!$type instanceof \ReflectionNamedType) {
84
            throw new \TypeError("Unable to replace argument \"{$argument->getName()}\".");
85
        }
86
87
        foreach (\array_keys($this->typeReplace) as $typehint) {
88
            if (!\is_a($type->getName(), $typehint, true)) {
89
                continue;
90
            }
91
92
            if (!($value = $this->typeReplace[$typehint]) instanceof \Closure) {
93
                return $value;
94
            }
95
96
            return $value($type->getName());
97
        }
98
99
        throw new \TypeError("Unable to replace argument \"{$argument->getName()}\".");
100
    }
101
}
102