The Contacts facade
The Contacts facade is the recommended entry point. Contacts::for($owner) returns the owner’s contact book, Contacts::verification() runs the token and code flow, and single-contact operations are flat. Every write goes through a dedicated action, so normalization, validation, primary handling and events are identical whichever entry point you use:
use RoundlyConsulting\Contacts\DataTransferObjects\ContactData;
use RoundlyConsulting\Contacts\Enums\ContactType;
use RoundlyConsulting\Contacts\Facades\Contacts;
// Add — normalized + validated, primary guaranteed unique per kind.
Contacts::for($user)->email('[email protected]')->label('Work')->primary()->add();
Contacts::for($user)->phone('+421 900 000 000')->label('Mobile')->add();
Contacts::for($user)->type('whatsapp')->value('+421900123456')->add(); // a registered custom kind
Contacts::for($user)->add(new ContactData(ContactType::Url, 'example.com'));
// Read and export.
Contacts::for($user)->all(); // ordered by position
Contacts::for($user)->ofType(ContactType::Email);
Contacts::for($user)->primary(ContactType::Email); // ?Contact
Contacts::for($user)->vCard(); // vCard 3.0 string
// One contact.
Contacts::update($contact, new ContactData(ContactType::Email, '[email protected]'));
Contacts::setPrimary($contact);
Contacts::delete($contact);
// Verification.
$token = Contacts::verification()->request($contact);
Contacts::verification()->confirm($contact, $token);
Contacts::verification()->markVerified($contact);Facade methods
| Method | Returns | Description |
|---|---|---|
for(Model $owner) | ContactBook | The owner’s contact book — fluent builders, add, sync, reads and vCard export. |
verification() | ContactVerification | The verification accessor: request(), confirm(), markVerified(). |
update(Contact $contact, ContactData $data) | Contact | Overwrite a contact; a primary flag promotes it, a new value or kind drops the verification. Fires ContactUpdated. |
delete(Contact $contact) | void | Soft delete; deleting the primary promotes the next one (auto_primary). Fires ContactDeleted. |
setPrimary(Contact $contact) | Contact | Promote, demoting the owner’s other primary of the same kind; fires PrimaryContactChanged. |
sharedWith(Connectable $owner) | Collection | Contacts connected to an owner (see Affiliations). |
validationRules(string $key = 'contacts') | array | contacts.* rules for a repeatable form. |
fake() | ContactsFake | Swap in the recording fake (see Testing). |
The contact book
Contacts::for($owner) — or $owner->contactBook() on a HasContacts model — returns a ContactBook scoped to that owner:
| Method | Returns | Description |
|---|---|---|
email() / phone() / url() / address(string $value) | PendingContact | Start a fluent contact of that kind. |
structuredAddress(AddressData|array $data) | PendingContact | Start an address contact backed by a structured address. |
type(ContactType|string $type) | PendingContact | Start a contact of any kind — a ContactType or a registered custom kind. |
add(ContactData $data) | Contact | Add a contact from a DTO; fires ContactAdded. |
sync(ContactType $type, array $items) | EloquentCollection | Reconcile the owner’s contacts of one kind to a list; a synced-away primary hands over to the first synced contact. |
all() | EloquentCollection | Every contact of the owner, by position. |
ofType(ContactType|string $type) | EloquentCollection | One kind, by position. |
primary(ContactType|string $type) | ?Contact | The primary contact of a kind, or null. |
vCard() | string | A vCard 3.0 string of the owner’s contacts. |
The verification accessor
Contacts::verification() returns a ContactVerification (see Verification):
| Method | Returns | Description |
|---|---|---|
request(Contact $contact) | string | Issue a code or token and return the plaintext; fires ContactVerificationRequested. |
confirm(Contact $contact, string $token) | Contact | Confirm the token and mark the contact verified; fires ContactVerified. |
markVerified(Contact $contact, ?CarbonInterface $at = null) | Contact | Mark verified without a token (idempotent); fires ContactVerified. |
The fluent builder
The book’s email(), phone(), url(), address(), structuredAddress() and type() start a PendingContact. Chain the setters, then call add():
$contact = Contacts::for($company)
->email('[email protected]') // or ->phone() / ->url() / ->address()
->label('Sales') // free-text label for this contact
->name('Northwind sales desk') // display name
->category('Suppliers') // group contacts
->meta(['hours' => '9-17']) // arbitrary JSON metadata
->primary() // ->primary(false) to opt out
->add(); // terminal: validates, saves, returns Contact
// Explicit type + value
Contacts::for($company)->type(ContactType::Social)->value('@northwind')->add();- email(), phone(), url(), address() — set the kind and the value in one call.
- type(ContactType|string) + value(string) — set them separately; a registered custom kind keeps its raw name, and without a type the kind is custom.
- label(), name(), category(), meta(array) — descriptive fields.
- primary(bool $primary = true) — request primary status.
- structuredAddress(AddressData|array) — attach a structured address (see Structured addresses).
- add() — terminal: validates, saves and returns the Contact.
ContactData
ContactData is the readonly DTO every write accepts. Build it with named arguments or from an array; normalized() returns a normalized copy:
use RoundlyConsulting\Contacts\DataTransferObjects\ContactData;
use RoundlyConsulting\Contacts\Enums\ContactType;
$data = new ContactData(
type: ContactType::Phone,
value: '+421 900 000 000',
label: 'Office',
name: null,
category: 'Headquarters',
isPrimary: false,
position: null, // null = next free position within the kind
meta: ['ext' => '204'],
kind: null, // null = the type's own value; set it for custom kinds
);
$data = ContactData::fromArray([
'type' => 'phone', // ContactType or string
'value' => '+421 900 000 000',
'is_primary' => true, // or 'isPrimary'
]);
$data->normalized()->value; // '+421900000000'Updating and deleting
use RoundlyConsulting\Contacts\Models\Contact;
$updated = Contacts::update($contact, new ContactData(
ContactType::Email,
'[email protected]',
label: 'Work',
));
$updated->isVerified(); // false — a new value needs a new verification
Contacts::delete($contact); // soft delete + ContactDeleted
Contact::withTrashed()->find($contact->id)->restore(); // addresses + connections are backupdate() re-normalizes and re-validates the value. The kind, value, label, category and meta are replaced with the DTO’s values — a null label or category clears it — while name and position change only when set. A changed value or kind drops the verification and voids any pending token. Passing isPrimary: true promotes the contact.
Moving a contact to another kind keeps one primary per kind: a primary entering a kind that already has one is demoted (unless isPrimary asks for it, which demotes the other instead). With auto_primary on, the kind it left promotes its next contact, and a kind it enters without a primary gets one. Deleting a primary promotes the next contact of its kind the same way.
Low-level access
// The raw relation still works — but it skips normalization, validation,
// auto-primary and events. Prefer the trait, the facade or the builder.
$user->contacts()->create([
'type' => 'email',
'name' => 'Jane Doe',
'value' => '[email protected]',
]);The contacts() relation, the scopes, the meta collection cast and direct create() calls all still work. Normalization, validation and auto-primary apply only through the action, facade and trait-sugar path. Dropping the verification on a value change applies everywhere, direct writes included.
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.