Completed
Push — master ( ed265b...39a9ed )
by Pierre
03:19
created

Dispatcher::publish()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
dl 0
loc 9
ccs 7
cts 7
cp 1
crap 1
rs 10
c 1
b 0
f 0
eloc 7
nc 1
nop 1
1
<?php
2
3
namespace App\Component\Pubsub;
4
5
use Closure;
6
use Exception;
7
use ReflectionFunction;
8
use ReflectionParameter;
9
use App\Component\Pubsub\EventInterface;
10
11
class Dispatcher implements DispatcherInterface
12
{
13
14
    /**
15
     * listener stack.
16
     * Struct : [resName][event][hash]
17
     *
18
     * @var array
19
     */
20
    protected $stack = array();
21
22
    /**
23
     * Subscribes the listener to the resource's events.
24
     * If $resName is *,
25
     * then the listener will be dispatched when the specified event
26
     * is fired.
27
     * If $event is *,
28
     * then the listener will be dispatched
29
     * for any dispatched event of the specified resource.
30
     * If $resName and $event is *,
31
     * the listener will be dispatched
32
     * for any dispatched event for any resource.
33
     *
34
     * @param ListenerInterface $listener
35
     * @param String $resName
36
     * @param Mixed $event
37
     * @return string
38
     */
39 1
    public function subscribe(
40
        ListenerInterface $listener,
41
        $resName = self::ANY,
42
        $event = self::ANY
43
    ): string {
44 1
        $hash = $listener->hash();
45 1
        $this->stack[$resName][$event][$hash] = $listener;
46 1
        return $hash;
47
    }
48
49
    /**
50
     * Subscribes the listener to the resource's events.
51
     * If $resName is *,
52
     * then the listener will be dispatched when the specified event
53
     * is fired.
54
     * If $event is *,
55
     * then the listener will be dispatched
56
     * for any dispatched event of the specified resource.
57
     * If $resName and $event is *,
58
     * the listener will be dispatched
59
     * for any dispatched event for any resource.
60
     *
61
     * @param Closure $closure
62
     * @param String $resName
63
     * @param Mixed $event
64
     * @return string
65
     */
66 2
    public function subscribeClosure(
67
        Closure $closure,
68
        $resName = self::ANY,
69
        $eventName = self::ANY
70
    ): string {
71 2
        $listener = $this->closureToListener($closure);
72 2
        $hash = $listener->hash();
73 2
        $this->stack[$resName][$eventName][$hash] = $listener;
74 2
        return $hash;
75
    }
76
77
    /**
78
     * Unsubscribes the listener from the resource's events
79
     *
80
     * @param string $hash
81
     * @param String $resName
82
     * @param Mixed $event
83
     * @return boolean
84
     */
85 3
    public function unsubscribe(
86
        string $hash,
87
        $resName = self::ANY,
88
        $eventName = self::ANY
89
    ): bool {
90 3
        if (isset($this->stack[$resName][$eventName][$hash])) {
91 2
            unset($this->stack[$resName][$eventName][$hash]);
92 2
            return true;
93
        }
94 1
        return false;
95
    }
96
97
    /**
98
     * Publishes an event to all the listeners
99
     * listening to the specified event
100
     * for the specified resource
101
     *
102
     * @param EventInterface $event
103
     * @return Dispatcher
104
     */
105 1
    public function publish(EventInterface $event): DispatcherInterface
106
    {
107 1
        $resName = $event->getResourceName();
108 1
        $eventName = $event->getEventName();
109
        $this
110 1
            ->dispatchAllHandlers($event)
111 1
            ->dispatchResourcedHandlers($resName, $event)
112 1
            ->dispatchResourcedEventedHandlers($resName, $eventName, $event);
113 1
        return $this;
114
    }
115
116
    /**
117
     * dispatch to all handlers the wildcard handlers
118
     *
119
     * @param EventInterface $event
120
     * @return void
121
     */
122
    protected function dispatchAllHandlers(
123
        EventInterface $event
124
    ): DispatcherInterface {
125
        if (isset($this->stack[self::ANY][self::ANY])) {
126
            foreach ($this->stack[self::ANY][self::ANY] as $listener) {
127
                $listener->publish($event);
128
            }
129
        }
130
        return $this;
131
    }
132
133
    /**
134
     * dispatch to handlers identified by resource name
135
     * despite the event
136
     *
137
     * @param string $resName
138
     * @param EventInterface $event
139
     * @return void
140
     */
141
    protected function dispatchResourcedHandlers(
142
        string $resName,
143
        EventInterface $event
144
    ): DispatcherInterface {
145
        if (isset($this->stack[$resName][self::ANY])) {
146
            foreach ($this->stack[$resName][self::ANY] as $listener) {
147
                $listener->publish($event);
148
            }
149
        }
150
        return $this;
151
    }
152
153
    /**
154
     * dispatch to handlers identified by resource name and event name
155
     *
156
     * @param string $resName
157
     * @param EventInterface $event
158
     * @return void
159
     */
160
    protected function dispatchResourcedEventedHandlers(
161
        string $resName,
162
        string $eventName,
163
        EventInterface $event
164
    ): DispatcherInterface {
165
        if (isset($this->stack[$resName][$eventName])) {
166
            foreach ($this->stack[$resName][$eventName] as $listener) {
167
                $listener->publish($event);
168
            }
169
        }
170
        return $this;
171
    }
172
173
    /**
174
     * transform closure to listener
175
     *
176
     * @param Closure $closure
177
     * @return ListenerInterface
178
     */
179
    protected function closureToListener(Closure $closure): ListenerInterface
180
    {
181
        $listener = new class ($closure) extends ListenerAbstract implements ListenerInterface
182
        {
183
            const ERR_CLOSURE_ARG_MISSING = 'Listener closure required at least one Event argument';
184
            const ERR_CLOSURE_ARG_INVALID = 'Listener closure arg type should comply EventInterface';
185
186
            /**
187
             * listener as closure
188
             *
189
             * @var Closure
190
             */
191
            protected $closure;
192
193
            /**
194
             * instanciate
195
             *
196
             * @param Closure $closure
197
             */
198
            public function __construct(Closure $closure)
199
            {
200
                $params = $this->getClosureParameters($closure);
201
                if (count($params) === 0) {
202
                    throw new Exception(self::ERR_CLOSURE_ARG_MISSING);
203
                }
204
                $argTypeName = (version_compare(phpversion(), '7.1', '<'))
205
                    ? (string) $params[0]->getType()
206
                    : $params[0]->getType()->getName();
207
                if ($argTypeName !== EventInterface::class) {
208
                    throw new Exception(self::ERR_CLOSURE_ARG_INVALID);
209
                }
210
                $this->closure = $closure;
211
            }
212
213
            /**
214
             * publish
215
             *
216
             * @param EventInterface $event
217
             * @return void
218
             */
219
            public function publish(EventInterface $event)
220
            {
221
                call_user_func($this->closure, $event);
222
            }
223
224
            /**
225
             * return an array of reflexion parameter
226
             *
227
             * @see https://www.php.net/manual/fr/class.reflectionparameter.php
228
             *
229
             * @param Closure $closure
230
             * @return ReflectionParameter[]
231
             */
232
            protected function getClosureParameters(Closure $closure): array
233
            {
234
                $reflection = new ReflectionFunction($closure);
235
                return $reflection->getParameters();
236
            }
237
        };
238
        return $listener;
239
    }
240
}
241