Passed
Pull Request — 1.x (#2)
by Kevin
01:26
created

Callback::argument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
namespace Zenstruck;
4
5
use Zenstruck\Callback\Argument;
6
use Zenstruck\Callback\Exception\UnresolveableArgument;
7
use Zenstruck\Callback\Parameter;
8
9
/**
10
 * @author Kevin Bond <[email protected]>
11
 */
12
final class Callback implements \Countable
13
{
14
    /** @var \ReflectionFunction */
15
    private $function;
16
17
    private function __construct(\ReflectionFunction $function)
18
    {
19
        $this->function = $function;
20
    }
21
22
    public function __toString(): string
23
    {
24
        if ($class = $this->function->getClosureScopeClass()) {
25
            return "{$class->getName()}:{$this->function->getStartLine()}";
26
        }
27
28
        return $this->function->getName();
29
    }
30
31
    /**
32
     * @param callable|\ReflectionFunction $value
33
     */
34
    public static function createFor($value): self
35
    {
36
        if (\is_callable($value)) {
37
            $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 $callback 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

37
            $value = new \ReflectionFunction(\Closure::fromCallable(/** @scrutinizer ignore-type */ $value));
Loading history...
38
        }
39
40
        if (!$value instanceof \ReflectionFunction) {
41
            throw new \InvalidArgumentException('$value must be callable.');
42
        }
43
44
        return new self($value);
45
    }
46
47
    /**
48
     * Invoke the callable with the passed arguments. Arguments of type
49
     * Zenstruck\Callback\Parameter are resolved before invoking.
50
     *
51
     * @param mixed|Parameter ...$arguments
52
     *
53
     * @return mixed
54
     *
55
     * @throws \ArgumentCountError   If there is a argument count mismatch
56
     * @throws UnresolveableArgument If the argument cannot be resolved
57
     */
58
    public function invoke(...$arguments)
59
    {
60
        $functionArgs = $this->arguments();
61
62
        foreach ($arguments as $key => $parameter) {
63
            if (!$parameter instanceof Parameter) {
64
                continue;
65
            }
66
67
            if (!\array_key_exists($key, $functionArgs)) {
68
                if (!$parameter->isOptional()) {
69
                    throw new \ArgumentCountError(\sprintf('No argument %d for callable. Expected type: "%s".', $key + 1, $parameter->type()));
70
                }
71
72
                $arguments[$key] = null;
73
74
                continue;
75
            }
76
77
            try {
78
                $arguments[$key] = $parameter->resolve($functionArgs[$key]);
79
            } catch (UnresolveableArgument $e) {
80
                throw new UnresolveableArgument(\sprintf('Unable to resolve argument %d for callback. Expected type: "%s". (%s)', $key + 1, $parameter->type(), $this), $e);
81
            }
82
        }
83
84
        return $this->function->invoke(...$arguments);
85
    }
86
87
    /**
88
     * Invoke the callable using the passed Parameter to resolve all callable
89
     * arguments.
90
     *
91
     * @param int $min Enforce a minimum number of arguments the callable must have
92
     *
93
     * @return mixed
94
     *
95
     * @throws \ArgumentCountError   If the number of arguments is less than $min
96
     * @throws UnresolveableArgument If the argument cannot be resolved
97
     */
98
    public function invokeAll(Parameter $parameter, int $min = 0)
99
    {
100
        if (\count($this) < $min) {
101
            throw new \ArgumentCountError("{$min} argument(s) of type \"{$parameter->type()}\" required ({$this}).");
102
        }
103
104
        $arguments = $this->arguments();
105
106
        foreach ($arguments as $key => $argument) {
107
            try {
108
                $arguments[$key] = $parameter->resolve($argument);
109
            } catch (UnresolveableArgument $e) {
110
                throw new UnresolveableArgument(\sprintf('Unable to resolve argument %d for callback. Expected type: "%s". (%s)', $key + 1, $parameter->type(), $this), $e);
111
            }
112
        }
113
114
        return $this->function->invoke(...$arguments);
115
    }
116
117
    /**
118
     * @return Argument[]
119
     */
120
    public function arguments(): array
121
    {
122
        return \array_map(
123
            static function(\ReflectionParameter $parameter) {
124
                return new Argument($parameter);
125
            },
126
            $this->function->getParameters()
127
        );
128
    }
129
130
    public function argument(int $index): Argument
131
    {
132
        if (!isset(($arguments = $this->arguments())[$index])) {
133
            throw new \OutOfRangeException(\sprintf('Argument %d does not exist for %s.', $index + 1, $this));
134
        }
135
136
        return $arguments[$index];
137
    }
138
139
    public function count(): int
140
    {
141
        return $this->function->getNumberOfParameters();
142
    }
143
}
144