Completed
Push — master ( 50c098...015012 )
by Freek
04:38
created

EventProjectionist::addProjectors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
namespace Spatie\EventProjector;
4
5
use Exception;
6
use Illuminate\Support\Collection;
7
use Spatie\EventProjector\Events\ProjectorFailedHandlingEvent;
8
use Spatie\EventProjector\Models\StoredEvent;
9
use Spatie\EventProjector\Projectors\Projector;
10
use Spatie\EventProjector\EventHandlers\EventHandler;
11
use Spatie\EventProjector\Events\FinishedEventReplay;
12
use Spatie\EventProjector\Events\StartingEventReplay;
13
use Spatie\EventProjector\Exceptions\InvalidEventHandler;
14
use Spatie\EventProjector\Events\ProjectorDidNotHandlePriorEvents;
15
16
class EventProjectionist
17
{
18
    /** @var \Illuminate\Support\Collection */
19
    protected $projectors;
20
21
    /** @var \Illuminate\Support\Collection */
22
    protected $reactors;
23
24
    /** @var bool */
25
    protected $isReplayingEvents = false;
26
27
    /** @var int */
28
    protected $replayChunkSize;
29
30
    public function __construct(array $config)
31
    {
32
        $this->projectors = collect();
33
34
        $this->reactors = collect();
35
36
        $this->replayChunkSize = $config['replay_chunk_size'] ?? 1000;
37
    }
38
39
    public function isReplayingEvents(): bool
40
    {
41
        return $this->isReplayingEvents;
42
    }
43
44
    public function addProjector($projector): self
45
    {
46
        $this->guardAgainstInvalidEventHandler($projector);
47
48
        if ($this->alreadyAdded('projector', $projector)) {
49
            return $this;
50
        }
51
52
        $this->projectors->push($projector);
53
54
        return $this;
55
    }
56
57
    public function addProjectors(array $projectors): self
58
    {
59
        collect($projectors)->each(function ($projector) {
60
            $this->addProjector($projector);
61
        });
62
63
        return $this;
64
    }
65
66
    public function getProjectors(): Collection
67
    {
68
        return $this->projectors;
69
    }
70
71
    public function getProjector(string $name): ?Projector
72
    {
73
        return $this
74
            ->instantiate($this->projectors)
75
            ->first(function (Projector $projector) use ($name) {
76
                return $projector->getName() === $name;
77
            });
78
    }
79
80
    public function addReactor($reactor): self
81
    {
82
        $this->guardAgainstInvalidEventHandler($reactor);
83
84
        if ($this->alreadyAdded('reactor', $reactor)) {
85
            return $this;
86
        }
87
88
        $this->reactors->push($reactor);
89
90
        return $this;
91
    }
92
93
    public function addReactors(array $reactors): self
94
    {
95
        collect($reactors)->each(function ($reactor) {
96
            $this->addReactor($reactor);
97
        });
98
99
        return $this;
100
    }
101
102
    public function getReactors(): Collection
103
    {
104
        return $this->reactors;
105
    }
106
107
    public function handle(StoredEvent $storedEvent)
108
    {
109
        $this
110
            ->callEventHandlers($this->projectors, $storedEvent)
111
            ->callEventHandlers($this->reactors, $storedEvent);
112
    }
113
114
    public function handleImmediately(StoredEvent $storedEvent)
115
    {
116
        $projectors = $this->instantiate($this->projectors);
117
118
        $projectors = $projectors->filter->shouldBeCalledImmediately();
119
120
        $this->callEventHandlers($projectors, $storedEvent);
121
    }
122
123
    protected function callEventHandlers(Collection $eventHandlers, StoredEvent $storedEvent): self
124
    {
125
        $eventHandlers
126
            ->pipe(function (Collection $eventHandler) {
127
                return $this->instantiate($eventHandler);
128
            })
129
            ->filter(function (EventHandler $eventHandler) use ($storedEvent) {
130
                if ($eventHandler instanceof Projector) {
131
                    if (! $eventHandler->hasReceivedAllPriorEvents($storedEvent)) {
132
                        event(new ProjectorDidNotHandlePriorEvents($eventHandler, $storedEvent));
133
134
                        return false;
135
                    }
136
                }
137
138
                return true;
139
            })
140
            ->each(function (EventHandler $eventHandler) use ($storedEvent) {
141
                $eventWasHandledSuccessfully = $this->callEventHandler($eventHandler, $storedEvent);
142
143
                if ($eventHandler instanceof Projector && $eventWasHandledSuccessfully) {
144
                    $eventHandler->rememberReceivedEvent($storedEvent);
145
                }
146
            });
147
148
        return $this;
149
    }
150
151
    protected function callEventHandler(EventHandler $eventHandler, StoredEvent $storedEvent): bool
152
    {
153
        if (! isset($eventHandler->handlesEvents)) {
0 ignored issues
show
Bug introduced by
Accessing handlesEvents on the interface Spatie\EventProjector\EventHandlers\EventHandler suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
154
            throw InvalidEventHandler::cannotHandleEvents($eventHandler);
155
        }
156
157
        $event = $storedEvent->event;
158
159
        if (! $method = $eventHandler->methodNameThatHandlesEvent($event)) {
160
            return true;
161
        }
162
163
        if (! method_exists($eventHandler, $method)) {
164
            throw InvalidEventHandler::eventHandlingMethodDoesNotExist($eventHandler, $event, $method);
0 ignored issues
show
Documentation introduced by
$eventHandler is of type object<Spatie\EventProje...tHandlers\EventHandler>, but the function expects a object<Spatie\EventProjector\Exceptions\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
165
        }
166
167
        try {
168
            app()->call([$eventHandler, $method], compact('event', 'storedEvent'));
169
        }
170
        catch (Exception $exception) {
171
            $eventHandler->handleException($exception);
0 ignored issues
show
Bug introduced by
The method handleException() does not seem to exist on object<Spatie\EventProje...tHandlers\EventHandler>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
172
173
            event(new ProjectorFailedHandlingEvent($eventHandler, $storedEvent, $exception));
0 ignored issues
show
Compatibility introduced by
$eventHandler of type object<Spatie\EventProje...tHandlers\EventHandler> is not a sub-type of object<Spatie\EventProje...r\Projectors\Projector>. It seems like you assume a child interface of the interface Spatie\EventProjector\EventHandlers\EventHandler to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
174
175
            return false;
176
        }
177
178
        return true;
179
    }
180
181
    public function replayEvents(Collection $projectors, int $afterStoredEventId, callable $onEventReplayed = null)
182
    {
183
        $this->isReplayingEvents = true;
184
185
        event(new StartingEventReplay($projectors));
186
187
        $projectors = $this->instantiate($projectors);
188
189
        $this->callMethod($projectors, 'onStartingEventReplay');
190
191
        StoredEvent::query()
192
            ->after($afterStoredEventId ?? 0)
193
            ->chunk($this->replayChunkSize, function (Collection $storedEvents) use ($projectors, $onEventReplayed) {
194
                $storedEvents->each(function (StoredEvent $storedEvent) use ($projectors, $onEventReplayed) {
195
                    $this->callEventHandlers($projectors, $storedEvent);
196
197
                    if ($onEventReplayed) {
198
                        $onEventReplayed($storedEvent);
199
                    }
200
                });
201
            });
202
203
        $this->isReplayingEvents = false;
204
205
        event(new FinishedEventReplay());
206
207
        $this->callMethod($projectors, 'onFinishedEventReplay');
208
    }
209
210
    protected function guardAgainstInvalidEventHandler($eventHandler)
211
    {
212
        if (! is_string($eventHandler)) {
213
            return;
214
        }
215
216
        if (! class_exists($eventHandler)) {
217
            throw InvalidEventHandler::doesNotExist($eventHandler);
218
        }
219
    }
220
221
    protected function instantiate(Collection $eventHandlers)
222
    {
223
        return $eventHandlers->map(function ($eventHandler) {
224
            if (is_string($eventHandler)) {
225
                $eventHandler = app($eventHandler);
226
            }
227
228
            return $eventHandler;
229
        });
230
    }
231
232
    protected function callMethod(Collection $eventHandlers, string $method): self
233
    {
234
        $eventHandlers
235
            ->filter(function (EventHandler $eventHandler) use ($method) {
236
                return method_exists($eventHandler, $method);
237
            })
238
            ->each(function (EventHandler $eventHandler) use ($method) {
239
                return app()->call([$eventHandler, $method]);
240
            });
241
242
        return $this;
243
    }
244
245
    protected function alreadyAdded(string $type, $eventHandler)
246
    {
247
        $eventHandlerClassName = is_string($eventHandler)
248
            ? $eventHandler
249
            : get_class($eventHandler);
250
251
        $variableName = "{$type}s";
252
253
        $currentEventHandlers = $this->$variableName->toArray();
254
255
        if (in_array($eventHandlerClassName, $currentEventHandlers)) {
256
            return $this;
257
        }
258
    }
259
260
    protected function getClassNames(Collection $eventHandlers): array
261
    {
262
        return $eventHandlers
263
            ->map(function ($eventHandler) {
264
                if (is_string($eventHandler)) {
265
                    return $eventHandler;
266
                }
267
268
                return get_class($eventHandler);
269
            })
270
            ->toArray();
271
    }
272
}
273