Models & queries
Query scopes
use RoundlyConsulting\Messages\Models\Message;
use RoundlyConsulting\Messages\Models\Participant;
use RoundlyConsulting\Messages\Models\Thread;
Thread::query()->direct();
Thread::query()->forParticipant($user);
Thread::query()->between($alice, $bob);
Thread::query()->inboxFor($user);
Thread::query()->visibleTo($user);
Message::query()->unreadFor($user);
Participant::query()->unread(); // never marked the thread read
Participant::query()->readUpTo($message); // read pointer at or past $message- direct() — direct threads only.
- forParticipant($model) — the model’s threads, newest activity first.
- between($a, $b) — the direct thread of exactly these two models.
- inboxFor($model) — the inbox query with eager loads and unread_count.
- visibleTo(?$model) — public threads plus the model’s own; public only when null.
- unreadFor($model) — messages the model has not read, excluding its own.
- Participant::unread() — participants who have never read their thread.
- Participant::readUpTo($message) — participants whose read pointer is at or past the message (what seenBy() runs).
Relations and attributes
// Thread
$thread->participants; // HasMany<Participant>
$thread->messages; // HasMany<Message>
$thread->latestMessage; // BelongsTo<Message>, via last_message_id
$thread->notifiableParticipants($sender); // Notifiable participant models, minus the sender
$thread->last_activity_at; // touched on every send, join and leave
// Message
$message->message; // the body column
$message->type; // MessageType::Text|System
$message->meta; // ?array — quote snapshot, system params, your own keys
$message->thread; // BelongsTo<Thread>
$message->sender; // MorphTo — null for system messages
$message->parent; // BelongsTo<Message> — the quoted message
$message->replies; // HasMany<Message>
$message->preview(); // truncated to preview.length, type-aware
// Participant
$participant->participant; // MorphTo — the User, Company, …
$participant->thread; // BelongsTo<Thread>
$participant->role; // ?ParticipantRole
$participant->last_read_message_id; // the read pointer — the message they had reached
$participant->read_at; // ?Carbon — when they last readAll three models use soft deletes and live in messaging_threads, messaging_messages and messaging_participants.
Swapping a model
The models are not final — extend one and point its models.* key at your subclass:
use RoundlyConsulting\Messages\Models\Message;
class ChatMessage extends Message
{
// your own casts, relations and methods
}
// config/messages.php
'models' => [
'message' => App\Models\ChatMessage::class,
'thread' => RoundlyConsulting\Messages\Models\Thread::class,
'participant' => RoundlyConsulting\Messages\Models\Participant::class,
],The configured classes are used at every call site — relations, actions, scopes, the manager and the prune command. Absent or blank config resolves the packaged model; any other value must be that model or a subclass of it, or it throws InvalidConfigurationException naming the key — a foreign class is never silently replaced.
Paginated listings
The facade paginates threads and a thread’s messages with the right eager loads and a deterministic order:
use RoundlyConsulting\Messages\Facades\Messages;
Messages::threads($user, perPage: 20); // $user's threads + every public thread
Messages::threads(); // public threads only
Messages::thread($thread)->messages(perPage: 25); // newest first, senders eager loadedthreads() lists newest activity first with the latest message and its sender eager-loaded; it runs the visibleTo() scope. Both default to 15 per page and accept page and pageName.
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.