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:
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.phpBagian 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:
{
"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
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:
config (belum publish): {"simbol":"Rp","desimal":0,"pemisah_ribuan":".","pemisah_desimal":","}Untuk mengubah nilainya, pengguna menerbitkan berkas config ke aplikasi sendiri:
php artisan vendor:publish --tag=kopitools-configOutput yang Dihasilkan:
INFO Publishing [kopitools-config] assets.
Copying file [packages/budi/kopitools/config/kopitools.php] to [config/kopitools.php] DONESetelah 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:
<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:
INFO Running migrations.
2026_01_01_000000_create_kas_mutasi_table ...................... 1.88ms DONEMemasang Paket dan Membuktikannya Dipakai #
Paket lokal dipasang lewat path repository di composer.json aplikasi:
"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:
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:
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):
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.