AbstractSagaManager::handleInvokationException()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 18
ccs 12
cts 12
cp 1
rs 9.4286
cc 2
eloc 12
nc 2
nop 3
crap 2
1
<?php
2
3
/*
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * The software is based on the Axon Framework project which is
17
 * licensed under the Apache 2.0 license. For more information on the Axon Framework
18
 * see <http://www.axonframework.org/>.
19
 * 
20
 * This software consists of voluntary contributions made by many individuals
21
 * and is licensed under the MIT license. For more information, see
22
 * <http://www.governor-framework.org/>.
23
 */
24
25
namespace Governor\Framework\Saga;
26
27
use Governor\Framework\Common\Logging\NullLogger;
28
use Psr\Log\LoggerInterface;
29
use Psr\Log\LoggerAwareInterface;
30
use Governor\Framework\Correlation\CorrelationDataHolder;
31
use Governor\Framework\Correlation\CorrelationDataProviderInterface;
32
use Governor\Framework\Correlation\SimpleCorrelationDataProvider;
33
use Governor\Framework\Correlation\MultiCorrelationDataProvider;
34
use Governor\Framework\Domain\EventMessageInterface;
35
36
/**
37
 * Base SagaManagerInterface implementation.
38
 *
39
 * @author    "David Kalosi" <[email protected]>
40
 * @license   <a href="http://www.opensource.org/licenses/mit-license.php">MIT License</a>
41
 */
