torann/address-format
Torann/address-format is a PHP package that formats postal addresses using country-specific conventions. Provide address components and a country code to generate correctly ordered, multi-line output, with Laravel support and configurable templates for consistent address display.
Verify your PHP floor before Composer does it for you. 2.0.0 requires PHP 8.2+. If any service in your estate is still on 8.1, pin ^1.x deliberately rather than letting the resolver hand you a version the runtime can't load.
composer require torann/address-format
php artisan vendor:publish --provider="Torann\AddressFormat\AddressFormatServiceProvider"
Read two things before writing any code.
config/address-format.php — the country → format-template map. This file is the package. Open the entries for your launch markets (US, GB, DE, JP) and check line order, separators, and which components appear at all. If a template is wrong for a market, that's a config override, not a code change.First use case: invoice PDFs and confirmation emails. Low risk, high visibility, and a formatting bug there surfaces as a support ticket rather than a failed shipment. Do not start at checkout or shipping-label generation.
Wire it behind an interface on day one:
// app/Contracts/AddressFormatter.php
interface AddressFormatter
{
public function format(Address $address, string $style = 'multi_line'): string;
}
// app/Services/TorannAddressFormatter.php
class TorannAddressFormatter implements AddressFormatter
{
public function format(Address $address, string $style = 'multi_line'): string
{
return AddressFormat::format([
'streetAddress' => $address->street_address,
'locality' => $address->city,
'administrativeArea' => $address->state,
'postalCode' => $address->postal_code,
'country' => $address->country_code,
], $style);
}
}
Bind it in a service provider and never reference the facade outside this adapter. That one habit is what makes the package's abandonment risk (5 stars, zero dependents, single maintainer) a non-event — a fork or a swap to commerceguys/addressing becomes a one-file change.
First-day checklist:
^1.x on purposePersist canonical structured columns (street_address, city, state, postal_code, country_code) as separate fields. Format only at render/print time. Never store the formatted string as the source of truth — a formatted blob cannot be re-localized when a template improves or a market is added. Cheap now, expensive to reverse.
The package takes arrays, so passing Eloquent attributes straight through means a renamed column silently drops a line. Wrap it:
final class Address
{
public function __construct(
public readonly ?string $street_address,
public readonly ?string $city,
public readonly ?string $state,
public readonly ?string $postal_code,
public readonly string $country_code,
) {}
public static function fromModel(ShippingAddress $m): self { /* ... */ }
}
Now a schema change is a single-point failure in one place, not silent misformatting in six. This is the highest-value pattern with this package, because the library performs no key validation — a mismatched key simply isn't there.
// app/Models/Order.php
protected function shippingAddressFormatted(): Attribute
{
return Attribute::make(
get: fn () => app(AddressFormatter::class)
->format($this->shippingAddress(), 'multi_line'),
);
}
Careful: this fires a container resolve plus a format on every attribute access, including inside loops. Use ->toArray() sparingly on collections of hundreds of orders, or eager-compute onto the model instance once.
<x-address :address="$order->shippingAddress()" style="multi_line" />
Keep the Blade layer dumb. If a template needs to change for one market, the config changes — not the views. Calling AddressFormat:: from a template reintroduces the coupling the adapter existed to prevent.
Most teams need exactly two shapes:
multi_line — confirmations, invoices, profile pages. Preserves the country's natural line breaks.single_line — carrier payloads, CRM exports, CSV, log lines. Comma-joined, no newlines.Build both at the adapter level so callers never think about separators. If the package only exposes one shape natively, derive the single-line variant from the same component array — don't do str_replace("\n", ", ", ...) on the multi-line output, because that breaks where line-break semantics differ.
public function format(Address $address, string $style = 'multi_line'): string
{
if (! $this->isSupported($address->country_code)) {
Log::warning('Unsupported country for address formatting', [
'country' => $address->country_code,
'order_id' => $address->order_id ?? null,
]);
return $this->fallbackFormat($address); // your own conservative template
}
return AddressFormat::format($this->toComponents($address), $style);
}
Never let the package guess. A plausible-but-wrong template produces a deliverable-looking label that fails downstream — the worst failure mode, because it is silent.
it('formats correctly for :dataset market', function (string $country, array $components, string $expected) {
expect(app(AddressFormatter::class)->format(Address::fromArray($components + ['country_code' => $country])))
->toBe($expected);
})->with('markets');
Commit fixtures for every supported market. These are your regression suite, your 2.0.0 upgrade gate, and your documentation when upstream goes quiet. They are the most durable artefact you will produce with this package.
Treat the formatter as one input to the label payload, not the payload itself. Carriers typically want structured fields, not concatenated strings — use the package for the human-readable part of the label and confirmation, and pass structured fields to the carrier API. Where a carrier genuinely demands a formatted string, keep that per-carrier mapping in the adapter, not scattered across jobs.
This is the biggest trap. The package is a formatter over already-structured fields, not a free-text parser. There is no "123 Main St, Springfield" → structured components path here. If your input is a single textarea, you need a parser or a paid verification vendor — this library does not solve that, and you'll discover it mid-sprint at the worst possible moment. Confirm your ingestion path produces structured fields before committing.
A mismatched array key isn't an exception — it's a missing line. 2.0.0 adds no schema validation to catch it. Symptoms to recognise:
null.Mitigate with the value object, upstream validation (required_if rules per country), and per-market tests. Do not rely on the library to complain.
Chasing per-locale output (French-language invoice for a German address) is your job, not the package's. It picks a template from the country code. Field labels ("Postal code" vs "PLZ" vs "〒") are not in scope either — you own the form layout and labels.
The diff is coding standards plus the PHP 8.2 floor. There is no new capability and no country-metadata refresh. Consequently:
If you override templates in config/address-format.php and run php artisan config:cache in deploy, the cached config is what ships. Adding a market means config:cache must re-run — put it explicitly in the deploy script. A "template not taking effect" bug is almost always a stale config cache. Also note in your runbook that the PHP floor changed at 2.0.0, so nobody downgrades the package later to fit an older runtime.
Country metadata changes: postal-code patterns, administrative-area naming, new subdivisions. 2.0.0 shipped no refresh, and the project is single-maintainer. Budget a small recurring allowance to review and patch templates for markets you care about. Because it's BSD-2-Clause, forking the config is trivial and legitimate — treat the templates as vendored data, not an upstream responsibility. Decide now who owns that patch if upstream goes quiet.
Japanese, Arabic, Hebrew, and Cyrillic-script addresses may render in the wrong visual order or with incorrect line handling depending on template and rendering target. PDF generation (DomPDF, wkhtmltopdf, Browsershot) each handle RTL differently. Test the actual output artefact — the string can be right in a unit test while the PDF is wrong.
"torann/address-format": "2.0.*" or "^2.0", chosen deliberately per service. Do not let the resolver decide. Record a sunset expectation for any service that cannot move past 1.x — there will be no maintenance branch and no backported fixes.
Instrument downstream: undeliverable shipments, failed address verification, support tickets mentioning address errors. The package won't tell you it produced bad output, so the delivery pipeline is your monitor.
commerceguys/addressing on the benchIf you later need CLDR-backed coverage, active maintenance, and community validation, the migration should cost you one adapter class — provided you resisted putting the facade in controllers and Blade. That resistance is the whole point of the adapter, and it costs about an hour up front.
How can I help you explore Laravel packages today?