Skip to content

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:

ModeDescriptionExample
Direct ContactModel has its own contact columns (phone_contact_id, email_contact_id)School, Employee
Polymorphic ContactUses contacts() relationship + pendingContacts extractionModels without dedicated contact columns

Why It Exists

  • Normalization: All contact data lives in one contacts table 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:

php
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

FormatColumn CreatedRelationshipCardinality
'phone'phone_contact_idphoneContact()Single (BelongsTo)
'social_media' => ['multiple' => true]social_media_contact_idssocialMediaContacts()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 into pendingContacts.
  • saved: Process pendingContacts — find or create Contact records, attach via contacts() 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:

  1. Calls Contact::firstOrCreate(['type' => $type, 'censored' => $val], ['encrypted' => $val])
  2. Collects the Contact IDs
  3. Syncs them via $this->contacts()->syncWithoutDetaching($ids)

setContactValue(string $field, mixed $value): void

Core setter. Handles three scenarios:

Value TypeBehavior
Plain string ('08123456789')firstOrCreate Contact record, set foreign key
UUID stringDirect foreign key assignment (contact exists)
nullClear 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:

php
$school->phone;     // Returns the phone contact value
$school->email;     // Returns the email contact value

For direct contact mode, automatically loads the relationship and returns $contact->value.


setAttribute($key, $value)

Intercepts mass assignment for contact fields. When you do:

php
$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:

php
$school->getContactValue('phone'); // "08123456789"

getContactValues(string $type): array

Explicit getter for multiple contacts:

php
$school->getContactValues('social_media'); // ["@sutomo", "@sutomo_school"]

contactRelation(string $field): BelongsTo

Returns a dynamic BelongsTo relationship for a single contact field:

php
$school->contactRelation('phone'); // BelongsTo where phone_contact_id = contacts.id

contactsRelation(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

php
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

php
$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

php
$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

php
$school->load('phoneContact', 'emailContact', 'socialMediaContacts');

Key Files

FilePurpose
app/Traits/Common/HasContact.phpThe trait — manages contact attachment, extraction, relationships
app/Models/Platform/Contact/Contact.phpThe centralized contacts model — stores type, censored value, encrypted value
app/Models/Any model that uses use HasContact + $contacts array