PHP Academy · บทเรียน

เหตุการณ์โดเมนและบริการโดเมน

บันทึกข้อเท็จจริงทางธุรกิจด้วยเหตุการณ์และบริการโดเมน

บทเรียน 3 จาก 413 ขั้นตอน

เหตุการณ์โดเมนและบริการโดเมน เป็นบทเรียน PHP Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PHP Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน

การบันทึกข้อเท็จจริงทางธุรกิจ

พฤติกรรมไม่ได้ทุกอย่างจะเหมาะกับการอยู่ภายในเอนทิตีเดียว เหตุการณ์โดเมน จะบันทึกข้อเท็จจริงทางธุรกิจที่มีความหมายและเกิดขึ้นแล้ว เช่น OrderPlaced เพื่อให้ส่วนอื่นของระบบตอบสนองได้ ส่วน บริการโดเมน จะเก็บตรรกะของโดเมนที่ไม่ได้เป็นของเอนทิตีหรืออ็อบเจ็กต์ค่าใดโดยธรรมชาติ บทเรียนนี้จะแสดงให้เห็นว่าทั้งสองรูปแบบช่วยให้แบบจำลองสื่อความหมายได้ชัดเจนโดยไม่ทำให้แอกกริเกตใหญ่เกินไปอย่างไร

เหตุการณ์โดเมนคืออะไร

เหตุการณ์โดเมน คือบันทึกที่ไม่เปลี่ยนรูปของสิ่งที่เกิดขึ้นในโดเมน โดยตั้งชื่อเป็น รูปอดีตกาล ตามภาษากลางของโดเมน เช่น OrderPlaced PaymentReceived และ CustomerRelocated เหตุการณ์นี้เก็บข้อมูลที่อธิบายข้อเท็จจริง รวมถึงเวลาที่เกิดขึ้น เหตุการณ์นี้เป็นอ็อบเจ็กต์ค่า ซึ่งสร้างขึ้นครั้งเดียวและไม่เปลี่ยนแปลงอีก

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
        public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
    ) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;

แอกกริเกตบันทึกเหตุการณ์

รากแอกกริเกตจะบันทึกเหตุการณ์เป็นส่วนหนึ่งของพฤติกรรม จากนั้นเปิดเผยเหตุการณ์เหล่านั้นเพื่อให้ชั้นแอปพลิเคชันส่งต่อ หลังจากธุรกรรมยืนยันสำเร็จ ตัวแอกกริเกตเองจะไม่ส่งต่อเหตุการณ์ แต่จะทำหน้าที่เพียง บันทึก เท่านั้น วิธีนี้ทำให้โดเมนไม่ผูกติดกับข้อกังวลด้านโครงสร้างพื้นฐาน

<?php
final class OrderPlaced {
    public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
    private array $events = [];
    protected function record(object $e): void { $this->events[] = $e; }
    public function releaseEvents(): array {
        $out = $this->events; $this->events = []; return $out;
    }
}
final class Order extends AggregateRoot {
    public function __construct(public readonly string $id) {
        $this->record(new OrderPlaced($id));
    }
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }

ส่งต่อหลังยืนยันธุรกรรม

บริการแอปพลิเคชันจะจัดเก็บแอกกริเกต ยืนยันธุรกรรมให้ เสร็จสมบูรณ์ แล้วจึงส่งต่อเหตุการณ์ที่ถูกปล่อยออกมา การส่งต่อ หลังยืนยันธุรกรรม ทำให้มั่นใจว่าตัวจัดการจะไม่ตอบสนองต่อข้อเท็จจริงที่ถูกยกเลิกไปพร้อมกับธุรกรรม ลำดับด้านล่างเป็นรูปแบบที่ปลอดภัยสำหรับผลข้างเคียง เช่น อีเมลหรือการเรียกใช้ระบบภายนอก

<?php
final class PlaceOrderService {
    public function __construct(
        private object $repo,
        private object $dispatcher
    ) {}
    public function handle(object $order): void {
        // 1. persist inside a transaction
        $this->repo->save($order);   // commit happens here
        // 2. only AFTER commit, release and dispatch
        foreach ($order->releaseEvents() as $event) {
            $this->dispatcher->dispatch($event);
        }
    }
}
echo "order persisted, then events dispatched", PHP_EOL;

