0Pricing
PHP Academy · บทเรียน

กรณีใช้งานและบริการแอปพลิเคชัน

แสดงการทำงานทางธุรกิจเป็นกรณีใช้งานที่ไม่ผูกกับเฟรมเวิร์ก

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

กรณีใช้งานคืออะไร

กรณีใช้งาน (หรือเรียกว่าบริการแอปพลิเคชันหรืออินเทอร์แอกเตอร์) บันทึกการดำเนินการเฉพาะของแอปพลิเคชันไว้เพียงหนึ่งรายการ เช่น ลงทะเบียนผู้ใช้ สร้างคำสั่งซื้อ หรือ ยกเลิกการสมัครสมาชิก กรณีใช้งานจะประสานงานเอนทิตีและพอร์ตเพื่อทำตามเจตนาเดียว สิ่งสำคัญคือกรณีใช้งาน ไม่ผูกกับเฟรมเวิร์ก: ไม่มี Request ไม่มี Response และไม่มีตัวช่วยส่วนกลาง มีเพียง PHP ธรรมดาที่เรียกใช้จากที่ใดก็ได้

อ็อบเจ็กต์คำสั่งและผลลัพธ์

กรณีใช้งานรับ อ็อบเจ็กต์ถ่ายโอนข้อมูลคำสั่ง ที่ไม่เปลี่ยนแปลงเป็นอินพุต และส่งคืน อ็อบเจ็กต์ถ่ายโอนข้อมูลผลลัพธ์ อ็อบเจ็กต์ถ่ายโอนข้อมูลเป็นเพียงตัวเก็บข้อมูล — ไม่มีพฤติกรรมและไม่มีตรรกะตรวจสอบนอกเหนือจากโครงสร้าง คุณสมบัติ readonly (PHP 8.1+) ทำให้แก้ไขโดยไม่ได้รับอนุญาตไม่ได้

<?php
final class RegisterUserCommand
{
    public function __construct(
        public readonly string $email,
        public readonly string $plainPassword,
    ) {}
}

final class RegisterUserResult
{
    public function __construct(public readonly string $userId) {}
}

โครงสร้างภายในบริการแอปพลิเคชัน

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

<?php
final class RegisterUser
{
    public function __construct(
        private Users $users,
        private PasswordHasher $hasher,
    ) {}

    public function __invoke(RegisterUserCommand $c): RegisterUserResult {
        if ($this->users->existsByEmail($c->email)) {
            throw new EmailAlreadyRegistered($c->email);
        }
        $user = User::register(
            UserId::generate(),
            new Email($c->email),
            $this->hasher->hash($c->plainPassword),
        );
        $this->users->add($user);
        return new RegisterUserResult((string) $user->id());
    }
}

เก็บตรรกะไว้ในเอนทิตี

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

<?php
final class User
{
    private function __construct(
        private UserId $id,
        private Email $email,
        private string $passwordHash,
        private bool $active = false,
    ) {}

    public static function register(UserId $id, Email $e, string $hash): self {
        return new self($id, $e, $hash); // invariants enforced here
    }
    public function activate(): void {
        if ($this->active) throw new AlreadyActive();
        $this->active = true;
    }
    public function id(): UserId { return $this->id; }
}

ขอบเขตธุรกรรม

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

<?php
interface TransactionManager {
    public function transactional(callable $work): mixed;
}

final class TransactionalRegisterUser
{
    public function __construct(
        private RegisterUser $inner,
        private TransactionManager $tx,
    ) {}

    public function __invoke(RegisterUserCommand $c): RegisterUserResult {
        return $this->tx->transactional(fn() => ($this->inner)($c));
    }
}

การตรวจสอบความถูกต้องควรอยู่ที่ใด

แบ่งการตรวจสอบความถูกต้องออกเป็นสองระดับ:

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

กรณีใช้งานจะถือว่าอินพุตมีรูปแบบถูกต้อง และทำหน้าที่บังคับใช้ความหมาย

<?php
final class Email
{
    public function __construct(public readonly string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $value");
        }
    }
}

try { new Email('nope'); } catch (Throwable $e) { echo $e->getMessage(), PHP_EOL; }
echo (new Email('a@b.com'))->value, PHP_EOL;

ส่งคืนผลลัพธ์โดยไม่ใช้เอชทีทีพี

