Completed
Push — master ( 8f8c03...b08b3c )
by Constantin
03:24
created

BddAggregateTestHelper::isClassOrSubClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 2
crap 2
1
<?php
2
/******************************************************************************
3
 * Copyright (c) 2016 Constantin Galbenu <[email protected]>             *
4
 ******************************************************************************/
5
6
namespace Gica\Cqrs\Testing;
7
8
9
use Gica\Cqrs\Command;
10
use Gica\Cqrs\Command\CommandApplier;
11
use Gica\Cqrs\Command\CommandSubscriber;
12
use Gica\Cqrs\Event;
13
use Gica\Cqrs\Event\EventDispatcher;
14
use Gica\Cqrs\Event\EventDispatcher\EventDispatcherBySubscriber;
15
use Gica\Cqrs\Event\EventsApplier\EventsApplierOnAggregate;
16
use Gica\Cqrs\Event\EventSubscriber\ManualEventSubscriber;
17
use Gica\Cqrs\Event\EventWithMetaData;
18
use Gica\Cqrs\Event\MetaData;
19
use Gica\Cqrs\Testing\Exceptions\ExpectedEventNotYielded;
20
use Gica\Cqrs\Testing\Exceptions\NoExceptionThrown;
21
use Gica\Cqrs\Testing\Exceptions\TooManyEventsFired;
22
use Gica\Cqrs\Testing\Exceptions\WrongEventClassYielded;
23
use Gica\Cqrs\Testing\Exceptions\WrongExceptionClassThrown;
24
use Gica\Cqrs\Testing\Exceptions\WrongExceptionMessageWasThrown;
25
use Gica\Types\Guid;
26
27
class BddAggregateTestHelper
28
{
29
    private $aggregateId;
30
31
    /** @var EventDispatcher */
32
    private $eventDispatcher;
33
34
    private $priorEvents = [];
35
36
    /** @var Command */
37
    private $command;
38
    private $aggregate;
39
40
    /** @var EventsApplierOnAggregate */
41
    private $eventsApplierOnAggregate;
42
43
    /** @var CommandApplier */
44
    private $commandApplier;
45
    /**
46
     * @var CommandSubscriber
47
     */
48
    private $commandSubscriber;
49
50 9
    public function __construct(
51
        CommandSubscriber $commandSubscriber
52
    )
53
    {
54 9
        $this->commandSubscriber = $commandSubscriber;
55 9
        $this->eventDispatcher = new EventDispatcherBySubscriber(new ManualEventSubscriber());
56 9
        $this->eventsApplierOnAggregate = new EventsApplierOnAggregate();
57 9
        $this->commandApplier = new CommandApplier();
58
59 9
        $this->priorEvents = [];
60 9
        $this->command = null;
61 9
    }
62
63 8
    public function getCommandSubscriber(): CommandSubscriber
64
    {
65 8
        return $this->commandSubscriber;
66
    }
67
68 9
    public function onAggregate($aggregate)
69
    {
70 9
        $this->aggregate = $aggregate;
71 9
        $this->aggregateId = 123;
72 9
    }
73
74 9
    public function given(...$priorEvents)
75
    {
76 9
        $this->priorEvents = $this->decorateEventsWithMetadata($priorEvents);
77 9
    }
78
79
    /**
80
     * @param Event[] $priorEvents
81
     * @return EventWithMetaData[]
82
     */
83
    private function decorateEventsWithMetadata(array $priorEvents)
84
    {
85 9
        return array_map(function (Event $event) {
86 9
            return $this->decorateEventWithMetaData($event);
87 9
        }, $priorEvents);
88
    }
89
90 8
    public function when($command)
91
    {
92 8
        $this->command = $command;
93 8
    }
94
95 4
    public function then(...$expectedEvents)
96
    {
97 4
        $this->eventsApplierOnAggregate->applyEventsOnAggregate($this->aggregate, $this->priorEvents);
98
99 4
        $newEvents = $this->executeCommand($this->command);
100
101 4
        $this->assertTheseEvents($expectedEvents, $newEvents);
102 1
    }
103
104
    /**
105
     * @param Command|null $command
106
     * @return array
107
     * @throws \Exception
108
     */
109 4
    public function executeCommand($command)
110
    {
111 4
        $this->checkCommand($command);
112
113 4
        $handler = $this->getCommandSubscriber()->getHandlerForCommand($command);
0 ignored issues
show
Bug introduced by
It seems like $command defined by parameter $command on line 109 can be null; however, Gica\Cqrs\Command\Comman...:getHandlerForCommand() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
114
115 4
        $newEventsGenerator = $this->commandApplier->applyCommand($this->aggregate, $command, $handler->getMethodName());
0 ignored issues
show
Bug introduced by
It seems like $command defined by parameter $command on line 109 can be null; however, Gica\Cqrs\Command\CommandApplier::applyCommand() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
116
117
        /** @var EventWithMetaData[] $eventsWithMetaData */
118 4
        $eventsWithMetaData = [];
119
120 4
        $newEvents = [];
121
122 4 View Code Duplication
        foreach ($newEventsGenerator as $event) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
123 4
            $eventWithMetaData = $this->decorateEventWithMetaData($event);
124
125 4
            $this->eventsApplierOnAggregate->applyEventsOnAggregate($this->aggregate, [$eventWithMetaData]);
126
127 4
            $eventsWithMetaData[] = $eventWithMetaData;
128 4
            $newEvents[] = $event;
129
        }
130
131 4
        foreach ($eventsWithMetaData as $eventWithMetaData) {
132 4
            $this->eventDispatcher->dispatchEvent($eventWithMetaData);
133
        }
134
135 4
        return $newEvents;
136
    }
