feat(core): Implement DDD, CQRS, and Event Sourcing architecture
- Add project README and Composer configuration with PSR-4 autoloading - Implement domain layer with AggregateRoot base class for event sourcing - Create Carrier aggregate with CarrierRegistered domain event - Add application layer with RegisterCarrierCommand and RegisterCarrierHandler - Implement infrastructure layer with EventStore interface and CassandraEventStore - Add CommandBus and Router for request handling and routing - Create demo test file to showcase carrier registration workflow - Establish foundation for event-driven architecture with Cassandra persistence
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Distributing Carriers
|
||||||
|
|
||||||
|
Distributing Carriers Application.
|
||||||
20
composer.json
Normal file
20
composer.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "mamad/distributing-carriers",
|
||||||
|
"description": "Distributing Carriers Application with DDD, CQRS, and Event Sourcing",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0",
|
||||||
|
"ext-cassandra": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"DistributingCarriers\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mamad",
|
||||||
|
"email": "mamad@example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
src/Application/Commands/RegisterCarrierCommand.php
Normal file
15
src/Application/Commands/RegisterCarrierCommand.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Application\Commands;
|
||||||
|
|
||||||
|
class RegisterCarrierCommand
|
||||||
|
{
|
||||||
|
public $name;
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
public function __construct(string $name, string $email)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->email = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Application/Handlers/RegisterCarrierHandler.php
Normal file
28
src/Application/Handlers/RegisterCarrierHandler.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Application\Handlers;
|
||||||
|
|
||||||
|
use DistributingCarriers\Application\Commands\RegisterCarrierCommand;
|
||||||
|
use DistributingCarriers\Domain\Carrier;
|
||||||
|
use DistributingCarriers\Infrastructure\EventStore;
|
||||||
|
|
||||||
|
class RegisterCarrierHandler
|
||||||
|
{
|
||||||
|
private $eventStore;
|
||||||
|
|
||||||
|
public function __construct(EventStore $eventStore)
|
||||||
|
{
|
||||||
|
$this->eventStore = $eventStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(RegisterCarrierCommand $command): void
|
||||||
|
{
|
||||||
|
$carrierId = uniqid('carrier_');
|
||||||
|
$carrier = Carrier::register($carrierId, $command->name, $command->email);
|
||||||
|
|
||||||
|
$this->eventStore->save($carrier->getId(), $carrier->getUncommittedChanges(), $carrier->getVersion());
|
||||||
|
$carrier->markChangesAsCommitted();
|
||||||
|
|
||||||
|
echo "Carrier registered with ID: " . $carrierId . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Domain/AggregateRoot.php
Normal file
52
src/Domain/AggregateRoot.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Domain;
|
||||||
|
|
||||||
|
abstract class AggregateRoot
|
||||||
|
{
|
||||||
|
protected $id;
|
||||||
|
protected $version = 0;
|
||||||
|
protected $changes = [];
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): int
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUncommittedChanges(): array
|
||||||
|
{
|
||||||
|
return $this->changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markChangesAsCommitted(): void
|
||||||
|
{
|
||||||
|
$this->changes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function recordThat(object $event): void
|
||||||
|
{
|
||||||
|
$this->apply($event);
|
||||||
|
$this->changes[] = $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function apply(object $event): void
|
||||||
|
{
|
||||||
|
$method = 'apply' . (new \ReflectionClass($event))->getShortName();
|
||||||
|
if (method_exists($this, $method)) {
|
||||||
|
$this->$method($event);
|
||||||
|
}
|
||||||
|
$this->version++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadFromHistory(array $history): void
|
||||||
|
{
|
||||||
|
foreach ($history as $event) {
|
||||||
|
$this->apply($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Domain/Carrier.php
Normal file
29
src/Domain/Carrier.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Domain;
|
||||||
|
|
||||||
|
use DistributingCarriers\Domain\Events\CarrierRegistered;
|
||||||
|
|
||||||
|
class Carrier extends AggregateRoot
|
||||||
|
{
|
||||||
|
private $name;
|
||||||
|
private $email;
|
||||||
|
|
||||||
|
public static function register(string $carrierId, string $name, string $email): self
|
||||||
|
{
|
||||||
|
$carrier = new self();
|
||||||
|
$carrier->recordThat(new CarrierRegistered([
|
||||||
|
'carrierId' => $carrierId,
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email
|
||||||
|
]));
|
||||||
|
return $carrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function applyCarrierRegistered(CarrierRegistered $event): void
|
||||||
|
{
|
||||||
|
$this->id = $event->carrierId;
|
||||||
|
$this->name = $event->name;
|
||||||
|
$this->email = $event->email;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Domain/Events/CarrierRegistered.php
Normal file
17
src/Domain/Events/CarrierRegistered.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Domain\Events;
|
||||||
|
|
||||||
|
class CarrierRegistered
|
||||||
|
{
|
||||||
|
public $carrierId;
|
||||||
|
public $name;
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
public function __construct(array $payload)
|
||||||
|
{
|
||||||
|
$this->carrierId = $payload['carrierId'] ?? null;
|
||||||
|
$this->name = $payload['name'] ?? null;
|
||||||
|
$this->email = $payload['email'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Infrastructure/CassandraEventStore.php
Normal file
71
src/Infrastructure/CassandraEventStore.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Infrastructure;
|
||||||
|
|
||||||
|
use Cassandra;
|
||||||
|
|
||||||
|
class CassandraEventStore implements EventStore
|
||||||
|
{
|
||||||
|
private $session;
|
||||||
|
|
||||||
|
public function __construct($session)
|
||||||
|
{
|
||||||
|
$this->session = $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(string $aggregateId, array $events, int $expectedVersion): void
|
||||||
|
{
|
||||||
|
// In a real implementation, we would check the version for concurrency control.
|
||||||
|
// For this example, we'll just append.
|
||||||
|
|
||||||
|
$statement = $this->session->prepare(
|
||||||
|
"INSERT INTO events (aggregate_id, version, event_type, payload, created_at) VALUES (?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$version = ++$expectedVersion;
|
||||||
|
$payload = json_encode($event);
|
||||||
|
$eventType = get_class($event);
|
||||||
|
$createdAt = new Cassandra\Timestamp();
|
||||||
|
|
||||||
|
$this->session->execute($statement, [
|
||||||
|
'arguments' => [
|
||||||
|
$aggregateId,
|
||||||
|
$version,
|
||||||
|
$eventType,
|
||||||
|
$payload,
|
||||||
|
$createdAt
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventsForAggregate(string $aggregateId): array
|
||||||
|
{
|
||||||
|
$statement = $this->session->prepare(
|
||||||
|
"SELECT event_type, payload FROM events WHERE aggregate_id = ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->session->execute($statement, ['arguments' => [$aggregateId]]);
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$eventType = $row['event_type'];
|
||||||
|
$payload = json_decode($row['payload'], true);
|
||||||
|
|
||||||
|
// Assuming we can reconstruct the event object from payload
|
||||||
|
// This is a simplified version.
|
||||||
|
if (class_exists($eventType)) {
|
||||||
|
// In a real app, you might use a serializer/deserializer
|
||||||
|
// Here we assume the event has a static fromArray or similar, or just public properties
|
||||||
|
// For simplicity, let's assume we just return the data or a generic object if class doesn't handle it
|
||||||
|
// But for the plan, let's try to instantiate.
|
||||||
|
// We'll leave it as an array or basic object for now if complex reconstruction is needed.
|
||||||
|
// Let's assume the event class has a constructor that takes the payload.
|
||||||
|
$events[] = new $eventType($payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Infrastructure/CommandBus.php
Normal file
24
src/Infrastructure/CommandBus.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Infrastructure;
|
||||||
|
|
||||||
|
class CommandBus
|
||||||
|
{
|
||||||
|
private $handlers = [];
|
||||||
|
|
||||||
|
public function register(string $commandClass, callable $handler): void
|
||||||
|
{
|
||||||
|
$this->handlers[$commandClass] = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(object $command): void
|
||||||
|
{
|
||||||
|
$commandClass = get_class($command);
|
||||||
|
if (!isset($this->handlers[$commandClass])) {
|
||||||
|
throw new \Exception("No handler registered for command: " . $commandClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
$handler = $this->handlers[$commandClass];
|
||||||
|
$handler($command);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/Infrastructure/EventStore.php
Normal file
9
src/Infrastructure/EventStore.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Infrastructure;
|
||||||
|
|
||||||
|
interface EventStore
|
||||||
|
{
|
||||||
|
public function save(string $aggregateId, array $events, int $expectedVersion): void;
|
||||||
|
public function getEventsForAggregate(string $aggregateId): array;
|
||||||
|
}
|
||||||
29
src/Infrastructure/Router.php
Normal file
29
src/Infrastructure/Router.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DistributingCarriers\Infrastructure;
|
||||||
|
|
||||||
|
class Router
|
||||||
|
{
|
||||||
|
private $routes = [];
|
||||||
|
|
||||||
|
public function add(string $method, string $path, callable $handler): void
|
||||||
|
{
|
||||||
|
$this->routes[] = [
|
||||||
|
'method' => $method,
|
||||||
|
'path' => $path,
|
||||||
|
'handler' => $handler
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(string $method, string $uri)
|
||||||
|
{
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
if ($route['method'] === $method && $route['path'] === $uri) {
|
||||||
|
return call_user_func($route['handler']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(404);
|
||||||
|
echo "Not Found";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use DistributingCarriers\Infrastructure\CommandBus;
|
||||||
|
use DistributingCarriers\Infrastructure\Router;
|
||||||
|
use DistributingCarriers\Infrastructure\CassandraEventStore;
|
||||||
|
use DistributingCarriers\Application\Commands\RegisterCarrierCommand;
|
||||||
|
use DistributingCarriers\Application\Handlers\RegisterCarrierHandler;
|
||||||
|
|
||||||
|
// Initialize Infrastructure
|
||||||
|
// Note: In a real app, you'd configure the Cassandra session here.
|
||||||
|
// For this demo, we'll mock it or assume it's passed in.
|
||||||
|
// Since we don't have a running Cassandra instance we can connect to easily without extension,
|
||||||
|
// we will assume the $session is created.
|
||||||
|
// $cluster = Cassandra::cluster()->build();
|
||||||
|
// $session = $cluster->connect('distributing_carriers');
|
||||||
|
|
||||||
|
// Mocking session for demonstration if extension is missing, or just placeholder.
|
||||||
|
$session = null; // Placeholder
|
||||||
|
|
||||||
|
$eventStore = new CassandraEventStore($session);
|
||||||
|
$commandBus = new CommandBus();
|
||||||
|
|
||||||
|
// Register Handlers
|
||||||
|
$commandBus->register(RegisterCarrierCommand::class, new RegisterCarrierHandler($eventStore));
|
||||||
|
|
||||||
|
// Router
|
||||||
|
$router = new Router();
|
||||||
|
|
||||||
|
$router->add('POST', '/register-carrier', function () use ($commandBus) {
|
||||||
|
// In a real app, parse body.
|
||||||
|
// For demo, we'll just create a dummy command.
|
||||||
|
$command = new RegisterCarrierCommand("Test Carrier", "test@example.com");
|
||||||
|
$commandBus->dispatch($command);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
|
||||||
|
|
||||||
|
$router->dispatch($method, $uri);
|
||||||
|
|||||||
66
tests/demo.php
Normal file
66
tests/demo.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Cassandra {
|
||||||
|
class Timestamp
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Mock Cassandra Session
|
||||||
|
class MockCassandraSession
|
||||||
|
{
|
||||||
|
public $executedStatements = [];
|
||||||
|
|
||||||
|
public function prepare($cql)
|
||||||
|
{
|
||||||
|
return new MockStatement($cql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute($statement, $options)
|
||||||
|
{
|
||||||
|
$this->executedStatements[] = [
|
||||||
|
'statement' => $statement->cql,
|
||||||
|
'arguments' => $options['arguments']
|
||||||
|
];
|
||||||
|
echo "Executed CQL: " . $statement->cql . "\n";
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockStatement
|
||||||
|
{
|
||||||
|
public $cql;
|
||||||
|
public function __construct($cql)
|
||||||
|
{
|
||||||
|
$this->cql = $cql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/Infrastructure/EventStore.php';
|
||||||
|
require_once __DIR__ . '/../src/Infrastructure/CassandraEventStore.php';
|
||||||
|
require_once __DIR__ . '/../src/Infrastructure/CommandBus.php';
|
||||||
|
require_once __DIR__ . '/../src/Domain/AggregateRoot.php';
|
||||||
|
require_once __DIR__ . '/../src/Domain/Events/CarrierRegistered.php';
|
||||||
|
require_once __DIR__ . '/../src/Domain/Carrier.php';
|
||||||
|
require_once __DIR__ . '/../src/Application/Commands/RegisterCarrierCommand.php';
|
||||||
|
require_once __DIR__ . '/../src/Application/Handlers/RegisterCarrierHandler.php';
|
||||||
|
|
||||||
|
use DistributingCarriers\Infrastructure\CommandBus;
|
||||||
|
use DistributingCarriers\Infrastructure\CassandraEventStore;
|
||||||
|
use DistributingCarriers\Application\Commands\RegisterCarrierCommand;
|
||||||
|
use DistributingCarriers\Application\Handlers\RegisterCarrierHandler;
|
||||||
|
|
||||||
|
echo "Starting Demo...\n";
|
||||||
|
|
||||||
|
$session = new MockCassandraSession();
|
||||||
|
$eventStore = new CassandraEventStore($session);
|
||||||
|
$commandBus = new CommandBus();
|
||||||
|
|
||||||
|
$commandBus->register(RegisterCarrierCommand::class, new RegisterCarrierHandler($eventStore));
|
||||||
|
|
||||||
|
$command = new RegisterCarrierCommand("Demo Carrier", "demo@example.com");
|
||||||
|
$commandBus->dispatch($command);
|
||||||
|
|
||||||
|
echo "Demo Completed.\n";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user