Contact (HasContact)
File: app/Traits/Common/HasContact.php
A Laravel Eloquent trait that attaches contact information (phone, WhatsApp, email, address, etc.) to any model through a centralized contacts table — with support for both single and multiple contact values per type.
Overview
Instead of adding phone, email, whatsapp columns to every model that needs contact info, the HasContact trait stores all contacts in a master contacts table and manages the relationships automatically.
Two modes:
| Mode | Description | Example |
|---|---|---|
| Direct Contact | Model has its own contact columns (phone_contact_id, email_contact_id) | School, Employee |
| Polymorphic Contact | Uses contacts() relationship + pendingContacts extraction | Models without dedicated contact columns |
Why It Exists
- Normalization: All contact data lives in one
contactstable instead of duplicated across models - Encryption: Contact values can be stored encrypted in the database
- Flexibility: Support for single (one phone) and multiple (many phone numbers) contacts per type
- Auto-magic: Set
$model->phone = '08123456789'and the trait handles lookup, creation, and relationship assignment
Configuration
Define the contact fields on your model via the $contacts property:
class School extends Model
{
use HasContact;
protected array $contacts = [
'phone', // single contact → phone_contact_id column
'email', // single contact → email_contact_id column
'whatsapp', // single contact → whatsapp_contact_id column
'social_media' => [// multiple contacts → social_media_contact_ids JSON column
'multiple' => true,
],
];
}How the $contacts Array Works
| Format | Column Created | Relationship | Cardinality |
|---|---|---|---|
'phone' | phone_contact_id | phoneContact() | Single (BelongsTo) |
'social_media' => ['multiple' => true] | social_media_contact_ids | socialMediaContacts() | Multiple (HasMany-like) |
Methods
bootHasContact(): void
Boots the trait, hooks into saving and saved events:
saving: If direct contact → JSON-encode multiple contact arrays. If not → extract contact fields from attributes intopendingContacts.saved: ProcesspendingContacts— find or createContactrecords, attach viacontacts()relationship.
extractContactsBeforeSave(): void
Before saving to the database, removes contact fields from the model's attributes (to prevent SQL "column not found" errors) and stores them in $pendingContacts.
processPendingContacts(): void
After the model is saved, iterates $pendingContacts and for each value:
- Calls
Contact::firstOrCreate(['type' => $type, 'censored' => $val], ['encrypted' => $val]) - Collects the Contact IDs
- Syncs them via
$this->contacts()->syncWithoutDetaching($ids)
setContactValue(string $field, mixed $value): void
Core setter. Handles three scenarios:
| Value Type | Behavior |
|---|---|
Plain string ('08123456789') | firstOrCreate Contact record, set foreign key |
| UUID string | Direct foreign key assignment (contact exists) |
null | Clear the foreign key |
| Array (multiple) | Iterate each item, create/find Contact, set JSON array of IDs |
__get($key)
Magic getter that intercepts contact field access:
$school->phone; // Returns the phone contact value
$school->email; // Returns the email contact valueFor direct contact mode, automatically loads the relationship and returns $contact->value.
setAttribute($key, $value)
Intercepts mass assignment for contact fields. When you do:
$school->fill(['phone' => '08123456789', 'email' => 'info@sutomo.sch.id']);The trait routes these to setContactValue() instead of trying to set non-existent columns.
getContactValue(string $type): ?string
Explicit getter for single contact:
$school->getContactValue('phone'); // "08123456789"getContactValues(string $type): array
Explicit getter for multiple contacts:
$school->getContactValues('social_media'); // ["@sutomo", "@sutomo_school"]contactRelation(string $field): BelongsTo
Returns a dynamic BelongsTo relationship for a single contact field:
$school->contactRelation('phone'); // BelongsTo where phone_contact_id = contacts.idcontactsRelation(string $field): HasMany
Returns a dynamic HasMany-like relationship for multiple contact fields. Uses a custom anonymous class that overrides addConstraints(), addEagerConstraints(), match(), and getResults() to handle JSON arrays of contact IDs.
How to Use
1. Add the trait to your model
use App\Traits\Common\HasContact;
class School extends Model
{
use HasContact;
protected array $contacts = [
'phone',
'email',
'whatsapp',
'social_media' => ['multiple' => true],
];
}2. Set contact values
$school = new School;
$school->phone = '08123456789';
$school->email = 'info@sutomo.sch.id';
$school->whatsapp = '081260007890';
$school->social_media = ['@sutomo', '@sutomo_school'];
$school->save();3. Read contact values
$school->phone; // "08123456789"
$school->email; // "info@sutomo.sch.id"
$school->social_media; // ["@sutomo", "@sutomo_school"]
// Explicit getters
$school->getContactValue('phone'); // "08123456789"
$school->getContactValues('social_media'); // ["@sutomo", "@sutomo_school"]4. Eager load relationships
$school->load('phoneContact', 'emailContact', 'socialMediaContacts');Key Files
| File | Purpose |
|---|---|
app/Traits/Common/HasContact.php | The trait — manages contact attachment, extraction, relationships |
app/Models/Platform/Contact/Contact.php | The centralized contacts model — stores type, censored value, encrypted value |
app/Models/ | Any model that uses use HasContact + $contacts array |