Design pattern adalah solusi yang sudah terbukti untuk masalah yang sering berulang dalam pengembangan software. Di Laravel, beberapa pattern sudah ada built-in (Observer, Factory, Facade), sementara yang lain perlu diimplementasikan sendiri. Yang penting diingat: pattern adalah alat, bukan tujuan. Gunakan hanya jika ada masalah nyata yang diselesaikannya.
1. Service Pattern
Memindahkan business logic dari controller ke class khusus (Service). Ini adalah pattern yang paling fundamental dan hampir selalu relevant di proyek medium ke atas.
// app/Services/PostService.php
class PostService
{
public function __construct(
private readonly Post $postModel,
private readonly SeoService $seo,
) {}
public function publish(Post $post, User $author): Post
{
$post->update([
'status' => 'published',
'published_at' => now(),
]);
$this->seo->generateMeta($post);
PostPublished::dispatch($post, $author);
return $post->fresh();
}
}
// Controller tetap tipis
class PostController extends Controller
{
public function publish(Post $post): RedirectResponse
{
$this->authorize('publish', $post);
$this->postService->publish($post, auth()->user());
return redirect()->route('posts.show', $post)->with('success', 'Post dipublikasikan.');
}
}
2. Repository Pattern
Abstraksi atas data access layer. Berguna ketika query kompleks berulang di banyak tempat, atau ketika ingin memisahkan data access dari business logic untuk keperluan testing.
// app/Repositories/PostRepository.php
class PostRepository
{
public function findPublished(array $filters = []): LengthAwarePaginator
{
return Post::query()
->with(['author', 'categories'])
->published()
->when($filters['category'] ?? null, fn ($q, $cat) => $q->inCategory($cat))
->when($filters['search'] ?? null, fn ($q, $s) => $q->search($s))
->latest('published_at')
->paginate(15);
}
}
Kapan TIDAK pakai Repository: Jika query sederhana dan tidak berulang di banyak tempat, Repository hanya menambah boilerplate. Eloquent langsung dari Service sudah cukup.
3. Observer Pattern
Laravel punya built-in Eloquent Observer untuk merespons model events tanpa mencemari model atau controller dengan side effects.
// app/Observers/PostObserver.php
class PostObserver
{
public function created(Post $post): void
{
Cache::tags(['posts'])->flush();
}
public function updated(Post $post): void
{
if ($post->wasChanged('status') && $post->status === 'published') {
dispatch(new GenerateSitemapJob());
$post->author->notify(new PostPublishedNotification($post));
}
Cache::forget("post.{$post->id}");
}
public function deleted(Post $post): void
{
Cache::tags(['posts'])->flush();
dispatch(new CheckBrokenLinksJob());
}
}
// Register di AppServiceProvider
Post::observe(PostObserver::class);
4. Strategy Pattern
Mendefinisikan keluarga algoritma yang bisa dipertukarkan. Sangat berguna untuk implementasi payment gateway, notifikasi, atau export berbagai format.
// Interface
interface PaymentGateway
{
public function charge(Order $order): PaymentResult;
}
// Implementasi konkret
class MidtransGateway implements PaymentGateway
{
public function charge(Order $order): PaymentResult { /* ... */ }
}
class XenditGateway implements PaymentGateway
{
public function charge(Order $order): PaymentResult { /* ... */ }
}
// Context yang menggunakan strategy
class PaymentService
{
public function process(Order $order, PaymentGateway $gateway): PaymentResult
{
return $gateway->charge($order);
}
}
// Di controller, strategy ditentukan dari request
$gateway = match($request->payment_method) {
'midtrans' => app(MidtransGateway::class),
'xendit' => app(XenditGateway::class),
default => throw new InvalidArgumentException('Unsupported payment method'),
};
5. Decorator Pattern
Menambahkan behavior ke object secara dinamis tanpa mengubah class-nya. Di Laravel, ini sering diimplementasikan via interface + wrapper class.
// Tambahkan caching ke repository tanpa mengubahnya
class CachedPostRepository implements PostRepositoryInterface
{
public function __construct(
private readonly PostRepository $repository,
private readonly int $ttl = 1800,
) {}
public function findPublished(array $filters = []): LengthAwarePaginator
{
$key = 'posts.' . md5(serialize($filters));
return Cache::remember($key, $this->ttl, fn () =>
$this->repository->findPublished($filters)
);
}
}
// Binding di Service Container
app()->bind(PostRepositoryInterface::class, CachedPostRepository::class);
6. Pipeline Pattern
Laravel punya built-in Pipeline class untuk memproses data melalui serangkaian steps. Berguna untuk validasi bertahap, transformasi data, atau middleware aplikasi.
$result = Pipeline::send($post)
->through([
ValidateSeoRequirements::class,
OptimizeImages::class,
GenerateSocialMeta::class,
NotifySubscribers::class,
])
->thenReturn();
Anti-Pattern yang Harus Dihindari
- Over-engineering — Jangan implementasikan 6 pattern sekaligus untuk fitur CRUD sederhana
- Anemic Domain Model — Model yang hanya berisi field tanpa behavior apapun, semua logika di service
- God Service — Service class 1000+ baris yang melakukan segalanya
- Repository tanpa tujuan — Repository yang isinya hanya thin wrapper Eloquent tanpa tambahan nilai
Pattern terbaik adalah pattern yang menyelesaikan masalah nyata dengan kompleksitas minimal. Mulai sederhana, tambahkan abstraksi hanya ketika kode mulai terasa sulit di-maintain atau di-test.