Step 00 — Squelette + intégration naïve

3 Bounded Contexts (Sales, Invoicing, LegacyFulfillment) avec :
- Domaines complets (agrégats, VOs, événements, invariants)
- Couche application (commands, queries, ports)
- Infrastructure in-memory (repos, gateway fake)
- Controllers HTTP Symfony
- Couplage naïf synchrone entre BC via NaiveSalesEventPublisher
- 20 tests unitaires et d'intégration passants
This commit is contained in:
2026-03-04 00:27:15 +01:00
commit a4a14e441b
86 changed files with 7059 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Command;
final readonly class CancelOrder
{
public function __construct(
public string $orderId,
) {}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Command;
use MiniShop\Sales\Application\Port\OrderRepository;
use MiniShop\Sales\Application\Port\SalesEventPublisher;
use MiniShop\Sales\Domain\Event\OrderCancelled;
use MiniShop\Sales\Domain\Model\OrderId;
final readonly class CancelOrderHandler
{
public function __construct(
private OrderRepository $orderRepository,
private SalesEventPublisher $publisher,
) {}
public function __invoke(CancelOrder $command): void
{
$order = $this->orderRepository->get(OrderId::fromString($command->orderId));
$order->cancel();
$this->orderRepository->save($order);
foreach ($order->releaseEvents() as $event) {
if ($event instanceof OrderCancelled) {
$this->publisher->publishOrderCancelled($event);
}
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Command;
final readonly class ConfirmOrder
{
public function __construct(
public string $orderId,
) {}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Command;
use MiniShop\Sales\Application\Port\OrderRepository;
use MiniShop\Sales\Application\Port\SalesEventPublisher;
use MiniShop\Sales\Domain\Event\OrderConfirmed;
use MiniShop\Sales\Domain\Model\OrderId;
final readonly class ConfirmOrderHandler
{
public function __construct(
private OrderRepository $orderRepository,
private SalesEventPublisher $publisher,
) {}
public function __invoke(ConfirmOrder $command): void
{
$order = $this->orderRepository->get(OrderId::fromString($command->orderId));
$order->confirm();
$this->orderRepository->save($order);
foreach ($order->releaseEvents() as $event) {
if ($event instanceof OrderConfirmed) {
$this->publisher->publishOrderConfirmed($event);
}
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Command;
final readonly class PlaceOrder
{
/**
* @param list<array{productName: string, quantity: int, unitPriceInCents: int, currency: string}> $lines
*/
public function __construct(
public string $orderId,
public string $customerId,
public array $lines,
) {}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Command;
use MiniShop\Sales\Application\Port\OrderRepository;
use MiniShop\Sales\Application\Port\SalesEventPublisher;
use MiniShop\Sales\Domain\Event\OrderPlaced;
use MiniShop\Sales\Domain\Model\CustomerId;
use MiniShop\Sales\Domain\Model\Money;
use MiniShop\Sales\Domain\Model\Order;
use MiniShop\Sales\Domain\Model\OrderId;
use MiniShop\Sales\Domain\Model\OrderLine;
use MiniShop\Shared\Technical\Clock;
final readonly class PlaceOrderHandler
{
public function __construct(
private OrderRepository $orderRepository,
private SalesEventPublisher $publisher,
private Clock $clock,
) {}
public function __invoke(PlaceOrder $command): void
{
$lines = array_map(
static fn (array $line): OrderLine => new OrderLine(
$line['productName'],
$line['quantity'],
new Money($line['unitPriceInCents'], $line['currency']),
),
$command->lines,
);
$order = Order::place(
OrderId::fromString($command->orderId),
CustomerId::fromString($command->customerId),
$lines,
$this->clock->now(),
);
$this->orderRepository->save($order);
foreach ($order->releaseEvents() as $event) {
if ($event instanceof OrderPlaced) {
$this->publisher->publishOrderPlaced($event);
}
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Port;
use MiniShop\Sales\Domain\Model\CustomerId;
use MiniShop\Sales\Domain\Model\Order;
use MiniShop\Sales\Domain\Model\OrderId;
interface OrderRepository
{
public function save(Order $order): void;
public function get(OrderId $id): Order;
/** @return list<Order> */
public function findByCustomer(CustomerId $customerId): array;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Port;
use MiniShop\Sales\Domain\Event\OrderCancelled;
use MiniShop\Sales\Domain\Event\OrderConfirmed;
use MiniShop\Sales\Domain\Event\OrderPlaced;
interface SalesEventPublisher
{
public function publishOrderPlaced(OrderPlaced $event): void;
public function publishOrderConfirmed(OrderConfirmed $event): void;
public function publishOrderCancelled(OrderCancelled $event): void;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Query;
final readonly class GetOrderById
{
public function __construct(
public string $orderId,
) {}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Query;
use MiniShop\Sales\Application\Port\OrderRepository;
use MiniShop\Sales\Domain\Model\Order;
use MiniShop\Sales\Domain\Model\OrderId;
final readonly class GetOrderByIdHandler
{
public function __construct(
private OrderRepository $orderRepository,
) {}
public function __invoke(GetOrderById $query): Order
{
return $this->orderRepository->get(OrderId::fromString($query->orderId));
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Query;
final readonly class ListOrdersByCustomer
{
public function __construct(
public string $customerId,
) {}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace MiniShop\Sales\Application\Query;
use MiniShop\Sales\Application\Port\OrderRepository;
use MiniShop\Sales\Domain\Model\CustomerId;
use MiniShop\Sales\Domain\Model\Order;
final readonly class ListOrdersByCustomerHandler
{
public function __construct(
private OrderRepository $orderRepository,
) {}
/** @return list<Order> */
public function __invoke(ListOrdersByCustomer $query): array
{
return $this->orderRepository->findByCustomer(CustomerId::fromString($query->customerId));
}
}