Completed
Push — master ( 767a20...54f797 )
by Freek
11:42
created

EventProjectionist   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 11
dl 0
loc 280
rs 8.6
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A isReplayingEvents() 0 4 1
A addProjector() 0 12 2
A addProjectors() 0 8 1
A getProjectors() 0 4 1
A getProjector() 0 8 1
A addReactor() 0 12 2
A addReactors() 0 8 1
A getReactors() 0 4 1
A storeEvent() 0 8 1
A handle() 0 6 1
A handleImmediately() 0 8 1
B callEventHandlers() 0 46 6
B callEventHandler() 0 24 4
B replayEvents() 0 28 2
A guardAgainstInvalidEventHandler() 0 10 3
A instantiate() 0 10 2
A callMethod() 0 12 1
A alreadyAdded() 0 14 3
A getClassNames() 0 12 2
1
<?php
2
3
namespace Spatie\EventProjector;
4
5
use Exception;
6
use Illuminate\Support\Collection;
7
use Spatie\EventProjector\Models\StoredEvent;
8
use Spatie\EventProjector\Projectors\Projector;
9
use Spatie\EventProjector\EventHandlers\EventHandler;
10
use Spatie\EventProjector\Events\FinishedEventReplay;
11
use Spatie\EventProjector\Events\StartingEventReplay;
12
use Spatie\EventProjector\Exceptions\InvalidEventHandler;
13
use Spatie\EventProjector\Events\EventHandlerFailedHandlingEvent;
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 $config;
29
30
    public function __construct(array $config)