ตัวจัดการตอบสนอง

เหตุการณ์แต่ละรายการสามารถมีตัวจัดการได้หลายตัว โดยไม่ผูกติดกับแอกกริเกตที่ทำให้เกิดเหตุการณ์นั้น Dispatcher ตาม PSR-14 จะส่งเหตุการณ์ไปยังตัวรับฟังที่ลงทะเบียนไว้ การเพิ่มการตอบสนองใหม่ เช่น การวิเคราะห์ข้อมูลหรือการแจ้งเตือน หมายถึงการเพิ่มตัวรับฟังหนึ่งตัว โดยไม่ต้องแก้ไขแอกกริเกต

<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }

final class Dispatcher {
    private array $listeners = [];
    public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
    public function dispatch(object $event): void {
        foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
    }
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));

เหตุการณ์ข้ามขอบเขต

เหตุการณ์โดเมนช่วยแยกแอกกริเกตที่ต้องรักษาความสอดคล้องโดยไม่ต้องใช้ธุรกรรมร่วมกัน OrderPlaced ในบริบทการขายสามารถกระตุ้นการจองสินค้าในอีกบริบทหนึ่งผ่านเหตุการณ์ได้ ภายในระบบ เหตุการณ์เหล่านี้เป็นอ็อบเจ็กต์ แต่เมื่อข้ามบริการ มักจะกลายเป็น เหตุการณ์เชื่อมต่อระบบ ที่ส่งผ่านตัวกลางรับส่งข้อความ ควรแยกเหตุการณ์โดเมนภายในออกจากเหตุการณ์เชื่อมต่อระบบภายนอกอย่างชัดเจน เพราะสัญญาสาธารณะไม่ควรเป็นแบบจำลองภายในของคุณ

บริการโดเมนคืออะไร

บริการโดเมน เก็บตรรกะของโดเมนที่:

  • เกี่ยวข้องกับ แอกกริเกตหรือเอนทิตีหลายรายการ จึงไม่เหมาะจะอยู่ในรายการใดรายการหนึ่ง และ
  • เป็นแนวคิดของโดเมนอย่างแท้จริง โดยแสดงออกผ่านภาษากลางของโดเมน

บริการนี้ ไร้สถานะ และทำงานกับอ็อบเจ็กต์โดเมน บริการ FundsTransfer ที่หักเงินจากบัญชีหนึ่งแล้วเพิ่มเงินให้บัญชีอีกบัญชีหนึ่งเป็นตัวอย่างที่พบได้ทั่วไป

บริการโดเมนในโค้ด

บริการนี้ประสานงานแอกกริเกตสองรายการ แต่ไม่เก็บสถานะของตนเอง ตรรกะนี้ไม่ได้เป็นของบัญชีใดบัญชีหนึ่ง จึงอยู่ในบริการที่ตั้งชื่อตามการดำเนินการของโดเมน

