Model Kustom
Panduan ini menunjukkan cara memperluas Laravel Nusa dengan model kustom dan mengintegrasikan data administratif Indonesia ke dalam model domain aplikasi Anda.
Memperluas Model Dasar
Model Provinsi Kustom
php
namespace App\Models;
use Creasi\Nusa\Models\Province as BaseProvince;
class Province extends BaseProvince
{
// Tambahkan atribut kustom
protected $appends = ['region_name', 'is_java'];
// Accessor kustom
public function getRegionNameAttribute(): string
{
$regions = [
'Sumatra' => ['11', '12', '13', '14', '15', '16', '17', '18', '19', '21'],
'Java' => ['31', '32', '33', '34', '35', '36'],
'Kalimantan' => ['61', '62', '63', '64', '65'],
'Sulawesi' => ['71', '72', '73', '74', '75', '76'],
'Eastern Indonesia' => ['81', '82', '91', '94', '95', '96'],
];
foreach ($regions as $region => $codes) {
if (in_array($this->code, $codes)) {
return $region;
}
}
return 'Tidak Diketahui';
}
// Accessor kustom
public function getIsJavaAttribute(): bool
{
return in_array($this->code, ['31', '32', '33', '34', '35', '36']);
}
// Scope kustom
public function scopeJava($query)
{
return $query->whereIn('code', ['31', '32', '33', '34', '35', '36']);
}
// Scope kustom
public function scopeOutsideJava($query)
{
return $query->whereNotIn('code', ['31', '32', '33', '34', '35', '36']);
}
// Metode kustom
public function getPopulationDensityCategory(): string
{
// Ini akan membutuhkan data populasi tambahan
if ($this->is_java) {
return 'Tinggi';
}
return 'Sedang'; // Logika sederhana
}
}
// Penggunaan
$javaProvinces = Province::java()->get();
$outsideJava = Province::outsideJava()->get();
foreach ($javaProvinces as $province) {
echo "{$province->name} berada di {$province->region_name}";
}
Model Alamat Kustom
php
namespace App\Models;
use Creasi\Nusa\Models\Address as BaseAddress;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Address extends BaseAddress
{
protected $fillable = [
'user_id',
'name',
'phone',
'province_code',
'regency_code',
'district_code',
'village_code',
'address_line',
'postal_code',
'is_default',
// Bidang kustom
'label', // 'Rumah', 'Kantor', 'Lainnya'
'notes', // Catatan tambahan
'latitude', // Koordinat kustom
'longitude',
'is_verified', // Status verifikasi alamat
'delivery_notes', // Instruksi pengiriman khusus
];
protected $casts = [
'is_default' => 'boolean',
'is_verified' => 'boolean',
'latitude' => 'decimal:8',
'longitude' => 'decimal:8',
];
protected $appends = ['formatted_label', 'distance_from_center'];
// Accessor kustom
public function getFormattedLabelAttribute(): string
{
return $this->label ? ucfirst($this->label) : 'Alamat';
}
// Accessor kustom dengan perhitungan
public function getDistanceFromCenterAttribute(): ?float
{
if (!$this->latitude || !$this->longitude || !$this->village) {
return null;
}
return $this->calculateDistance(
$this->latitude,
$this->longitude,
$this->village->latitude,
$this->village->longitude
);
}
// Scope kustom
public function scopeVerified($query)
{
return $query->where('is_verified', true);
}
public function scopeByLabel($query, $label)
{
return $query->where('label', $label);
}
public function scopeWithinRadius($query, $lat, $lon, $radiusKm)
{
return $query->whereNotNull('latitude')
->whereNotNull('longitude')
->whereRaw("
(6371 * acos(
cos(radians(?)) *
cos(radians(latitude)) *
cos(radians(longitude) - radians(?)) +
sin(radians(?)) *
sin(radians(latitude))
)) <= ?
", [$lat, $lon, $lat, $radiusKm]);
}
// Metode kustom
public function markAsVerified(): void
{
$this->update(['is_verified' => true]);
}
public function updateCoordinates(float $lat, float $lon): void
{
$this->update([
'latitude' => $lat,
'longitude' => $lon,
]);
}
private function calculateDistance($lat1, $lon1, $lat2, $lon2): float
{
$earthRadius = 6371; // km
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
}
// Penggunaan
$homeAddresses = Address::byLabel('home')->get();
$verifiedAddresses = Address::verified()->get();
$nearbyAddresses = Address::withinRadius(-6.200000, 106.816666, 10)->get();
Model Spesifik Domain
Model Toko dengan Lokasi
php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Creasi\Nusa\Contracts\HasAddress;
use Creasi\Nusa\Models\Concerns\WithAddress;
class Store extends Model implements HasAddress
{
use WithAddress;
protected $fillable = [
'name',
'description',
'category',
'phone',
'email',
'website',
'opening_hours',
'province_code',
'regency_code',
'district_code',
'village_code',
'address_line',
'postal_code',
'latitude',
'longitude',
'is_active',
];
protected $casts = [
'opening_hours' => 'array',
'is_active' => 'boolean',
'latitude' => 'decimal:8',
'longitude' => 'decimal:8',
];
protected $appends = ['full_location', 'region'];
// Accessor kustom
public function getFullLocationAttribute(): string
{
return $this->full_address;
}
public function getRegionAttribute(): string
{
if (!$this->province) {
return 'Tidak Diketahui';
}
$javaProvinces = ['31', '32', '33', '34', '35', '36'];
return in_array($this->province_code, $javaProvinces) ? 'Jawa' : 'Luar Jawa';
}
// Scope
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeInProvince($query, $provinceCode)
{
return $query->where('province_code', $provinceCode);
}
public function scopeInRegency($query, $regencyCode)
{
return $query->where('regency_code', $regencyCode);
}
public function scopeByCategory($query, $category)
{
return $query->where('category', $category);
}
public function scopeNearby($query, $lat, $lon, $radiusKm = 10)
{
return $query->whereNotNull('latitude')
->whereNotNull('longitude')
->whereRaw("
(6371 * acos(
cos(radians(?)) *
cos(radians(latitude)) *
cos(radians(longitude) - radians(?)) +
sin(radians(?)) *
sin(radians(latitude))
)) <= ?
", [$lat, $lon, $lat, $radiusKm]);
}
// Metode
public function getDistanceFrom(float $lat, float $lon): ?float
{
if (!$this->latitude || !$this->longitude) {
return null;
}
return $this->calculateDistance($lat, $lon, $this->latitude, $this->longitude);
}
private function calculateDistance($lat1, $lon1, $lat2, $lon2): float
{
$earthRadius = 6371;
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
}
// Penggunaan
$stores = Store::active()
->inProvince('33')
->byCategory('restaurant')
->with(['province', 'regency'])
->get();
$nearbyStores = Store::nearby(-6.200000, 106.816666, 5)
->active()
->get();
Model Zona Pengiriman
php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Creasi\Nusa\Models\{Province, Regency, District, Village};
class DeliveryZone extends Model
{
protected $fillable = [
'name',
'type', // 'province', 'regency', 'district', 'village'
'code', // Kode administratif
'delivery_fee',
'estimated_days',
'is_active',
'notes',
];
protected $casts = [
'delivery_fee' => 'decimal:2',
'estimated_days' => 'integer',
'is_active' => 'boolean',
];
// Relasi dinamis berdasarkan tipe
public function administrativeRegion()
{
switch ($this->type) {
case 'province':
return $this->belongsTo(Province::class, 'code', 'code');
case 'regency':
return $this->belongsTo(Regency::class, 'code', 'code');
case 'district':
return $this->belongsTo(District::class, 'code', 'code');
case 'village':
return $this->belongsTo(Village::class, 'code', 'code');
default:
return null;
}
}
// Scope
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeByType($query, $type)
{
return $query->where('type', $type);
}
// Metode
public function covers(string $villageCode): bool
{
$village = Village::find($villageCode);
if (!$village) {
return false;
}
switch ($this->type) {
case 'province':
return $village->province_code === $this->code;
case 'regency':
return $village->regency_code === $this->code;
case 'district':
return $village->district_code === $this->code;
case 'village':
return $village->code === $this->code;
default:
return false;
}
}
public static function findForAddress(string $villageCode): ?self
{
$village = Village::find($villageCode);
if (!$village) {
return null;
}
// Periksa urutan spesifisitas: desa/kelurahan -> kecamatan -> kabupaten/kota -> provinsi
$zones = [
['type' => 'village', 'code' => $village->code],
['type' => 'district', 'code' => $village->district_code],
['type' => 'regency', 'code' => $village->regency_code],
['type' => 'province', 'code' => $village->province_code],
];
foreach ($zones as $zone) {
$deliveryZone = self::active()
->where('type', $zone['type'])
->where('code', $zone['code'])
->first();
if ($deliveryZone) {
return $deliveryZone;
}
}
return null;
}
}
// Penggunaan
$deliveryZone = DeliveryZone::findForAddress('33.75.01.1002');
if ($deliveryZone) {
echo "Biaya pengiriman: Rp " . number_format($deliveryZone->delivery_fee);
echo "Estimasi pengiriman: {$deliveryZone->estimated_days} hari";
}
Kelas Layanan
Layanan Lokasi
php
namespace App\Services;
use Creasi\Nusa\Models\{Province, Regency, District, Village};
use Illuminate\Support\Collection;
class LocationService
{
public function getLocationHierarchy(string $villageCode): ?array
{
$village = Village::with(['district', 'regency', 'province'])
->find($villageCode);
if (!$village) {
return null;
}
return [
'village' => [
'code' => $village->code,
'name' => $village->name,
'postal_code' => $village->postal_code,
],
'district' => [
'code' => $village->district->code,
'name' => $village->district->name,
],
'regency' => [
'code' => $village->regency->code,
'name' => $village->regency->name,
],
'province' => [
'code' => $village->province->code,
'name' => $village->province->name,
],
'full_address' => $this->buildFullAddress($village),
];
}
public function findNearestRegency(float $lat, float $lon): ?Regency
{
$regencies = Regency::whereNotNull('latitude')
->whereNotNull('longitude')
->get();
$nearest = null;
$minDistance = PHP_FLOAT_MAX;
foreach ($regencies as $regency) {
$distance = $this->calculateDistance(
$lat, $lon,
$regency->latitude, $regency->longitude
);
if ($distance < $minDistance) {
$minDistance = $distance;
$nearest = $regency;
}
}
return $nearest;
}
public function getRegionStatistics(): array
{
return [
'provinces' => Province::count(),
'regencies' => Regency::count(),
'districts' => District::count(),
'villages' => Village::count(),
'java_provinces' => Province::whereIn('code', ['31', '32', '33', '34', '35', '36'])->count(),
'cities' => Regency::where('name', 'like', '%Kota%')->count(),
'regencies_proper' => Regency::where('name', 'like', '%Kabupaten%')->count(),
];
}
public function validateAddressHierarchy(array $addressData): bool
{
$village = Village::find($addressData['village_code']);
if (!$village) {
return false;
}
return $village->district_code === $addressData['district_code'] &&
$village->regency_code === $addressData['regency_code'] &&
$village->province_code === $addressData['province_code'];
}
private function buildFullAddress(Village $village): string
{
return implode(', ', array_filter([
$village->name,
$village->district->name,
$village->regency->name,
$village->province->name,
$village->postal_code,
]));
}
private function calculateDistance(float $lat1, float $lon1, float $lat2, float $lon2): float
{
$earthRadius = 6371;
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
}
// Penggunaan
$locationService = new LocationService();
$hierarchy = $locationService->getLocationHierarchy('33.75.01.1002');
$nearestRegency = $locationService->findNearestRegency(-6.200000, 106.816666);
$stats = $locationService->getRegionStatistics();
Konfigurasi
Pengikatan Model Kustom
php
// Di AppServiceProvider Anda
use Creasi\Nusa\Contracts;
use App\Models\{Province, Address};
public function register()
{
// Ikat model kustom
$this->app->bind(Contracts\Province::class, Province::class);
$this->app->bind(Contracts\Address::class, Address::class);
}
Konfigurasi Kustom
php
// config/creasi/nusa.php
return [
'connection' => env('CREASI_NUSA_CONNECTION', 'nusa'),
'addressable' => \App\Models\Address::class,
'routes_enable' => env('CREASI_NUSA_ROUTES_ENABLE', true),
'routes_prefix' => env('CREASI_NUSA_ROUTES_PREFIX', 'nusa'),
// Pengaturan kustom
'custom_models' => [
'province' => \App\Models\Province::class,
'store' => \App\Models\Store::class,
],
'delivery' => [
'default_fee' => 10000,
'default_days' => 3,
],
];
Contoh-contoh ini menunjukkan cara memperluas Laravel Nusa dengan model kustom yang menambahkan logika bisnis, atribut tambahan, dan fungsionalitas spesifik domain sambil mempertahankan relasi data administratif inti.