Download the PHP package syriable/laravel-messenger without Composer
On this page you can find all versions of the php package syriable/laravel-messenger. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download syriable/laravel-messenger
More information about syriable/laravel-messenger
Files in syriable/laravel-messenger
Package laravel-messenger
Short Description Laravel messenger engine for modern apps
License MIT
Homepage https://github.com/syriable/laravel-messenger
Informations about the package laravel-messenger
Laravel Messenger
A headless, backend-only one-to-one messaging domain platform for Laravel. Think Facebook Messenger / Instagram DMs / WhatsApp direct messages β not support tickets, channels or forums.
It is Laravel-native, event-driven, performance-oriented and extensible by composition. It ships no UI, controllers, routes, policies or assets β your application owns presentation and authorization; the package owns the messaging domain.
Features
- π¬ One-to-one conversations β exactly one persistent conversation between any two participants, created lazily on the first message.
- π€ Morphable participants β users, admins, sellers, support agentsβ¦ any Eloquent model.
- π Attachments β first-class upload lifecycle, storage, validation and metadata (images, PDFs, zips). No external media packages.
- β©οΈ Lightweight replies β WhatsApp-style message references, never threads.
- π₯ Inbox & unread tracking β denormalized counters and activity ordering for fast, N+1-free reads.
- ποΈ Per-participant state β archive, star, block, spam, clear β all participant-specific; the conversation stays neutral.
- π§Ή Clear without deleting β a visibility reset; history reappears when a new message arrives.
- π‘οΈ Block / spam β mutual: while in place neither side can send, history is preserved.
- π© Message reporting β report specific messages.
- π‘ Optional realtime β event-driven broadcasting (Reverb / Pusher / Echo). Works fully without it.
- π§© Composable send pipeline β plug in your own validation, filtering and moderation.
Installation
Publishing the migrations is required β the package ships them as customisable stubs and does not run them automatically. The quickest path is the bundled installer, which publishes the stubs (and optionally the config) and can run the migration in one step:
Or do it manually β publish, then migrate:
Optionally publish the config file:
Need to detect a missing-migration state at boot in your host app? Call
Syriable\Messenger\Commands\InstallCommand::tablesExist().The migrations use microsecond-precision timestamps (
timestamp(6)) so the clear/visibility boundary stays correct when events share a wall-clock second. If you publish a fresh copy over an older install, re-check those columns.
Setup
Add the Messageable trait and MessengerParticipant contract to any model that can take part in a conversation:
Participants are morphable, so different model types can message each other (e.g. a Buyer and a SupportAgent).
Register a morph map (recommended for production)
Participant identity is stored as the model's getMorphClass() β by default the
fully-qualified class name (App\Models\User). If you later rename or move that
class, every stored participant_type becomes stale and the participant's
conversations silently disappear from their inbox. Register a morph map
before you run the first migration so the database stores a stable alias instead
of the raw class name:
With the map in place, participant_type stores 'user' instead of
'App\Models\User', making your data portable across class renames. If you adopt
a morph map on an existing install, migrate the stored participant_type
(and sender_type) values to the new aliases in the same deployment.
Usage
Sending messages
A conversation is created automatically on the first message β conversations are never empty.
A valid message must contain a body, at least one attachment, or both.
A reply_to reference must point to an existing message in the same conversation that is still visible to the sender (i.e. created after the sender's clear timestamp). A reply on a brand-new conversation, to a message from another conversation, or to a message the sender has cleared is rejected with InvalidReplyException. Empty (zero-byte) and over-limit attachments are rejected by the send pipeline; oversized original filenames are truncated to fit storage.
Reading the inbox & messages
Cursors are keyset (not offset) and exclude the cursor message itself, so
they stay correct as new messages arrive and never re-scan skipped rows. The
result is always returned in chronological order regardless of direction, and a
cursor that does not belong to the conversation throws InvalidArgumentException.
Inbox N+1.
inbox()is N+1-free for the package's own relations, but it does not load the polymorphic model behind each participant unless you ask it to. If you render participant names or avatars, pass['with_participant_models' => true]so the Users are loaded in one grouped query β otherwise resolvingotherParticipantFor($alice)->participantlazily issues one query per conversation.
Conversation state (per participant)
Reporting a message
Handling domain exceptions in the host application
The package is headless: when a messaging rule is violated it throws a typed
domain exception and never converts it to an HTTP response, a
ValidationException, or a flash message. Translating these into your UI/API is
the host application's job. Every package exception extends a single base class,
Syriable\Messenger\Exceptions\MessengerException (which extends
RuntimeException), so you can catch them all in one place or handle subclasses
individually.
| Exception | Thrown when | Suggested mapping |
|---|---|---|
ConversationBlockedException |
Sending into a conversation either side has blocked or marked as spam | 403 / inline notice |
InvalidMessageException |
The message has no body and no attachments, or the body exceeds max_body_length |
422 |
InvalidAttachmentException |
An attachment is empty, too large, over the per-message count, or a disallowed type/mime | 422 |
InvalidReplyException |
reply_to points outside the conversation or to a message the sender has cleared |
422 |
InvalidParticipantException |
The actor is not a member of the conversation, or a participant does not exist (with the optional existence guard) | 403 / 404 |
InvalidReportException |
A report's reason/note exceeds its limit, or (with the optional guard) the reporter is not a participant | 422 |
Prefer an explicit redirect target over
back().back()relies on theRefererheader; API clients, Inertia/Livewire flows that strip it, and direct POSTs fall back to/, silently dropping the error flash. Redirect to a named route (the conversation view) so the error is always rendered. The same mapping applies in API controllers (return a JSON error) and Livewire/Inertia layers.Duplicate submissions are a host responsibility. The package has no idempotency guard by design β calling
send()twice with the same body stores two messages. Prevent double-submits in your UI (disable the button on submit, debounce, or carry a request id you de-duplicate on) just as you would for any form POST.
Events
Every lifecycle operation dispatches an immutable, past-tense domain event you can listen to:
MessageSent, ConversationCreated, ConversationArchived / ConversationUnarchived, ConversationStarred / ConversationUnstarred, ConversationBlocked / ConversationUnblocked, ConversationMarkedAsSpam / ConversationUnmarkedAsSpam, ConversationCleared, ConversationRead, ConversationMarkedAsUnread, MessageReported.
Realtime broadcasting
Broadcasting is optional and event-driven β it is never coupled into the actions. It is disabled by default; turn it on by setting MESSENGER_BROADCASTING_ENABLED=true. The published configuration defaults:
When enabled, a MessageSentBroadcast is broadcast on messenger.conversation.{id} (as message.sent). Listen with Laravel Echo:
Private channels require a channel authorization callback in your host application. Without one, Echo subscriptions to private channels will fail with a 403:
If you set
private => falsein the config, messages broadcast on a public channel with no access control β anyone who knows a conversation ID can subscribe. Only use this in trusted internal environments.
The broadcast is a lightweight notification. It carries the message's core fields plus a metadata-only attachment summary β has_attachments and an attachments array of { id, name, mime_type, size } β so clients can render attachment-only or mixed messages without a follow-up request. It intentionally does not include file contents or URLs (those are disk/authorization concerns); load the message (e.g. Messenger::messages()) or override broadcastWith() if you need more.
Customizing the send pipeline
Messages pass through a composable, configurable pipeline before they are stored. Add your own moderation / filtering pipes:
A pipe implements Syriable\Messenger\Contracts\SendPipe:
The default pipes provide the package's core guarantees (valid participants, mutual block/spam, non-empty messages, attachment limits, valid replies). The pipeline is yours to customise, but removing a default pipe removes the guarantee it provides β e.g. dropping
EnsureMessageHasContentlets empty messages persist. Add pipes freely; only remove a default one when you intend to drop its check. Note thatEnsureAttachmentsAreValidvalidates client-reported type/size/count metadata by default. Setmessenger.attachments.verify_real_mimetotrueto additionally check the server-detected (content-sniffed) MIME against the allow-list, catching a payload renamed to a permitted extension. For deeper guarantees (virus scanning, archive-bomb checks), add your own pipe. Seedocs/ARCHITECTURE.md.
Authorization
The package is not responsible for business authorization (no policies, roles or ACL). Your application decides who may message whom. The package only enforces internal messaging constraints: blocked / spam conversations, participant membership and message validity.
Reads require participation. Conversation-scoped operations enforce membership: Messenger::messages($conversation, $viewer) (and the participant-state actions archive, clear, block, markAsRead, β¦) throw InvalidParticipantException when the viewer is not a participant β they do not return an empty result. Catch it and map to 403/404. Note that Messenger::between() resolves the conversation for any caller who knows the participant pair; only the membership-scoped operations enforce the check.
Consistent with this, message reporting is participant-only by default: Messenger::report() rejects a report from an identity that is not a member of the message's conversation (InvalidReportException). Set messenger.reports.participants_only to false to restore the unrestricted headless contract and authorise reporting in your application instead.
Two security guards ship on by default (set either to false to opt out):
messenger.validation.verify_participants_existβ the send pipeline rejects a sender/recipient that does not exist in the database (preventing "ghost" participants). Costs two indexed existence checks on first send.messenger.reports.participants_onlyβ participant-only reporting, as above.
Security notes
Because the package is headless and host-owned, a few responsibilities sit with your application:
- Attachment access.
$attachment->urlreturnsStorage::disk($disk)->url($path)with no signing or authorization. If you store attachments on a public disk, those URLs are world-readable. Use a private disk and either serve files through an authorized controller, or hand out a short-lived signed link with the bundled helper$attachment->temporaryUrl($minutes)(on a driver that supports temporary URLs, e.g. S3). The package never gates file access for you. - Mass assignment. Package models use
$guarded = []and are intended to be written only through the package's actions (Messenger::send(),report(), etc.), never filled directly from request input. Do not doMessage::create($request->all())or$participant->update($request->all())β that would let callers tamper with fields likeunread_count,blocked_atorsender_id. Treat the models as internal domain objects. - Blocked / spam conversations stay in the inbox. Blocking or marking spam prevents sending (mutually) but, per the v1 spec, keeps history visible and stored β so these conversations still appear in
Messenger::inbox(). Each returnedConversationexposes the participant'sblocked_at/spammed_atstate for your UI to badge, or pass['exclude_blocked' => true, 'exclude_spam' => true]to drop them from the result entirely. - Deleting participants is host-owned. The morphable design precludes database foreign keys, so deleting a host participant model does not cascade: their
messenger_participants, messages, attachments and reports remain, andmorphToaccessors like$message->senderthen resolve tonull. Treat those relations as nullable in your UI. When you delete an account, also remove its messenger rows.
Pruning attachment files
Messages are immutable and the package never hard-deletes, so when you delete messages/conversations yourself the underlying attachment files stay on disk. Reclaim them with the bundled command, which removes files under the configured attachments directory that no longer have a matching database row:
Or programmatically (returns the orphaned paths):
Pruning is explicit and opt-in β it never runs automatically β so it is safe against the immutability model.
Database & concurrency
The send path is built for parallel writes: the lazy first-message race recovers
by attaching to the winning conversation, block/spam is re-checked under a row
lock inside the transaction, the unread counter increments atomically in SQL, and
the write transaction is retried a bounded number of times on transient
concurrency errors (deadlock, lock-wait timeout, SQLite database is locked).
The suite runs on SQLite, MySQL 8 and PostgreSQL 16 in CI.
SQLite serialises all writers, so under heavy parallel write load it can still
raise database is locked faster than the retries absorb. For production with
meaningful concurrency, use MySQL or PostgreSQL. If you do run SQLite, enable
WAL and a busy timeout so the driver waits for the lock instead of failing
immediately:
Architecture
See docs/ARCHITECTURE.md for the full design: thin models, single-responsibility actions, read-only queries, DTOs, the send pipeline, domain events and the performance / denormalization strategy.
Testing
License
The MIT License (MIT). Please see License File for more information.
All versions of laravel-messenger with dependencies
spatie/laravel-package-tools Version ^1.16
illuminate/contracts Version ^11.0||^12.0||^13.0