Skip to content

Commit 5b3534b

Browse files
committed
🆕 Introduce ES persistence with a FS event store.
1 parent fa8d716 commit 5b3534b

19 files changed

+431
-9
lines changed

app.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
require_once __DIR__ . '/vendor/autoload.php';
3+
4+
use Shouze\ParkedLife\Domain;
5+
use Shouze\ParkedLife\EventSourcing;
6+
use Shouze\ParkedLife\Adapters;
7+
use Shouze\ParkedLife\Ports;
8+
9+
// 1. We start from pure domain code
10+
$userId = new Domain\UserId('shouze');
11+
$fleet = Domain\VehicleFleet::ofUser($userId);
12+
$platenumber = 'AM 069 GG';
13+
$fleet->registerVehicle($platenumber, 'My benz');
14+
$fleet->parkVehicle($platenumber, Domain\Location::fromString('4.1, 3.12'), new \DateTimeImmutable());
15+
16+
// 2. We build our sourceable stream
17+
$streamName = new EventSourcing\StreamName(sprintf('vehicle_fleet-%s', $userId));
18+
$stream = new EventSourcing\Stream($streamName, $fleet->popRecordedChanges());
19+
20+
// 3. We adapt the domain to the infra through event sourcing
21+
$serializer = new EventSourcing\EventSerializer(
22+
new Domain\EventMapping,
23+
new Symfony\Component\Serializer\Serializer(
24+
[
25+
new Symfony\Component\Serializer\Normalizer\PropertyNormalizer(
26+
null,
27+
new Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter
28+
)
29+
],
30+
[ new Symfony\Component\Serializer\Encoder\JsonEncoder ]
31+
)
32+
);
33+
$eventStore = new Adapters\FilesystemEventStore(__DIR__.'/var/eventstore', $serializer, new Ports\FileHelper);
34+
$eventStore->commit($stream);

composer.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"autoload": {
66
"psr-4": { "Shouze\\ParkedLife\\": "src/" }
77
},
8-
"require": {},
8+
"require": {
9+
"symfony/serializer": "^3.1"
10+
},
911
"require-dev": {
1012
"behat/behat": "^3.2",
1113
"phpspec/phpspec": "^3.1",

composer.lock

+77-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

events.sh

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
php app.php ; find var -type f | xargs cat Dim 25 sep 23:22:08 2016
2+
{"event_name":"vehicle_was_registered.fleet.parkedlife","data":{"user_id":"shouze","platenumber":"AM 069 GG"}}
3+
{"event_name":"vehicle_was_described.fleet.parkedlife","data":{"user_id":"shouze","platenumber":"AM 069 GG","description":"My benz"}}
4+
{"event_name":"vehicle_was_parked.fleet.parkedlife","data":{"user_id":"shouze","platenumber":"AM 069 GG","latitude":4.1,"longitude":3.12,"timestamp":1474838529}}

history

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ vendor/bin/behat --append-snippets --snippets-for=FeatureContext
88
composer require --dev phpspec/phpspec
99
# Create phpspec.yml with psr4_prefix
1010
vendor/bin/phpspec run
11+
# Add a decent serializer
12+
composer require "symfony/serializer"

src/Adapters/FilesystemEventStore.php

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Shouze\ParkedLife\Adapters;
5+
6+
use SplFileObject;
7+
use Shouze\ParkedLife\EventSourcing\EventStore;
8+
use Shouze\ParkedLife\EventSourcing\EventSerializer;
9+
use Shouze\ParkedLife\EventSourcing\Stream;
10+
use Shouze\ParkedLife\EventSourcing\StreamName;
11+
use Shouze\ParkedLife\Ports\FileHelper;
12+
13+
class FilesystemEventStore implements EventStore
14+
{
15+
private $baseDir;
16+
17+
private $eventSerializer;
18+
19+
private $fileHelper;
20+
21+
public function __construct(string $baseDir, EventSerializer $eventSerializer, FileHelper $fileHelper)
22+
{
23+
$this->baseDir = $baseDir;
24+
$this->eventSerializer = $eventSerializer;
25+
$this->fileHelper = $fileHelper;
26+
}
27+
28+
public function commit(Stream $eventStream)
29+
{
30+
$filename = $this->filename($eventStream->getStreamName());
31+
$content = '';
32+
foreach ($eventStream->getChanges() as $change) {
33+
$content .= $this->eventSerializer->serialize($change).PHP_EOL;
34+
}
35+
36+
$this->fileHelper->appendSecurely($filename, $content);
37+
}
38+
39+
public function fetch(StreamName $streamName): Stream
40+
{
41+
$filename = $this->filename($streamName);
42+
$lines = $this->fileHelper->readIterator($this->filename($streamName));
43+
$events = new ArrayIterator();
44+
foreach ($lines as $serializedEvent) {
45+
$events->append($this->eventSerializer->deserialize($serializedEvent));
46+
}
47+
$lines = null; // immediately removes the descriptor.
48+
49+
return new Stream($streamName, $events);
50+
}
51+
52+
private function filename(StreamName $streamName): string
53+
{
54+
$hash = sha1((string)$streamName);
55+
56+
return
57+
$this->baseDir.'/'.
58+
substr($hash, 0, 2).'/'.
59+
substr($hash, 2, 2).'/'.
60+
$hash
61+
;
62+
}
63+
}

src/Domain/EventMapping.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Shouze\ParkedLife\Domain;
5+
6+
use Shouze\ParkedLife\EventSourcing\EventMapping as AbstractEventMapping;
7+
8+
final class EventMapping extends AbstractEventMapping
9+
{
10+
public function __construct()
11+
{
12+
self::$mapping = [
13+
'vehicle_was_registered.fleet.parkedlife' => VehicleWasRegistered::class,
14+
'vehicle_was_described.fleet.parkedlife' => VehicleWasDescribed::class,
15+
'vehicle_was_parked.fleet.parkedlife' => VehicleWasParked::class,
16+
];
17+
}
18+
}

src/Domain/UserId.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public function __construct(string $value)
1212
$this->value = $value;
1313
}
1414