137
138 9
    private function decorateEventWithMetaData(Event $event): EventWithMetaData
139
    {
140 9
        return new EventWithMetaData($event, $this->factoryMetaData());
141
    }
142
143 5
    public function thenShouldFailWith($expectedExceptionClass, $expectedExceptionMessage = null)
144
    {
145 5
        $this->checkCommand($this->command);
146
147
        try {
148 4
            $handler = $this->getCommandSubscriber()->getHandlerForCommand($this->command);
149
150 4
            $this->eventsApplierOnAggregate->applyEventsOnAggregate($this->aggregate, $this->priorEvents);
151
152 4
            iterator_to_array(
153 4
                $this->commandApplier->applyCommand(
154 4
                    $this->aggregate, $this->command, $handler->getMethodName()));
155
156 1
            throw new NoExceptionThrown(
157 1
                sprintf("Exception '%s' was is expected, but none was thrown", $expectedExceptionClass));
158
159 4
        } catch (\Throwable $thrownException) {
160
161 4
            if ($thrownException instanceof NoExceptionThrown) {
162 1
                throw $thrownException;//rethrown
163
            }
164
165 3
            if (!$this->isClassOrSubClass($expectedExceptionClass, $thrownException)) {
166 1
                throw new WrongExceptionClassThrown(
167
                    sprintf(
168 1
                        "Exception '%s' was expected, but '%s(%s)' was thrown",
169
                        $expectedExceptionClass,
170
                        get_class($thrownException),
171 1
                        $thrownException->getMessage()));
172
            }
173
174 2
            if ($expectedExceptionMessage && $thrownException->getMessage() != $expectedExceptionMessage) {
175 1
                throw new WrongExceptionMessageWasThrown(
176
                    sprintf(
177 1
                        "Exception with message '%s' was expected, but '%s' was thrown",
178
                        $expectedExceptionMessage,
179 1
                        $thrownException->getMessage()));
180
            }
181
182
        }
183 1
    }
184
185 4
    public function assertTheseEvents(array $expectedEvents, array $actualEvents)
186
    {
187 4
        $expectedEvents = array_values($expectedEvents);
188 4
        $actualEvents = array_values($actualEvents);
189
190 4
        $this->checkForToFewEvents($expectedEvents, $actualEvents);
191 2
        $this->checkForToManyEvents(count($actualEvents) - count($expectedEvents));
192 1
    }
193
194 4
    private function checkForToFewEvents(array $expectedEvents, array $actualEvents)
195
    {
196 4
        foreach ($expectedEvents as $k => $expectedEvent) {
197 4
            if (!isset($actualEvents[$k])) {
198 1
                throw new ExpectedEventNotYielded(
199 1
                    "Expected event no. $k not fired (should have class: " . get_class($expectedEvent) . ")");
200
            }
201
202 4
            $actualEvent = $actualEvents[$k];
203
204 4
            if ($this->hashEvent($expectedEvent) != $this->hashEvent($actualEvent)) {
205 1
                throw new WrongEventClassYielded(
206 4
                    "Wrong event no. {$k} of class " . get_class($expectedEvent) . " emitted");
207
            }
208
        }
209 2
    }
210
211 2
    private function checkForToManyEvents(int $additionalCount)
212
    {
213 2
        if ($additionalCount > 0) {
214 1
            throw new TooManyEventsFired(
215 1
                sprintf("Additional %d events fired", $additionalCount));
216
        }
217 1
    }
218
219 4
    public function hashEvent($event)
220
    {
221 4
        return array_merge(['___class' => get_class($event)], (array)($event));
222
    }
223
224 9
    private function factoryMetaData(): MetaData
225
    {
226 9
        return new MetaData(
227 9
            $this->aggregateId, get_class($this->aggregate), new \DateTimeImmutable(), new Guid()
228
        );
229
    }
230
231 3
    private function isClassOrSubClass(string $parentClass, $childClass): bool
232
    {
233 3
        return get_class($childClass) == $parentClass || is_subclass_of($childClass, $parentClass);
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $parentClass can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
234
    }
235
236 9
    private function checkCommand($command)
237
    {
238 9
        if (!$command instanceof Command) {
239 1
            throw new \Exception("Command is missing. Have you called method when()?");
240
        }
241
    }
242
}