galahad/laravel-addressing
Laravel package for managing postal addresses: bundled country, state and city data plus address models and migrations you can attach to users or orders. Seed geographic records and store structured addresses without building the schema yourself.
Before you install anything, check the version constraint against your app. 4.0.0 is a framework-support major — it adds Laravel 13 support and modernizes dependencies — so it is almost certainly a laravel/framework: ^13 (and likely a newer PHP floor) release. On Laravel 11 or 12, you want ^3.3, not ^4.0:
composer require glhd/laravel-addressing:^3.3 # Laravel <=12
composer require glhd/laravel-addressing:^4.0 # Laravel 13
Then the usual ten minutes. The package is small and single-purpose, so read the source before wiring it in — with zero dependents and no star history, vendor/glhd/laravel-addressing/src will tell you more than the README:
php artisan vendor:publish --provider="Glhd\Addressing\AddressingServiceProvider"
php artisan migrate
Then, in order:
src/. The changelog says "modernizes the dependencies a bit," which is vague enough to hide method renames or tightened signatures. Open https://github.com/glhd/laravel-addressing/compare/3.3.0...4.0.0 and scan the actual diff — that is the only reliable breaking-change list you will get from this release.config/addressing.php if one is published. Note the model class binding (packages like this almost always let you swap Address for your own subclass) and any country/state data source setting. This is the file you will actually edit in production.line_1, line_2, locality, administrative_area, postal_code, country_code) and, critically, the morph columns (addressable_type / addressable_id). Expect these to be unchanged in 4.0.0 — this release ships no addressing features.HasAddresses in src/Concerns. That trait is the API surface you'll use 90% of the time.Your first use case should be the simplest one that exercises the polymorphic relation:
use Glhd\Addressing\Concerns\HasAddresses;
class Customer extends Model
{
use HasAddresses;
}
// tinker
$customer = Customer::first();
$customer->addresses()->create([
'line_1' => '123 Main St',
'locality' => 'Portland',
'administrative_area' => 'OR',
'postal_code' => '97201',
'country_code' => 'US',
]);
$customer->addresses; // collection
If that round-trips, the package is doing its job. If the trait name or method names differ, adjust — the pattern holds regardless. Note that 4.0.0 gives you no new behavior to test against: if you are upgrading from 3.x, the only thing that can break is framework/PHP compatibility and anything the dependency modernization touched.
Polymorphic everywhere, not belongs-to-one. The reason to reach for this package instead of five nullable columns on users is that addresses are reusable across Customer, Order, Warehouse, Vendor. Model them that way from day one. Resist the temptation to add billing_address_id foreign keys; instead expose semantic helpers on the parent model:
public function billingAddress(): ?Address
{
return $this->addresses->firstWhere('pivot_kind', 'billing'); // or a `type` column
}
If the package ships no notion of address kind (billing vs shipping vs mailing), add a kind/label column via your own migration rather than building separate tables or separate models.
Snapshot addresses onto orders; don't reference them. This is the single most important pattern and it's independent of the package. An order must render the address as it was at checkout. Either copy the address rows into the order's own morph relation ($order->addresses()->create($customerAddress->only([...]))) or store a JSON snapshot column. If you just point the order at the same addresses row, editing a customer's address silently rewrites history.
Validate at the edge, not in the model. Put the rules in a FormRequest or a dedicated AddressData DTO:
'country_code' => ['required', 'string', 'size:2', Rule::in(array_keys($countries))],
'postal_code' => ['required', 'string', 'max:20'],
'administrative_area' => ['nullable', 'string', 'max:64'],
Postal-code and state validation is country-specific and almost never worth hand-rolling — pair this package with a country/state data source (e.g. league/iso3166 or a states package). 4.0.0 does not refresh any country/state data, so whatever list ships is still frozen at its last content change.
Wrap writes in an application service, not controllers. Because this is an unvetted package, isolate it behind your own class so swapping implementations later is a one-file change:
class AddressBook
{
public function saveFor(Model $owner, AddressData $data, string $kind = 'shipping'): Address
{
return $owner->addresses()->updateOrCreate(
['kind' => $kind],
$data->toArray()
);
}
}
This wrapper is also your upgrade path: when 5.0.0 lands (or the package dies), the framework-version churn stops at one class.
Formatting belongs in a presenter. Addresses get rendered as multi-line blocks in emails, PDFs, and admin tables. Write one formatter — a Blade component <x-address :address="$order->shippingAddress" /> — and use it everywhere. Country-specific formatting (Japan vs US vs Germany) is a rabbit hole; start generic.
Use the package's factory in your own tests. If it ships a factory, Address::factory() keeps your test setup short. If not, write one — you'll use it constantly. On the 3.3 → 4.0 upgrade, your test suite is the thing that tells you whether the "modernized dependencies" changed behavior.
^3.3 if you're staying on 11/12; ^4.0 once you move to 13. Do not use * or a bare glhd/laravel-addressing — a surprise major on a package with this release cadence is exactly the scenario pinning exists for. Commit the lockfile.php artisan migrate before vendor:publish means you get the package's default schema and then a duplicate-table error when you publish. If you need to alter the schema, publish and edit before the first migrate, and consider --tag=migrations.Relation::enforceMorphMap(), the addressable_type column stores your alias. Changing a class name later orphans rows. Register aliases early and never rename them; if you already have data, write a one-off migration. (Relevant during a 4.0 upgrade if you also rename models.)cascadeOnDelete. If not, deleting a Customer leaves orphan addresses rows, and a later addressable_id reuse can attach a stranger's address. Add a model deleting hook or a DB constraint.WHERE addressable_type = ? AND addressable_id = ?. Verify it exists; add it if the package migration omitted it. Same for postal_code and country_code if you query by region.->addresses unless the model uses SoftDeletes and you scope it. If the package's model doesn't, add deleted_at in your own migration plus a custom model bound via config, or hard-delete.php artisan config:cache freezes the model binding. If you swap the model via config, re-cache after deploy — and don't resolve the class name at bootstrap time in a way that breaks under cached config. Worth re-verifying after a major upgrade, since dependency modernization sometimes changes when things are resolved.AddressBook wrapper so removal is a day's work, (c) write your own tests around the behavior you depend on. If the address shape is genuinely simple, honestly evaluate whether 40 lines of migration plus a trait in app/Support beats a dependency here — especially since you now have to track majors just to stay on a supported framework.config() or a container binding rather than a hard-coded Address::class.How can I help you explore Laravel packages today?