hasyirin/laravel-address

Polymorphic address management for Laravel with hierarchical geographical data and built-in Malaysian dataset.
1,062
Install
composer require hasyirin/laravel-address
Latest Version:v4.0.5
PHP:^8.4
Laravel:12 · 13
Older releases:10 · 11
License:MIT
Last Updated:Jul 19, 2026
Links: GitHub  ·  Packagist
Maintainer: hasyirin

Laravel Address

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

A Laravel package for managing addresses with polymorphic relationships and hierarchical geographical data (countries, states, districts, subdistricts, and post offices).

Comes with built-in geographical data for Malaysia and a seeder command to populate reference tables.

Requirements

  • PHP 8.4+
  • Laravel 12 or 13

Installation

Install the package via Composer:

composer require hasyirin/laravel-address

Publish and run the migrations:

php artisan vendor:publish --tag="laravel-address-migrations"
php artisan migrate

Optionally publish the config file:

php artisan vendor:publish --tag="laravel-address-config"

Seeding Geographical Data

Seed countries, states, districts, subdistricts, and post offices:

php artisan address:seed

The command loads data from the package's data/ directory. To use your own data, place JSON files in your application's base_path('data') directory following the same structure.

Usage

Preparing Your Model

Add the InteractsWithAddresses trait to any model that should have addresses:

use Hasyirin\Address\Concerns\InteractsWithAddresses;
use Hasyirin\Address\Contracts\Addressable;

class User extends Model implements Addressable
{
    use InteractsWithAddresses;
}

Creating Addresses

$user->address()->create([
    'type' => 'primary',
    'line_1' => '123 Main Street',
    'line_2' => 'Suite 4B',
    'line_3' => 'Taman Example',
    'postcode' => '50000',
    'country_id' => $country->id,
    'state_id' => $state->id,
    'post_office_id' => $postOffice->id,
    'latitude' => 3.1390,
    'longitude' => 101.6869,
    'properties' => ['notes' => 'Front gate access'],
]);

Using the Factory

The Address model includes a factory for testing:

use Hasyirin\Address\Models\Address;

Address::factory()
    ->for($user, 'addressable')
    ->create([
        'type' => 'primary',
        'line_1' => 'No. 1, Jalan Test',
        'postcode' => '40000',
    ]);

Retrieving Addresses

// Primary address (returns a default empty instance if none exists)
$user->address;

// All addresses
$user->addresses;

// Address by type
$user->getAddress('billing');

Querying by Type

The ofType scope accepts a string, array, or enum:

use Hasyirin\Address\Models\Address;

Address::ofType('shipping')->get();
Address::ofType(['billing', 'shipping'])->get();

Formatting

// Single-line comma-separated string
$address->formatted();
// "123 Main Street, Suite 4B, Taman Example, 50000, Kuala Lumpur, Selangor, Malaysia"

// Exclude state or country
$address->formatted(state: false);
$address->formatted(country: false);

// Uppercase
$address->formatted(capitalize: true);

HTML Rendering

// Multi-line with <p> tags
$address->render();

// Inline comma-separated
$address->render(inline: true);

// With options
$address->render(state: false, country: false, capitalize: true, margin: 1);

Copying an Address

Create an unsaved copy of an address (without the polymorphic relationship or type):

$copy = $address->copy();
$copy->type = 'billing';
$copy->addressable()->associate($anotherModel);
$copy->save();

Locality

Designate a "local" country, state, and district by setting their codes in config/address.php:

'locality' => [
    'country' => 'MYS',
    'state' => '14',
    'district' => '02',
],

Resolve them anywhere via the static helper — it enforces the parent chain, so a state only counts as local if its country also matches the configured code:

Country::local();   // ?Country
State::local();     // ?State — matches state code AND parent country
District::local();  // ?District — matches district code AND parent state AND parent country

If any level is null in the config, the corresponding local() returns null.

Compose with the local() query scope:

District::query()->local()->where(...)->get();

The result is memoized per request via cache()->memo(). saved and deleted listeners on each model cascade-clear caches down the chain (Country save clears State and District caches too). For manual invalidation:

Country::clearLocalCache();

Configuration

// config/address.php

return [
    // Codes used to resolve `Country::local()`, `State::local()`, `District::local()`
    'locality' => [
        'country' => null,  // e.g. 'MYS'
        'state' => null,    // e.g. '14'
        'district' => null, // e.g. '02'
    ],

    // Override model classes
    'models' => [
        'address' => \Hasyirin\Address\Models\Address::class,
        'country' => \Hasyirin\Address\Models\Country::class,
        'state' => \Hasyirin\Address\Models\State::class,
        'district' => \Hasyirin\Address\Models\District::class,
        'subdistrict' => \Hasyirin\Address\Models\Subdistrict::class,
        'post-office' => \Hasyirin\Address\Models\PostOffice::class,
    ],

    // Override table names
    'tables' => [
        'addresses' => 'addresses',
        'countries' => 'countries',
        'states' => 'states',
        'districts' => 'districts',
        'subdistricts' => 'subdistricts',
        'post_offices' => 'post_offices',
    ],
];

Data Structure

The package uses a hierarchical location model:

Country
 └── State
      ├── District
      │    └── Subdistrict
      └── Post Office (with postcodes)

Each Address belongs to a Country, State, and optionally a PostOffice, and is polymorphically attached to any model via the addressable morph relationship.

Models

Model Key Fields
Country code (ISO 3166-1 alpha-3), alpha_2, name
State code, name, belongs to Country
District code, name, belongs to State
Subdistrict code, name, belongs to District
PostOffice name, postcodes (JSON array), belongs to State
Address type, line_1-line_3, postcode, latitude, longitude, properties (JSON)

All models use soft deletes.

Extending Models

To use your own model classes, extend the package models and update the config:

use Hasyirin\Address\Models\Address as BaseAddress;

class Address extends BaseAddress
{
    // your customizations
}
// config/address.php
'models' => [
    'address' => \App\Models\Address::class,
    // ...
],

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Credits

License

The MIT License (MIT). Please see License File for more information.

Related Packages

brnysn/laravel-world

The simplest way to add country, state and city to Laravel application.

3,407 3
doctrine/dbal

Powerful PHP database abstraction layer (DBAL) with many features for database s...

644,549,754 9,698
laravel/framework

The Laravel Framework.

593,882,149 34,959
laravel/tinker

Powerful REPL for the Laravel framework.

507,193,856 7,441
laravel/serializable-closure

Laravel Serializable Closure provides an easy and secure way to serialize closur...

423,477,751 607

Version History

Version Released PHP Laravel License
v4.0.5 ^8.4 ^12.0||^13.0 MIT
v4.0.4 ^8.4 ^12.0||^13.0 MIT
v4.0.3 ^8.4 ^12.0||^13.0 MIT
v4.0.2 ^8.4 ^12.0||^13.0 MIT
v4.0.1 ^8.4 ^12.0||^13.0 MIT