Skip to content

WithAddresses Trait

The WithAddresses trait adds multiple polymorphic address relationships to your models, allowing them to have multiple associated addresses with complete Indonesian administrative region data.

Namespace

php
Creasi\Nusa\Models\Concerns\WithAddresses

Usage

Basic Implementation

php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Creasi\Nusa\Models\Concerns\WithAddresses;

class Company extends Model
{
    use WithAddresses;
    
    protected $fillable = [
        'name',
        'description'
    ];
}

Database Setup

The trait uses Laravel Nusa's built-in address table. Ensure you've published and run the migrations:

bash
php artisan vendor:publish --tag=creasi-migrations
php artisan migrate

Features

Multiple Address Relationships

Access all associated addresses:

php
$company = Company::find(1);
$addresses = $company->addresses;

foreach ($addresses as $address) {
    echo "Type: {$address->type}";
    echo "Address: {$address->address_line}";
    echo "Location: {$address->village->name}, {$address->regency->name}";
}

Creating Multiple Addresses

php
$company = Company::find(1);

// Headquarters
$company->addresses()->create([
    'type' => 'headquarters',
    'address_line' => 'Jl. Sudirman No. 123',
    'province_code' => '31',
    'regency_code' => '31.71',
    'district_code' => '31.71.01',
    'village_code' => '31.71.01.1001'
]);

// Branch office
$company->addresses()->create([
    'type' => 'branch',
    'address_line' => 'Jl. Malioboro No. 456',
    'province_code' => '34',
    'regency_code' => '34.71',
    'district_code' => '34.71.02',
    'village_code' => '34.71.02.1005'
]);

// Warehouse
$company->addresses()->create([
    'type' => 'warehouse',
    'address_line' => 'Jl. Industri No. 789',
    'province_code' => '33',
    'regency_code' => '33.74',
    'district_code' => '33.74.05',
    'village_code' => '33.74.05.1010'
]);

Common Use Cases

1. Multi-Location Businesses

php
class Company extends Model
{
    use WithAddresses;
    
    protected $fillable = ['name', 'description'];
    
    public function getHeadquartersAttribute()
    {
        return $this->addresses()->where('type', 'headquarters')->first();
    }
    
    public function getBranchesAttribute()
    {
        return $this->addresses()->where('type', 'branch')->get();
    }
    
    public function getWarehousesAttribute()
    {
        return $this->addresses()->where('type', 'warehouse')->get();
    }
    
    public function scopeWithLocationsIn($query, $provinceCode)
    {
        return $query->whereHas('addresses', function ($q) use ($provinceCode) {
            $q->where('province_code', $provinceCode);
        });
    }
    
    public function getLocationCountAttribute()
    {
        return $this->addresses()->count();
    }
    
    public function getCoverageAreasAttribute()
    {
        return $this->addresses()
            ->with('province')
            ->get()
            ->pluck('province.name')
            ->unique()
            ->values();
    }
}

// Usage
$company = Company::with(['addresses.village.district.regency.province'])->first();

echo "Headquarters: " . $company->headquarters?->address_line;
echo "Branches: " . $company->branches->count();
echo "Coverage: " . $company->coverage_areas->implode(', ');

2. Customer Management

php
class Customer extends Model
{
    use WithAddresses;
    
    protected $fillable = [
        'name',
        'email',
        'phone'
    ];
    
    public function getHomeAddressAttribute()
    {
        return $this->addresses()->where('type', 'home')->first();
    }
    
    public function getOfficeAddressAttribute()
    {
        return $this->addresses()->where('type', 'office')->first();
    }
    
    public function getShippingAddressesAttribute()
    {
        return $this->addresses()->where('type', 'shipping')->get();
    }
    
    public function addShippingAddress(array $addressData)
    {
        return $this->addresses()->create(array_merge($addressData, [
            'type' => 'shipping'
        ]));
    }
    
