Compare commits
4 Commits
d963da40e1
...
dfb0658064
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb0658064 | ||
|
|
a014c36f63 | ||
|
|
5326f40b2b | ||
|
|
6b5222ad84 |
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\Aggregates\Carrier;
|
||||
use DistributingCarriers\Infrastructure\EventSourcing\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/Aggregates/AggregateRoot.php
Normal file
52
src/Domain/Aggregates/AggregateRoot.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Domain\Aggregates;
|
||||
|
||||
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/Aggregates/Carrier.php
Normal file
29
src/Domain/Aggregates/Carrier.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Domain\Aggregates;
|
||||
|
||||
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/EventSourcing/CassandraEventStore.php
Normal file
71
src/Infrastructure/EventSourcing/CassandraEventStore.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\EventSourcing;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
9
src/Infrastructure/EventSourcing/EventStore.php
Normal file
9
src/Infrastructure/EventSourcing/EventStore.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\EventSourcing;
|
||||
|
||||
interface EventStore
|
||||
{
|
||||
public function save(string $aggregateId, array $events, int $expectedVersion): void;
|
||||
public function getEventsForAggregate(string $aggregateId): array;
|
||||
}
|
||||
24
src/Infrastructure/Messaging/CommandBus.php
Normal file
24
src/Infrastructure/Messaging/CommandBus.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\Messaging;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
8
src/Infrastructure/Routing/IMiddleware.php
Normal file
8
src/Infrastructure/Routing/IMiddleware.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\Routing;
|
||||
|
||||
interface IMiddleware
|
||||
{
|
||||
public function process(IRequestHandler $handler): void;
|
||||
}
|
||||
8
src/Infrastructure/Routing/IRequestHandler.php
Normal file
8
src/Infrastructure/Routing/IRequestHandler.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\Routing;
|
||||
|
||||
interface IRequestHandler
|
||||
{
|
||||
public function handle(): void;
|
||||
}
|
||||
11
src/Infrastructure/Routing/IRoute.php
Normal file
11
src/Infrastructure/Routing/IRoute.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\Routing;
|
||||
|
||||
interface IRoute
|
||||
{
|
||||
public function getMethod(): string;
|
||||
public function getPath(): string;
|
||||
public function getHandler(): IRequestHandler;
|
||||
public function getMiddlewares(): array;
|
||||
}
|
||||
39
src/Infrastructure/Routing/Route.php
Normal file
39
src/Infrastructure/Routing/Route.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\Routing;
|
||||
|
||||
class Route implements IRoute
|
||||
{
|
||||
private string $method;
|
||||
private string $path;
|
||||
private IRequestHandler $handler;
|
||||
private array $middlewares;
|
||||
|
||||
public function __construct(string $method, string $path, IRequestHandler $handler, array $middlewares = [])
|
||||
{
|
||||
$this->method = $method;
|
||||
$this->path = $path;
|
||||
$this->handler = $handler;
|
||||
$this->middlewares = $middlewares;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getHandler(): IRequestHandler
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function getMiddlewares(): array
|
||||
{
|
||||
return $this->middlewares;
|
||||
}
|
||||
}
|
||||
63
src/Infrastructure/Routing/Router.php
Normal file
63
src/Infrastructure/Routing/Router.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace DistributingCarriers\Infrastructure\Routing;
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $globalMiddlewares = [];
|
||||
|
||||
public function add(string $method, string $path, IRequestHandler $handler, array $middlewares = []): self
|
||||
{
|
||||
$this->routes[] = new Route($method, $path, $handler, $middlewares);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addGlobalMiddleware(IMiddleware $middleware): self
|
||||
{
|
||||
$this->globalMiddlewares[] = $middleware;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dispatch(string $method, string $uri): void
|
||||
{
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route->getMethod() === $method && $route->getPath() === $uri) {
|
||||
$middlewares = array_merge($this->globalMiddlewares, $route->getMiddlewares());
|
||||
$this->executeMiddlewareStack($middlewares, $route->getHandler());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Not Found']);
|
||||
}
|
||||
|
||||
private function executeMiddlewareStack(array $middlewares, IRequestHandler $handler): void
|
||||
{
|
||||
$next = $handler;
|
||||
|
||||
foreach (array_reverse($middlewares) as $middleware) {
|
||||
$next = new class($middleware, $next) implements IRequestHandler {
|
||||
private $middleware;
|
||||
private $next;
|
||||
|
||||
public function __construct(IMiddleware $middleware, IRequestHandler $next)
|
||||
{
|
||||
$this->middleware = $middleware;
|
||||
$this->next = $next;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->middleware->process($this->next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$next->handle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use DistributingCarriers\Infrastructure\Messaging\CommandBus;
|
||||
use DistributingCarriers\Infrastructure\Routing\Router;
|
||||
use DistributingCarriers\Infrastructure\EventSourcing\CassandraEventStore;
|
||||
use DistributingCarriers\Application\Commands\RegisterCarrierCommand;
|
||||
use DistributingCarriers\Application\Handlers\RegisterCarrierHandler;
|
||||
|
||||
// Initialize Cassandra Connection
|
||||
$cluster = Cassandra::cluster()
|
||||
->withContactPoints(getenv('CASSANDRA_HOST') ?: 'cassandra')
|
||||
->withPort((int)(getenv('CASSANDRA_PORT') ?: 9042))
|
||||
->build();
|
||||
|
||||
$session = $cluster->connect('event_store');
|
||||
|
||||
// Initialize Infrastructure
|
||||
$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) {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!isset($input['name']) || !isset($input['email'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing required fields: name, email']);
|
||||
return;
|
||||
}
|
||||
|
||||
$command = new RegisterCarrierCommand($input['name'], $input['email']);
|
||||
$commandBus->dispatch($command);
|
||||
|
||||
http_response_code(201);
|
||||
echo json_encode(['message' => 'Carrier registered successfully']);
|
||||
});
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||||
|
||||
$router->dispatch($method, $uri);
|
||||
|
||||
Reference in New Issue
Block a user