Skip to content

Instantly share code, notes, and snippets.

@audinue
Created May 16, 2025 03:30
Show Gist options
  • Save audinue/accb6c1db53c5c4a963a0ff2fc0919f9 to your computer and use it in GitHub Desktop.
Save audinue/accb6c1db53c5c4a963a0ff2fc0919f9 to your computer and use it in GitHub Desktop.
<?php
// ===================================================
// 🧠 Domain (Entity)
// ===================================================
namespace Domain {
class Product
{
function __construct(
public readonly int $id,
public readonly string $name,
public readonly float $price
) {}
}
}
// ===================================================
// 💼 Application Business Rules (Use Case)
// ===================================================
namespace Application {
use Domain\Product;
interface ProductRepository
{
/** @return Product[] */
function getAll(): array;
}
class GetAllProducts
{
/**
* @param ProductRepository $products
*/
function __construct(private ProductRepository $products) {}
/**
* @return Product[]
*/
function execute(): array
{
return $this->products->getAll();
}
}
}
// ===================================================
// 🧝 Interface Adapter (Controller + Presenter)
// ===================================================
namespace InterfaceAdapter {
interface ProductPresenter
{
/**
* @param \Domain\Product[] $products
*/
function present(array $products): void;
}
class ProductController
{
function __construct(
private \Application\GetAllProducts $useCase,
private ProductPresenter $presenter
) {}
function showProductList(): void
{
$products = $this->useCase->execute();
$this->presenter->present($products);
}
}
}
// ===================================================
// ⚙️ Infrastructure (I/O Implementation)
// ===================================================
namespace Infrastructure {
use Domain\Product;
class InMemoryProductRepository implements \Application\ProductRepository
{
function getAll(): array
{
return [
new Product(1, 'Apple', 10),
new Product(2, 'Banana', 20),
new Product(3, 'Cherry', 30),
];
}
}
class DumpProductPresenter implements \InterfaceAdapter\ProductPresenter
{
function present(array $products): void
{
foreach ($products as $p) {
echo "{$p->id}. {$p->name} - Rp{$p->price}\n";
}
echo "Total: " . count($products) . " produk\n";
}
}
}
// ===================================================
// 🚀 Bootstrap / Runtime Wiring
// ===================================================
namespace {
// manual autoload ala warung kopi
$repo = new \Infrastructure\InMemoryProductRepository();
$presenter = new \Infrastructure\DumpProductPresenter();
$useCase = new \Application\GetAllProducts($repo);
$controller = new \InterfaceAdapter\ProductController($useCase, $presenter);
// jalanin entrypoint
$controller->showProductList();
}
// ===================================================
// 📃 Test / Proof of Concept
// ===================================================
namespace Test {
class FakeRepo implements \Application\ProductRepository {
function getAll(): array {
return [
new \Domain\Product(999, 'TEST', 123.45)
];
}
}
class CollectingPresenter implements \InterfaceAdapter\ProductPresenter {
public array $log = [];
function present(array $products): void {
$this->log = $products;
}
}
$repo = new FakeRepo();
$presenter = new CollectingPresenter();
$useCase = new \Application\GetAllProducts($repo);
$controller = new \InterfaceAdapter\ProductController($useCase, $presenter);
$controller->showProductList();
assert(count($presenter->log) === 1);
assert($presenter->log[0]->name === 'TEST');
echo "\n[TEST PASSED]\n";
}
@startuml
title Clean Architecture
hide empty members
package Domain {
class Product {
id: int
name: string
price: float
}
}
package Application {
interface ProductRepository {
getAll(): Product[]
}
class GetAllProducts {
execute(): Product[]
}
Product <.. ProductRepository
ProductRepository <-- GetAllProducts
}
package InterfaceAdapter {
interface ProductPresenter {
present(Product[]): void
}
class ProductController {
showProductList(): void
}
Product <.. ProductPresenter
GetAllProducts <--- ProductController
ProductPresenter <-- ProductController
}
package Infrastructure {
class InMemoryProductRepository {
getAll(): Product[]
}
class DumpProductPresenter {
present(Product[]): void
}
ProductRepository <|... InMemoryProductRepository
ProductPresenter <|... DumpProductPresenter
}
package Bootstrap {
class Main {}
GetAllProducts <---- Main
ProductController <--- Main
DumpProductPresenter <-- Main
InMemoryProductRepository <-- Main
}
@enduml
// Entities
type Product = {
id: number;
name: string;
price: number;
};
// Use cases
type LoadProducts = () => Promise<Product[]>;
const getProducts = (loadProducts: LoadProducts) => () => loadProducts();
// Adapters
type GetProducts = () => Promise<Product[]>;
type PresentProducts = (products: Product[]) => void;
const showProducts =
(getProducts: GetProducts, presentProducts: PresentProducts) => async () =>
presentProducts(await getProducts());
// Infrastructures
const loadProductsFromMemory: LoadProducts = async () => [
{ id: 1, name: "Apple", price: 1 },
{ id: 2, name: "Banana", price: 2 },
{ id: 3, name: "Cherry", price: 3 },
];
import React from "react";
import { createContext, useContext, useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
class DependencyContainer<T> {
constructor(
private factories: Record<string, (self: any) => any> = {},
private objects: Record<string, any> = {}
) {}
set<K extends keyof T | (string & {}), V>(
key: K,
factory: (self: DependencyContainer<T>) => V
): DependencyContainer<T & { [P in K]: V }> {
return new DependencyContainer({ ...this.factories, [key]: factory });
}
get<K extends keyof T>(key: K): T[K] {
let object = this.objects[key as any];
if (!object) {
object = this.factories[key as any](this);
this.objects[key as any] = object;
}
return object;
}
}
const Container = createContext(
new DependencyContainer({})
.set("loadProducts", () => loadProductsFromMemory)
.set("getProducts", (x) => getProducts(x.get("loadProducts")))
.set(
"showProducts",
(x) => (presentProducts: PresentProducts) =>
showProducts(x.get("getProducts"), presentProducts)
)
);
const App = () => {
const container = useContext(Container);
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
container.get("showProducts")(setProducts)();
}, []);
return (
<ul>
{products.map((product) => (
<li>{product.name}</li>
))}
</ul>
);
};
createRoot(document.getElementById("root")).render(<App />);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment