Kalau kamu sudah mulai ngoding Laravel dan sering lihat kelas yang constructornya diisi parameter aneh-aneh — “ini dari mana datangnya? Kan tidak pernah di-new manual?” — itu adalah Dependency Injection yang sedang bekerja.
Topik ini sering terasa misterius karena Laravel melakukan banyak hal “otomatis” di balik layar. Artikel ini akan buka tutupnya satu per satu: mulai dari kenapa DI ada, lalu tiga cara pakainya, sampai bagaimana Laravel tahu harus inject apa.
Kita mulai dari masalahnya dulu.
🤔 Masalah yang DI Selesaikan
Bayangin kamu lagi bikin fitur artikel di blog. Ada ArticleController yang butuh ArticleService untuk logika bisnisnya.
Cara paling naif yang langsung terpikirkan:
class ArticleController extends Controller
{
public function index()
{
// Langsung bikin object di dalam controller
$service = new ArticleService();
return $service->getAll();
}
}
Kelihatannya tidak ada masalah. Tapi coba bayangkan tiga skenario ini:
Skenario 1: ArticleService butuh ArticleRepository untuk akses database. Jadi kamu harus new ArticleRepository() di dalam ArticleService. Dan ArticleRepository butuh koneksi database. Dan seterusnya — kamu tiba-tiba sedang merakit rantai panjang object secara manual.
Skenario 2: Kamu mau test ArticleController. Tapi new ArticleService() di dalamnya langsung memanggil database sungguhan — test-mu tidak bisa jalan tanpa database aktif.
Skenario 3: Kamu mau ganti ArticleService dengan versi berbeda (misalnya yang pakai cache). Kamu harus cari semua tempat yang nulis new ArticleService() dan ganti satu per satu.
Dependency Injection adalah solusinya. Alih-alih kelas yang membuat dependensinya sendiri, dependensi itu diberikan dari luar. Kelas cukup bilang “aku butuh ini” — urusan siapa yang menyediakan, bukan urusannya lagi.
🔧 Constructor Injection — “Kasih Sebelum Mulai”
Analoginya
Bayangin kamu seorang chef baru yang mulai kerja di restoran. Sebelum kamu mulai masak apapun, manajernya sudah menyiapkan semua yang kamu butuhkan di meja dapur: pisau, talenan, dan bumbu dasar.
Kamu tidak perlu keluar dan beli sendiri. Kamu tidak perlu tahu bumbu itu dari warung mana. Kamu tinggal masak.
Constructor Injection persis seperti itu — semua yang dibutuhkan kelas sudah tersedia sejak object pertama kali dibuat, sebelum method apapun dipanggil.
Kapan Cocok?
Kalau dependensi dipakai di banyak method dalam kelas yang sama. Inject sekali di constructor, pakai di mana saja.
Kodenya
Studi kasus: ArticleController butuh ArticleService untuk semua operasinya.
📁 app/Services/ArticleService.php
<?php
namespace App\Services;
use App\Models\Article;
use Illuminate\Support\Collection;
class ArticleService
{
public function getAll(): Collection
{
return Article::with('user')->latest()->get();
}
public function getPublished(): Collection
{
return Article::with('user')
->where('status', 'published')
->latest('published_at')
->get();
}
public function findOrFail(int $id): Article
{
return Article::findOrFail($id);
}
}
📁 app/Http/Controllers/ArticleController.php
<?php
namespace App\Http\Controllers;
use App\Services\ArticleService;
use Illuminate\Http\JsonResponse;
class ArticleController extends Controller
{
// $articleService tersimpan sebagai property —
// bisa dipakai oleh index(), show(), store(), dan method lain
public function __construct(
private ArticleService $articleService
) {}
public function index(): JsonResponse
{
$articles = $this->articleService->getPublished();
return response()->json($articles);
}
public function show(int $id): JsonResponse
{
$article = $this->articleService->findOrFail($id);
return response()->json($article);
}
}
📌 Perhatikan — ArticleController tidak pernah nulis new ArticleService(). Dia hanya mendeklarasikan bahwa dia butuh ArticleService. Laravel yang akan menyediakannya secara otomatis saat controller dipanggil.
Cara Kerja di Baliknya
Ketika ada request masuk ke route yang menuju ArticleController, Laravel membaca constructor-nya: “Oh, butuh ArticleService. Oke, aku buatkan.” Lalu object ArticleService dibuat dan dimasukkan ke constructor secara otomatis. Ini yang disebut auto-resolve — dan kita akan bahas lebih dalam di bagian IoC Container.
💉 Method Injection — “Kasih Saat Dibutuhkan”
Analoginya
Masih di restoran yang sama. Kali ini ada tamu yang pesan menu spesial yang butuh peralatan khusus — blow torch untuk membakar crème brûlée.
Blow torch itu tidak perlu ada di meja kamu sepanjang waktu. Kamu tidak pakai setiap hari. Kamu minta ke dapur hanya ketika ada pesanan menu itu saja.
Method Injection seperti itu — dependensi hanya disediakan ketika method tertentu dipanggil, tidak perlu ada sepanjang hidup object.
Kapan Cocok?
Kalau dependensi hanya dipakai di satu method tertentu — tidak perlu dibawa ke seluruh kelas.
Kodenya
Studi kasus: ada CommentController yang butuh ArticleService hanya di satu method — untuk validasi apakah artikel yang mau dikomentari itu ada.
📁 app/Http/Controllers/CommentController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCommentRequest;
use App\Services\ArticleService;
use App\Services\CommentService;
use Illuminate\Http\JsonResponse;
class CommentController extends Controller
{
// CommentService dipakai di banyak method → layak di constructor
public function __construct(
private CommentService $commentService
) {}
// ArticleService hanya dibutuhkan di method ini saja → method injection
public function store(StoreCommentRequest $request, ArticleService $articleService): JsonResponse
{
// Pastikan artikel yang dituju benar-benar ada
$article = $articleService->findOrFail($request->article_id);
$comment = $this->commentService->create(
$request->validated(),
$article,
$request->user()
);
return response()->json($comment, 201);
}
}
📌 ArticleService hanya ada di parameter method store(). Method lain di controller ini tidak akan mendapat — dan tidak butuh — ArticleService sama sekali. Laravel tetap auto-resolve dengan cara yang sama.
Method Injection di Route Closure
Ada satu tempat lagi yang sering dipakai untuk method injection: langsung di route closure. Cocok untuk endpoint ringan yang tidak butuh controller tersendiri.
📁 routes/api.php
use App\Services\ArticleService;
use Illuminate\Http\Request;
// Laravel inject ArticleService langsung ke closure ini
Route::get('/articles/published', function (Request $request, ArticleService $service) {
return $service->getPublished();
});
📌 Persis seperti di controller — tulis type-hint di parameter, Laravel yang sediakan. Tidak perlu new ArticleService() di mana pun.
📦 IoC Container — “Otak di Balik Semuanya”
Analoginya
Sekarang kamu mungkin bertanya: “Oke, tapi Laravel tahu dari mana harus inject apa?”
Bayangin ada gudang besar di restoran itu. Gudang ini menyimpan daftar semua peralatan yang tersedia, stoknya, dan aturan khusus kalau ada peralatan yang perlu disiapkan dengan cara tertentu sebelum diberikan.
Ketika chef butuh sesuatu, chef tidak pergi ke gudang sendiri. Ada petugas gudang yang bertugas: membaca kebutuhan chef, mengambil dari gudang, menyiapkan sesuai aturan, lalu mengantarkan.
IoC Container (Inversion of Control Container) adalah petugas gudang itu. Di Laravel, dia diakses lewat app() atau App::make(). Dan aturan-aturan khusus tadi? Itu yang kita tulis di Service Provider.
Auto-resolve — Saat Kelas Tidak Butuh Konfigurasi
Untuk kelas biasa seperti ArticleService yang tidak punya dependensi kompleks, IoC Container bisa langsung membuatnya tanpa perlu instruksi apapun. Cukup type-hint, Laravel resolve sendiri.
// Laravel resolve ArticleService otomatis — kamu tidak perlu daftarkan apapun
$service = app(ArticleService::class);
// Atau lewat facade
$service = App::make(ArticleService::class);
Ini jalan karena Laravel membaca constructor ArticleService, melihat dependensinya, membuat dependensi itu dulu, lalu membuat ArticleService-nya. Rekursif sampai semua dependensi terpenuhi.
Binding — Saat Interface Perlu Dipetakan ke Implementasi
Auto-resolve tidak bisa jalan untuk interface. Kalau kamu type-hint dengan ArticleRepositoryInterface, Laravel tidak tahu implementasi mana yang harus dipakai — ada banyak kemungkinan.
Di sinilah binding dibutuhkan.
Studi kasus: CommentService bergantung pada CommentRepositoryInterface, bukan langsung ke Eloquent.
📁 app/Contracts/CommentRepositoryInterface.php
<?php
namespace App\Contracts;
use App\Models\Comment;
use Illuminate\Support\Collection;
interface CommentRepositoryInterface
{
public function getUnapproved(): Collection;
public function approve(int $commentId): void;
}
📁 app/Repositories/EloquentCommentRepository.php
<?php
namespace App\Repositories;
use App\Contracts\CommentRepositoryInterface;
use App\Models\Comment;
use Illuminate\Support\Collection;
class EloquentCommentRepository implements CommentRepositoryInterface
{
public function getUnapproved(): Collection
{
return Comment::where('is_approved', false)->latest()->get();
}
public function approve(int $commentId): void
{
Comment::findOrFail($commentId)->update(['is_approved' => true]);
}
}
📁 app/Providers/AppServiceProvider.php
Di sinilah kita “mendaftarkan aturan” ke IoC Container — interface ini dipasangkan ke implementasi ini.
<?php
namespace App\Providers;
use App\Contracts\CommentRepositoryInterface;
use App\Repositories\EloquentCommentRepository;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// "Kalau ada yang minta CommentRepositoryInterface,
// berikan EloquentCommentRepository."
$this->app->bind(
CommentRepositoryInterface::class,
EloquentCommentRepository::class
);
}
}
📁 app/Services/CommentService.php
Service ini tidak tahu apakah yang dipakai Eloquent, cache, atau API — dia hanya tahu kontraknya.
<?php
namespace App\Services;
use App\Contracts\CommentRepositoryInterface;
use Illuminate\Support\Collection;
class CommentService
{
public function __construct(
private CommentRepositoryInterface $commentRepository
) {}
public function getUnapproved(): Collection
{
return $this->commentRepository->getUnapproved();
}
public function approve(int $commentId): void
{
$this->commentRepository->approve($commentId);
}
}
📌 Kalau besok kamu mau ganti implementasi repository — misalnya pakai cache untuk performa lebih cepat — kamu hanya ubah satu baris di AppServiceProvider. CommentService, controller, dan semua kode lain tidak perlu disentuh sama sekali.
bind vs singleton — Kapan Pakai Yang Mana?
Ada dua cara binding yang paling sering dipakai, dan perbedaannya penting:
// bind → buat object BARU setiap kali diminta
$this->app->bind(
CommentRepositoryInterface::class,
EloquentCommentRepository::class
);
// singleton → buat object SEKALI, lalu pakai ulang object yang sama
$this->app->singleton(
ArticleService::class,
ArticleService::class
);
bind | singleton | |
|---|---|---|
| Jumlah object yang dibuat | Baru setiap request ke container | Sekali, lalu di-reuse |
| Cocok untuk | Repository, kelas yang butuh state bersih | Service stateless, koneksi, konfigurasi |
| Contoh di Laravel | Form Request, Action kecil | Cache, Queue, Auth Guard |
📌 Untuk kebanyakan service di blog kita, singleton sudah cukup — tidak ada state yang perlu di-reset antar penggunaan. Pakai bind kalau kamu butuh object yang benar-benar fresh setiap kali.
🗺️ Gambaran Besar — Alur Lengkap
Ini alur yang terjadi dari request masuk sampai response keluar, dikaitkan dengan DI:
Request masuk
↓
Router menemukan ArticleController@store
↓
IoC Container membaca constructor ArticleController
→ Butuh ArticleService
↓
IoC Container membaca constructor ArticleService
→ Tidak ada dependensi → langsung dibuat
↓
ArticleService selesai dibuat
↓
ArticleController dibuat, ArticleService dimasukkan ke constructor
↓
Method store() dipanggil
→ Butuh StoreArticleRequest (method injection) → dibuat
↓
Response dikembalikan
Dan struktur file-nya:
app/
├── Contracts/
│ └── CommentRepositoryInterface.php ← kontrak repository
│
├── Http/
│ ├── Controllers/
│ │ ├── ArticleController.php ← constructor injection
│ │ └── CommentController.php ← constructor + method injection
│ └── Requests/
│ └── StoreCommentRequest.php
│
├── Services/
│ ├── ArticleService.php ← di-inject lewat constructor
│ └── CommentService.php ← bergantung pada interface
│
├── Repositories/
│ └── EloquentCommentRepository.php ← implementasi konkret
│
└── Providers/
└── AppServiceProvider.php ← tempat binding interface → implementasi
🧭 Kapan Pakai Yang Mana?
Constructor Injection ketika dependensi dipakai di banyak method dalam satu kelas. Ini pilihan default — kalau ragu, pakai ini.
Method Injection ketika dependensi hanya dipakai di satu method tertentu, atau di route closure yang terlalu kecil untuk dibuatkan controller sendiri.
IoC Container + Binding ketika kamu type-hint dengan interface, atau butuh kontrol lebih atas cara Laravel membuat suatu object (misalnya mau inject konfigurasi tertentu).
📊 Perbandingan Singkat
| Constructor | Method | IoC Binding | |
|---|---|---|---|
| Dependensi tersedia di | Semua method kelas | Hanya method itu | Di mana saja |
| Cocok untuk | Dependensi utama kelas | Dependensi sesekali | Interface ke implementasi |
| Ditulis di mana | __construct() | Parameter method | AppServiceProvider |
| Perlu didaftarkan? | Tidak (auto-resolve) | Tidak (auto-resolve) | Ya, kalau pakai interface |
💡 Ingat!Waktu pertama kali lihat constructor yang isinya penuh type-hint, wajar kalau rasanya “ini ngapain? Langsungnewaja kan lebih simpel.”
Tapi begitu kamu mau nulis test, atau mau ganti implementasi tanpa meledakkan separuh aplikasi — kamu akan ngerti kenapa DI ada. Kelas yang dependensinya disuntikkan dari luar itu jauh lebih mudah ditukar, dites, dan dirawat.
Laravel sudah menyediakan semua infrastrukturnya. Kamu tinggal type-hint dengan benar, dan biarkan IoC Container bekerja.
Kalau kamu sudah paham OOP dasar dan mulai ngoding Laravel, cepat atau lambat kamu pasti ketemu kata “SOLID”.
Dan biasanya reaksinya satu dari dua hal: “Kedengarannya keren tapi aku nggak ngerti apa maksudnya”, atau “Aku hapal singkatannya tapi nggak tahu kapan pakainya.”
Artikel ini akan ngebahas semua lima prinsip — satu per satu, pakai contoh nyata dari aplikasi blog/CMS sederhana. Kamu akan lihat kode yang melanggar prinsipnya dulu, baru kita perbaiki. Karena menurut aku, cara tercepat paham “kenapa ini penting” adalah melihat sendiri apa yang rusak kalau kita abaikan.
Konteks Studi Kasus
Kita akan bangun fitur-fitur dari sebuah blog sederhana. Ada tiga entitas utama:
- Article — artikel yang ditulis penulis
- Comment — komentar dari pembaca
- User — penulis dan pembaca
Struktur tabelnya seperti ini:
users → id, name, email, role
articles → id, user_id, title, body, status, published_at
comments → id, article_id, user_id, body, is_approved
Oke, kita mulai.
S — Single Responsibility Principle
“Satu kelas, satu alasan untuk berubah.”
Analoginya
Bayangin ada seorang tukang masak di restoran. Dia masak, dia juga yang cuci piring, sekaligus yang kasir, sekaligus yang anter makanan ke meja.
Kalau lagi rame, semua rusak sekaligus. Kalau mau ganti cara kasir (dari cash ke kartu), harus ganggu tukang masaknya dulu.
Itulah masalah kelas yang punya terlalu banyak tanggung jawab.
Kode yang Melanggar
Ini contoh ArticleController yang sering kelihatan di proyek pemula:
class ArticleController extends Controller
{
public function store(Request $request)
{
// Validasi — harusnya bukan urusan controller
$request->validate([
'title' => 'required|min:5',
'body' => 'required|min:50',
'status' => 'required|in:draft,published',
]);
// Logika bisnis — harusnya bukan urusan controller
$article = Article::create([
'user_id' => auth()->id(),
'title' => $request->title,
'body' => $request->body,
'status' => $request->status,
'published_at' => $request->status === 'published' ? now() : null,
]);
// Notifikasi — harusnya JELAS bukan urusan controller
Mail::to('editor@blog.com')->send(new ArticleSubmitted($article));
return response()->json($article, 201);
}
}
Sekarang tanya ke dirimu: berapa alasan kelas ini bisa berubah?
- Kalau aturan validasi berubah
- Kalau logika bisnis pembuatan artikel berubah
- Kalau cara notifikasi berubah (dari email ke Slack misalnya)
Tiga alasan. Artinya tiga tanggung jawab. Ini melanggar SRP.
Kode yang Benar
Kita pisah tiap tanggung jawab ke file masing-masing.
📁 app/Http/Requests/StoreArticleRequest.php
Form Request khusus untuk validasi. Laravel punya fitur ini — manfaatkan.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreArticleRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|min:5',
'body' => 'required|min:50',
'status' => 'required|in:draft,published',
];
}
}
📁 app/Services/ArticleService.php
Service untuk logika bisnis. Di sinilah “aturan main” fitur artikel tinggal.
<?php
namespace App\Services;
use App\Models\Article;
use App\Models\User;
use App\Notifications\ArticleSubmittedNotification;
class ArticleService
{
public function create(array $data, User $author): Article
{
$article = Article::create([
'user_id' => $author->id,
'title' => $data['title'],
'body' => $data['body'],
'status' => $data['status'],
'published_at' => $data['status'] === 'published' ? now() : null,
]);
// Notifikasi jadi tanggung jawab service, bukan controller
$author->notify(new ArticleSubmittedNotification($article));
return $article;
}
}
📁 app/Http/Controllers/ArticleController.php
Controller sekarang hanya “penghubung” — terima request, lempar ke service, kembalikan response.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreArticleRequest;
use App\Services\ArticleService;
class ArticleController extends Controller
{
public function __construct(
private ArticleService $articleService
) {}
public function store(StoreArticleRequest $request): JsonResponse
{
// validated() sudah otomatis karena pakai Form Request
$article = $this->articleService->create(
$request->validated(),
$request->user()
);
return response()->json($article, 201);
}
}
📌 Sekarang kalau aturan validasi berubah, kamu hanya sentuh StoreArticleRequest. Kalau logika bisnis berubah, kamu hanya sentuh ArticleService. Controller tidak perlu diubah sama sekali.
O — Open/Closed Principle
“Terbuka untuk ditambah, tertutup untuk diubah.”
Analoginya
Bayangin colokan listrik di dinding. Kamu bisa colokkan apapun ke sana — charger HP, kipas, lampu. Colokannya tidak perlu diubah tiap kali kamu beli perangkat baru.
OCP prinsipnya sama: kelas yang sudah ada dan bekerja dengan baik tidak perlu disentuh ketika ada kebutuhan baru. Kamu cukup tambahkan kode baru, bukan ubah yang lama.
Kode yang Melanggar
Katakanlah kita punya fitur notifikasi ketika artikel dipublish. Kita tulis seperti ini:
class ArticlePublishedNotifier
{
public function notify(Article $article, string $channel): void
{
if ($channel === 'email') {
Mail::to($article->user)->send(new ArticlePublishedMail($article));
} elseif ($channel === 'slack') {
Http::post(config('services.slack.webhook'), [
'text' => "Artikel baru: {$article->title}",
]);
}
// Besok kalau ada WhatsApp, harus tambah elseif lagi di SINI
// Artinya kita UBAH kelas yang sudah jalan — berbahaya!
}
}
Setiap kali ada channel notifikasi baru, kamu harus buka dan ubah file ini. Ini melanggar OCP.
Kode yang Benar
Kita pakai interface sebagai kontrak, lalu tiap channel jadi kelas sendiri.
📁 app/Contracts/ArticleNotificationChannel.php
<?php
namespace App\Contracts;
use App\Models\Article;
interface ArticleNotificationChannel
{
public function send(Article $article): void;
}
📁 app/Notifications/Channels/EmailChannel.php
<?php
namespace App\Notifications\Channels;
use App\Contracts\ArticleNotificationChannel;
use App\Mail\ArticlePublishedMail;
use App\Models\Article;
use Illuminate\Support\Facades\Mail;
class EmailChannel implements ArticleNotificationChannel
{
public function send(Article $article): void
{
Mail::to($article->user)->send(new ArticlePublishedMail($article));
}
}
📁 app/Notifications/Channels/SlackChannel.php
<?php
namespace App\Notifications\Channels;
use App\Contracts\ArticleNotificationChannel;
use App\Models\Article;
use Illuminate\Support\Facades\Http;
class SlackChannel implements ArticleNotificationChannel
{
public function send(Article $article): void
{
Http::post(config('services.slack.webhook'), [
'text' => "Artikel baru dipublish: {$article->title}",
]);
}
}
📁 app/Services/ArticlePublishedNotifier.php
Notifier tidak lagi tahu detail tiap channel — dia cukup loop dan panggil
send().
<?php
namespace App\Services;
use App\Contracts\ArticleNotificationChannel;
use App\Models\Article;
class ArticlePublishedNotifier
{
/** @param ArticleNotificationChannel[] $channels */
public function __construct(
private array $channels
) {}
public function notify(Article $article): void
{
foreach ($this->channels as $channel) {
$channel->send($article);
}
}
}
📌 Kalau besok kamu butuh notifikasi WhatsApp, kamu cukup buat WhatsAppChannel baru yang implements ArticleNotificationChannel. ArticlePublishedNotifier tidak perlu disentuh sama sekali. Terbuka untuk ditambah, tertutup untuk diubah.
L — Liskov Substitution Principle
“Subclass harus bisa menggantikan parent-nya tanpa bikin program rusak.”
Analoginya
Bayangin kamu punya remote TV. Semua remote TV — merek apapun — punya tombol Power, Volume, dan Channel. Kalau kamu ganti remote TV lama dengan yang baru, TV-nya harus tetap jalan normal.
Kalau remote baru tiba-tiba bikin TV meledak waktu kamu tekan Volume, itu masalahnya ada di remote — dia tidak bisa menggantikan tugasnya dengan benar.
LSP bilang: subclass tidak boleh “mengkhianati” janji yang sudah dibuat parent-nya.
Kode yang Melanggar
Kita punya abstract class BaseComment dan dua turunannya:
abstract class BaseComment
{
abstract public function approve(): void;
abstract public function getContent(): string;
}
class RegularComment extends BaseComment
{
public function approve(): void
{
$this->is_approved = true;
$this->save();
}
public function getContent(): string
{
return $this->body;
}
}
// MASALAH DI SINI ↓
class SystemComment extends BaseComment
{
public function approve(): void
{
// System comment tidak bisa di-approve — tapi parent-nya menjanjikan bisa!
throw new \Exception("System comments cannot be approved.");
}
public function getContent(): string
{
return $this->message;
}
}
Sekarang kalau ada kode seperti ini:
foreach ($comments as $comment) {
$comment->approve(); // Meledak kalau $comment adalah SystemComment
}
SystemComment tidak bisa menggantikan BaseComment dengan aman — ini melanggar LSP.
Kode yang Benar
Solusinya: pisah kontrak menjadi lebih spesifik. Jangan paksa kelas mewarisi kemampuan yang tidak relevan untuknya.
📁 app/Contracts/ApprovableInterface.php
Hanya komentar yang bisa di-approve yang perlu implements ini.
<?php
namespace App\Contracts;
interface ApprovableInterface
{
public function approve(): void;
public function reject(): void;
}
📁 app/Models/Comment.php
<?php
namespace App\Models;
use App\Contracts\ApprovableInterface;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model implements ApprovableInterface
{
public function approve(): void
{
$this->update(['is_approved' => true]);
}
public function reject(): void
{
$this->update(['is_approved' => false]);
}
}
📁 app/Models/SystemLog.php
Log sistem punya method-nya sendiri — tidak dipaksa punya
approve().
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SystemLog extends Model
{
public function getContent(): string
{
return $this->message;
}
// Tidak ada approve() — karena memang tidak relevan
}
📌 Sekarang kode yang butuh fitur approve hanya akan menerima objek yang implements ApprovableInterface. Tidak ada lagi kemungkinan throw new Exception yang mengejutkan di tengah loop.
I — Interface Segregation Principle
“Jangan paksa kelas implement method yang tidak dia butuhkan.”
Analoginya
Bayangin ada formulir pendaftaran untuk karyawan baru. Di formulir itu ada pertanyaan: “Berapa lama kamu bisa menahan napas dalam air?”
Untuk programmer, pertanyaan itu tidak relevan sama sekali. Tapi mereka tetap harus isi karena formulirnya sama untuk semua orang.
ISP bilang: buat interface yang spesifik, bukan interface raksasa yang isinya terlalu banyak.
Kode yang Melanggar
Kita punya satu interface besar untuk semua aksi yang bisa dilakukan user di blog:
interface UserActionInterface
{
public function writeArticle(): void;
public function editArticle(): void;
public function deleteArticle(): void;
public function approveComment(): void;
public function banUser(): void; // Hanya admin yang bisa
public function manageSettings(): void; // Hanya admin yang bisa
}
Sekarang kelas RegularAuthor dipaksa implement method yang tidak relevan:
class RegularAuthor implements UserActionInterface
{
public function writeArticle(): void { /* oke */ }
public function editArticle(): void { /* oke */ }
public function deleteArticle(): void { /* oke */ }
public function approveComment(): void { /* oke */ }
// Dipaksa implement ini, padahal tidak masuk akal
public function banUser(): void
{
throw new \Exception("Regular authors cannot ban users.");
}
public function manageSettings(): void
{
throw new \Exception("Regular authors cannot manage settings.");
}
}
Kode yang Benar
Pecah interface besar tadi menjadi interface-interface kecil yang spesifik.
📁 app/Contracts/CanWriteArticle.php
<?php
namespace App\Contracts;
interface CanWriteArticle
{
public function writeArticle(): void;
public function editArticle(): void;
public function deleteArticle(): void;
}
📁 app/Contracts/CanModerateContent.php
<?php
namespace App\Contracts;
interface CanModerateContent
{
public function approveComment(): void;
public function rejectComment(): void;
}
📁 app/Contracts/CanAdminister.php
<?php
namespace App\Contracts;
interface CanAdminister
{
public function banUser(): void;
public function manageSettings(): void;
}
Sekarang tiap role hanya implements apa yang relevan:
📁 app/Models/Author.php
<?php
namespace App\Models;
use App\Contracts\CanWriteArticle;
// Author hanya perlu CanWriteArticle — tidak ada method yang dipaksakan
class Author extends User implements CanWriteArticle
{
public function writeArticle(): void { /* ... */ }
public function editArticle(): void { /* ... */ }
public function deleteArticle(): void { /* ... */ }
}
📁 app/Models/Admin.php
<?php
namespace App\Models;
use App\Contracts\CanAdminister;
use App\Contracts\CanModerateContent;
use App\Contracts\CanWriteArticle;
// Admin bisa melakukan semuanya — implements semua yang relevan
class Admin extends User implements CanWriteArticle, CanModerateContent, CanAdminister
{
public function writeArticle(): void { /* ... */ }
public function editArticle(): void { /* ... */ }
public function deleteArticle(): void { /* ... */ }
public function approveComment(): void { /* ... */ }
public function rejectComment(): void { /* ... */ }
public function banUser(): void { /* ... */ }
public function manageSettings(): void { /* ... */ }
}
📌 Author tidak tahu soal banUser(). Admin punya segalanya. Dan kalau besok kamu buat role Moderator yang cuma bisa moderasi komentar, kamu tinggal implements CanModerateContent saja — tanpa mewarisi method yang tidak perlu.
D — Dependency Inversion Principle
“Kelas bergantung pada abstraksi, bukan pada implementasi konkret.”
Analoginya
Bayangin lampu di rumahmu. Lampu itu tidak “bergantung” pada PLN secara langsung. Dia bergantung pada colokan listrik — sebuah standar abstrak.
Karena itu, kamu bisa pasang solar panel di rumah, dan lampunya tetap nyala tanpa perlu ganti lampunya. Yang berubah hanya sumber listriknya — bukan lampunya.
DIP bilang: kelas kamu harus bergantung pada interface/abstraksi, bukan pada class konkret tertentu. Karena kalau langsung bergantung pada implementasi, ganti implementasi = ubah semua kode yang bergantung padanya.
Kode yang Melanggar
CommentService langsung bergantung pada implementasi konkret database:
class CommentService
{
// Langsung bikin object Eloquent di dalam service
// Artinya CommentService "tahu" dan bergantung pada Eloquent
public function getUnapprovedComments(): Collection
{
return Comment::where('is_approved', false)->get();
}
public function approve(int $commentId): void
{
$comment = Comment::findOrFail($commentId);
$comment->update(['is_approved' => true]);
}
}
Masalahnya: kalau kamu mau test CommentService tanpa database sungguhan, tidak bisa. Dan kalau besok kamu ganti cara ambil data (misalnya dari cache atau API), kamu harus ubah CommentService langsung.
Kode yang Benar
Kita buat interface sebagai “colokan” — lalu CommentService bergantung pada interface itu, bukan pada Eloquent.
📁 app/Contracts/CommentRepositoryInterface.php
<?php
namespace App\Contracts;
use Illuminate\Support\Collection;
interface CommentRepositoryInterface
{
public function getUnapproved(): Collection;
public function approve(int $commentId): void;
public function findOrFail(int $commentId): mixed;
}
📁 app/Repositories/EloquentCommentRepository.php
Implementasi konkret yang menggunakan Eloquent.
<?php
namespace App\Repositories;
use App\Contracts\CommentRepositoryInterface;
use App\Models\Comment;
use Illuminate\Support\Collection;
class EloquentCommentRepository implements CommentRepositoryInterface
{
public function getUnapproved(): Collection
{
return Comment::where('is_approved', false)->get();
}
public function approve(int $commentId): void
{
$comment = $this->findOrFail($commentId);
$comment->update(['is_approved' => true]);
}
public function findOrFail(int $commentId): Comment
{
return Comment::findOrFail($commentId);
}
}
📁 app/Services/CommentService.php
Service sekarang bergantung pada interface, bukan Eloquent langsung.
<?php
namespace App\Services;
use App\Contracts\CommentRepositoryInterface;
use Illuminate\Support\Collection;
class CommentService
{
// Bergantung pada ABSTRAKSI (interface), bukan implementasi konkret
public function __construct(
private CommentRepositoryInterface $commentRepository
) {}
public function getUnapprovedComments(): Collection
{
return $this->commentRepository->getUnapproved();
}
public function approve(int $commentId): void
{
$this->commentRepository->approve($commentId);
}
}
📁 app/Providers/AppServiceProvider.php
Di sinilah kita “sambungkan” interface ke implementasinya — satu baris binding.
public function register(): void
{
$this->app->bind(
\App\Contracts\CommentRepositoryInterface::class,
\App\Repositories\EloquentCommentRepository::class
);
}
📌 Sekarang kalau kamu mau test CommentService, tinggal buat FakeCommentRepository yang implements interface yang sama — tanpa sentuh database sungguhan. Dan kalau kamu mau ganti ke cache atau API, cukup ganti satu baris di AppServiceProvider. CommentService tidak perlu tahu apa yang berubah.
🗺️ Gambaran Besar — Semuanya Bersama
Ini bagaimana semua file dari kelima prinsip duduk di struktur Laravel:
app/
├── Contracts/
│ ├── ArticleNotificationChannel.php ← OCP: kontrak channel notifikasi
│ ├── ApprovableInterface.php ← LSP: kontrak untuk yang bisa di-approve
│ ├── CanWriteArticle.php ← ISP: kontrak spesifik per kemampuan
│ ├── CanModerateContent.php ← ISP
│ ├── CanAdminister.php ← ISP
│ └── CommentRepositoryInterface.php ← DIP: kontrak repository
│
├── Http/
│ ├── Controllers/
│ │ └── ArticleController.php ← SRP: hanya urusan HTTP
│ └── Requests/
│ └── StoreArticleRequest.php ← SRP: hanya urusan validasi
│
├── Services/
│ ├── ArticleService.php ← SRP: logika bisnis artikel
│ ├── ArticlePublishedNotifier.php ← OCP: kirim ke semua channel
│ └── CommentService.php ← DIP: bergantung pada interface
│
├── Repositories/
│ └── EloquentCommentRepository.php ← DIP: implementasi konkret
│
├── Notifications/
│ └── Channels/
│ ├── EmailChannel.php ← OCP: channel spesifik
│ └── SlackChannel.php ← OCP: channel spesifik
│
└── Models/
├── Comment.php ← LSP: implements ApprovableInterface
├── Author.php ← ISP: hanya CanWriteArticle
└── Admin.php ← ISP: semua kemampuan
🧭 Ringkasan — SOLID dalam Satu Kalimat
| Prinsip | Satu Kalimat | Sinyal Pelanggaran |
|---|---|---|
| S — Single Responsibility | Satu kelas, satu alasan berubah | Controller yang validasi + bisnis + notifikasi |
| O — Open/Closed | Tambah fitur dengan kode baru, bukan ubah yang lama | if/elseif panjang untuk tipe yang terus bertambah |
| L — Liskov Substitution | Subclass tidak boleh “mengkhianati” janji parent | Method yang throw new Exception karena tidak relevan |
| I — Interface Segregation | Interface kecil dan spesifik, bukan interface raksasa | Class yang implement method kosong atau throw Exception |
| D — Dependency Inversion | Bergantung pada abstraksi, bukan implementasi | new ClassName() di dalam service atau controller |
💡 Ingat!
Waktu pertama kali belajar SOLID, wajar kalau rasanya “ribet banget, bikin interface segitu banyak untuk hal sesederhana ini?”
Jawabannya: untuk aplikasi kecil, memang tidak perlu semua ini. Tapi begitu timmu tumbuh, fiturnya bertambah, dan kamu harus ganti library payment atau tambah channel notifikasi — kamu akan sangat bersyukur sudah punya abstraksi yang jelas.
SOLID bukan tentang menulis lebih banyak kode. Ini tentang menulis kode yang mudah diganti tanpa takut merusak hal lain. Di dunia nyata, itu nilainya sangat besar.