Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Message Bundle Laravel Package

ornicar/message-bundle

ornicar/message-bundle is a lightweight Symfony bundle for adding private user-to-user messaging to an application. It provides Doctrine-backed message and conversation entities plus integration points for inbox-style communication, making it easy to wire basic messaging into existing Symfony projec...

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: you are not installing ornicar/message-bundle in a Laravel app. It is a Symfony bundle, it is explicitly discontinued (the author redirects to FriendsOfSymfony/FOSMessageBundle), and it assumes Symfony's DI container, event dispatcher, Doctrine ORM 2.x with annotations, and Symfony\Component\Security\Core\User\UserInterface. There is no service provider, no facade, no config/message.php. Your "getting started" is therefore a 30–90 minute reference read, not a composer require.

Where to look first (in the repo, not the README):

  • Entity/Message.php, Entity/Thread.php, Entity/Participant.php, Entity/MessageMetadata.php — the actual schema. This is the bundle's only durable value.
  • DependencyInjection/Configuration.php — the full config surface; this is your de-facto documentation.
  • Resources/config/*.xml — the service graph (message.composer, message.thread_manager, message.repository.*). These three service IDs carry ~90% of real usage in any Symfony app that has it.
  • MessageComposer.php and ThreadManager.php — the two interfaces worth copying signatures from.

First use case to reproduce in Laravel (the smallest meaningful slice):

// app/Actions/Messages/SendMessage.php
final class SendMessage
{
    public function __construct(
        private ThreadRepository $threads,
        private MessageRepository $messages,
        private Dispatcher $events,
    ) {}

    public function handle(User $from, User $to, string $subject, string $body): Thread
    {
        $thread = $this->threads->findOrCreateBetween($from, $to); // wraps in transaction
        $message = $this->messages->create($thread, $from, $subject, $body);
        $this->events->dispatch(new MessageSent($message));

        return $thread;
    }
}

If you cannot get "open a thread between A and B, append a message, return the thread" working against your own threads / messages / participants tables, stop — the bundle's model is already telling you something is wrong with your framing.

Migration/port checklist before you write code:

  1. Confirm you are not on a legacy Symfony 2.x app (in which case: pin, do not port, and schedule an FOSMessageBundle upgrade).
  2. If Laravel: decide whether you need true threading or just Laravel's built-in notifications + a messages table. Most "inbox" asks do not need threads.
  3. If you do need threads: copy the entity relationships (three tables + a read-state table) and delete the bundle reference from your design doc.

Implementation Patterns

The domain model in one sentence: a Thread groups Messages; Participants link users to threads with per-participant lastSeenAt; MessageMetadata tracks per-participant read/deleted state per message. Almost every bug in a messaging feature is a metadata state mismatch or an orphaned participant — keep this map on a sticky note.

Pattern 1 — Isolate the port behind interfaces. Everything your app depends on should be your namespace; the "bundle-shaped" adapter lives only in one folder. This is the single highest-value pattern because you may later swap to FOSMessageBundle semantics or a maintained alternative.

namespace App\Contracts;

interface MessageComposer
{
    public function send(User $from, Thread $thread, string $subject, string $body): Message;
}

interface ThreadRepository
{
    public function findOrCreateBetween(User $a, User $b): Thread;
    public function inboxFor(User $user, int $perPage = 25): LengthAwarePaginator;
    public function sentBy(User $user, int $perPage = 25): LengthAwarePaginator;
}

Pattern 2 — Inbox queries belong in scopes, not controllers. Model the bundle's getParticipantInboxThreads as a query scope so unread counts are computed in one place:

// app/Models/Thread.php
public function scopeInboxFor(Builder $q, User $user): Builder
{
    return $q->whereHas('participants', fn ($p) => $p->where('user_id', $user->id))
             ->withCount(['messages as unread_count' => function ($m) use ($user) {
                 $m->whereDoesntHave('metadata', function ($md) use ($user) {
                     $md->where('user_id', $user->id)->whereNotNull('read_at');
                 });
             }])
             ->latest('updated_at');
}

Pattern 3 — Read state is a controller action, not middleware. The bundle mutates MessageMetadata when a participant views a thread. If you put that in middleware or an updated_at hook, background badge polls mark everything read. Do it explicitly on the conversation view:

public function show(Thread $thread, MarkThreadRead $markRead)
{
    $this->authorize('view', $thread);
    $markRead->handle($thread, auth()->user());

    return view('messages.show', ['thread' => $thread->load('messages.sender')]);
}

Pattern 4 — Events on write, listeners for fan-out. The bundle dispatches events on send; reproduce them as Laravel events so mail/notifications/pusher wiring never leaks into the send action:

Event::listen(MessageSent::class, function (MessageSent $e) {
    Notification::send($e->message->thread->otherParticipants($e->message->sender), new NewMessage($e->message));
});

This gives you a seam for adding real-time delivery later without rewriting the composer.

Pattern 5 — findOrCreateBetween must be transactional and race-safe. The bundle's thread manager is trivial; when you port it, wrap it and add a uniqueness constraint on the participant pair. Two concurrent "reply" clicks otherwise create two threads:

public function findOrCreateBetween(User $a, User $b): Thread
{
    return DB::transaction(function () use ($a, $b) {
        [$x, $y] = collect([$a->id, $b->id])->sort()->values()->all();

        return Thread::firstOrCreate(
            ['user_low_id' => $x, 'user_high_id' => $y], // unique index on (user_low_id, user_high_id)
        );
    });
}

Pattern 6 — Soft-deletes on both message and metadata. The bundle's MessageMetadata carries a deleted flag per participant, not a deletion of the message row. Copy this. "Delete for me" and "delete for everyone" are different product features and users will file bugs about both within a week of launch.

Gotchas and Tips

  • This is not a Laravel package and will not install cleanly. Modern Laravel 10/11 on PHP 8.2+ will hit Composer constraint conflicts against symfony/symfony ~2.x. If you are tempted to composer require it to "just look at it," don't — clone the repo into /tmp and read the entities.
  • Do not require it. Ever. Discontinued, ~1 star, 0 dependents, no security response channel. Adding it to composer.json is a supply-chain decision, not a feature decision. The correct adoption footprint is zero.
  • The README lies by omission. It describes a friendly API and Twig helpers, which exist, but the README.md does not document config keys, service IDs, or the metadata table's role. All of that lives in DependencyInjection/Configuration.php and Resources/config/*.xml. Treat the README as marketing copy written at first release and never updated.
  • ParticipantInterface is the hidden coupling. The bundle assumes your User entity implements it. When porting, resist the urge to match this — prefer a participants table with user_id so a user can participate in many threads with independent state (read cursor, muted flag). Modeling "user == participant" is a dead end.
  • Debugging ported read state. If "unread" badges are wrong, check in this order: (1) is lastSeenAt being updated by the mark-as-read action and not by rendering, (2) is MessageMetadata being created on send for the recipient as well as the sender, (3) does your unread count exclude the sender's own messages. The bundle creates metadata lazily and this is the #1 source of confusion for anyone reading it as a spec.
  • Config quirks worth reading anyway even though you won't install it: the bundle's config exposes user class, message class, and thread class overrides. This is why the pattern of "interface + configurable implementation" is worth copying into your own config/messages.php.
  • Extension points that matter for a port: the message.composer, message.thread_manager, and message.repository.* services are all interface-backed. That is the one design lesson worth carrying forward. Do the same in Laravel: bind MessageComposer::class to EloquentMessageComposer::class in a small MessagingServiceProvider and call interfaces from controllers.
  • Do not adopt FOSMessageBundle in a Laravel app either just because it is the successor. It is still Symfony-only. If you must have a package, gate it on: commits in the last 12 months, an active security channel, declared Laravel 10/11 support, and at least a few real dependents. This bundle scores zero on all four; so will most messaging packages you find by Googling "Symfony message bundle" out of a 2013 Stack Overflow thread.
  • Data retention is not in the box. The bundle has no purge command, no GDPR export, no soft-delete cascade. If you are building anything user-to-user, decide retention and hard-delete rules before the schema, because retrofitting ON DELETE CASCADE against a live messages table is a maintenance window you will not enjoy.
  • What to record in your decision log: "messaging: build small in-house using threads/participants/messages/message_metadata schema inspired by ornicar/message-bundle; options considered and why rejected (discontinued, Symfony-only, no security channel)." This prevents the next search from reopening the same question.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core