𝗠𝗮𝘀𝘁𝗲𝗿𝗶𝗻𝗴 𝗘𝘃𝗲𝗻𝘁-𝗗𝗿𝗶𝘃𝗲𝗻 𝗗𝗲𝗰𝗼𝘂𝗽𝗹𝗶𝗻𝗴 𝗶𝗻 𝗟𝗮𝗿𝗮𝘃𝗲𝗹
Your Laravel controllers often become dumping grounds for business logic.
You start with a simple registration flow. Soon, you add email notifications, Slack alerts, audit logs, and API calls to one single method. This creates a fat controller.
Fat controllers make your code fragile. They are hard to test. They break the Single Responsibility Principle.
You do not need complex tools like RabbitMQ to fix this. Laravel has a built-in event system that works for most needs.
The problem with tight coupling: If a newsletter API is slow, your user registration slows down. If a mail service fails, the entire request fails.
The solution: Event-Driven Architecture.
Events act as a middle layer. Your controller announces an action. Listeners react to that action independently.
A lean controller looks like this:
public function register(RegisterRequest $request) { $user = User::create($request->validated());
UserRegistered::dispatch($user);
return response()->json(['message' => 'Success'], 201);
}
The controller now only handles data persistence. It does not care about side effects.
You gain three major benefits:
- Performance: Users get a response immediately. Heavy tasks run in the background using the ShouldQueue interface.
- Resilience: If a service is down, the listener can retry the task without breaking the main application.
- Extensibility: You can add new features, like push notifications, by adding a new listener. You do not touch the original controller.
Best practices to follow:
- Focus on side effects: Use events for post-processing. Do not use them for core logic that must happen instantly.
- Use descriptive names: Use past-tense names like OrderPlaced or UserRegistered. This shows the action already happened.
- Avoid over-abstraction: If a piece of code is simple and used in one place, a function call is better than an event.
Use Eloquent Observers for database changes. Use Events for business actions.
Refactoring to events is about durability. It makes your code easier to debug and faster to test.
Pick one noisy side effect in your controller and move it to a listener today.
Try this sandbox example: https://onlinephp.io/c/1f7b2
ก้าวข้าม Fat Controllers: เชี่ยวชาญการทำ Decoupling ด้วย Event-Driven ใน Laravel
คุณเคยพบว่าตัวเองกำลังจ้องมอง method ใน controller ที่มีความยาวหลายร้อยบรรทัดบ้างไหม? คุณกำลังจัดการกับการตรวจสอบข้อมูล (validation), การบันทึกข้อมูลลงฐานข้อมูล, การส่งอีเมลต้อนรับ, การแจ้งเตือนผ่าน Slack และการบันทึก log ทั้งหมดนี้ภายในฟังก์ชันเดียว
หากใช่ ยินดีด้วย! คุณกำลังเผชิญกับปัญหา "Fat Controller"
ในบทความนี้ เราจะมาเจาะลึกถึงปัญหาของ Fat Controller และดูว่าเราจะสามารถใช้ Event-Driven Architecture ใน Laravel เพื่อทำ Decoupling (การแยกส่วนประกอบ) เพื่อสร้างโค้ดที่สะอาดขึ้น ยืดหยุ่นขึ้น และบำรุงรักษาง่ายขึ้นได้อย่างไร
ปัญหาของ Fat Controller
ในตอนแรก เมื่อคุณสร้างแอปพลิเคชันขนาดเล็ก การเขียนทุกอย่างลงใน Controller อาจดูเหมือนเป็นเรื่องที่รวดเร็วและง่ายดาย แต่เมื่อแอปพลิเคชันของคุณเติบโตขึ้น Controller ของคุณจะเริ่มกลายเป็น "ถังขยะ" ที่รวมทุกอย่างไว้ด้วยกัน
สัญญาณเตือนว่าคุณมี Fat Controller:
- ความรับผิดชอบมากเกินไป (Violation of Single Responsibility Principle): Controller ของคุณไม่ได้ทำหน้าที่แค่รับ request และส่ง response แต่ยังทำหน้าที่จัดการ business logic, การสื่อสารกับบริการภายนอก และการจัดการข้อมูลด้วย
- ยากต่อการทดสอบ (Difficult to Test): การเขียน Unit Test สำหรับ Controller ที่ทำหน้าที่หลายอย่างนั้นเป็นเรื่องยากมาก เพราะคุณต้องจำลอง (mock) ทุกอย่าง ตั้งแต่ฐานข้อมูลไปจนถึงบริการส่งอีเมล
- โค้ดที่ซ้ำซ้อน (Code Duplication): หากคุณต้องการทำกระบวนการเดียวกันในที่อื่น (เช่น ผ่าน CLI command) คุณอาจต้องคัดลอกโค้ดจาก Controller ไปวางไว้ที่อื่น
- ความเปราะบาง (Fragility): การเปลี่ยนแปลงเล็กน้อยในส่วนหนึ่งของ logic อาจส่งผลกระทบต่อส่วนอื่น ๆ ใน Controller เดียวกันโดยไม่ตั้งใจ
ทางออก: การทำ Decoupling ด้วย Event-Driven Architecture
Decoupling คือกระบวนการลดความเกี่ยวพัน (dependencies) ระหว่างส่วนต่าง ๆ ของแอปพลิเคชัน แทนที่ Controller จะต้อง "รู้" ว่าต้องทำอะไรบ้างหลังจากบันทึกข้อมูลสำเร็จ มันจะแค่ "ประกาศ" (dispatch) ว่าเหตุการณ์บางอย่างได้เกิดขึ้นแล้ว และปล่อยให้ส่วนอื่น ๆ มาจัดการต่อเอง
นี่คือที่ที่ Events และ Listeners ใน Laravel เข้ามามีบทบาท
แนวคิดพื้นฐาน:
- Event: คือ "สิ่งที่เกิดขึ้น" ในระบบ (เช่น
UserRegistered,OrderPlaced) - Listener: คือ "สิ่งที่ต้องทำ" เมื่อเหตุการณ์นั้นเกิดขึ้น (เช่น
SendWelcomeEmail,UpdateInventory)
การนำไปใช้งานจริง: ตัวอย่างการลงทะเบียนผู้ใช้
ลองมาดูตัวอย่างการเปลี่ยนจาก Fat Controller มาเป็น Event-Driven
แบบเดิม: Fat Controller
public function store(Request $request)
{
// 1. Validate request
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
// 2. Create user
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
// 3. Send welcome email
Mail::to($user->email)->send(new WelcomeMail($user));
// 4. Notify Slack
Slack::notify("New user registered: {$user->email}");
// 5. Log the activity
Log::info("User registered: {$user->id}");
return response()->json($user, 201);
}
จะเห็นว่า Controller นี้ต้องรับผิดชอบทั้งการจัดการ User, การส่งอีเมล, การแจ้งเตือน Slack และการทำ Log
แบบใหม่: การใช้ Events และ Listeners
ขั้นตอนที่ 1: สร้าง Event
ใช้คำสั่ง Artisan เพื่อสร้าง Event:
php artisan make:event UserRegistered
ในไฟล์ UserRegistered.php:
class UserRegistered
{
use Dispatchable, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
ขั้นตอนที่ 2: สร้าง Listeners
สร้าง Listener สำหรับแต่ละงานที่ต้องการทำ:
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:listener NotifySlackOnRegistration --event=UserRegistered
php artisan make:listener LogUserRegistration --event=UserRegistered
ตัวอย่างใน SendWelcomeEmail.php:
class SendWelcomeEmail
{
public function handle(UserRegistered $event)
{
Mail::to($event->user->email)->send(new WelcomeMail($event->user));
}
}
ขั้นตอนที่ 3: ลงทะเบียน Event และ Listeners
ใน EventServiceProvider.php:
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
NotifySlackOnRegistration::class,
LogUserRegistration::class,
],
];
ขั้นตอนที่ 4: ปรับปรุง Controller
ตอนนี้ Controller ของคุณจะเหลือเพียงแค่หน้าที่หลักของมัน:
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
// ประกาศว่าเหตุการณ์เกิดขึ้นแล้ว!
event(new UserRegistered($user));
return response()->json($user, 201);
}
ประโยชน์ที่ได้รับ
- Single Responsibility Principle (SRP): Controller มีหน้าที่แค่จัดการ request และสร้าง user เท่านั้น ส่วนงานอื่น ๆ ถูกแยกออกไปอย่างชัดเจน
- Scalability: หากในอนาคตคุณต้องการเพิ่มระบบส่ง SMS เมื่อมีการลงทะเบียน คุณเพียงแค่สร้าง Listener ใหม่และลงทะเบียนมัน โดยไม่ต้องแตะต้องโค้ดใน Controller เลย
- Testability: คุณสามารถเขียน Unit Test สำหรับแต่ละ Listener แยกกันได้อย่างอิสระ และทดสอบ Controller ได้ง่ายขึ้นโดยการตรวจสอบว่า Event ถูก dispatch ออกมาหรือไม่
- Asynchronous Processing: คุณสามารถทำให้ Listeners ทำงานแบบ background ได้ง่าย ๆ เพียงแค่ implement อินเทอร์เฟซ
ShouldQueueซึ่งจะช่วยเพิ่มประสิทธิภาพ (performance) ให้กับแอปพลิเคชันของคุณอย่างมาก
บทสรุป
การเปลี่ยนจาก Fat Controller มาเป็น Event-Driven Architecture อาจต้องใช้เวลาในการวางโครงสร้างในช่วงแรก แต่ผลลัพธ์ที่ได้คือโค้ดที่มีคุณภาพสูงขึ้น บำรุงรักษาง่าย และพร้อมสำหรับการเติบโตของธุรกิจ
เริ่มจากการมองหา Controller ที่เริ่ม "บวม" และลองนำ Events มาใช้ดู แล้วคุณจะพบว่าชีวิตการเขียน Laravel ของคุณง่ายขึ้นอย่างเห็นได้ชัด!