Copy docker run --rm zchencow/innozverse-php:latest php -r '
<?php
declare(strict_types=1);
// ══════════════════════════════════════════════════════════
// PART 1: Core Infrastructure
// ══════════════════════════════════════════════════════════
// DI Container — lazy service resolution
class Container {
private array $bindings = [];
private array $instances = [];
public function bind(string $id, callable $factory): void {
$this->bindings[$id] = $factory;
}
public function singleton(string $id, callable $factory): void {
$this->bindings[$id] = function() use ($id, $factory) {
if (!isset($this->instances[$id])) {
$this->instances[$id] = $factory($this);
}
return $this->instances[$id];
};
}
public function make(string $id): mixed {
if (!isset($this->bindings[$id])) throw new \RuntimeException("No binding: {$id}");
return ($this->bindings[$id])($this);
}
}
// Event system
interface EventListener { public function handle(string $event, array $payload): void; }
class EventBus {
private array $listeners = [];
private array $emitted = [];
public function on(string $event, callable|EventListener $listener): void {
$this->listeners[$event][] = $listener;
}
public function emit(string $event, array $payload = []): void {
$this->emitted[] = $event;
foreach ($this->listeners[$event] ?? [] as $l) {
is_callable($l) ? $l($payload) : $l->handle($event, $payload);
}
}
public function emitted(): array { return $this->emitted; }
}
// Traits
trait Timestampable {
private string $createdAt;
private ?string $updatedAt = null;
public function stamp(): void { $this->createdAt = date("Y-m-d H:i:s"); }
public function touch(): void { $this->updatedAt = date("Y-m-d H:i:s"); }
public function getCreatedAt(): string { return $this->createdAt; }
public function getUpdatedAt(): ?string { return $this->updatedAt; }
}
trait Validatable {
abstract protected function rules(): array;
public function validate(): array {
$errors = [];
foreach ($this->rules() as $field => $rule) {
$value = $this->$field ?? null;
if (str_contains($rule, "required") && ($value === null || $value === ""))
$errors[] = "{$field}: required";
if (str_contains($rule, "positive") && is_numeric($value) && $value <= 0)
$errors[] = "{$field}: must be positive";
}
return $errors;
}
}
// ══════════════════════════════════════════════════════════
// PART 2: Domain Models
// ══════════════════════════════════════════════════════════
enum OrderStatus: string {
case Pending = "pending";
case Confirmed = "confirmed";
case Shipped = "shipped";
case Delivered = "delivered";
case Cancelled = "cancelled";
public function label(): string {
return match($this) {
self::Pending => "⏳ Pending",
self::Confirmed => "✅ Confirmed",
self::Shipped => "🚚 Shipped",
self::Delivered => "📦 Delivered",
self::Cancelled => "❌ Cancelled",
};
}
public function canTransitionTo(OrderStatus $next): bool {
return in_array($next, match($this) {
self::Pending => [self::Confirmed, self::Cancelled],
self::Confirmed => [self::Shipped, self::Cancelled],
self::Shipped => [self::Delivered],
default => [],
});
}
}
class Product {
use Timestampable, Validatable;
public function __construct(
public readonly int $id,
public string $name,
public string $category,
public float $price,
public int $stock,
) { $this->stamp(); }
protected function rules(): array {
return ["name" => "required", "price" => "required|positive", "stock" => "required"];
}
public function inStock(): bool { return $this->stock > 0; }
public function value(): float { return $this->price * $this->stock; }
public function deduct(int $n): void {
if ($n > $this->stock) throw new \RuntimeException("Insufficient stock: {$this->name}");
$this->stock -= $n;
$this->touch();
}
public function toArray(): array {
return ["id"=>$this->id,"name"=>$this->name,"category"=>$this->category,
"price"=>$this->price,"stock"=>$this->stock,"inStock"=>$this->inStock()];
}
}
class Order {
use Timestampable;
private OrderStatus $status = OrderStatus::Pending;
public function __construct(
public readonly int $id,
public readonly int $productId,
public readonly string $productName,
public readonly int $qty,
public readonly float $total,
public readonly string $region,
) { $this->stamp(); }
public function transition(OrderStatus $next, EventBus $bus): void {
if (!$this->status->canTransitionTo($next)) {
throw new \RuntimeException("Cannot transition {$this->status->value} -> {$next->value}");
}
$this->status = $next;
$this->touch();
$bus->emit("order.{$next->value}", $this->toArray());
}
public function getStatus(): OrderStatus { return $this->status; }
public function toArray(): array {
return ["id"=>$this->id,"productId"=>$this->productId,"productName"=>$this->productName,
"qty"=>$this->qty,"total"=>$this->total,"region"=>$this->region,
"status"=>$this->status->value];
}
}
// ══════════════════════════════════════════════════════════
// PART 3: Repositories
// ══════════════════════════════════════════════════════════
class ProductRepository {
private array $store = [];
private int $seq = 0;
public function save(Product $p): Product {
$this->store[$p->id] = $p; return $p;
}
public function find(int $id): ?Product { return $this->store[$id] ?? null; }
public function findAll(): array { return array_values($this->store); }
public function count(): int { return count($this->store); }
public function inStock(): array { return array_values(array_filter($this->store, fn($p) => $p->inStock())); }
public function byCategory(string $c): array {
return array_values(array_filter($this->store, fn($p) => $p->category === $c));
}
}
class OrderRepository {
private array $store = [];
private int $seq = 1000;
public function save(Order $o): Order { $this->store[$o->id] = $o; return $o; }
public function find(int $id): ?Order { return $this->store[$id] ?? null; }
public function findAll(): array { return array_values($this->store); }
public function nextId(): int { return ++$this->seq; }
public function totalRevenue(): float { return array_sum(array_column(array_map(fn($o) => $o->toArray(), $this->store), "total")); }
}
// ══════════════════════════════════════════════════════════
// PART 4: Application Service
// ══════════════════════════════════════════════════════════
class OrderService {
public function __construct(
private ProductRepository $products,
private OrderRepository $orders,
private EventBus $bus,
) {}
public function placeOrder(int $productId, int $qty, string $region): Order {
$product = $this->products->find($productId)
?? throw new \RuntimeException("Product #{$productId} not found");
$errors = $product->validate();
if ($errors) throw new \RuntimeException("Invalid product: " . implode(", ", $errors));
if (!$product->inStock() || $product->stock < $qty)
throw new \RuntimeException("Insufficient stock: {$product->name}");
$product->deduct($qty);
$order = new Order(
$this->orders->nextId(), $productId, $product->name,
$qty, round($product->price * $qty, 2), $region
);
$this->orders->save($order);
$this->products->save($product);
$this->bus->emit("order.placed", $order->toArray());
if ($product->stock < 3) {
$this->bus->emit("stock.low", ["product" => $product->name, "remaining" => $product->stock]);
}
return $order;
}
}
// ══════════════════════════════════════════════════════════
// PART 5: Wire It All Up
// ══════════════════════════════════════════════════════════
$container = new Container();
$container->singleton("bus", fn() => new EventBus());
$container->singleton("productRepo", fn() => new ProductRepository());
$container->singleton("orderRepo", fn() => new OrderRepository());
$container->singleton("orderService", fn($c) => new OrderService(
$c->make("productRepo"), $c->make("orderRepo"), $c->make("bus")
));
$bus = $container->make("bus");
$productRepo = $container->make("productRepo");
$orderService = $container->make("orderService");
$orderRepo = $container->make("orderRepo");
// Event listeners
$notifications = [];
$bus->on("order.placed", function($d) use (&$notifications) {
$notifications[] = "📧 Email: Order #{$d["id"]} placed — {$d["productName"]} ×{$d["qty"]} \${$d["total"]}";
});
$bus->on("order.confirmed", function($d) use (&$notifications) {
$notifications[] = "📧 Email: Order #{$d["id"]} confirmed";
});
$bus->on("order.shipped", function($d) use (&$notifications) {
$notifications[] = "📦 SMS: Order #{$d["id"]} shipped";
});
$bus->on("stock.low", function($d) use (&$notifications) {
$notifications[] = "⚠️ Alert: Low stock — {$d["product"]} ({$d["remaining"]} left)";
});
// Seed products
$products = [
new Product(1, "Surface Pro", "laptop", 864.00, 5),
new Product(2, "Surface Book", "laptop", 1299.00, 3),
new Product(3, "Surface Pen", "accessory", 49.99, 20),
new Product(4, "Office 365", "software", 99.99, 999),
new Product(5, "USB-C Hub", "hardware", 29.99, 8),
];
foreach ($products as $p) $productRepo->save($p);
echo "=== Capstone: Mini MVC E-Commerce ===" . PHP_EOL;
echo PHP_EOL . "--- Seeded " . $productRepo->count() . " products ---" . PHP_EOL;
// ══════════════════════════════════════════════════════════
// PART 6: Integration Test Suite
// ══════════════════════════════════════════════════════════
$passed = 0; $failed = 0;
function check(string $label, bool $result): void {
global $passed, $failed;
if ($result) { $passed++; echo " ✓ {$label}" . PHP_EOL; }
else { $failed++; echo " ✗ {$label}" . PHP_EOL; }
}
echo PHP_EOL . "▶ Repository Layer" . PHP_EOL;
check("productRepo has 5 products", $productRepo->count() === 5);
check("find by id returns Product", $productRepo->find(1) instanceof Product);
check("find unknown returns null", $productRepo->find(99) === null);
check("inStock excludes zero-stock", count($productRepo->inStock()) === 5);
check("byCategory laptop = 2", count($productRepo->byCategory("laptop")) === 2);
echo PHP_EOL . "▶ Order Placement" . PHP_EOL;
try {
$o1 = $orderService->placeOrder(1, 2, "West");
$o2 = $orderService->placeOrder(3, 10, "East");
$o3 = $orderService->placeOrder(4, 1, "North");
check("order #1 placed", $o1->getStatus() === OrderStatus::Pending);
check("order #2 placed", $o2->getStatus() === OrderStatus::Pending);
check("stock deducted", $productRepo->find(1)->stock === 3);
check("stock deducted (Pen ×10)", $productRepo->find(3)->stock === 10);
check("orders saved = 3", $orderRepo->count() === 3);
check("revenue correct", abs($orderRepo->totalRevenue() - (1728.00+499.90+99.99)) < 0.01);
} catch (\Throwable $e) { $failed++; echo " ✗ placeOrder: " . $e->getMessage() . PHP_EOL; }
echo PHP_EOL . "▶ Insufficient Stock" . PHP_EOL;
try { $orderService->placeOrder(1, 100, "North"); check("should have thrown", false); }
catch (\RuntimeException $e) { check("insufficient stock throws", str_contains($e->getMessage(), "Insufficient")); }
try { $orderService->placeOrder(99, 1, "East"); check("should have thrown", false); }
catch (\RuntimeException $e) { check("unknown product throws", str_contains($e->getMessage(), "not found")); }
echo PHP_EOL . "▶ Order Status Machine" . PHP_EOL;
$o1->transition(OrderStatus::Confirmed, $bus);
check("pending -> confirmed OK", $o1->getStatus() === OrderStatus::Confirmed);
try { $o1->transition(OrderStatus::Pending, $bus); check("should have thrown", false); }
catch (\RuntimeException $e) { check("invalid transition throws", true); }
$o1->transition(OrderStatus::Shipped, $bus);
check("confirmed -> shipped OK", $o1->getStatus() === OrderStatus::Shipped);
echo PHP_EOL . "▶ Events" . PHP_EOL;
$emitted = $bus->emitted();
check("order.placed emitted 3 times", count(array_keys($emitted, "order.placed")) === 3);
check("stock.low emitted (Pen ×10)", in_array("stock.low", $emitted));
check("order.confirmed emitted", in_array("order.confirmed", $emitted));
check("order.shipped emitted", in_array("order.shipped", $emitted));
check("notifications count >= 6", count($notifications) >= 6);
echo PHP_EOL . "▶ Functional Pipeline (analytics)" . PHP_EOL;
function pipe(mixed $v, array $fns): mixed { return array_reduce($fns, fn($c, $fn) => $fn($c), $v); }
$analytics = pipe($productRepo->findAll(), [
fn($ps) => array_filter($ps, fn($p) => $p->inStock()),
fn($ps) => array_values($ps),
fn($ps) => array_map(fn($p) => ["name" => $p->name, "value" => $p->value()], $ps),
fn($ps) => array_filter($ps, fn($p) => $p["value"] > 100),
fn($ps) => array_values($ps),
]);
check("pipeline: in-stock + value>100", count($analytics) > 0);
check("pipeline: all have value > 100", min(array_column($analytics, "value")) > 100);
// ══════════════════════════════════════════════════════════
// PART 7: Final Output
// ══════════════════════════════════════════════════════════
echo PHP_EOL . "─── Notifications ───" . PHP_EOL;
foreach ($notifications as $n) echo " " . $n . PHP_EOL;
echo PHP_EOL . "─── Analytics Pipeline ───" . PHP_EOL;
foreach ($analytics as $a) printf(" %-15s value=\$%,.2f%s", $a["name"], $a["value"], PHP_EOL);
echo PHP_EOL . "─── Order Summary ───" . PHP_EOL;
foreach ($orderRepo->findAll() as $o) {
printf(" Order #%d %-15s ×%d \$%.2f %-8s %s%s",
$o->id, $o->productName, $o->qty, $o->total, $o->region, $o->getStatus()->label(), PHP_EOL);
}
printf(" Total revenue: \$%.2f%s", $orderRepo->totalRevenue(), PHP_EOL);
echo PHP_EOL . str_repeat("═", 52) . PHP_EOL;
$total = $passed + $failed;
printf(" Tests: %d/%d passed%s", $passed, $total, PHP_EOL);
echo ($failed === 0 ? " 🎉 All tests passed!" : " ⚠️ {$failed} test(s) failed") . PHP_EOL;
echo str_repeat("═", 52) . PHP_EOL;
'