Class di folder app/ hanya bisa dipakai oleh satu aplikasi. Begitu fungsi yang sama dibutuhkan proyek kedua — format angka, integrasi payment gateway, komponen UI internal — tempat yang tepat adalah paket composer: aplikasi kecil dalam aplikasi, lengkap dengan service provider, konfigurasi, dan berkas yang bisa diterbitkan (publish). Chapter ini membangun paket utilitas budi/kopitools sungguhan di dalam sandbox, memasangnya ke aplikasi latihan, lalu membuktikan aplikasi tersebut memakainya. Ringkasan lengkap topik ini tersedia di dokumentasi packages .

Anatomi Paket Laravel #

Paket kita tinggal di dalam repo aplikasi, di packages/budi/kopitools/, dengan struktur berikut:

text
packages/budi/kopitools/
├── composer.json
├── config/kopitools.php
├── database/migrations/2026_01_01_000000_create_kas_mutasi_table.php
├── lang/id/mutasi.php
├── lang/en/mutasi.php
├── resources/views/kartu-kas.blade.php
└── src/
    ├── Facades/Kas.php
    ├── Kas.php
    └── KopitoolsServiceProvider.php

Bagian pentingnya ada di composer.json. Field autoload memetakan namespace ke folder src/, sedangkan extra.laravel memberi tahu Laravel provider dan alias apa yang disumbangkan paket:

json
{
    "name": "budi/kopitools",
    "description": "Utilitas format kas untuk aplikasi demo pengembangan package.",
    "type": "library",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Budi\\Kopitools\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Budi\\Kopitools\\KopitoolsServiceProvider"
            ],
            "aliases": {
                "Kas": "Budi\\Kopitools\\Facades\\Kas"
            }
        }
    }
}

Service Provider Paket #

Service provider menjadi pintu masuk paket ke aplikasi — konsep yang sudah kita dalami di chapter Service Providers . Provider paket memakai aturan yang sama: register() untuk binding, boot() untuk semua yang menyentuh service framework:

php
<?php

namespace Budi\Kopitools;

use Illuminate\Support\ServiceProvider;

class KopitoolsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/kopitools.php', 'kopitools');

        $this->app->singleton(Kas::class, function ($app) {
            return new Kas($app['config']->get('kopitools'));
        });
    }

    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->publishes([
                __DIR__.'/../config/kopitools.php' => config_path('kopitools.php'),
            ], 'kopitools-config');

            $this->publishes([
                __DIR__.'/../resources/views' => resource_path('views/vendor/kopitools'),
            ], 'kopitools-views');

            $this->publishes([
                __DIR__.'/../lang' => lang_path('vendor/kopitools'),
            ], 'kopitools-lang');
        }

        $this->loadViewsFrom(__DIR__.'/../resources/views', 'kopitools');

        $this->loadTranslationsFrom(__DIR__.'/../lang', 'kopitools');

        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
    }
}

Bagian register() mengikat class Kas sebagai singleton dengan bacaan konfigurasi yang sudah di-merge. Bagian boot() melakukan tiga pekerjaan: mendaftarkan berkas yang boleh diterbitkan pengguna lewat tag, lalu memuat view, terjemahan, dan migrasi langsung dari folder paket.

Konfigurasi: Merge Bawaan, Publish untuk Override #

mergeConfigFrom() menjamin config('kopitools.*') selalu terisi default milik paket walau pengguna belum menerbitkan apa pun:

Output yang Dihasilkan:

text
config (belum publish): {"simbol":"Rp","desimal":0,"pemisah_ribuan":".","pemisah_desimal":","}

Untuk mengubah nilainya, pengguna menerbitkan berkas config ke aplikasi sendiri:

bash
php artisan vendor:publish --tag=kopitools-config

Output yang Dihasilkan:

text
  INFO  Publishing [kopitools-config] assets.

  Copying file [packages/budi/kopitools/config/kopitools.php] to [config/kopitools.php]  DONE

