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
<?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:

MethodDijalankan saatPekerjaan yang cocok
register()Semua provider berurutan, satu per satuBinding ke container saja
boot()Setelah semua register() selesaiRoute, 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:

bash
php artisan make:provider PaymentServiceProvider

Output yang Dihasilkan:

text
  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
<?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:

text
  POST       pembayaran/charge ........................................... pembayaran.charge › app/Providers/PaymentServiceProvider.php:20

Pendaftaran 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
<?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:

text
  PASS  Tests\Feature\PaymentProviderTest
  ✓  provider mengikat gateway dan mendaftarkan route                      0.28s

  Tests:    1 passed (3 assertions)
  Duration: 0.41s

Provider 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:

php
$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:

text
request / : status 200
provider Riwayat setelah request  : belum dimuat
provider Riwayat setelah resolve  : dimuat

Method 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.