inisiatif/model-shared
Shared Laravel models for Inisiatif Zakat Indonesia covering occupation, education level, geographic regions (country down to village), and marital status, plus a Donor model with configurable branch and employee relations.
This is an internal, domain-specific package for Indonesian zakat/NGO apps. Treat the changelog as the primary documentation — the README only covers installation and the dynamic-relation trick.
1. Install and pin
composer require inisiatif/model-shared:^2.10
Pin a minor version. Releases land frequently and several have been pure typo/bug fixes (2.7.5/2.7.6 table-key typos, 2.9.1 migration-registrar typo, 2.8.3 id/uuid fix), so ^2.10.6 is safer than *. 2.10.6 is purely additive (new models), so it's a low-risk bump from 2.10.x — but it still adds migrations to your batch (see Gotchas).
2. Find out what you actually got, before migrating
The package auto-registers a service provider (and migrations) via Laravel discovery. Before running php artisan migrate against anything real:
php artisan vendor:publish --provider="Inisiatif\ModelShared\ModelSharedServiceProvider" # if supported
ls vendor/inisiatif/model-shared/src/Models
ls vendor/inisiatif/model-shared/database/migrations
Read the migrations. The provider is named ...Registrar classes per model group (Donor, Partner, Account/FundingSource, etc. per the release notes), so migrations ship inside the package and run as part of your app's migration batch. Use php artisan migrate --pretend first on an existing database.
2.10.6 ("menambahkan beberapa model baru") is the case where this matters most: the release note doesn't name the new models, so ls src/Models and git diff the migrations directory against 2.10.5 is the only way to know what tables and columns you just inherited.
3. Look at these files first
src/Models/ — Donor, Partner, Degree, Occupation, MaritalStatus, Region/Province/District/Village, Bank, Outflow, Donation, FundingCategory, FundingType — plus whatever 2.10.6 added.src/Models/Concerns/ or a base model — check $table, $primaryKey, $incrementing, $keyType, $timestamps. Release 2.7.4 added getTable()-style helpers and getPackageRegistered(); those are the cheapest way to introspect what's registered.4. First use case: the donor form
Reference data (occupation, degree, marital status, region) is the lowest-risk entry point — read-only lookups with no coupling to your schema:
use Inisiatif\ModelShared\Models\Degree;
use Inisiatif\ModelShared\Models\Province;
$degrees = Cache::rememberForever('ref.degrees', fn () => Degree::orderBy('urutan')->get(['id', 'name']));
$provinces = Cache::rememberForever('ref.provinces', fn () => Province::orderBy('name')->get(['id', 'name']));
Wire these into Blade/Inertia selects first. Only then tackle Donor and the financial models (Donation, Outflow, FundingCategory), which are opinionated and much harder to bend.
Dynamic relations for Donor — do it once, in AppServiceProvider::boot()
The package deliberately doesn't know about your Branch or Employee models. Register the relations in your application provider, not a package provider, and before anything queries Donor:
use Inisiatif\ModelShared\Models\Donor;
public function boot(): void
{
Donor::resolveRelationUsing('branch', fn (Donor $m) =>
$m->belongsTo(\App\Models\Branch::class, 'branch_id'));
Donor::resolveRelationUsing('employee', fn (Donor $m) =>
$m->belongsTo(\App\Models\Employee::class, 'employeeid'));
}
The FK names (branch_id vs employeeid) are inconsistent in the upstream example — confirm against the actual migration columns, don't copy the README blindly.
Region cascade
Build province → regency → district → village as a dependent-select chain, one query per level:
$regencies = Regency::where('province_id', $request->integer('province_id'))->orderBy('name')->get();
For search boxes (release 2.5.0 added a region search API), check vendor/inisiatif/model-shared/routes/ — if routes are auto-loaded you may be exposing public endpoints unintentionally. Disable/scope them in your provider if so.
Reference data as an enum-like layer
Wrap shared reference models in your own thin accessors so a future package swap or schema change touches one file:
final class MaritalStatuses
{
public static function options(): array
{
return MaritalStatus::query()->pluck('name', 'id')->all();
}
}
Extending rather than editing
Prefer subclassing with an explicit table when your app needs extra columns:
class Donor extends \Inisiatif\ModelShared\Models\Donor
{
protected $table = 'donors'; // verify via (new \Inisiatif\ModelShared\Models\Donor)->getTable()
protected $fillable = [...]; // shared models are usually $guarded = []
}
If you already have a donors table, do not install the package's donor migration; subclass and keep your schema, then use resolveRelationUsing for the joins.
Testing
There are no factories. Build your own fixtures and use RefreshDatabase so the package migrations run in CI, which is also your earliest warning that a version bump changes a column — or, as in 2.10.6, adds whole new tables:
protected function setUp(): void
{
parent::setUp();
$this->seed(\Database\Seeders\ReferenceDataSeeder::class);
}
Upgrades
Read the release notes as a migration checklist (they are written as commit subjects: "menambahkan email donor", "menambahkan donor_phone_id ke dalam migrasi donor", "menambahkan beberapa model baru"). Diff the package's database/migrations between tags, then run php artisan migrate --pretend in staging. "Added models" releases add tables — decide per model whether you want the package's table or will disable that registrar because your app already owns it.
donor_phone_id, email on donors) or create entirely new tables you never asked for — 2.10.6 is exactly this, dressed up as a feature release. Take a backup and review diffs; never let composer update and migrate happen together unreviewed in production.Partner got HasUuids in 2.8.2, then 2.8.3 "fixed id and uuid". Assume inconsistency across models — including the freshly added 2.10.6 ones: check $keyType/$incrementing on each model before adding a FK column, or you'll get integer-vs-char join mismatches that fail silently or throw on strict MySQL.resolveRelationUsing is invisible to static analysis and IDEs, collides with an existing branch()/employee() method if one exists, and must be registered before with('branch') is used — including in queued jobs, where the app provider does re-boot, but in any custom-bootstrapped console command it may not. Add a test asserting Donor::first()->branch resolves.2.7.5→2.7.6 rolled back a table key change; 2.8.1 fixed a PartnerModelRegistrar typo). If a model suddenly queries the wrong table after an upgrade, check the registrar class first. Newly added models are the least battle-tested — a typo in one of them is the most likely failure mode in 2.10.6.2.9.2 was "perbaikan salah env" — a config value reads the wrong env key. After install, dump the package config (config/model-shared.php if published) and confirm every value resolves in your .env before debugging application code.spatie/laravel-query-builder is a hard dependency. If your app pins a different major, Composer will fight you. Resolve that conflict at install time rather than patching around it.Region::all(). Index province_id/regency_id lookups and paginate any admin UI. Consider caching province/regency lists only.Donation, DonationDetail, Outflow, FundingCategory encode someone else's accounting assumptions. Read the migrations, then either adopt them wholesale (including their status columns) or keep your own and only reuse Donor, Region, and the reference tables.resolveRelationUsing for inbound relations, subclassing with $table/$fillable overrides, getTable()/getPackageRegistered() from 2.7.4 for introspection (use the latter to enumerate what 2.10.6 registered), and the per-model registrar classes if you need to disable a migration on a project that already owns that table.How can I help you explore Laravel packages today?