Setelah config/kopitools.php berada di tangan pengguna, ubahan di situ menang atas default paket. Kita mengganti simbol menjadi IDR pada salinan tersebut dan hasilnya langsung terlihat: rupiah(150000) yang tadinya Rp150.000 kini mengembalikan IDR150.000. Pola merge-then-publish inilah yang membuat paket bisa dipakai dalam satu menit namun tetap bisa dikustomisasi total.

Views, Terjemahan, dan Migrasi Milik Paket #

loadViewsFrom() mendaftarkan namespace kopitools:: sehingga view berikut bisa dirender dari mana saja:

blade
<div class="kartu-kas">
    <strong>{{ __('kopitools::mutasi.saldo') }}: {{ $total }}</strong>
</div>

Terjemahan di lang/ ikut melengkapi: __('kopitools::mutasi.saldo') menghasilkan Saldo Kas saat locale id dan Cash Balance saat locale en. Sementara itu loadMigrationsFrom() menyumbang migrasi ke aplikasi pemasang:

Output yang Dihasilkan:

text
  INFO  Running migrations.

  2026_01_01_000000_create_kas_mutasi_table ...................... 1.88ms DONE

Memasang Paket dan Membuktikannya Dipakai #

Paket lokal dipasang lewat path repository di composer.json aplikasi:

json
"require": {
    "budi/kopitools": "*@dev"
},
"repositories": [
    {
        "type": "path",
        "url": "packages/budi/kopitools"
    }
]

Setelah composer update budi/kopitools, composer menjalankan package:discover dan provider paket aktif tanpa menyentuh bootstrap/providers.php sedikit pun. Aplikasi kini bisa memakai facade Kas serta view paket dari route biasa:

php
Route::get('/ringkasan-kas', function () {
    app()->setLocale('id');

    return view('kopitools::kartu-kas', [
        'total' => Kas::rupiah(150000),
    ]);
});

Feature test berikut membuktikan seluruh rantai bekerja: alias facade, view namespaced, terjemahan, sampai migrasi paket:

text
ringkasan-kas: 200 isi="<div class="kartu-kas"> <strong>Saldo Kas: Rp150.000</strong> </div>"
kas_mutasi: id, deskripsi, jumlah, created_at, updated_at

  PASS  Tests\Feature\KopitoolsPaketTest
  ✓  aplikasi memakai paket kopitools
  ✓  paket menyumbang migrasi sendiri

  Tests:    2 passed (3 assertions)

Menguji dan Menerbitkan Paket #

Paket yang hidup terpisah dari aplikasi host butuh harness uji sendiri, dan standar ekosistemnya adalah orchestra/testbench: paket tersebut menyediakan aplikasi Laravel minimal di dalam test. Pengujian paket kita akan terlihat seperti ini (contoh tampilan):

php
use Budi\Kopitools\Kas;
use Budi\Kopitools\KopitoolsServiceProvider;
use Orchestra\Testbench\TestCase;

class KasTest extends TestCase
{
    protected function getPackageProviders($app)
    {
        return [KopitoolsServiceProvider::class];
    }

    public function test_rupiah_memakai_simbol_dari_config(): void
    {
        config(['kopitools.simbol' => 'IDR']);

        $this->assertSame('IDR150.000', $this->app->make(Kas::class)->rupiah(150000));
    }
}

Untuk menerbitkan paket ke dunia, alurnya standar composer: inisialisasi git, tag versi (git tag v0.1.0), dorong ke repositori publik, lalu daftarkan di Packagist agar orang lain cukup menjalankan composer require budi/kopitools. Selama pengembangan harian, path repository seperti di atas menghemat siklus edit-rebuild karena vendor/budi/kopitools hanya tautan simbolis ke folder packages/ kita.

Paket terpasang dan provider-nya aktif tanpa satu baris pun diedit di bootstrap/providers.php. Bagaimana Laravel tahu harus memuatnya? Jawabannya adalah mekanisme package discovery, yang kita bedah tuntas di chapter berikutnya.