    public function getPreferredShippingAddressAttribute()
    {
        return $this->addresses()
            ->where('type', 'shipping')
            ->where('is_default', true)
            ->first() ?? $this->home_address;
    }
    
    public function setDefaultShippingAddress($addressId)
    {
        // Remove default from all shipping addresses
        $this->addresses()
            ->where('type', 'shipping')
            ->update(['is_default' => false]);
        
        // Set new default
        return $this->addresses()
            ->where('id', $addressId)
            ->where('type', 'shipping')
            ->update(['is_default' => true]);
    }
}

// Usage
$customer = Customer::first();

$customer->addShippingAddress([
    'address_line' => 'Jl. Delivery No. 123',
    'village_code' => '33.74.01.1001',
    'district_code' => '33.74.01',
    'regency_code' => '33.74',
    'province_code' => '33'
]);

$shippingAddress = $customer->preferred_shipping_address;

3. Event Management

php
class Event extends Model
{
    use WithAddresses;
    
    protected $fillable = [
        'title',
        'description',
        'event_date'
    ];
    
    protected $casts = [
        'event_date' => 'datetime'
    ];
    
    public function getVenuesAttribute()
    {
        return $this->addresses()->where('type', 'venue')->get();
    }
    
    public function getAccommodationsAttribute()
    {
        return $this->addresses()->where('type', 'accommodation')->get();
    }
    
    public function addVenue(array $venueData)
    {
        return $this->addresses()->create(array_merge($venueData, [
            'type' => 'venue'
        ]));
    }
    
    public function addAccommodation(array $accommodationData)
    {
        return $this->addresses()->create(array_merge($accommodationData, [
            'type' => 'accommodation'
        ]));
    }
    
    public function getLocationSummaryAttribute()
    {
        $venues = $this->venues;
        
        if ($venues->isEmpty()) {
            return 'Venues TBA';
        }
        
        $cities = $venues->map(function ($venue) {
            return $venue->regency->name;
        })->unique();
        
        return $cities->count() === 1 
            ? $cities->first()
            : $cities->count() . ' cities';
    }
}

4. Logistics Management

php
class LogisticsProvider extends Model
{
    use WithAddresses;
    
    protected $fillable = [
        'name',
        'service_type'
    ];
    
    public function getWarehousesAttribute()
    {
        return $this->addresses()->where('type', 'warehouse')->get();
    }
    
    public function getDistributionCentersAttribute()
    {
        return $this->addresses()->where('type', 'distribution_center')->get();
    }
    
    public function getServiceAreasAttribute()
    {
        return $this->addresses()
            ->with('province')
            ->get()
            ->groupBy('province.name')
            ->map(function ($addresses, $provinceName) {
                return [
                    'province' => $provinceName,
                    'locations' => $addresses->count(),
                    'types' => $addresses->pluck('type')->unique()->values()
                ];
            });
    }
    
    public function canServeLocation($provinceCode, $regencyCode = null)
    {
        $query = $this->addresses()->where('province_code', $provinceCode);
        
        if ($regencyCode) {
            $query->where('regency_code', $regencyCode);
        }
        
        return $query->exists();
    }
    
    public function getNearestFacility($provinceCode, $regencyCode, $type = null)
    {
        $query = $this->addresses()
            ->where('province_code', $provinceCode)
            ->where('regency_code', $regencyCode);
        
        if ($type) {
            $query->where('type', $type);
        }
        
        return $query->first();
    }
}

// Usage
$provider = LogisticsProvider::first();

$canServe = $provider->canServeLocation('33', '33.74');
$warehouse = $provider->getNearestFacility('33', '33.74', 'warehouse');
$serviceAreas = $provider->service_areas;

Advanced Usage

Address Type Management

php
class AddressableModel extends Model
{
    use WithAddresses;
    