มีสองรูปแบบสำหรับส่งคืนข้อมูลโดยยังไม่ผูกกับเฟรมเวิร์ก:

  • ส่งคืนอ็อบเจ็กต์ถ่ายโอนข้อมูลผลลัพธ์ (เรียบง่ายและทำงานแบบพร้อมกัน)
  • พอร์ตผลลัพธ์ / ผู้นำเสนอ — กรณีใช้งานส่งผลลัพธ์ไปยังขอบเขตผลลัพธ์ที่ฉีดเข้ามา ทำให้อะแดปเตอร์เป็นผู้กำหนดรูปแบบ (เจสัน, HTML, CLI) วิธีนี้ทำให้แม้แต่รูปแบบการตอบกลับก็อยู่นอกแกนหลัก
<?php
interface RegisterUserOutput {
    public function present(RegisterUserResult $r): void;
}

final class RegisterUserWithPresenter {
    public function __construct(private Users $users, private PasswordHasher $h) {}
    public function __invoke(RegisterUserCommand $c, RegisterUserOutput $out): void {
        $user = User::register(UserId::generate(), new Email($c->email), $this->h->hash($c->plainPassword));
        $this->users->add($user);
        $out->present(new RegisterUserResult((string) $user->id()));
    }
}

เหตุการณ์โดเมนจากกรณีใช้งาน

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

<?php
trait RecordsEvents {
    private array $events = [];
    protected function record(object $e): void { $this->events[] = $e; }
    public function releaseEvents(): array {
        $e = $this->events; $this->events = []; return $e;
    }
}

final class UserRegistered {
    public function __construct(public readonly string $userId) {}
}

// Use case calls $user->releaseEvents() and hands them to a dispatcher
echo 'event recorded pattern', PHP_EOL;

หนึ่งคลาสต่อหนึ่งกรณีใช้งาน

ควรใช้ คลาสการทำงานเดียว (มีเมธอดสาธารณะหนึ่งเมธอด ซึ่งมักเป็น __invoke) แทนบริการขนาดใหญ่ที่มีสิบเมธอด ข้อดีคือ:

  • มีความรับผิดชอบเดียวและตั้งชื่อได้ชัดเจน (CancelSubscription แทน SubscriptionService::cancel)
  • ตัวสร้างฉีดเฉพาะสิ่งที่การดำเนินการ นี้ต้องใช้
  • ครอบด้วยเดคอเรเตอร์ได้ง่าย (ธุรกรรม การบันทึกเหตุการณ์ การอนุญาต)

การเชื่อมต่อที่จุดประกอบระบบ

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

<?php
$pdo      = new PDO('sqlite::memory:');
$users    = new PdoUsers($pdo);
$hasher   = new BcryptHasher();
$register = new RegisterUser($users, $hasher);

// Decorate with a transaction boundary
$register = new TransactionalRegisterUser($register, new PdoTransactionManager($pdo));

// Driving adapter calls it
$result = $register(new RegisterUserCommand('dev@coddykit.com', 's3cret!'));
echo $result->userId, PHP_EOL;

ข้อกังวลร่วมข้ามส่วนด้วยเดคอเรเตอร์

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

<?php
interface RegisterUserHandler {
    public function __invoke(RegisterUserCommand $c): RegisterUserResult;
}

final class LoggingRegisterUser implements RegisterUserHandler {
    public function __construct(
        private RegisterUserHandler $inner,
        private LoggerInterface $log,
    ) {}
    public function __invoke(RegisterUserCommand $c): RegisterUserResult {
        $this->log->info('register.start', ['email' => $c->email]);
        $r = ($this->inner)($c);
        $this->log->info('register.ok', ['id' => $r->userId]);
        return $r;
    }
}

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

กฎ "อีเมลต้องไม่ซ้ำและมีรูปแบบถูกต้อง" ควรอยู่ที่ใด?

สรุป

กรณีใช้งานที่ไม่ผูกกับเฟรมเวิร์กทำให้ได้ชั้นแอปพลิเคชันที่สะอาด:

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

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

บทเรียน “กรณีใช้งานและบริการแอปพลิเคชัน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “กรณีใช้งานและบริการแอปพลิเคชัน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ 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. จากสถาปัตยกรรมแบบเลเยอร์สู่สถาปัตยกรรมสะอาด
  2. อธิบายพอร์ตและอะแดปเตอร์
  3. กรณีใช้งานและบริการแอปพลิเคชัน
  4. การกลับทิศทางการพึ่งพาในการใช้งานจริง
← กลับไปที่ PHP Academy