31
    {
32
        $this->projectors = collect();
33
34
        $this->reactors = collect();
35
36
        $this->config = $config;
0 ignored issues
show
Documentation Bug introduced by
It seems like $config of type array is incompatible with the declared type integer of property $config.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 storeEvent(ShouldBeStored $event)
108
    {
109
        $storedEvent = $this->config['stored_event_model']::createForEvent($event);
110
111
        $this->handleImmediately($storedEvent);
112
113
        dispatch(new HandleStoredEventJob($storedEvent))->onQueue($this->config['queue']);
114
    }
115
116
    public function handle(StoredEvent $storedEvent)
117
    {
118
        $this
119
            ->callEventHandlers($this->projectors, $storedEvent)
120
            ->callEventHandlers($this->reactors, $storedEvent);
121
    }
122
123
    public function handleImmediately(StoredEvent $storedEvent)
124
    {
125
        $projectors = $this->instantiate($this->projectors);
126
127
        $projectors = $projectors->filter->shouldBeCalledImmediately();
128
129
        $this->callEventHandlers($projectors, $storedEvent);
130
    }
131
132
    protected function callEventHandlers(Collection $eventHandlers, StoredEvent $storedEvent): self
133
    {
134
        $eventHandlers
135
            ->pipe(function (Collection $eventHandlers) {
136
                return $this->instantiate($eventHandlers);
137
            })
138
            ->filter(function (EventHandler $eventHandler) use ($storedEvent) {
139
                if (! $eventHandler instanceof Projector) {
140
                    return true;
141
                }
142
143
                $event = $storedEvent->event;
0 ignored issues
show
Unused Code introduced by
$event is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
144
145
                /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
146
                if (! $method = $eventHandler->methodNameThatHandlesEvent($event)) {
147
                    return false;
148
                }
149
150
                if (! method_exists($eventHandler, $method)) {
151
                    throw InvalidEventHandler::eventHandlingMethodDoesNotExist($eventHandler, $event, $method);
152
                }
153
                */
154
155
                return true;
156
            })
157
            ->filter(function (EventHandler $eventHandler) use ($storedEvent) {
158
                if ($eventHandler instanceof Projector) {
159
                    if (! $eventHandler->hasReceivedAllPriorEvents($storedEvent)) {
160
                        event(new ProjectorDidNotHandlePriorEvents($eventHandler, $storedEvent));
161
162
                        return false;
163
                    }
164
                }
165
166
                return true;
167
            })
168
            ->each(function (EventHandler $eventHandler) use ($storedEvent) {
169
                $eventWasHandledSuccessfully = $this->callEventHandler($eventHandler, $storedEvent);
170
171
                if ($eventHandler instanceof Projector && $eventWasHandledSuccessfully) {
172
                    $eventHandler->rememberReceivedEvent($storedEvent);
173
                }
174
            });
175
176
        return $this;
177
    }
178
179
    protected function callEventHandler(EventHandler $eventHandler, StoredEvent $storedEvent): bool
180
    {
181
        $event = $storedEvent->event;
182
183
        if (! $method = $eventHandler->methodNameThatHandlesEvent($event)) {
184
            return true;
185
        }
186
187
        try {
188
            app()->call([$eventHandler, $method], compact('event', 'storedEvent'));
189
        } catch (Exception $exception) {
190
            if (! $this->config['catch_exceptions']) {
191
                throw $exception;
192
            }
193
194
            $eventHandler->handleException($exception);
195
196
            event(new EventHandlerFailedHandlingEvent($eventHandler, $storedEvent, $exception));
197
198
            return false;
199
        }
200
201
        return true;
202
    }
203
204
    public function replayEvents(Collection $projectors, int $afterStoredEventId = 0, callable $onEventReplayed = null)
205
    {
206
        $this->isReplayingEvents = true;
207
208
        event(new StartingEventReplay($projectors));
209
210
        $projectors = $this->instantiate($projectors);
211
212
        $this->callMethod($projectors, 'onStartingEventReplay');
213
214
        StoredEvent::query()
215
            ->after($afterStoredEventId ?? 0)
216
            ->chunk($this->config['replay_chunk_size'], function (Collection $storedEvents) use ($projectors, $onEventReplayed) {
217
                $storedEvents->each(function (StoredEvent $storedEvent) use ($projectors, $onEventReplayed) {
218
                    $this->callEventHandlers($projectors, $storedEvent);
219
220
                    if ($onEventReplayed) {
221
                        $onEventReplayed($storedEvent);
222
                    }
223
                });
224
            });
225
226
        $this->isReplayingEvents = false;
227
228
        event(new FinishedEventReplay());
229
230
        $this->callMethod($projectors, 'onFinishedEventReplay');
231
    }
232
233
    protected function guardAgainstInvalidEventHandler($eventHandler)
234
    {
235
        if (! is_string($eventHandler)) {
236
            return;
237
        }
238
239
        if (! class_exists($eventHandler)) {
240
            throw InvalidEventHandler::doesNotExist($eventHandler);
241
        }
242
    }
243
244
    protected function instantiate(Collection $eventHandlers)
245
    {
246
        return $eventHandlers->map(function ($eventHandler) {
247
            if (is_string($eventHandler)) {
248
                $eventHandler = app($eventHandler);
249
            }
250
251
            return $eventHandler;
252
        });
253
    }
254
255
    protected function callMethod(Collection $eventHandlers, string $method): self
256
    {
257
        $eventHandlers
258
            ->filter(function (EventHandler $eventHandler) use ($method) {
259
                return method_exists($eventHandler, $method);
260
            })
261
            ->each(function (EventHandler $eventHandler) use ($method) {
262
                return app()->call([$eventHandler, $method]);
263
            });
264
265
        return $this;
266
    }
267
268
    protected function alreadyAdded(string $type, $eventHandler)
269
    {
270
        $eventHandlerClassName = is_string($eventHandler)
271
            ? $eventHandler
272
            : get_class($eventHandler);
273
274
        $variableName = "{$type}s";
275
276
        $currentEventHandlers = $this->$variableName->toArray();
277
278
        if (in_array($eventHandlerClassName, $currentEventHandlers)) {
279
            return $this;
280
        }
281
    }
282
283
    protected function getClassNames(Collection $eventHandlers): array
284
    {
285
        return $eventHandlers
286
            ->map(function ($eventHandler) {
287
                if (is_string($eventHandler)) {
288
                    return $eventHandler;
289
                }
290
291
                return get_class($eventHandler);
292
            })
293
            ->toArray();
294
    }
295
}
296