42
abstract class AbstractSagaManager implements SagaManagerInterface, LoggerAwareInterface
43
{
44
45
    /**
46
     * @var SagaRepositoryInterface
47
     */
48
    private $sagaRepository;
49
50
    /**
51
     * @var SagaFactoryInterface
52
     */
53
    private $sagaFactory;
54
55
    /**
56
     * @var array
57
     */
58
    private $sagaTypes = [];
59
60
    /**
61
     * @var boolean
62
     */
63
    private $suppressExceptions = true;
64
65
    /**
66
     * @var LoggerInterface
67
     */
68
    private $logger;
69
70
    /**
71
     * @var CorrelationDataProviderInterface
72
     */
73
    private $correlationDataProvider;
74
75
    /**
76
     * @param SagaRepositoryInterface $sagaRepository
77
     * @param SagaFactoryInterface $sagaFactory
78
     * @param array $sagaTypes
79
     */
80 18
    public function __construct(
81
        SagaRepositoryInterface $sagaRepository,
82
        SagaFactoryInterface $sagaFactory,
83
        array $sagaTypes = []
84
    ) {
85 18
        $this->sagaRepository = $sagaRepository;
86 18
        $this->sagaFactory = $sagaFactory;
87 18
        $this->sagaTypes = $sagaTypes;
88 18
        $this->correlationDataProvider = new SimpleCorrelationDataProvider();
89 18
        $this->logger = new NullLogger();
90 18
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 18
    public function handle(EventMessageInterface $event)
96
    {
97 18
        foreach ($this->sagaTypes as $sagaType) {
98 18
            $associationValues = $this->extractAssociationValues(
99 18
                $sagaType,
100
                $event
101 18
            );
102
103 18
            if (null !== $associationValues && !empty($associationValues)) {
104 16
                $sagaOfTypeInvoked = $this->invokeExistingSagas(
105 16
                    $event,
106 16
                    $sagaType,
107
                    $associationValues
108 16
                );
109 15
                $initializationPolicy = $this->getSagaCreationPolicy(
110 15
                    $sagaType,
111
                    $event
112 15
                );
113 15
                if ($initializationPolicy->getCreationPolicy() === SagaCreationPolicy::ALWAYS
114 15
                    || (!$sagaOfTypeInvoked && $initializationPolicy->getCreationPolicy()
115 13
                        === SagaCreationPolicy::IF_NONE_FOUND)
116 15
                ) {
117 11
                    $this->startNewSaga(
118 11
                        $event,
119 11
                        $sagaType,
120 11
                        $initializationPolicy->getInitialAssociationValue()
0 ignored issues
show
Bug introduced by
It seems like $initializationPolicy->g...itialAssociationValue() can be null; however, startNewSaga() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
121 11
                    );
122 11
                }
123 15
            }
124 17
        }
125 17
    }
126
127 9
    private function containsAny(
128
        AssociationValuesInterface $associationValues,
129
        array $toFind
130
    ) {
131 9
        foreach ($toFind as $valueToFind) {
132 9
            if ($associationValues->contains($valueToFind)) {
133 9
                return true;
134
            }
135
        }
136
137
        return false;
138
    }
139
140 11
    private function startNewSaga(
141
        EventMessageInterface $event,
142
        $sagaType,
143
        AssociationValue $associationValue
144
    ) {
145 11
        $newSaga = $this->sagaFactory->createSaga($sagaType);
146 11
        $newSaga->getAssociationValues()->add($associationValue);
147 11
        $this->preProcessSaga($newSaga);
148
149
        try {
150 11
            $this->doInvokeSaga($event, $newSaga);
151 11
        } finally {
152 11
            $this->sagaRepository->add($newSaga);
153
        }
154 11
    }
155
156 16
    private function invokeExistingSagas(
157
        EventMessageInterface $event,
158
        $sagaType,
159
        $associationValues
160
    ) {
161 16
        $sagas = [];
162
163 16
        foreach ($associationValues as $associationValue) {
164 16
            $sagas = $this->sagaRepository->find($sagaType, $associationValue);
165 16
        }
166
167 16
        $sagaOfTypeInvoked = false;
168
169 16
        foreach ($sagas as $sagaId) {
170 9
            $saga = $this->loadAndInvoke($event, $sagaId, $associationValues);
171
172 8
            if (null !== $saga) {
173 8
                $sagaOfTypeInvoked = true;
174 8
            }
175 15
        }
176
177 15
        return $sagaOfTypeInvoked;
178
    }
179
180 9
    private function loadAndInvoke(
181
        EventMessageInterface $event,
182
        $sagaId,
183
        array $associations
184
    ) {
185 9
        $saga = $this->sagaRepository->load($sagaId);
186
187 9
        if (null === $saga || !$saga->isActive() || !$this->containsAny(
188 9
                $saga->getAssociationValues(),
189
                $associations
190 9
            )
191 9
        ) {
192 2
            return null;
193
        }
194
195 9
        $this->preProcessSaga($saga);
196 9
        $exception = null;
197
198
        try {
199 9
            $this->logger->info(
200 9
                "Saga {saga} is handling event {event}",
201
                [
202 9
                    'saga' => $sagaId,
203 9
                    'event' => $event->getPayloadType()
204 9
                ]
205 9
            );
206 9
            $saga->handle($event);
207 9
        } catch (\Exception $ex) {
208 2
            $exception = $ex;
209 9
        } finally {
210 9
            $this->logger->info(
211 9
                "Saga {saga} is committing event {event}",
212
                [
213 9
                    'saga' => $sagaId,
214 9
                    'event' => $event->getPayloadType()
215 9
                ]
216 9
            );
217 9
            $this->commit($saga);
218
        }
219
220 9
        if (null !== $exception) {
221 2
            $this->handleInvokationException($exception, $event, $saga);
222 1
        }
223
224 8
        return $saga;
225
    }
226
227 11
    private function doInvokeSaga(
228
        EventMessageInterface $event,
229
        SagaInterface $saga
230
    ) {
231
        try {
232 11
            CorrelationDataHolder::setCorrelationData($this->correlationDataProvider->correlationDataFor($event));
233 11
            $saga->handle($event);
234 11
        } catch (\RuntimeException $ex) {
235
            $this->handleInvokationException($ex, $event, $saga);
236
        }
237 11
    }
238
239 2
    private function handleInvokationException(
240
        \Exception $ex,
241
        EventMessageInterface $event,
242
        SagaInterface $saga
243
    ) {
244 2
        if ($this->suppressExceptions) {
245 1
            $this->logger->error(
246 1
                "An exception occurred while a Saga {name} was handling an Event {event}: {exception}",
247
                [
248 1
                    'name' => get_class($saga),
249 1
                    'event' => $event->getPayloadType(),
250 1
                    'exception' => $ex->getMessage()
251 1
                ]
252 1
            );
253 1
        } else {
254 1
            throw $ex;
255
        }
256 1
    }
257
258
    /**
259
     * Commits the given <code>saga</code> to the registered repository.
260
     *
261
     * @param SagaInterface $saga the Saga to commit.
262
     */
263 9
    protected function commit(SagaInterface $saga)
264
    {
265 9
        $this->sagaRepository->commit($saga);
266 9
    }
267
268
    /**
269
     * Perform pre-processing of sagas that have been newly created or have been loaded from the repository. This
270
     * method is invoked prior to invocation of the saga instance itself.
271
     *
272
     * @param SagaInterface $saga The saga instance for preprocessing
273
     */
274 3
    protected function preProcessSaga(SagaInterface $saga)
275
    {
276
277 3
    }
278
279
    /**
280
     * Returns the Saga Initialization Policy for a Saga of the given <code>sagaType</code> and <code>event</code>.
281
     * This policy provides the conditions to create new Saga instance, as well as the initial association of that
282
     * saga.
283
     *
284
     * @param string $sagaType The type of Saga to get the creation policy for
285
     * @param EventMessageInterface $event The Event that is being dispatched to Saga instances
286
     * @return SagaInitializationPolicy the initialization policy for the Saga
287
     */
288
    abstract protected function getSagaCreationPolicy(
289
        $sagaType,
290
        EventMessageInterface $event
291
    );
292
293
    /**
294
     * Extracts the AssociationValues from the given <code>event</code> as relevant for a Saga of given
295
     * <code>sagaType</code>. A single event may be associated with multiple values.
296
     *
297
     * @param string $sagaType The type of Saga about to handle the Event
298
     * @param EventMessageInterface $event The event containing the association information
299
     * @return array the AssociationValues indicating which Sagas should handle given event
300
     */
301
    abstract protected function extractAssociationValues(
302
        $sagaType,
303
        EventMessageInterface $event
304
    );
305
306
    /**
307
     * Sets whether or not to suppress any exceptions that are cause by invoking Sagas. When suppressed, exceptions are
308
     * logged. Defaults to <code>true</code>.
309
     *
310
     * @param boolean $suppressExceptions whether or not to suppress exceptions from Sagas.
311
     */
312 6
    public function setSuppressExceptions($suppressExceptions)
313
    {
314 6
        $this->suppressExceptions = $suppressExceptions;
315 6
    }
316
317
318
    /**
319
     * @param LoggerInterface $logger
320
     * @return null
321
     */
322 3
    public function setLogger(LoggerInterface $logger)
323
    {
324 3
        $this->logger = $logger;
325 3
    }
326
327
    /**
328
     * @return array
329
     */
330
    public function getManagedSagaTypes()
331
    {
332
        return $this->sagaTypes;
333
    }
334
335
    /**
336
     * Sets the correlation data provider for this SagaManager. It will provide the data to attach to messages sent by
337
     * Sagas managed by this manager.
338
     *
339
     * @param CorrelationDataProviderInterface $correlationDataProvider the correlation data provider for this SagaManager
340
     */
341 1
    public function setCorrelationDataProvider(CorrelationDataProviderInterface $correlationDataProvider)
342
    {
343 1
        $this->correlationDataProvider = $correlationDataProvider;
344 1
    }
345
346
    /**
347
     * Sets the given <code>correlationDataProviders</code>. Each will provide data to attach to messages sent by Sagas
348
     * managed by this manager. When multiple providers provide different values for the same key, the latter provider
349
     * will overwrite any values set earlier.
350
     *
351
     * @param array $correlationDataProviders the correlation data providers for this SagaManager
352
     */
353 1
    public function setCorrelationDataProviders(array $correlationDataProviders)
354
    {
355 1
        $this->correlationDataProvider = new MultiCorrelationDataProvider($correlationDataProviders);
356 1
    }
357
358
}
359