Completed
Push — master ( 8a4be1...fe7d42 )
by Constantin
06:22
created

CommandDispatcher::applyCommandAndReturnEvents()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 15
cts 15
cp 1
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 16
nc 3
nop 2
crap 3
1
<?php
2
/******************************************************************************
3
 * Copyright (c) 2016 Constantin Galbenu <[email protected]>             *
4
 ******************************************************************************/
5
6
namespace Gica\Cqrs\Command;
7
8
9
use Gica\Cqrs\Aggregate\AggregateRepository;
10
use Gica\Cqrs\Command;
11
use Gica\Cqrs\Command\CommandDispatcher\AuthenticatedIdentityReaderService;
12
use Gica\Cqrs\Command\CommandDispatcher\ConcurrentProofFunctionCaller;
13
use Gica\Cqrs\Command\Exception\CommandValidationFailed;
14
use Gica\Cqrs\Command\ValueObject\CommandHandlerAndAggregate;
15
use Gica\Cqrs\Event;
16
use Gica\Cqrs\Event\EventDispatcher;
17
use Gica\Cqrs\Event\EventsApplier\EventsApplierOnAggregate;
18
use Gica\Cqrs\Event\EventSubscriber;
19
use Gica\Cqrs\Event\EventWithMetaData;
20
use Gica\Cqrs\Event\FutureEvent;
21
use Gica\Cqrs\Event\MetaData;
22
use Gica\Cqrs\FutureEventsStore;
23
24
class CommandDispatcher
25
{
26
    const MAXIMUM_SAVE_RETRIES = 50;
27
28
    /**
29
     * @var CommandSubscriber
30
     */
31
    private $commandSubscriber;
32
    /**
33
     * @var EventSubscriber
34
     */
35
    private $eventDispatcher;
36
    /**
37
     * @var CommandApplier
38
     */
39
    private $commandApplier;
40
    /**
41
     * @var AggregateRepository
42
     */
43
    private $aggregateRepository;
44
    /**
45
     * @var ConcurrentProofFunctionCaller
46
     */
47
    private $concurrentProofFunctionCaller;
48
    /**
49
     * @var CommandValidator
50
     */
51
    private $commandValidator;
52
    /**
53
     * @var AuthenticatedIdentityReaderService
54
     */
55
    private $authenticatedIdentityServiceReader;
56
    /**
57
     * @var FutureEventsStore
58
     */
59
    private $futureEventsStore;
60
    /**
61
     * @var \Gica\Cqrs\Event\EventsApplier\EventsApplierOnAggregate
62
     */
63
    private $eventsApplierOnAggregate;
64
65 5
    public function __construct(
66
        CommandSubscriber $commandSubscriber,
67
        EventDispatcher $eventDispatcher,
68
        CommandApplier $commandApplier,
69
        AggregateRepository $aggregateRepository,
70
        ConcurrentProofFunctionCaller $concurrentProofFunctionCaller,
71
        CommandValidator $commandValidator,
72
        AuthenticatedIdentityReaderService $authenticatedIdentityServiceReader,
73
        FutureEventsStore $futureEventsStore,
74
        EventsApplierOnAggregate $eventsApplierOnAggregate
75
    )
76
    {
77 5
        $this->commandSubscriber = $commandSubscriber;
78 5
        $this->eventDispatcher = $eventDispatcher;
0 ignored issues
show
Documentation Bug introduced by
It seems like $eventDispatcher of type object<Gica\Cqrs\Event\EventDispatcher> is incompatible with the declared type object<Gica\Cqrs\Event\EventSubscriber> of property $eventDispatcher.

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...
79 5
        $this->commandApplier = $commandApplier;
80 5
        $this->aggregateRepository = $aggregateRepository;
81 5
        $this->concurrentProofFunctionCaller = $concurrentProofFunctionCaller;
82 5
        $this->commandValidator = $commandValidator;
83 5
        $this->authenticatedIdentityServiceReader = $authenticatedIdentityServiceReader;
84 5
        $this->futureEventsStore = $futureEventsStore;
85 5
        $this->eventsApplierOnAggregate = $eventsApplierOnAggregate;
86 5
    }
87
88 4
    public function dispatchCommand(Command $command)
89
    {
90 4
        $errors = $this->commandValidator->validateCommand($command);
91
92 4
        if ($errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
93 1
            throw new CommandValidationFailed($errors);
94
        }
95
96
        /** @var EventWithMetaData[] $eventsWithMetaData */
97 3
        list($eventsWithMetaData, $futureEventsWithMetaData) = $this->concurrentProofFunctionCaller->executeFunction(function () use ($command) {
98 3
            return $this->tryDispatchCommandAndSaveAggregate($command);
99 3
        }, self::MAXIMUM_SAVE_RETRIES);
100
101 2
        foreach ($eventsWithMetaData as $eventWithMetaData) {
102 2
            $this->eventDispatcher->dispatchEvent($eventWithMetaData);
0 ignored issues
show
Bug introduced by
The method dispatchEvent() does not seem to exist on object<Gica\Cqrs\Event\EventSubscriber>.

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...
103
        }
104
105 2
        $this->futureEventsStore->scheduleEvents($futureEventsWithMetaData);
106 2
    }
107
108 3
    private function tryDispatchCommandAndSaveAggregate(Command $command)
109
    {
110 3
        $commandHandlerAndAggregate = $this->loadCommandHandlerAndAggregate($command);
111
112 3
        $eventsWithMetaData = $this->applyCommandAndReturnEvents($command, $commandHandlerAndAggregate);
113
114 2
        list($eventsForNowWithMetaData, $eventsForTheFutureWithMetaData) = $this->splitFutureEvents($eventsWithMetaData);
115
116 2
        $this->aggregateRepository->saveAggregate($command->getAggregateId(), $commandHandlerAndAggregate->getAggregate(), $eventsForNowWithMetaData);
117
118 2
        return [$eventsForNowWithMetaData, $eventsForTheFutureWithMetaData];
119
    }
120
121
    /**
122
     * @param EventWithMetaData[] $decoratedEvents
123
     * @return array
124
     */
125 2
    private function splitFutureEvents($decoratedEvents)
126
    {
127 2
        $nowEvents = [];
128 2
        $futureEvents = [];
129
130 2
        foreach ($decoratedEvents as $decoratedEvent) {
131 2
            if ($this->isFutureEvent($decoratedEvent->getEvent())) {
132 1
                $futureEvents[] = $decoratedEvent;
133
            } else {
134 2
                $nowEvents[] = $decoratedEvent;
135
            }
136
        }
137
138 2
        return [$nowEvents, $futureEvents];
139
    }
140
141 4
    public function canExecuteCommand(Command $command): bool
142
    {
143
        try {
144 4
            $errors = $this->commandValidator->validateCommand($command);
145 4
            if ($errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
146 1
                return false;
147
            }
148 3
            $commandHandlerAndAggregate = $this->loadCommandHandlerAndAggregate($command);
149 3
            $this->applyCommandAndReturnEvents($command, $commandHandlerAndAggregate);
150 2
            return true;
151 1
        } catch (\Exception $exception) {
152 1
            return false;
153
        }
154
    }
155
156 4
    private function loadCommandHandlerAndAggregate(Command $command): CommandHandlerAndAggregate
157
    {
158 4
        $handler = $this->commandSubscriber->getHandlerForCommand($command);
159
160 4
        $aggregate = $this->aggregateRepository->loadAggregate($handler->getHandlerClass(), $command->getAggregateId());
161
162 4
        return new CommandHandlerAndAggregate($handler, $aggregate);
163
    }
164
165 2
    private function decorateEventWithMetaData(Event $event, MetaData $metaData): EventWithMetaData
166
    {
167 2
        return new EventWithMetaData($event, $metaData);
168
    }
169
170
    /**
171
     * @param Command $command
172
     * @param CommandHandlerAndAggregate $handlerAndAggregate
173
     * @return EventWithMetaData[]
174
     */
175 4
    private function applyCommandAndReturnEvents(Command $command, CommandHandlerAndAggregate $handlerAndAggregate)
176
    {
177 4
        $aggregate = $handlerAndAggregate->getAggregate();
178 4
        $handler = $handlerAndAggregate->getCommandHandler();
179
180 4
        $metaData = new MetaData(
181 4
            $command->getAggregateId(),
182
            get_class($aggregate),
183 4
            new \DateTimeImmutable(),
184 4
            $this->authenticatedIdentityServiceReader->getAuthenticatedIdentityId());
185
186 4
        $newEventsGenerator = $this->commandApplier->applyCommand($aggregate, $command, $handler->getMethodName());
187
188
        /** @var EventWithMetaData[] $eventsWithMetaData */
189 4
        $eventsWithMetaData = [];
190
191 4
        foreach ($newEventsGenerator as $event) {
192 2
            $eventWithMetaData = $this->decorateEventWithMetaData($event, $metaData);
193
194 2
            if (!$this->isFutureEvent($event)) {
195 2
                $this->eventsApplierOnAggregate->applyEventsOnAggregate($aggregate, [$eventWithMetaData]);
196
            }
197
198 2
            $eventsWithMetaData[] = $eventWithMetaData;
199
        }
200
201 2
        return $eventsWithMetaData;
202
    }
203
204 2
    private function isFutureEvent($event): bool
205
    {
206 2
        return $event instanceof FutureEvent;
207
    }
208
}