• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / Event / 4146879500

pending completion
4146879500

push

github

Olivier Laviale
Tidy

200 of 200 new or added lines in 9 files covered. (100.0%)

191 of 200 relevant lines covered (95.5%)

6.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.96
/lib/EventHookReflection.php
1
<?php
2

3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace ICanBoogie;
13

14
use Closure;
15
use InvalidArgumentException;
16
use LogicException;
17
use ReflectionException;
18
use ReflectionFunction;
19
use ReflectionFunctionAbstract;
20
use ReflectionMethod;
21
use ReflectionParameter;
22

23
use function assert;
24
use function count;
25
use function explode;
26
use function get_debug_type;
27
use function implode;
28
use function is_array;
29
use function is_callable;
30
use function is_object;
31
use function is_string;
32
use function is_subclass_of;
33
use function preg_match;
34
use function spl_object_hash;
35
use function strpos;
36

37
/**
38
 * Reflection of an event hook.
39
 *
40
 * @internal
41
 */
42
final class EventHookReflection
43
{
44
    /**
45
     * @var array<string, EventHookReflection>
46
     *     Where _key_ is a hook key.
47
     */
48
    private static array $instances = [];
49

50
    /**
51
     * Creates instance from an event hook.
52
     *
53
     * @throws InvalidArgumentException if `$hook` is not a valid event hook.
54
     * @throws ReflectionException
55
     */
56
    public static function from(callable $hook): self
57
    {
58
        self::assert_valid($hook);
18✔
59

60
        $key = self::make_key($hook);
18✔
61

62
        return self::$instances[$key] ??= new self(self::resolve_reflection($hook));
18✔
63
    }
64

65
    /**
66
     * Makes key from event hook.
67
     *
68
     * @throws ReflectionException
69
     */
70
    private static function make_key(callable $hook): string
71
    {
72
        if (is_array($hook)) {
18✔
73
            return implode('#', $hook);
2✔
74
        }
75

76
        if ($hook instanceof Closure) {
16✔
77
            $reflection = new ReflectionFunction($hook);
12✔
78

79
            return $reflection->getFileName() . '#' . $reflection->getStartLine() . '#' . $reflection->getEndLine();
12✔
80
        }
81

82
        if (is_object($hook)) {
4✔
83
            return spl_object_hash($hook);
2✔
84
        }
85

86
        assert(is_string($hook));
87

88
        return $hook;
2✔
89
    }
90

91
    /**
92
     * Asserts that the event hook is valid.
93
     *
94
     * @throws InvalidArgumentException if `$hook` is not a valid event hook.
95
     */
96
    public static function assert_valid(mixed $hook): void
97
    {
98
        is_callable($hook) or throw new InvalidArgumentException(
27✔
99
            format("The event hook must be a callable, %type given: :hook", [
27✔
100
                'type' => get_debug_type($hook),
27✔
101
                'hook' => $hook
27✔
102
            ])
27✔
103
        );
27✔
104
    }
105

106
    /**
107
     * Asserts that the number of parameters is valid.
108
     *
109
     * @param ReflectionParameter[] $parameters
110
     */
111
    public static function assert_valid_parameters_number(array $parameters): void
112
    {
113
        $n = count($parameters);
18✔
114

115
        if ($n < 1) {
18✔
116
            throw new LogicException("Expecting at least 1 parameter got none.");
1✔
117
        }
118

119
        if ($n > 2) {
17✔
120
            throw new LogicException("Expecting at most 2 parameters got $n.");
1✔
121
        }
122
    }
123

124
    /**
125
     * Resolves hook reflection.
126
     *
127
     * @throws ReflectionException
128
     */
129
    private static function resolve_reflection(callable $hook): ReflectionFunctionAbstract
130
    {
131
        if (is_object($hook)) {
18✔
132
            return new ReflectionMethod($hook, '__invoke');
14✔
133
        }
134

135
        if (is_array($hook)) {
4✔
136
            return new ReflectionMethod($hook[0], $hook[1]);
2✔
137
        }
138

139
        if (is_string($hook) && strpos($hook, '::')) {
2✔
140
            [ $class, $method ] = explode('::', $hook);
2✔
141

142
            return new ReflectionMethod($class, $method);
2✔
143
        }
144

145
        assert(is_string($hook) || $hook instanceof Closure);
146

147
        return new ReflectionFunction($hook);
×
148
    }
149

150
    /**
151
     * Returns the class of a parameter reflection.
152
     *
153
     * Contrary of the {@link ReflectionParameter::getClass()} method, the class does not need to
154
     * be available to be successfully retrieved.
155
     *
156
     * @return class-string
157
     */
158
    private static function resolve_parameter_class(ReflectionParameter $param): string
159
    {
160
        if (!preg_match('/([\w\\\]+)\s\$/', $param, $matches)) {
16✔
161
            throw new LogicException("The parameter `$param->name` is not typed.");
1✔
162
        }
163

164
        /** @phpstan-ignore-next-line  */
165
        return $matches[1]
15✔
166
            ?? throw new LogicException("Unable to resolve class from parameters `$param->name");
15✔
167
    }
168

169
    private ReflectionFunctionAbstract $reflection;
170

171
    /**
172
     * @var string The event type resolved from the event hook parameters.
173
     */
174
    public readonly string $type;
175

176
    private function __construct(ReflectionFunctionAbstract $reflection)
177
    {
178
        $this->reflection = $reflection;
18✔
179
        $this->type = $this->resolve_type();
18✔
180
    }
181

182
    /**
183
     * Returns the event type resolved from the event hook parameters.
184
     */
185
    private function resolve_type(): string
186
    {
187
        $parameters = $this->reflection->getParameters();
18✔
188

189
        self::assert_valid_parameters_number($parameters);
18✔
190

191
        [ $event_param, $sender_param ] = $parameters + [ 1 => null ];
16✔
192

193
        assert($event_param instanceof ReflectionParameter);
194

195
        try {
196
            $event_class = self::resolve_parameter_class($event_param);
16✔
197
        } catch (LogicException $e) {
1✔
198
            throw new LogicException(
1✔
199
                "The parameter `$event_param->name` must be an instance of `ICanBoogie\Event`.",
1✔
200
                previous: $e
1✔
201
            );
1✔
202
        }
203

204
        assert(is_subclass_of($event_class, Event::class));
205

206
        if (!$sender_param) {
15✔
207
            return $event_class;
5✔
208
        }
209

210
        $sender_class = self::resolve_parameter_class($sender_param);
10✔
211

212
        /** @var Event $event_class */
213

214
        return $event_class::for($sender_class);
10✔
215
    }
216
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc