---
title: "Primary contacts & ordering — Contacts for Laravel | Roundly"
description: "One primary per owner and kind — auto-assigned, swapped in a transaction, handed to the next contact when it leaves — plus per-kind ordering."
url: https://roundly-consulting.com/open-source/docs/contacts-for-laravel/primary-contacts
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Contacts for Laravel](https://roundly-consulting.com/open-source/docs/contacts-for-laravel.md)

# Primary contacts & ordering

Each owner has at most one primary contact per kind, always. With auto\_primary on (the default) a kind that has contacts also keeps a primary: the first one added becomes it, and when the primary is deleted, synced away or moved to another kind, the next contact by position is promoted. Marking another contact primary demotes the previous one inside a database transaction:

```php
$first = $user->addEmail('jane@example.com');     // first email: primary automatically
$second = $user->addEmail('jane@work.example.com', primary: true);

$first->fresh()->is_primary;     // false — demoted in the same transaction
$second->fresh()->is_primary;    // true

$phone = $user->addPhone('+421900000000');   // each kind keeps its own primary
$phone->is_primary;                          // true

Contacts::setPrimary($first);                // promote again; fires PrimaryContactChanged

Contacts::delete($first);                    // the primary leaves its kind...
$second->fresh()->is_primary;                // true — ...and the next by position takes over
```

- Primaries are scoped by owner and raw kind — email, phone and each registered custom kind keep their own.
- Ownerless contacts share one primary per kind among themselves.
- PrimaryContactChanged fires once the promotion’s transaction commits, with the new primary and the demoted one (or null) — for automatic promotions too.
- The flag is written with a query on the stored row, so the promoted contact fires no Eloquent saving or updated model event — listen for PrimaryContactChanged — and unsaved changes on the contact you pass to setPrimary() are not saved.
- With auto\_primary off, nothing is promoted for you.
- A restored primary comes back as a secondary if its kind promoted another contact meanwhile.
- A soft-deleted contact holds no primary slot: promoting it throws PrimaryContactConflict.
- With require\_owner\_for\_primary on, promoting an ownerless contact throws PrimaryContactConflict, and ownerless kinds are never auto-promoted.

## Concurrent promotions

Two concurrent promotions in one owner and kind can never both win. Every promotion — setPrimary() and each add, update, delete and sync that promotes — runs in one transaction that reads the contact as stored, locks its kind group and only then demotes the other primary. On PostgreSQL and SQLite the one-primary index (see Database schema) also refuses a primary written past the lock, and the promotion retries once against the committed winner; on MySQL the lock alone serialises promotions.

## Ordering

Every contact has a position within its kind. New contacts take the next free slot (starting at 0) unless ContactData sets one explicitly; sync() assigns positions by input order:

```php
$user->addPhone('+421900000001');   // position 0
$user->addPhone('+421900000002');   // position 1 — next free slot within the kind

// Pin a position explicitly
$user->addContact(new ContactData(ContactType::Phone, '+421900000003', position: 0));

$user->contactsOfType(ContactType::Phone);            // ordered by position, then id
Contact::query()->forOwner($user)->ordered()->get();
```

## 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/contacts-for-laravel.md)