    const ADDRESS_TYPES = [
        'home' => 'Home Address',
        'office' => 'Office Address',
        'shipping' => 'Shipping Address',
        'billing' => 'Billing Address',
        'warehouse' => 'Warehouse',
        'branch' => 'Branch Office',
        'headquarters' => 'Headquarters'
    ];
    
    public function getAddressesByTypeAttribute()
    {
        return $this->addresses->groupBy('type');
    }
    
    public function getAddressTypesAttribute()
    {
        return $this->addresses->pluck('type')->unique()->values();
    }
    
    public function hasAddressType($type)
    {
        return $this->addresses()->where('type', $type)->exists();
    }
    
    public function getAddressByType($type)
    {
        return $this->addresses()->where('type', $type)->get();
    }
    
    public function removeAddressType($type)
    {
        return $this->addresses()->where('type', $type)->delete();
    }
}

Bulk Address Operations

php
class AddressManager
{
    public static function bulkCreateAddresses($model, array $addressesData)
    {
        $addresses = collect($addressesData)->map(function ($data) use ($model) {
            return array_merge($data, [
                'addressable_id' => $model->id,
                'addressable_type' => get_class($model),
                'created_at' => now(),
                'updated_at' => now()
            ]);
        });
        
        return \DB::table('addresses')->insert($addresses->toArray());
    }
    
    public static function syncAddresses($model, array $addressesData)
    {
        // Delete existing addresses
        $model->addresses()->delete();
        
        // Create new addresses
        return static::bulkCreateAddresses($model, $addressesData);
    }
    
    public static function getLocationStatistics($modelClass)
    {
        return $modelClass::with('addresses.province')
            ->get()
            ->flatMap(function ($model) {
                return $model->addresses;
            })
            ->groupBy('province.name')
            ->map(function ($addresses, $provinceName) {
                return [
                    'province' => $provinceName,
                    'count' => $addresses->count(),
                    'types' => $addresses->pluck('type')->unique()->values()
                ];
            });
    }
}

Validation

Multiple Address Validation

php
class CompanyAddressRequest extends FormRequest
{
    public function rules()
    {
        return [
            'addresses' => 'required|array|min:1',
            'addresses.*.type' => 'required|string|in:headquarters,branch,warehouse',
            'addresses.*.address_line' => 'required|string|max:255',
            'addresses.*.village_code' => 'required|exists:nusa.villages,code',
            'addresses.*.postal_code' => 'nullable|string|size:5'
        ];
    }
    
    public function messages()
    {
        return [
            'addresses.*.type.in' => 'Address type must be headquarters, branch, or warehouse.',
            'addresses.*.village_code.exists' => 'The selected village is invalid.'
        ];
    }
}

Unique Address Type Validation

php
class CustomerAddressRequest extends FormRequest
{
    public function rules()
    {
        $customerId = $this->route('customer')->id ?? null;
        
        return [
            'type' => [
                'required',
                'string',
                Rule::unique('addresses')
                    ->where('addressable_type', Customer::class)
                    ->where('addressable_id', $customerId)
                    ->ignore($this->address)
            ],
            'address_line' => 'required|string|max:255',
            'village_code' => 'required|exists:nusa.villages,code'
        ];
    }
}

Performance Tips

1. Eager Loading

php
// Good
$companies = Company::with([
    'addresses.village.district.regency.province'
])->get();

// Bad - N+1 queries
$companies = Company::all();
foreach ($companies as $company) {
    foreach ($company->addresses as $address) {
        echo $address->village->name; // Multiple queries
    }
}

2. Selective Loading by Type

php
$companies = Company::with([
    'addresses' => function ($query) {
        $query->where('type', 'headquarters')
              ->with('village.regency.province');
    }
])->get();

3. Counting Addresses

php
$companies = Company::withCount([
    'addresses',
    'addresses as branches_count' => function ($query) {
        $query->where('type', 'branch');
    },
    'addresses as warehouses_count' => function ($query) {
        $query->where('type', 'warehouse');
    }
])->get();

Released under the MIT License.