Models
Wirechat ships with six core models:
Conversation, Participant, Message, Group, Attachment, and Action.
Together they handle threads, membership, message delivery, uploads, group metadata, and action history.
As of 0.5.1, Wirechat resolves these model classes from config, which means you can extend them in your app without forking the package.
#How Model Resolution Works
Wirechat resolves the following classes from config/wirechat.php:
'models' => [
'action' => \Wirechat\Wirechat\Models\Action::class,
'attachment' => \Wirechat\Wirechat\Models\Attachment::class,
'conversation' => \Wirechat\Wirechat\Models\Conversation::class,
'group' => \Wirechat\Wirechat\Models\Group::class,
'message' => \Wirechat\Wirechat\Models\Message::class,
'participant' => \Wirechat\Wirechat\Models\Participant::class,
],
Each configured class must extend the corresponding base Wirechat model. This matters because the package depends on inherited relationships, casts, scopes, cleanup hooks, and helper methods.
Recommended approach:
- Extend the base model instead of rewriting it from scratch.
- Keep the package tables and core columns unless you intentionally own the whole Wirechat data layer.
- Preserve relationship names and enum casts because package jobs, UI flows, and scopes call them directly.
- If you override
boot(),booted(), or__construct(), callparent. - Add your app-specific logic as extra scopes, relations, traits, accessors, or convenience helpers.
Good customizations:
- Add accessors and convenience methods.
- Add app-specific relationships.
- Add observers or traits.
- Add non-breaking casts or guarded or fillable adjustments that still respect the package schema.
Risky customizations:
- Removing required columns.
- Renaming tables without also updating the full schema contract.
- Replacing relationship methods with incompatible behavior.
- Overriding deletion hooks without preserving package cleanup.
Custom model example:
namespace App\Models;
use Wirechat\Wirechat\Enums\ConversationType;
use Wirechat\Wirechat\Models\Conversation as BaseConversation;
class WirechatConversation extends BaseConversation
{
protected $casts = [
'type' => ConversationType::class,
'updated_at' => 'datetime',
'disappearing_started_at' => 'datetime',
'archived_at' => 'datetime',
];
public function supportTicket()
{
return $this->belongsTo(\App\Models\SupportTicket::class, 'ticket_id');
}
}
Then register it:
'models' => [
'conversation' => \App\Models\WirechatConversation::class,
],
You do not need a dedicated App\Models\Wirechat namespace.
A regular class in App\Models works fine as long as it extends the matching Wirechat base model.
#Conversation
Conversation is the top-level thread model.
Every private chat, self chat, and group chat starts here.
This record coordinates participants, messages, disappearing-message state, and conversation-level visibility rules.
Core fields:
| Field | Purpose |
|---|---|
id |
Primary key for the thread. |
type |
Conversation type enum: private, self, or group. |
disappearing_started_at |
When disappearing mode was enabled. |
disappearing_duration |
Disappearing duration in seconds. |
created_at |
Thread creation timestamp. |
updated_at |
Latest activity timestamp used by unread and delete or clear logic. |
#Important Relationships
participants(): HasMany
Returns the membership rows for everyone in the conversation.
$conversation->participants()->with('participantable')->get()
messages(): HasMany
Returns the messages that belong to the conversation.
$conversation->messages()->latest()->get()
lastMessage(): HasOne
Returns the latest message for previews and list ordering.
$conversation->lastMessage
group(): HasOne
Returns the group metadata row when the conversation is a group thread.
$conversation->group
attachments(): Builder
Builds an attachment query for all message attachments in the conversation.
$conversation->attachments()->latest()->get()
actions(): MorphMany
Returns actions recorded directly against the conversation model.
$conversation->actions()->latest()->get()
#Important Helpers
participant(Model|Authenticatable $user, bool $withoutGlobalScopes = false): ?Participant
Resolves the participant row for a given app model.
$conversation->participant(auth()->user())
addParticipant(Model $user, ParticipantRole $role = ParticipantRole::PARTICIPANT, bool $undoAdminRemovalAction = false): Participant
Adds a participant while enforcing private, self, and admin-removal rules.
$conversation->addParticipant($user)
peerParticipant(Model|Authenticatable $reference): ?Participant
Resolves the other participant in a private conversation.
$conversation->peerParticipant(auth()->user())
peerParticipants(Model $reference): Collection
Returns every participant except the given reference model.
$conversation->peerParticipants(auth()->user())
getReceiver()
Resolves the other side of a private conversation for UI usage. This helper depends on an authenticated user being available internally, so it is best used inside auth-aware UI flows. When you already have the current user model, prefer peerParticipant() because it accepts the current user explicitly.
$peerParticipant = $conversation->peerParticipant(auth()->user())
markAsRead(?Model $user = null): void
Updates conversation_read_at for the given user or the authenticated user.
$conversation->markAsRead()
readBy(Model|Participant $user): bool
Checks whether the conversation is fully read for a participant.
$conversation->readBy(auth()->user())
getUnreadCountFor(Model $model): int
Counts unread messages for a given participantable model.
$conversation->getUnreadCountFor(auth()->user())
hasDisappearingTurnedOn(): bool
Returns true when disappearing mode is active.
$conversation->hasDisappearingTurnedOn()
turnOnDisappearing(int $durationInSeconds): void
Enables disappearing messages and currently requires at least one hour.
$conversation->turnOnDisappearing(86400)
turnOffDisappearing(): void
Disables disappearing messages.
$conversation->turnOffDisappearing()
deleteFor(Model|Authenticatable $user): ?bool
Deletes the conversation for one participant and may fully delete the record when package rules allow.
$conversation->deleteFor(auth()->user())
hasBeenDeletedBy(Model|Authenticatable $user): bool
Checks whether delete-for-user is still active for a participant.
$conversation->hasBeenDeletedBy(auth()->user())
clearFor(Model|Authenticatable $user): void
Clears conversation history for one participant without fully deleting the thread.
$conversation->clearFor(auth()->user())
isPrivate(): bool
Returns true when the conversation is a private conversation between two different users.
$conversation->isPrivate()
isSelf(): bool
Returns true when the conversation is a self conversation for a single user.
$conversation->isSelf()
isGroup(): bool
Returns true when the conversation is a group conversation.
$conversation->isGroup()
isOwner(Model|Authenticatable $model): bool
Checks whether the given user is the owner of the conversation. In group conversations this is the group owner. In private conversations both participants are effectively owners, so this can return true for either side.
$conversation->isOwner(auth()->user())
isAdmin(Model|Authenticatable $model): bool
Checks whether the given user has admin-level access in the conversation. In groups this returns true for admins and for the owner, because the owner is also treated as an admin.
$conversation->isAdmin(auth()->user())
#Important Query Helpers
whereParticipantable(Model $participantable)
Filters conversations that contain a specific model as participant.
Conversation::query()->whereParticipantable(auth()->user())->get()
whereHasParticipant($userId, $userType)
Filters conversations by raw participantable id and morph type.
Conversation::query()->whereHasParticipant($user->getKey(), $user->getMorphClass())->get()
withoutBlanks()
Hides conversations with no visible messages for the authenticated user.
Conversation::query()->withoutBlanks()->get()
withoutCleared()
Hides conversations cleared by the authenticated participant until new activity appears.
Conversation::query()->withoutCleared()->get()
withoutDeleted()
Hides conversations deleted by the authenticated participant until new activity appears.
Conversation::query()->withoutDeleted()->get()
withDeleted()
Includes conversations even if the authenticated participant has deleted them.
Conversation::query()->withDeleted()->get()
Legacy helpers receiverParticipant() and authParticipant() still exist, but new code should prefer peerParticipant() and participant().
Typical safe customizations:
- Add app-specific relations such as tickets, orders, or inbox metadata.
- Add accessors for labels or badges.
- Add custom scopes for conversation lists.
- Keep
participants()andmessages()intact because most of Wirechat depends on them.
#Participant
Participant is the membership pivot model between a conversation and one of your application's models.
This is the model that makes Wirechat polymorphic.
It is one of the most important models to keep compatible because message visibility, role checks, and delete or clear behavior all depend on it.
Core fields:
| Field | Purpose |
|---|---|
conversation_id |
The thread this membership belongs to. |
participantable_id |
The underlying app model id. |
participantable_type |
The morph class for the underlying app model. |
role |
Participant role enum, including owner and admin. |
exited_at |
Timestamp used to mark members who left a conversation. |
last_active_at |
Participant activity timestamp. |
conversation_read_at |
Last read marker for unread logic. |
conversation_cleared_at |
Last clear-history marker. |
conversation_deleted_at |
Last delete-for-user marker. |
#Important Relationships
participantable(): MorphTo
Returns the real app model behind the participant row.
$participant->participantable
conversation(): BelongsTo
Returns the parent conversation.
$participant->conversation
messages(): HasMany
Returns messages sent by this participant.
$participant->messages()->latest()->get()
latestMessage(): HasOne
Resolves the most recent message sent by this participant.
$participant->latestMessage
actions(): MorphMany
Returns actions recorded against this participant row.
$participant->actions()->latest()->get()
performedActions(): MorphMany
Returns actions this participant performed as an actor.
$participant->performedActions()->latest()->get()
#Important Helpers
isAdmin(): bool
Returns true when the participant is an admin or the owner. The owner is also treated as an admin by Wirechat.
$participant->isAdmin()
isOwner(): bool
Returns true only when this participant is the conversation owner.
$participant->isOwner()
exitConversation(): bool
Marks the participant as exited while enforcing package rules such as blocking owners from leaving their own group.
$participant->exitConversation()
hasExited(): bool
Checks whether the participant already left the conversation.
$participant->hasExited()
isRemovedByAdmin(): bool
Checks whether an admin-removal action exists for this participant.
$participant->isRemovedByAdmin()
removeByAdmin(Model|Authenticatable $admin): void
Records the admin-removal action using the admin's participant row as the actor, then downgrades the removed member's role.
$participant->removeByAdmin(auth()->user())
hasDeletedConversation(bool $checkDeletionExpired = false): bool
Checks whether delete-for-user is active, optionally comparing it against the conversation's latest update timestamp.
$participant->hasDeletedConversation(true)
#Important Query Helpers
whereParticipantable(Model|Authenticatable $model)
Filters participant rows for a specific app model.
Participant::query()->whereParticipantable(auth()->user())->first()
withExited()
Includes participants hidden by the default exited scope.
Participant::query()->withExited()->get()
withoutParticipantable(Model|Authenticatable $user)
Excludes a specific participantable model from the query.
$conversation->participants()->withoutParticipantable(auth()->user())->get()
Typical safe customizations:
- Add participant-level profile helpers.
- Add app-specific membership metadata.
- Add convenience methods that build on role and visibility state.
- Preserve the default global scopes unless you are intentionally replacing the membership rules.
#Message
Message represents an individual chat item inside a conversation.
Wirechat now treats participant_id as the source of truth for ownership and sender identity, so this model should stay aligned with the participant layer.
Core fields:
| Field | Purpose |
|---|---|
conversation_id |
Parent conversation id. |
participant_id |
Sender participant id. |
reply_id |
Parent message reference for threaded replies. |
body |
Message text or link content. |
type |
Message type enum such as text, link, or attachment. |
kept_at |
Timestamp used to preserve a disappearing message. |
deleted_at |
Soft-delete timestamp for delete-for-everyone flows. |
Wirechat will automatically keep the type aligned with the message body.
If the body contains a recognized URL, the message is marked as link. If the body no longer contains a URL,
the type falls back to text. Attachment messages remain attachment regardless of body changes.
#Important Relationships
conversation(): BelongsTo
Returns the parent conversation.
$message->conversation
participant(): BelongsTo
Returns the sender participant row.
$message->participant
attachment(): MorphOne
Returns the attachment model when the message is an attachment message.
$message->attachment
parent(): BelongsTo
Returns the message this message replies to.
$message->parent
reply(): HasOne
Returns a reply to this message when one exists.
$message->reply
actions(): MorphMany
Returns actions recorded against the message, including delete-for-user actions.
$message->actions()->latest()->get()
#Important Accessors And Helpers
$message->user
Returns the underlying app model behind the sender participant.
$sender = $message->user
$message->sendable
Legacy accessor alias for $message->user. New code should prefer $message->user.
$message->sendable
$message->sendable_id
Legacy accessor for the underlying participantable id. New code should usually work through $message->participant or $message->user instead.
$message->sendable_id
$message->sendable_type
Legacy accessor for the underlying participantable morph type. New code should usually work through $message->participant or $message->user instead.
$message->sendable_type
$message->resolved_link
Normalizes the message body into a full URL when it is a standalone link. Returns null for mixed-content messages.
$url = $message->resolved_link
hasAttachment(): bool
Checks whether the message currently has an attachment record.
$message->hasAttachment()
isAttachment(): bool
Checks whether the message type is attachment.
$message->isAttachment()
isLink(): bool
Checks whether the message type is link. This is set automatically when the message body contains a URL.
$message->isLink()
readBy(Model|Participant $user): bool
Checks whether the message is effectively read for a participant or app model.
$message->readBy(auth()->user())
ownedBy(Model $user): bool
Checks whether the given user owns the message by comparing against the sender participant.
$message->ownedBy(auth()->user())
belongsToAuth(): bool
Checks whether the currently authenticated user owns the message.
$message->belongsToAuth()
hasReply(): bool
Checks whether this message already has a reply.
$message->hasReply()
hasParent(): bool
Checks whether this message is itself a reply to another message.
$message->hasParent()
deleteFor(Model|Authenticatable $user): ?bool
Deletes the message only for one participant and may fully delete it once package rules are satisfied.
$message->deleteFor(auth()->user())
deleteForEveryone(Model $user): void
Deletes the message globally when the actor owns it or when a group admin is allowed to do it.
$message->deleteForEveryone(auth()->user())
isEmoji(): bool
Checks whether the message body contains only emoji characters.
$message->isEmoji()
#Important Query Helpers
whereIsNotOwnedBy(Model|Authenticatable $user)
Filters out messages owned by the given participantable model.
$conversation->messages()->whereIsNotOwnedBy(auth()->user())->get()
#Group
Group stores the metadata for group conversations.
The actual thread still lives in Conversation; Group adds group-specific information on top of it.
This model is a good place to add app-specific room metadata, as long as you keep the package permission flags intact.
Core fields:
| Field | Purpose |
|---|---|
conversation_id |
Parent conversation id for the group thread. |
name |
Group name. |
description |
Group description or about text. |
type |
Group type enum. |
allow_members_to_send_messages |
Permission flag for regular members. |
allow_members_to_add_others |
Permission flag for inviting others. |
allow_members_to_edit_group_info |
Permission flag for editing name and description. |
admins_must_approve_new_members |
Approval flag used during join flows. |
#Important Relationships
conversation(): BelongsTo
Returns the parent conversation.
$group->conversation
cover(): MorphOne
Returns the cover attachment model when the group has one.
$group->cover
#Important Accessors And Helpers
$group->cover_url
Returns the resolved URL for the cover attachment when it exists.
$group->cover_url
isOwnedBy(Model|Authenticatable $user): bool
Checks whether the given app model is the owner of the group conversation.
$group->isOwnedBy(auth()->user())
allowsMembersToSendMessages(): bool
Checks whether regular members are allowed to send messages in the group.
$group->allowsMembersToSendMessages()
allowsMembersToAddOthers(): bool
Checks whether regular members are allowed to invite other users.
$group->allowsMembersToAddOthers()
allowsMembersToEditGroupInfo(): bool
Checks whether regular members can update the group name or description.
$group->allowsMembersToEditGroupInfo()
$group->admins_must_approve_new_members
This is a stored flag rather than a helper method and is useful when building join-request or approval flows.
$group->admins_must_approve_new_members
Typical safe customizations:
- Add organization or workspace relations.
- Add visual metadata such as themes, tags, or external ids.
- Add read-only accessors for display data.
- Preserve the permission flags and cover-cleanup behavior.
#Attachment
Attachment stores uploaded file metadata for attachable records.
In practice, this is commonly used for message attachments and group covers.
This model is tightly coupled to storage config, URL generation, and file cleanup.
Core fields:
| Field | Purpose |
|---|---|
attachable_id |
Parent model id. |
attachable_type |
Parent model morph class. |
file_path |
Storage path on the configured disk. |
file_name |
Stored file name. |
original_name |
Original uploaded file name. |
mime_type |
Stored MIME type. |
url |
Computed URL accessor based on storage config. |
#Important Relationships
attachable(): MorphTo
Returns the model this attachment belongs to, usually a Message or Group.
$attachment->attachable
#Important Accessors
$attachment->url
Returns a public URL or a temporary private URL depending on the global Wirechat storage configuration.
$attachment->url
$attachment->size
Returns the file size in bytes when the file exists on disk.
$attachment->size
$attachment->formatted_size
Returns the file size in a human-readable format.
$attachment->formatted_size
$attachment->extension
Returns an extension-like value derived from the MIME type.
$attachment->extension
$attachment->clean_mime_type
Returns the MIME subtype, such as png, pdf, or zip.
$attachment->clean_mime_type
If you customize this model, preserve the filesystem cleanup behavior unless you are intentionally replacing it with your own storage lifecycle.
#Action
Action is Wirechat's audit-style polymorphic action model.
It is used for package-level state changes such as deletions and admin removals.
Core fields:
| Field | Purpose |
|---|---|
actionable_id |
Target model id. |
actionable_type |
Target model morph class. |
actor_id |
Actor model id. |
actor_type |
Actor model morph class. |
type |
Action enum such as delete or removed-by-admin. |
data |
Optional extra payload for the action. |
#Important Relationships
actionable(): MorphTo
Returns the model the action was performed on.
$action->actionable
actor(): MorphTo
Returns the model that performed the action. In package flows this is often a Participant.
$action->actor
#Important Query Helpers
whereActor(Model $actor)
Filters actions created by a specific actor.
Action::query()->whereActor($participant)->get()
withoutActor(Model $model)
Excludes actions created by a specific actor.
Action::query()->withoutActor($participant)->get()
Common package uses:
- Message delete-for-user actions.
- Participant removed-by-admin actions.
- Other audit-style state changes attached to conversations, messages, or participants.
If you add custom action types in your app, extending this model is a clean place to centralize action formatting or app-specific helpers.
#Security And Consistency
The goal of configurable models is to give your team flexibility without forcing a fork of the package. Wirechat still protects the integration by requiring each configured model to extend the matching base model.
That gives you a strong default contract:
- Package queries, jobs, scopes, and relationships continue to work.
- Core authorization and visibility logic stays in place unless you intentionally override it.
- Your team can focus on domain-specific behavior instead of rebuilding the package internals.
If your developers only need custom labels, extra relations, metadata, or app-specific helper methods, subclassing the default models is the safest path.