Chapter sebelumnya kita menulis binding langsung ke variabel $app demi eksperimen — cara yang jelas tidak bisa dipakai di aplikasi nyata, karena tidak ada tempat untuk menjalankannya. Tempat itu ada dan namanya service provider: kelas yang dijalankan setiap kali framework booting, menjadi titik tunggal tempat binding, macro, route, dan listener didaftarkan. Semua bootstrap Laravel — dari service router sampai validator — dipasang lewat provider. Chapter ini membahas siklus hidupnya, lalu membangun provider sungguhan lengkap dengan binding dan route.
Backbone Bootstrap Aplikasi #
Daftar provider aplikasi kita berada di bootstrap/providers.php. Beginilah isinya pada instalasi baru:
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];Satu baris, satu kelas — dulu Laravel memuat puluhan provider bawaan di config/app.php, tetapi sejak Laravel 11 provider inti framework dimuat lewat jalur terpisah sehingga berkas ini hanya milik kita. Provider tambahan dari package dibaca otomatis dari manifest composer. Cara kerjanya sederhana: saat booting, framework mengeksekusi method register() lalu boot() setiap provider pada daftar ini.
Siklus register dan boot #
Kedua method tersebut bukan kembar dengan nama berbeda — urutan eksekusinya yang menjadi kuncinya:
| Method | Dijalankan saat | Pekerjaan yang cocok |
|---|---|---|
register() | Semua provider berurutan, satu per satu | Binding ke container saja |
boot() | Setelah semua register() selesai | Route, view composer, macro, listener, semua yang memakai service |
Aturan praktisnya: di dalam register() kita belum boleh bergantung pada service milik provider lain, karena provider lain mungkin belum sempat mendaftarkan binding-nya. Di dalam boot() semua binding dari semua provider dijamin sudah tersedia — itulah saat aman untuk memakai Route, View, atau service apa pun. Generator make:provider memberi kerangka yang menegaskan pembagian ini:
php artisan make:provider PaymentServiceProviderOutput yang Dihasilkan:
INFO Provider [app/Providers/PaymentServiceProvider.php] created successfully. Provider Nyata: Binding plus Route #
Kita isi provider tersebut dengan dua pekerjaan: mengikat PaymentGateway ke implementasi sandbox di register(), dan mendaftarkan route pemakaian gateway tersebut di boot():
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\GatewaySandbox;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PaymentGateway::class, GatewaySandbox::class);
}
public function boot(): void
{
Route::middleware('auth')->post('/pembayaran/charge', function (Request $request, PaymentGateway $gateway) {
return $gateway->charge(
$request->input('kartu'),
(int) $request->input('jumlah'),
);
})->name('pembayaran.charge');
}
}Setelah menambahkan provider ke bootstrap/providers.php, route hasil pendaftaran boot() langsung terlihat di aplikasi:
POST pembayaran/charge ........................................... pembayaran.charge › app/Providers/PaymentServiceProvider.php:20Pendaftaran route dari dalam provider memang bukan praktik untuk route aplikasi biasa — routes/web.php tempatnya — tetapi inilah mekanisme yang dipakai setiap package untuk menyumbang route sendiri tanpa menyentuh berkas kita. Feature test berikut memastikan kedua hasil provider bekerja: binding terpasang dan route hidup:
<?php
namespace Tests\Feature;
use App\Contracts\PaymentGateway;
use App\Models\User;
use App\Services\GatewaySandbox;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PaymentProviderTest extends TestCase
{
use RefreshDatabase;
public function test_provider_mengikat_gateway_dan_mendaftarkan_route(): void
{
$budi = User::factory()->create(['name' => 'Budi']);
$this->assertInstanceOf(
GatewaySandbox::class,
$this->app->make(PaymentGateway::class),
);
$response = $this->actingAs($budi)->post('/pembayaran/charge', [
'kartu' => '4242424242424242',
'jumlah' => 150000,
]);
$response->assertOk();
$response->assertSee('OK: Rp150.000 via kartu ****4242 (sandbox)');
}
}Output yang Dihasilkan:
PASS Tests\Feature\PaymentProviderTest
✓ provider mengikat gateway dan mendaftarkan route 0.28s
Tests: 1 passed (3 assertions)
Duration: 0.41sProvider Tertunda #
Provider yang binding-nya jarang dipakai bisa menunda pemuatannya sampai service benar-benar dibutuhkan lewat interface DeferrableProvider. Kita cek buktinya lewat skrip runner yang boot lewat HTTP kernel, meminta satu request biasa, lalu me-resolve service yang dijanjikan provider tersebut:
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle($request = Illuminate\Http\Request::create('/', 'GET'));
echo 'request / : status '.$response->getStatusCode().PHP_EOL;
echo 'provider Riwayat setelah request : '
.($app->providerIsLoaded(RiwayatServiceProvider::class) ? 'dimuat' : 'belum dimuat').PHP_EOL;
$riwayat = $app->make(RiwayatBelanja::class);
echo 'provider Riwayat setelah resolve : '
.($app->providerIsLoaded(RiwayatServiceProvider::class) ? 'dimuat' : 'belum dimuat').PHP_EOL;Output yang Dihasilkan:
request / : status 200
provider Riwayat setelah request : belum dimuat
provider Riwayat setelah resolve : dimuatMethod provides() pada provider tertunda mendata service yang ia janjikan; container mencatat daftar tersebut dan baru mengeksekusi register() ketika salah satu service itu di-resolve. Request pertama lewat tanpa memuat provider — bukti deferral bekerja.
Provider Sendiri atau AppServiceProvider #
Batas antara keduanya adalah batas cakupan, bukan batas teknik. Binding satu-dua buah untuk aplikasi sendiri cukup di AppServiceProvider; provider baru layak dibuat ketika ada kelompok fungsional yang jelas — payment, integrasi pihak ketiga, atau package kita sendiri. Package juga memakai provider untuk hal-hal di luar binding: method boot() bisa mendaftarkan middleware, sedangkan method publishes() menyediakan berkas konfigurasi yang bisa diterbitkan pengguna ke folder config mereka — detail lengkapnya ada di dokumentasi packages
.
Provider sudah memasang semua binding ke container. Pertanyaan berikutnya: bagaimana konsumsi dependency tersebut tersebar ke seluruh penjuru aplikasi — controller, job, listener, command — dengan pola yang konsisten? Jawabannya adalah dependency injection, topik chapter berikutnya yang merujuk langsung ke dokumentasi providers sebagai pendamping.