Completed
Push — 1.x ( d5d19c...d72eab )
by Kevin
15s queued 13s
created

Callback::invoke()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 17
c 0
b 0
f 0
nc 10
nop 1
dl 0
loc 30
rs 8.8333
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
        try {
85
            return $this->function->invoke(...$arguments);
86
        } catch (\ArgumentCountError $e) {
87
            throw new \ArgumentCountError(\sprintf('Too few arguments passed to "%s". Expected %d, got %s.', $this, $this->function->getNumberOfRequiredParameters(), \count($arguments)), 0, $e);
88
        }
89
    }
90
91
    /**
92
     * Invoke the callable using the passed Parameter to resolve all callable
93
     * arguments.
94
     *
95
     * @param int $min Enforce a minimum number of arguments the callable must have
96
     *
97
     * @return mixed
98
     *
99
     * @throws \ArgumentCountError   If the number of arguments is less than $min
100
     * @throws UnresolveableArgument If the argument cannot be resolved
101
     */
102
    public function invokeAll(Parameter $parameter, int $min = 0)
103
    {
104
        if (\count($this) < $min) {
105
            throw new \ArgumentCountError("{$min} argument(s) of type \"{$parameter->type()}\" required ({$this}).");
106
        }
107
108
        $arguments = $this->arguments();
109
110
        foreach ($arguments as $key => $argument) {
111
            try {
112
                $arguments[$key] = $parameter->resolve($argument);
113
            } catch (UnresolveableArgument $e) {
114
                throw new UnresolveableArgument(\sprintf('Unable to resolve argument %d for callback. Expected type: "%s". (%s)', $key + 1, $parameter->type(), $this), $e);
115
            }
116
        }
117
118
        return $this->function->invoke(...$arguments);
119
    }
120
121
    /**
122
     * @return Argument[]
123
     */
124
    public function arguments(): array
125
    {
126
        return \array_map(
127
            static function(\ReflectionParameter $parameter) {
128
                return new Argument($parameter);
129
            },
130
            $this->function->getParameters()
131
        );
132
    }
133
134
    public function argument(int $index): Argument
135
    {
136
        if (!isset(($arguments = $this->arguments())[$index])) {
137
            throw new \OutOfRangeException(\sprintf('Argument %d does not exist for %s.', $index + 1, $this));
138
        }
139
140
        return $arguments[$index];
141
    }
142
143
    public function count(): int
144
    {
145
        return $this->function->getNumberOfParameters();
146
    }
147
}
148