<?php
final class Account {
    public function __construct(public readonly string $id, private int $cents) {}
    public function withdraw(int $c): void {
        if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
        $this->cents -= $c;
    }
    public function deposit(int $c): void { $this->cents += $c; }
    public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
    public function transfer(Account $from, Account $to, int $cents): void {
        $from->withdraw($cents);
        $to->deposit($cents);
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400

บริการโดเมนเทียบกับบริการแอปพลิเคชัน

อย่าสับสนระหว่างสองสิ่งนี้:

  • บริการโดเมน มีตรรกะทางธุรกิจและอยู่ในชั้นโดเมน โดยไม่รู้อะไรเกี่ยวกับธุรกรรมหรือการสื่อสารผ่านเว็บ
  • บริการแอปพลิเคชัน ประสานงานกรณีการใช้งานหนึ่งกรณี โดยโหลดแอกกริเกตจากรีโพซิทอรี เรียกใช้ตรรกะของโดเมน ยืนยันธุรกรรม และส่งต่อเหตุการณ์ บริการนี้เป็นผู้ประสานงานที่มีหน้าที่บางเบาและไม่มีกฎทางธุรกิจของตนเอง

หลีกเลี่ยงกับดักของบริการ

บริการโดเมนถูกใช้มากเกินไปได้ง่าย หากตรรกะ สามารถอยู่บนเอนทิตีหรืออ็อบเจ็กต์ค่าได้ ก็ควรให้ตรรกะอยู่ที่นั่น นี่คือวิธีหลีกเลี่ยงแบบจำลองที่ไร้พฤติกรรม ควรใช้บริการโดเมนเฉพาะเมื่อการดำเนินการครอบคลุมหลายแอกกริเกตจริง ๆ หรือไม่มีที่อยู่ตามธรรมชาติ มิฉะนั้นคุณจะดึงพฤติกรรมออกจากเอนทิตี และสร้างโค้ดเชิงกระบวนงานขึ้นมาใหม่

บริการแอปพลิเคชันทำหน้าที่ประสานงาน

เมื่อนำทุกอย่างมาประกอบกัน: บริการแอปพลิเคชันแบบบางจะโหลดแอกเกรกตจากคลังข้อมูล เรียกใช้ บริการโดเมนสำหรับตรรกะที่ครอบคลุมหลายแอกเกรกต บันทึกผลลัพธ์ และส่งเหตุการณ์ต่อ บริการนี้ทำหน้าที่ประสานงานเท่านั้น — ไม่มีกฎธุรกิจเป็นของตนเอง

<?php
final class Account {
    public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
    public function transfer(Account $from, Account $to, int $c): void {
        if ($c > $from->cents) { throw new DomainException('insufficient'); }
        $from->cents -= $c; $to->cents += $c;
    }
}
final class TransferApp { // application service: orchestration only
    public function __construct(private MoneyTransferService $domain) {}
    public function run(Account $a, Account $b, int $c): void {
        $this->domain->transfer($a, $b, $c); // then repo->save + dispatch
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250

ตรวจสอบอย่างรวดเร็ว

เหตุการณ์และบริการ

สรุปทบทวน

คุณได้บันทึกข้อเท็จจริงทางธุรกิจและตรรกะที่ไม่รู้ว่าจะวางไว้ที่ใด เหตุการณ์โดเมนคือระเบียนที่ไม่เปลี่ยนแปลงและใช้รูปอดีต ซึ่งแอกเกรกต บันทึกไว้ แล้วชั้นแอปพลิเคชัน ส่งต่อหลังการยืนยันการทำรายการ การทำเช่นนี้ช่วยแยกปฏิกิริยาต่าง ๆ ออกจากกันและข้ามขอบเขตของแอกเกรกต ใช้ บริการโดเมนเก็บตรรกะโดเมนที่ไม่มีสถานะและครอบคลุมหลายแอกเกรกต โดยแยกจากบริการแอปพลิเคชันแบบบางที่เพียงประสานงานกรณีใช้งาน ใช้ทั้งสองอย่างอย่างพอเหมาะ เพื่อให้พฤติกรรมยังคงอยู่บนเอนทิตีและออบเจ็กต์ค่าในจุดที่ควรอยู่

เริ่มต้นได้ฟรี

เรียนรู้ PHP ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
49
บทเรียน
195

คำถามที่พบบ่อย

บทเรียน “เหตุการณ์โดเมนและบริการโดเมน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “เหตุการณ์โดเมนและบริการโดเมน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PHP Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “เหตุการณ์โดเมนและบริการโดเมน”

บันทึกข้อเท็จจริงทางธุรกิจด้วยเหตุการณ์และบริการโดเมน คุณปฏิบัติ PHP Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PHP Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน PHP Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “เหตุการณ์โดเมนและบริการโดเมน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน PHP Academy นี้ได้ไหม

ได้ บทเรียน PHP Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. องค์ประกอบพื้นฐาน DDD: เอนทิตีและออบเจ็กต์ค่า
  2. แอกกริเกต รีโพซิทอรี และแฟกทอรี
  3. เหตุการณ์โดเมนและบริการโดเมน
  4. บริบทที่มีขอบเขตและการทำแผนผังบริบท
← กลับไปที่ PHP Academy