Kalau kamu pernah nulis Cache::get('key') atau Log::info('pesan') di Laravel, kamu sudah pakai Facade — mungkin tanpa sadar.
Dan kalau kamu pernah buka AppServiceProvider dan nulis $this->app->bind(...) di dalamnya, kamu sudah pakai Service Provider — mungkin juga tanpa terlalu paham apa yang sebenarnya terjadi.
Dua topik ini sering diajarkan bersamaan, dan sering bikin bingung karena terasa tumpang tindih. Padahal tugasnya sangat berbeda: Service Provider mendaftarkan sesuatu ke container, Facade mengakses sesuatu dari container. Yang satu memasukkan, yang lain mengambil.
Kita mulai dari masing-masing, baru lihat bagaimana keduanya bekerja bersama.
📦 Service Provider — “Petugas Gudang yang Mengisi Rak”
Analoginya
Bayangin sebuah toko kelontong. Sebelum toko buka, ada petugas gudang yang tugasnya mengisi rak-rak — menaruh beras di rak A, minyak di rak B, gula di rak C. Dia juga yang memutuskan: kalau stok minyak merek X habis, ganti dengan merek Y yang rasanya sama.
Petugas ini bekerja sebelum toko buka. Saat pelanggan datang, rak sudah terisi. Pelanggan tidak perlu tahu dari mana barang-barang itu datang — mereka tinggal ambil.
Service Provider adalah petugas gudang itu. Dia bekerja saat Laravel bootstrap — sebelum request pertama masuk — untuk mendaftarkan semua yang dibutuhkan aplikasi ke IoC Container.
Dua Method yang Selalu Ada
Setiap Service Provider punya dua method, dan urutannya tidak boleh ditukar:
register() → isi rak (daftarkan binding ke container)
boot() → buka toko (jalankan sesuatu setelah semua rak terisi)
Kenapa harus dipisah? Karena boot() boleh mengakses binding dari provider lain — tapi register() tidak boleh. Kalau kamu akses service di register(), service itu mungkin belum didaftarkan oleh provider lain.
Kodenya — Service Provider Dasar
Studi kasus: kita buat Service Provider untuk mendaftarkan ArticleService dan semua binding yang dibutuhkan blog.
📁 app/Providers/BlogServiceProvider.php
<?php
namespace App\Providers;
use App\Contracts\ArticleRepositoryInterface;
use App\Contracts\CommentRepositoryInterface;
use App\Repositories\EloquentArticleRepository;
use App\Repositories\EloquentCommentRepository;
use App\Models\Article;
use App\Observers\ArticleObserver;
use Illuminate\Support\ServiceProvider;
class BlogServiceProvider extends ServiceProvider
{
public function register(): void
{
// Daftarkan interface → implementasi konkret
// "Kalau ada yang minta ArticleRepositoryInterface, beri EloquentArticleRepository"
$this->app->bind(
ArticleRepositoryInterface::class,
EloquentArticleRepository::class
);
$this->app->bind(
CommentRepositoryInterface::class,
EloquentCommentRepository::class
);
// Daftarkan sebagai singleton — object dibuat sekali, dipakai ulang
// Cocok untuk service yang tidak punya state yang perlu di-reset
$this->app->singleton('blog.stats', function ($app) {
return new \App\Services\BlogStatisticsService(
$app->make(ArticleRepositoryInterface::class)
);
});
}
public function boot(): void
{
// Di sini aman untuk akses service dari provider lain
// karena semua register() sudah selesai dijalankan
// Daftarkan observer — pantau event Model
Article::observe(ArticleObserver::class);
// Daftarkan view composer — inject data ke semua view yang butuh
view()->composer('layouts.sidebar', function ($view) {
$view->with('recentArticles',
$this->app->make(ArticleRepositoryInterface::class)->getRecent(5)
);
});
}
}
📌 BlogServiceProvider tidak pernah dipanggil langsung di kode kamu. Laravel yang memanggilnya otomatis saat bootstrap, berdasarkan daftar di bootstrap/app.php (Laravel 11) atau config/app.php (Laravel sebelumnya).
Mendaftarkan Provider ke Aplikasi
📁 bootstrap/app.php (Laravel 11)
$app = Application::create(basePath: dirname(__DIR__))
->withProviders([
// Provider buatan kita didaftarkan di sini
\App\Providers\BlogServiceProvider::class,
])
->withRouting(...)
->withMiddleware(...);
Tiga Jenis Binding yang Paling Sering Dipakai
// 1. bind → buat object baru setiap kali diminta dari container
$this->app->bind(ArticleRepositoryInterface::class, EloquentArticleRepository::class);
// 2. singleton → buat sekali, pakai object yang sama seterusnya
$this->app->singleton(CacheService::class, CacheService::class);
// 3. instance → daftarkan object yang sudah jadi (sudah di-new sebelumnya)
$this->app->instance('config.blog', new BlogConfig([
'per_page' => 10,
'allow_comments' => true,
]));
📌 Pilih bind kalau tiap penggunaan butuh state bersih. Pilih singleton untuk service stateless yang berat dibuat ulang (koneksi, konfigurasi, cache driver). Pilih instance kalau object-nya sudah kamu buat sendiri sebelum di-bind.
🪟 Facade — “Label di Rak yang Bisa Langsung Diambil”
Analoginya
Kembali ke toko kelontong. Pelanggan datang dan langsung ambil gula dari rak B. Mereka tidak perlu tahu gula itu dari pabrik mana, truk apa yang ngantarnya, atau petugas gudang mana yang naruhnya.
Mereka cuma lihat label “GULA” di rak — dan langsung ambil.
Facade adalah label itu. Dia memberikan cara singkat dan statis untuk mengakses service dari IoC Container, tanpa harus inject lewat constructor atau resolve manual.
Tanpa Facade vs Dengan Facade
Ini perbedaan yang paling jelas:
// Tanpa Facade — harus inject lewat constructor atau resolve manual
class ArticleController extends Controller
{
public function __construct(
private \Illuminate\Cache\CacheManager $cache
) {}
public function show(Article $article): JsonResponse
{
$cached = $this->cache->remember("article.{$article->id}", 3600, fn() => $article);
return response()->json($cached);
}
}
// Dengan Facade — langsung panggil dari mana saja
use Illuminate\Support\Facades\Cache;
class ArticleController extends Controller
{
public function show(Article $article): JsonResponse
{
$cached = Cache::remember("article.{$article->id}", 3600, fn() => $article);
return response()->json($cached);
}
}
Keduanya melakukan hal yang persis sama. Facade hanya shortcut — di balik layar, Cache::remember() tetap memanggil object CacheManager yang ada di IoC Container.
Cara Kerja Facade di Balik Layar
Ini yang bikin Facade terasa “ajaib” padahal mekanismenya sederhana.
Setiap Facade punya satu method yang wajib ada: getFacadeAccessor(). Method ini cukup mengembalikan key yang dipakai untuk mengambil service dari IoC Container.
📁 vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php
Ini file asli milik Laravel — bukan buatan kita. Isinya sangat pendek.
<?php
namespace Illuminate\Support\Facades;
class Cache extends Facade
{
// Kembalikan key yang dipakai untuk ambil service dari container
protected static function getFacadeAccessor(): string
{
return 'cache'; // ← ini key-nya
}
}
Lalu di base class Facade, ada sihir PHP bernama __callStatic() yang bekerja:
// Di Illuminate\Support\Facades\Facade (disederhanakan)
public static function __callStatic(string $method, array $args): mixed
{
// 1. Ambil service dari container berdasarkan key
$instance = static::getFacadeRoot(); // → app('cache')
// 2. Panggil method yang diminta di object itu
return $instance->$method(...$args);
}
Jadi Cache::remember(...) secara harfiah berarti: “ambil object ‘cache’ dari container, lalu panggil method remember() di object itu.”
📌 __callStatic() adalah magic method PHP yang dipanggil otomatis ketika kamu memanggil method statis yang tidak ada. Facade memanfaatkan ini untuk “meneruskan” semua panggilan ke object aslinya di container.
Membuat Facade Sendiri
Kalau kamu punya service kustom dan mau aksesnya semudah Cache::get() — kamu bisa buat Facade sendiri.
Studi kasus: kita buat Facade untuk BlogStatisticsService.
Langkah 1 — Service-nya sudah ada (sudah didaftarkan di BlogServiceProvider tadi dengan key 'blog.stats'):
// Sudah ada di BlogServiceProvider::register():
$this->app->singleton('blog.stats', function ($app) {
return new \App\Services\BlogStatisticsService(...);
});
Langkah 2 — Buat kelas Facade-nya:
📁 app/Facades/BlogStats.php
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class BlogStats extends Facade
{
protected static function getFacadeAccessor(): string
{
// Harus cocok dengan key yang didaftarkan di Service Provider
return 'blog.stats';
}
}
Langkah 3 — Langsung pakai di mana saja:
use App\Facades\BlogStats;
// Di controller, blade, atau mana saja — tanpa inject apapun
$totalArticles = BlogStats::getTotalArticles();
$totalComments = BlogStats::getTotalComments();
$popularArticle = BlogStats::getMostRead();
📌 Tiga file yang terlibat: Service (logika aslinya), Service Provider (yang mendaftarkan ke container dengan key tertentu), Facade (yang menunjuk ke key itu). Kalau salah satu tidak ada, Facade tidak akan jalan.
🔗 Keduanya Bekerja Bersama
Ini yang paling penting untuk dipahami: Service Provider dan Facade tidak bisa dipisahkan.
Facade hanya bisa bekerja kalau service yang ditunjuknya sudah didaftarkan ke container oleh Service Provider. Urutannya selalu:
Service Provider register() → IoC Container punya 'blog.stats'
↓
Facade getFacadeAccessor() mengembalikan 'blog.stats'
↓
__callStatic() ambil object dari container → panggil method
Kalau kamu pakai BlogStats::getTotalArticles() tapi lupa daftarkan 'blog.stats' di Service Provider, Laravel akan lempar error: “Target [blog.stats] is not instantiable.”
⚖️ Facade vs Dependency Injection — Mana yang Lebih Baik?
Ini pertanyaan yang sering muncul, dan jawabannya: tergantung konteks.
// Pakai Facade — cocok untuk:
// - Utility yang dipakai sesekali
// - Kode di luar class (helper, config, migration)
// - Prototipe cepat
Cache::put("article.{$id}", $article, 3600);
Log::info("Article {$id} diakses");
// Pakai DI — cocok untuk:
// - Dependensi utama sebuah class
// - Kode yang perlu di-test (Facade bisa di-mock, tapi DI lebih eksplisit)
// - Ketika ingin jelas "class ini bergantung pada apa"
class ArticleService
{
public function __construct(
private ArticleRepositoryInterface $repository, // ← jelas dan eksplisit
private CacheService $cache
) {}
}
📌 Di Laravel sendiri, keduanya dipakai berdampingan. Tidak ada aturan “harus pilih satu”. Tapi untuk service utama yang menjadi tulang punggung fitur — DI lebih disarankan karena lebih eksplisit dan mudah dites.
Facade Bisa Di-Mock di Test
Satu keunggulan Facade yang sering tidak diketahui pemula: kamu bisa fake Facade di unit test tanpa inject apapun.
// Di test — fake Cache supaya tidak menyentuh Redis/Memcached sungguhan
Cache::fake(); // atau Cache::shouldReceive('remember')->andReturn($fakeArticle);
// Di test — fake Mail supaya tidak kirim email sungguhan
Mail::fake();
// Setelah aksi yang ditest:
Mail::assertSent(ArticlePublishedMail::class);
🗺️ Gambaran Besar — Posisi Tiap File
app/
├── Providers/
│ ├── AppServiceProvider.php ← binding umum aplikasi
│ └── BlogServiceProvider.php ← binding khusus fitur blog
│
├── Facades/
│ └── BlogStats.php ← Facade kustom → menunjuk ke 'blog.stats'
│
├── Services/
│ └── BlogStatisticsService.php ← service asli yang bekerja
│
└── Contracts/
├── ArticleRepositoryInterface.php
└── CommentRepositoryInterface.php
Alur hubungannya:
BlogServiceProvider::register()
→ app()->singleton('blog.stats', BlogStatisticsService::class)
↓
IoC Container menyimpan 'blog.stats'
↓
BlogStats::getFacadeAccessor() → 'blog.stats'
↓
BlogStats::getTotalArticles() → __callStatic() → app('blog.stats')->getTotalArticles()
🧭 Kapan Pakai Yang Mana?
Buat Service Provider ketika: Kamu punya service, repository, atau konfigurasi yang perlu didaftarkan ke container — terutama kalau melibatkan interface yang perlu di-bind ke implementasi konkret. Satu fitur besar layak punya satu Service Provider sendiri.
Buat Facade ketika: Kamu punya service yang sering dipakai di banyak tempat dan ingin cara aksesnya singkat — tanpa harus inject di setiap constructor. Terutama untuk utility (cache, log, storage) yang bukan dependensi utama sebuah class.
Tetap pakai DI ketika: Service itu adalah dependensi inti sebuah class, dan kamu ingin kejelasan eksplisit tentang “class ini bergantung pada apa” — terutama untuk kode yang akan di-test.
📊 Perbandingan Singkat
| Service Provider | Facade | |
|---|---|---|
| Tugasnya | Daftarkan service ke container | Akses service dari container |
| Dijalankan kapan | Saat bootstrap, sebelum request masuk | Saat dipanggil |
| Ditulis di mana | app/Providers/ | app/Facades/ |
| Wajib ada method | register(), boot() | getFacadeAccessor() |
| Tanpa ini | Container kosong, DI dan Facade tidak jalan | Harus inject manual lewat DI |
💡 Ingat!
Waktu pertama kali lihat Cache::get() dan Log::info(), wajar kalau rasanya “ini static method biasa” — padahal di baliknya ada object penuh dari container yang bekerja.
Dan waktu pertama kali buka AppServiceProvider dan nulis bind(), wajar kalau rasanya “ini ngisi apa ke mana?” — padahal yang kamu isi itu adalah fondasi yang memungkinkan semua DI dan Facade bekerja.
Setelah kamu paham keduanya bekerja bersama — Service Provider mengisi container, Facade mengambil dari container — banyak hal di Laravel yang tadinya terasa “ajaib” akan mulai masuk akal.