15-
public function __toString()
15+
public function __toString(): string
1616
{
1717
return $this->value;
1818
}

src/Domain/VehicleFleet.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,20 @@ public function whenVehicleWasDescribed(VehicleWasDescribed $change)
4545
}
4646

4747
public function parkVehicle(string $platenumber, Location $where, \DateTimeInterface $when)
48+
{
49+
$this->record(new VehicleWasParked($this->getAggregateId(), $platenumber, $where->getLatitude(), $where->getLongitude(), $when->getTimestamp()));
50+
}
51+
52+
public function whenVehicleWasParked(VehicleWasParked $change)
4853
{
4954
try {
50-
$vehicle = $this->vehicleWithPlatenumber($platenumber);
55+
$vehicle = $this->vehicleWithPlatenumber($change->getPlatenumber());
5156
} catch (\LogicException $e) {
52-
throw new \LogicException(sprintf('Cannot park unknown vehicle %s', $platenumber, 0, $e));
57+
throw new \LogicException(sprintf('Cannot park unknown vehicle %s', $change->getPlatenumber(), 0, $e));
5358
}
5459

60+
$where = new Location($change->getLatitude(), $change->getLongitude());
61+
$when = new \DateTimeImmutable(sprintf('@%ld', $change->getTimestamp()));
5562
$vehicle->park($where, $when);
5663
}
5764

src/Domain/VehicleWasParked.php

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Shouze\ParkedLife\Domain;
5+
6+
use Shouze\ParkedLife\EventSourcing\Change;
7+
8+
final class VehicleWasParked implements Change
9+
{
10+
private $userId;
11+
12+
private $platenumber;
13+
14+
private $latitude;
15+
16+
private $longitude;
17+
18+
private $timestamp;
19+
20+
public function __construct(string $userId, string $platenumber, float $latitude, float $longitude, int $timestamp)
21+
{
22+
$this->userId = $userId;
23+
$this->platenumber = $platenumber;
24+
$this->latitude = $latitude;
25+
$this->longitude = $longitude;
26+
$this->timestamp = $timestamp;
27+
}
28+
29+
public function getAggregateId(): string
30+
{
31+
return $this->userId;
32+
}
33+
34+
public function getPlatenumber(): string
35+
{
36+
return $this->platenumber;
37+
}
38+
39+
public function getLatitude(): float
40+
{
41+
return $this->latitude;
42+
}
43+
44+
public function getLongitude(): float
45+
{
46+
return $this->longitude;
47+
}
48+
49+
public function getTimestamp(): int
50+
{
51+
return $this->timestamp;
52+
}
53+
}

src/EventSourcing/AggregateRoot.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
declare(strict_types=1);
23

34
namespace Shouze\ParkedLife\EventSourcing;
45

@@ -36,7 +37,7 @@ public function popRecordedChanges(): \Iterator
3637

3738
$this->recordedChanges = [];
3839

39-
return $pendingChanges;
40+
return new \ArrayIterator($pendingChanges);
4041
}
4142

4243
protected function record(Change $change)

src/EventSourcing/Change.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55

66
interface Change
77
{
8-
public function getAggregateId();
8+
public function getAggregateId(): string;
99
}

src/EventSourcing/EventMapping.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Shouze\ParkedLife\EventSourcing;
5+
6+
abstract class EventMapping
7+
{
8+
protected static $mapping = [];
9+
10+
public function getEventNameFor(Change $change): string
11+
{
12+
return $this->valueOfKeyIfExist(get_class($change), array_flip(self::$mapping));
13+
}
14+
15+
private function valueOfKeyIfExist($key, array $array): string
16+
{
17+
if (false === array_key_exists($key, $array)) {
18+
throw new \LogicException(sprintf('Missing key %s in the event mapping', $key));
19+
}
20+
21+
return $array[$key];
22+
}
23+
}

0 commit comments

Comments
 (0)