Primary addresses
Each owner has at most one primary address per type — a customer can have a primary billing and a primary shipping address at the same time. Promoting one — through setPrimary(), or by adding or updating an address with isPrimary: true / ->primary() — demotes the others in the same owner + type group, in one transaction under a lock on the group, so a failure half-way leaves the old primary in place and two concurrent promotions cannot both win:
use RoundlyConsulting\Addresses\Enums\AddressType;
use RoundlyConsulting\Addresses\Facades\Addresses;
$address = Addresses::for($payer)->ofType(AddressType::Office)->first();
Addresses::for($payer)->setPrimary($address); // promote this, demote siblings
$payer->setPrimaryAddress($address); // trait shortcut, same guardprimaryAddress() without a type returns the first primary address of any type; pass a type to be specific.
Primary on create
Mark the address as primary while you create it — ->primary() on the builder or isPrimary: true on AddressData — and its siblings are demoted in the same step:
use RoundlyConsulting\Addresses\Enums\AddressType;
$customer->newAddress()
->type(AddressType::Shipping)
->in('Košice')->at('Main 2')->postalCode('04001')->country('SK')
->primary() // the previous primary shipping address is demoted
->save();
$customer->primaryAddress(AddressType::Shipping); // the address just savedOwnership guard
The address book’s setPrimary() — and the setPrimaryAddress() shortcut — first checks that the address belongs to the owner: the stored row’s addressable_type must equal getMorphClass() and its addressable_id the owner’s key — the copy you pass in is never trusted. Otherwise it throws AddressOwnershipException, so an address id from a request can never reshuffle another owner’s addresses:
use RoundlyConsulting\Addresses\Exceptions\AddressOwnershipException;
use RoundlyConsulting\Addresses\Facades\Addresses;
try {
Addresses::for($customer)->setPrimary($otherCustomersAddress);
} catch (AddressOwnershipException $e) {
abort(403); // 'The given address does not belong to this model.'
}Deleted addresses
A soft-deleted address cannot become primary — setPrimary() throws TrashedAddressException (restore it first), again checked against the stored row. Deleting the primary leaves its type without one until you promote another. The deleted address keeps its flag, so restore() undoes the delete: it comes back as the primary only while its owner + type group has no other live primary; otherwise it returns as a plain address:
use RoundlyConsulting\Addresses\Exceptions\TrashedAddressException;
use RoundlyConsulting\Addresses\Facades\Addresses;
Addresses::delete($address); // the primary — its type has none until you promote another
try {
Addresses::for($customer)->setPrimary($address);
} catch (TrashedAddressException $e) {
// 'A deleted address cannot be made primary; restore it first.'
}
$address->restore(); // back as the primary while no other live primary holds the slotDemoting
There is no public call that clears a whole group’s primary flag. To demote a single address, update it with isPrimary: false; the update is a full write, so carry over every other field:
use RoundlyConsulting\Addresses\DataTransferObjects\AddressData;
use RoundlyConsulting\Addresses\Facades\Addresses;
// demote one address — a full write, so carry over every other field
Addresses::update($address, AddressData::make(
city: $address->city,
street: $address->street,
postalCode: $address->postal_code,
countryIso: $address->country_iso,
name: $address->name,
type: $address->type,
isPrimary: false,
meta: $address->meta,
));The PrimaryAddressChanged event
PrimaryAddressChanged fires whenever an address becomes the primary of its type — setPrimary() or setPrimaryAddress(), adding one with isPrimary: true or ->primary(), or updating one with isPrimary: true. It fires after AddressCreated / AddressUpdated, once the promotion is written, and not when the address already was the primary.
Writes that bypass the package — raw Eloquent create() or update(), factories — do not demote siblings. On PostgreSQL and SQLite the partial unique index refuses a second live primary; on other databases nothing stops such a write.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.