Design Pattern trong Laravel & PHP: Giải Thích Dễ Hiểu Để Đi Làm & Phỏng Vấn
Design Pattern không phải là thứ gì xa lạ — chúng là những giải pháp đã được kiểm chứng để giải quyết các bài toán lập trình lặp đi lặp lại. Laravel áp dụng hầu hết các pattern này xuyên suốt framework. Hiểu chúng sẽ giúp bạn viết code tốt hơn, debug nhanh hơn, và tự tin hơn khi phỏng vấn.
Nội dung bài viết
1. Dependency Injection (DI)
Dependency Injection
Creational — Truyền phụ thuộc từ bên ngoài vào class
Một câu để nhớ: Thay vì class tự đi mua nguyên liệu, ai đó sẽ giao nguyên liệu tới tận nơi cho class.
class OrderService {
public function placeOrder($data) {
$payment = new StripePayment(); // Phụ thuộc cứng
$mailer = new Mailer(); // Phụ thuộc cứng
$payment->charge($data['amount']);
$mailer->send($data['email'], 'Đơn hàng đã đặt!');
}
}Vấn đề: muốn test OrderService thì phải kéo theo cả StripePayment và Mailer thật — không mock được.
class OrderService {
public function __construct(
private PaymentInterface $payment,
private MailerInterface $mailer,
) {}
public function placeOrder($data) {
$this->payment->charge($data['amount']);
$this->mailer->send($data['email'], 'Đơn hàng đã đặt!');
}
}
// Laravel tự inject khi resolve
$service = app(OrderService::class);- Dễ test: chỉ cần mock interface, không cần class thật
- Đổi payment từ Stripe → PayPal? Chỉ đổi 1 dòng binding
- Tuân thủ nguyên tắc SOLID (D — Dependency Inversion)
- Code rõ ràng hơn: dependencies được khai báo tường minh
Câu hỏi phỏng vấn thường gặp
- Q1.Dependency Injection là gì? Tại sao dùng?
- Q2.Sự khác nhau giữa Constructor Injection và Method Injection?
- Q3.DI giúp ích gì cho việc viết Unit Test?
2. Service Container
Service Container
Laravel IoC Container — Tự động inject dependencies
Một câu để nhớ: Service Container là kho đăng ký + nhà máy tự động — bạn đăng ký "khi cần X thì tạo Y", Laravel tự làm phần còn lại.
// app/Providers/AppServiceProvider.php
public function register(): void {
// Bind interface → implementation
$this->app->bind(
PaymentInterface::class,
StripePayment::class
);
// Singleton: chỉ tạo 1 lần duy nhất
$this->app->singleton(
CacheManager::class,
fn($app) => new CacheManager($app['config']['cache'])
);
// Instance cố định
$this->app->instance(
'api.key',
env('STRIPE_SECRET')
);
}// Trong Controller — Laravel inject tự động
class OrderController extends Controller {
public function __construct(
private PaymentInterface $payment // → StripePayment
) {}
}
// Resolve thủ công khi cần
$payment = app(PaymentInterface::class);
$payment = resolve(PaymentInterface::class);
// Resolve với tham số
$report = app(ReportService::class, ['format' => 'pdf']);bind() = tạo mới mỗi lần gọi. singleton() = tạo 1 lần, tái sử dụng. instance() = dùng object đã có sẵn.- Giải quyết dependency tự động — không cần new thủ công
- Đổi implementation chỉ cần sửa 1 dòng binding
- Quản lý vòng đời object (singleton vs transient)
- Nền tảng cho toàn bộ Laravel: Route, Middleware, Queue...
Câu hỏi phỏng vấn thường gặp
- Q1.Service Container khác gì Service Provider?
- Q2.Khi nào dùng bind() vs singleton()?
- Q3.Giải thích IoC (Inversion of Control) là gì?
3. Repository Pattern
Repository Pattern
Structural — Tách biệt logic truy vấn database
Một câu để nhớ: Repository là thư viện tra cứu — Controller hỏi "cho tôi user id=5", Repository biết cách lấy dữ liệu từ đâu (DB, cache, API...).
// 1. Interface — định nghĩa hợp đồng
interface UserRepositoryInterface {
public function find(int $id): ?User;
public function all(): Collection;
public function create(array $data): User;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
public function findByEmail(string $email): ?User;
}
// 2. Implementation — thực thi với Eloquent
class UserRepository implements UserRepositoryInterface {
public function find(int $id): ?User {
return User::find($id);
}
public function all(): Collection {
return User::orderBy('created_at', 'desc')->get();
}
public function create(array $data): User {
return User::create($data);
}
public function findByEmail(string $email): ?User {
return User::where('email', $email)->first();
}
// ...
}
// 3. Binding — đăng ký vào container
// AppServiceProvider.php
$this->app->bind(
UserRepositoryInterface::class,
UserRepository::class
);
// 4. Sử dụng trong Controller
class UserController extends Controller {
public function __construct(
private UserRepositoryInterface $users
) {}
public function show(int $id) {
$user = $this->users->find($id);
return $user
? response()->json($user)
: response()->json(['error' => 'Not found'], 404);
}
}- Controller sạch — không chứa query logic
- Dễ viết Unit Test (mock repository thay vì DB thật)
- Đổi từ MySQL → MongoDB? Chỉ viết thêm MongoUserRepository
- Tái sử dụng query logic ở nhiều nơi
- Tuân thủ Single Responsibility Principle
Câu hỏi phỏng vấn thường gặp
- Q1.Repository Pattern là gì? Lợi ích so với dùng Eloquent trực tiếp?
- Q2.Tại sao nên bind Interface thay vì Implementation?
- Q3.Repository Pattern có nhược điểm gì không?
- Q4.Sự khác biệt giữa Repository và Service?
4. Singleton Pattern
Singleton Pattern
Creational — Đảm bảo chỉ có 1 instance duy nhất
Một câu để nhớ: Singleton như tổng giám đốc công ty — chỉ có 1 người, ai cũng gặp người đó, không bao giờ có 2.
class DatabaseConnection {
private static ?self $instance = null;
private PDO $pdo;
// Constructor private — không ai new được từ ngoài
private function __construct() {
$this->pdo = new PDO(
'mysql:host=localhost;dbname=myapp',
'root', 'secret'
);
}
// Clone disabled
private function __clone() {}
public static function getInstance(): static {
if (static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
public function query(string $sql): array {
return $this->pdo->query($sql)->fetchAll();
}
}
// Sử dụng
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();
var_dump($db1 === $db2); // true — cùng 1 object// Cách Laravel làm — KHUYẾN NGHỊ hơn PHP thuần
// AppServiceProvider.php
$this->app->singleton(Logger::class, function ($app) {
return new Logger(
storage_path('logs/app.log'),
$app['config']['app.log_level']
);
});
// Mọi nơi trong app đều nhận cùng 1 instance
class OrderService {
public function __construct(private Logger $logger) {}
public function placeOrder($data) {
$this->logger->info('Order placed', $data);
}
}
class PaymentService {
public function __construct(private Logger $logger) {}
// Đây cũng là Logger instance đó — không tạo mới
}singleton() trong Service Container thay vì tự implement static Singleton. Code testable hơn, tránh global state.- Tiết kiệm tài nguyên: không tạo kết nối DB mới mỗi request
- Đảm bảo consistency: mọi nơi dùng cùng 1 config/logger
- Dễ quản lý shared state (cache, queue connection...)
Câu hỏi phỏng vấn thường gặp
- Q1.Singleton Pattern là gì? Khi nào nên dùng?
- Q2.Nhược điểm của Singleton là gì? (global state, khó test)
- Q3.Làm sao test code có Singleton? (override binding trong test)
- Q4.Singleton vs Static class — khác nhau thế nào?
5. Factory Pattern
Factory Pattern
Creational — Tạo object mà không cần biết class cụ thể
Một câu để nhớ: Factory như nhà máy — bạn đặt hàng "tôi cần Payment", nhà máy tự quyết định dây chuyền nào sản xuất.
interface NotificationInterface {
public function send(string $to, string $message): bool;
}
class EmailNotification implements NotificationInterface {
public function send(string $to, string $message): bool {
// Gửi email...
return true;
}
}
class SmsNotification implements NotificationInterface {
public function send(string $to, string $message): bool {
// Gửi SMS...
return true;
}
}
class PushNotification implements NotificationInterface {
public function send(string $to, string $message): bool {
// Push notification...
return true;
}
}
// Factory
class NotificationFactory {
public static function create(string $channel): NotificationInterface {
return match($channel) {
'email' => new EmailNotification(),
'sms' => new SmsNotification(),
'push' => new PushNotification(),
default => throw new InvalidArgumentException(
"Unknown channel: {$channel}"
),
};
}
}
// Sử dụng
$channel = $user->preferred_channel; // 'email' | 'sms' | 'push'
$notifier = NotificationFactory::create($channel);
$notifier->send($user->contact, 'Đơn hàng đã xác nhận!');// database/factories/UserFactory.php
class UserFactory extends Factory {
public function definition(): array {
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => Hash::make('password'),
'role' => 'user',
];
}
// States — biến thể của factory
public function admin(): static {
return $this->state(['role' => 'admin']);
}
public function suspended(): static {
return $this->state(['suspended_at' => now()]);
}
}
// Dùng trong Test
$user = User::factory()->create(); // 1 user bình thường
$admin = User::factory()->admin()->create(); // 1 admin
$users = User::factory()->count(50)->create(); // 50 users- Tách biệt logic tạo object khỏi logic nghiệp vụ
- Dễ thêm loại mới — chỉ thêm class + 1 case trong factory
- Client code không phụ thuộc vào concrete class
- Model Factory giúp tạo test data nhanh, nhất quán
Câu hỏi phỏng vấn thường gặp
- Q1.Factory Pattern là gì? Khác Simple Factory vs Factory Method vs Abstract Factory?
- Q2.Khi nào nên dùng Factory thay vì new trực tiếp?
- Q3.Laravel Model Factory dùng pattern gì?
6. Observer Pattern
Observer Pattern
Behavioral — Lắng nghe và phản ứng với sự kiện
Một câu để nhớ: Observer như đăng ký nhận thông báo — khi có sự kiện xảy ra (user đăng ký), tất cả "người đăng ký" đều được thông báo tự động.
// 1. Tạo Observer
// php artisan make:observer UserObserver --model=User
class UserObserver {
// Chạy SAU khi tạo user mới
public function created(User $user): void {
// Gửi email chào mừng
Mail::to($user->email)->send(new WelcomeMail($user));
// Tạo profile mặc định
$user->profile()->create([
'avatar' => 'default.png',
'bio' => '',
]);
// Log sự kiện
Log::info("New user registered: {$user->email}");
}
// Chạy TRƯỚC khi update
public function updating(User $user): void {
if ($user->isDirty('email')) {
$user->email_verified_at = null; // Reset verification
}
}
// Chạy SAU khi xoá
public function deleted(User $user): void {
// Dọn dẹp dữ liệu liên quan
Storage::deleteDirectory("users/{$user->id}");
}
}
// 2. Đăng ký Observer — AppServiceProvider hoặc Model
// Cách 1: trong boot() của AppServiceProvider
User::observe(UserObserver::class);
// Cách 2: dùng attribute (Laravel 10+)
#[ObservedBy(UserObserver::class)]
class User extends Model { ... }// ❌ Không dùng Observer — Controller bị phình to
class UserController {
public function store(Request $request) {
$user = User::create($request->validated());
// Những thứ này không liên quan đến controller
Mail::to($user->email)->send(new WelcomeMail($user));
$user->profile()->create(['avatar' => 'default.png']);
Log::info("New user: {$user->email}");
Cache::tags('users')->flush();
return response()->json($user, 201);
}
}
// ✅ Dùng Observer — Controller sạch sẽ
class UserController {
public function store(Request $request) {
$user = User::create($request->validated());
// Observer tự động xử lý email, profile, log, cache...
return response()->json($user, 201);
}
}- Controller/Service sạch — không chứa side effects
- Tự động kích hoạt khi model thay đổi — không cần nhớ gọi
- Dễ thêm hành vi mới mà không đụng code cũ
- Tuân thủ Open/Closed Principle
Câu hỏi phỏng vấn thường gặp
- Q1.Observer Pattern là gì? Ví dụ thực tế trong Laravel?
- Q2.Observer khác gì Events/Listeners trong Laravel?
- Q3.Nhược điểm của Observer? (logic ẩn, khó debug)
- Q4.Khi nào dùng Observer, khi nào dùng Event/Listener?
7. Tổng kết — Khi Nào Dùng Cái Nào?
| Pattern | Loại | Dùng khi |
|---|---|---|
| Dependency Injection | Creational | Luôn dùng — nền tảng của clean code |
| Service Container | Creational | Cần quản lý dependency tự động trong app |
| Repository | Structural | App có nhiều query phức tạp, cần test DB |
| Singleton | Creational | Config, Logger, DB Connection — shared resource |
| Factory | Creational | Cần tạo nhiều loại object theo điều kiện |
| Observer | Behavioral | Cần thực hiện side-effect khi model thay đổi |
📚 Thứ tự học đề xuất
- 1Dependency Injection — hiểu khái niệm core
- 2Service Container — cách Laravel implement DI
- 3Repository Pattern — áp dụng vào project thật
- 4Factory + Singleton — khi gặp use case cụ thể
- 5Observer — refactor side-effects trong project