Création d'une application de chat avec Laravel

Création d'une application de chat avec Laravel

Construire une application de chat avec Laravel, ce n’est pas seulement assembler quelques routes, une base de données et une interface jolie à regarder. C’est surtout créer une expérience vivante, fluide, presque invisible pour l’utilisateur, où les messages apparaissent au bon moment, les conversations restent lisibles, les statuts donnent un peu de contexte, et tout cela fonctionne avec la simplicité élégante que Laravel sait offrir. Quand on pense à une application de chat, on imagine souvent un produit très moderne, avec des messages instantanés, des notifications, des indicateurs de frappe, peut-être même des pièces jointes, des discussions privées et des salons de groupe. La bonne nouvelle, c’est qu’avec Laravel, ce type de projet devient beaucoup plus accessible qu’on ne le croit au départ. Le framework apporte une structure claire, des outils puissants pour l’authentification, les événements, le broadcasting, la gestion des files d’attente, l’ORM Eloquent, et tout ce qu’il faut pour construire quelque chose de propre sans se perdre dans la complexité.

Dans cet article, nous allons construire une application de chat pas à pas, avec une logique pensée pour un vrai projet de production, mais expliquée de manière claire et progressive. L’idée n’est pas seulement de montrer du code, mais de comprendre pourquoi chaque choix est utile. Nous allons partir d’une base Laravel moderne, ajouter l’authentification, créer les modèles essentiels, préparer les migrations, gérer les conversations et les messages, puis mettre en place l’actualisation en temps réel grâce au broadcasting. Nous verrons aussi comment afficher les messages côté interface et comment organiser le projet pour qu’il reste maintenable lorsque les fonctionnalités grandissent. À la fin, vous aurez une vision complète de la manière dont on peut construire une application de chat solide avec Laravel, sans tomber dans un assemblage fragile qui fonctionne à moitié.

Pourquoi choisir Laravel pour une application de chat

Laravel est un excellent choix pour une application de chat parce qu’il équilibre très bien productivité et lisibilité. Dans un projet de chat, on a généralement plusieurs préoccupations en même temps : l’authentification des utilisateurs, la gestion des conversations privées ou de groupe, la persistance des messages, la synchronisation en temps réel, la sécurité, et parfois les notifications. Beaucoup de frameworks peuvent faire cela, mais Laravel le fait avec une syntaxe expressive, une architecture rassurante, et un écosystème qui permet d’aller vite sans sacrifier la qualité. Eloquent simplifie énormément la gestion des relations entre utilisateurs, conversations et messages. Les événements et les listeners donnent une manière naturelle de diffuser un nouveau message vers les bons clients. Le système de files d’attente permet de déléguer certaines tâches lourdes sans bloquer l’expérience utilisateur. Et la communauté Laravel, très active, rend la recherche de solutions beaucoup plus simple au quotidien.

Un autre avantage important, c’est que Laravel permet de garder le backend propre et le frontend flexible. Vous pouvez bâtir votre interface avec Blade si vous voulez aller vite, ou bien combiner Laravel avec Livewire, Inertia, Vue, React, ou même Alpine pour une expérience plus dynamique. Pour une application de chat, cette liberté est précieuse, car chaque équipe a ses habitudes et ses contraintes. Certains projets ont besoin d’une interface légère et simple, d’autres veulent une expérience riche, avec recherche instantanée, présence en ligne, aperçu des messages non lus, et mises à jour en temps réel. Laravel s’adapte à ces besoins sans vous enfermer dans une seule manière de faire.

Ce que nous allons construire

Pour rendre les choses concrètes, imaginons une application de chat simple mais sérieuse. Chaque utilisateur pourra se connecter, voir ses conversations, ouvrir une discussion privée avec un autre utilisateur, envoyer des messages, recevoir les messages instantanément sans recharger la page, et consulter l’historique de ses échanges. Nous allons aussi prévoir une structure capable d’évoluer vers des salons de groupe, des pièces jointes et des notifications. Le but est de poser des fondations propres dès le début, parce qu’un chat mal structuré devient vite difficile à faire évoluer.

L’architecture générale sera la suivante : une table users pour les comptes, une table conversations pour représenter les discussions, une table pivot pour relier les participants à une conversation, et une table messages pour stocker les échanges. Côté application, un utilisateur se connecte, récupère la liste de ses conversations, ouvre une conversation, puis envoie un message. Au moment où le message est enregistré, un événement est déclenché et diffusé en temps réel aux participants concernés. L’interface écoute cet événement et ajoute immédiatement le nouveau message à l’écran. C’est simple sur le papier, mais très puissant dans les faits.

Préparer le projet Laravel

Commençons par créer le projet. On peut partir d’un projet Laravel classique, puis ajouter l’authentification avec un starter kit comme Breeze. L’objectif est d’obtenir rapidement un système d’inscription et de connexion, sans réinventer la roue.

composer create-project laravel/laravel chat-app
cd chat-app
php artisan serve

Ensuite, ajoutons l’authentification de base. Breeze est souvent un choix agréable pour ce genre de projet, surtout si l’on veut une base propre et facile à personnaliser.

composer require laravel/breeze --dev
php artisan breeze:install blade
npm install
npm run build
php artisan migrate

Avec cela, nous avons déjà une base fonctionnelle. Les utilisateurs peuvent s’inscrire, se connecter et accéder à une application sécurisée. À partir de là, nous pouvons nous concentrer sur le cœur du chat. L’intérêt de partir d’une base d’authentification standard, c’est qu’on gagne du temps et qu’on profite des bonnes pratiques du framework sans tout recoder à la main.

Concevoir les modèles de données

La réussite d’une application de chat dépend énormément de la qualité de son modèle de données. Une structure claire dès le départ évite les problèmes de cohérence plus tard. Pour un chat privé entre deux personnes, on pourrait être tenté de créer simplement une table de messages avec un expéditeur et un destinataire. Cela fonctionne au début, mais dès qu’on veut ajouter des groupes ou un historique de conversations, cette approche devient plus rigide. Il est donc plus intéressant de séparer la notion de conversation de la notion de message.

Nous allons donc créer trois structures principales : conversations, conversation_user et messages. La table conversations représente un fil de discussion. La table pivot conversation_user relie les utilisateurs à une conversation. La table messages enregistre chaque message et référence la conversation ainsi que l’auteur.

Migration des conversations

// database/migrations/2026_01_01_000001_create_conversations_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('conversations', function (Blueprint $table) {
            $table->id();
            $table->string('title')->nullable();
            $table->boolean('is_group')->default(false);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('conversations');
    }
};

Cette table est volontairement simple. Le champ title permet de gérer les groupes plus tard, et is_group aide à distinguer une conversation privée d’un groupe. Dans un chat réellement privé, on peut laisser le titre vide et générer l’affichage à partir des participants.

Migration de la table pivot

// database/migrations/2026_01_01_000002_create_conversation_user_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('conversation_user', function (Blueprint $table) {
            $table->id();
            $table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->timestamp('last_read_at')->nullable();
            $table->timestamps();

            $table->unique(['conversation_id', 'user_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('conversation_user');
    }
};

Le champ last_read_at est très utile. Il permet de savoir à quel moment un utilisateur a consulté la conversation, ce qui ouvre la porte aux messages non lus, aux badges de notification et à l’expérience de lecture plus avancée. Même si on ne l’exploite pas tout de suite, il est bon de le prévoir dès maintenant.

Migration des messages

// database/migrations/2026_01_01_000003_create_messages_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->id();
            $table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->text('body');
            $table->timestamp('read_at')->nullable();
            $table->timestamps();

            $table->index(['conversation_id', 'created_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('messages');
    }
};

Le champ body contient le contenu du message. Le champ read_at peut servir à marquer un message comme lu dans des scénarios plus avancés. L’index sur conversation_id et created_at est important, car on affichera souvent les messages d’une conversation dans l’ordre chronologique.

Une fois ces migrations prêtes, on peut les exécuter.

php artisan migrate

Créer les modèles Eloquent

Après la base de données, il faut définir les modèles qui vont rendre la manipulation des données naturelle dans Laravel. C’est ici que l’on ressent toute la force d’Eloquent. Au lieu d’écrire sans cesse des jointures manuelles, on décrit les relations et on laisse le framework faire le reste.

Modèle Conversation

// app/Models/Conversation.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Conversation extends Model
{
    protected $fillable = ['title', 'is_group'];

    protected $casts = [
        'is_group' => 'boolean',
    ];

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)
            ->withTimestamps()
            ->withPivot('last_read_at');
    }

    public function messages(): HasMany
    {
        return $this->hasMany(Message::class);
    }
}

Modèle Message

// app/Models/Message.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Message extends Model
{
    protected $fillable = [
        'conversation_id',
        'user_id',
        'body',
        'read_at',
    ];

    protected $casts = [
        'read_at' => 'datetime',
    ];

    public function conversation(): BelongsTo
    {
        return $this->belongsTo(Conversation::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Mise à jour du modèle User

// app/Models/User.php

public function conversations()
{
    return $this->belongsToMany(\App\Models\Conversation::class)
        ->withTimestamps()
        ->withPivot('last_read_at');
}

public function messages()
{
    return $this->hasMany(\App\Models\Message::class);
}

Ces relations sont la colonne vertébrale de l’application. Elles permettent de naviguer facilement entre les utilisateurs, leurs conversations et leurs messages. C’est exactement ce genre de structure qui rend Laravel agréable à utiliser dans des projets complexes.

Créer une conversation entre deux utilisateurs

Dans une application de chat privé, une étape essentielle consiste à créer une conversation entre deux personnes, ou à retrouver une conversation existante si elle est déjà là. Il est inutile de créer plusieurs discussions pour la même paire d’utilisateurs, sinon l’expérience devient confuse très rapidement. Il faut donc vérifier si une conversation commune existe avant d’en créer une nouvelle.

Voici un exemple de service simple pour gérer cette logique.

// app/Services/ConversationService.php

namespace App\Services;

use App\Models\Conversation;
use App\Models\User;
use Illuminate\Support\Collection;

class ConversationService
{
    public function getOrCreatePrivateConversation(User $userA, User $userB): Conversation
    {
        $conversation = Conversation::where('is_group', false)
            ->whereHas('users', function ($query) use ($userA) {
                $query->where('users.id', $userA->id);
            })
            ->whereHas('users', function ($query) use ($userB) {
                $query->where('users.id', $userB->id);
            })
            ->first();

        if ($conversation) {
            return $conversation;
        }

        $conversation = Conversation::create([
            'title' => null,
            'is_group' => false,
        ]);

        $conversation->users()->attach([$userA->id, $userB->id]);

        return $conversation;
    }
}

Cette logique peut paraître simple, mais elle évite déjà beaucoup de désordre. On centralise la création de conversation dans un service dédié, ce qui rend le code plus lisible et plus facile à tester. Dans un vrai projet, cette méthode pourrait être optimisée différemment, par exemple avec une table de correspondance canonique pour les conversations privées, mais l’idée générale reste la même : ne pas dupliquer inutilement les échanges.

Construire le contrôleur des messages

Le contrôleur va recevoir les messages envoyés par l’utilisateur, vérifier les droits d’accès, enregistrer le message dans la base de données, puis déclencher la diffusion en temps réel.

// app/Http/Controllers/MessageController.php

namespace App\Http\Controllers;

use App\Events\MessageSent;
use App\Models\Conversation;
use App\Models\Message;
use Illuminate\Http\Request;

class MessageController extends Controller
{
    public function store(Request $request, Conversation $conversation)
    {
        $request->validate([
            'body' => ['required', 'string', 'max:5000'],
        ]);

        $this->authorizeConversation($conversation);

        $message = Message::create([
            'conversation_id' => $conversation->id,
            'user_id' => auth()->id(),
            'body' => $request->body,
        ]);

        broadcast(new MessageSent($message->load('user')))->toOthers();

        return response()->json([
            'message' => $message->load('user'),
        ]);
    }

    protected function authorizeConversation(Conversation $conversation): void
    {
        abort_unless(
            $conversation->users()->where('users.id', auth()->id())->exists(),
            403
        );
    }
}

Ce contrôleur fait plusieurs choses importantes. D’abord, il valide le contenu du message afin d’éviter les données vides ou trop longues. Ensuite, il vérifie que l’utilisateur fait bien partie de la conversation. Enfin, il crée le message et le diffuse à tous les autres clients connectés à cette discussion. Ce flux est au cœur de tout chat moderne.

Déclencher un événement pour le temps réel

Laravel gère très bien les événements. Dans notre cas, nous voulons qu’un message nouveau soit envoyé à ceux qui participent à la conversation, sans qu’ils aient besoin de recharger la page. Pour cela, on crée un événement broadcastable.

// app/Events/MessageSent.php

namespace App\Events;

use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public Message $message;

    public function __construct(Message $message)
    {
        $this->message = $message;
    }

    public function broadcastOn(): array
    {
        return [
            new PresenceChannel('conversation.' . $this->message->conversation_id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    public function broadcastWith(): array
    {
        return [
            'id' => $this->message->id,
            'conversation_id' => $this->message->conversation_id,
            'user' => [
                'id' => $this->message->user->id,
                'name' => $this->message->user->name,
            ],
            'body' => $this->message->body,
            'created_at' => $this->message->created_at->toDateTimeString(),
        ];
    }
}

L’utilisation d’un PresenceChannel est intéressante lorsqu’on veut aussi connaître les participants présents dans la conversation. Cela ouvre la porte aux statuts en ligne, aux indicateurs de présence, et à une interface plus vivante. Si votre application n’a pas besoin de présence, un PrivateChannel peut aussi convenir. Ici, nous choisissons une approche qui laisse la place à des évolutions futures.

Configurer le broadcasting

Le broadcasting peut sembler intimidant au début, mais en pratique il devient très logique une fois qu’on comprend le principe : un événement se produit côté serveur, puis le serveur l’annonce à un canal précis, et le frontend écoute ce canal. Laravel facilite cette mécanique grâce à son intégration avec plusieurs solutions de diffusion. Selon votre stack, vous pouvez utiliser Pusher ou une solution auto-hébergée. L’important est de garder la configuration cohérente.

Dans le fichier .env, on peut avoir quelque chose comme ceci :

BROADCAST_CONNECTION=reverb
REVERB_APP_ID=chat-app
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secret
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http

Puis, dans la configuration correspondante, on s’assure que l’application est bien branchée sur le driver choisi. L’idée générale reste la même : dès qu’un message est envoyé, il doit être diffusé vers le bon canal. Ce mécanisme est le moteur invisible du temps réel.

Autoriser l’accès aux canaux

Pour qu’un utilisateur ne puisse écouter que les conversations auxquelles il appartient, il faut autoriser l’accès au canal de diffusion. Laravel permet de faire cela proprement dans routes/channels.php.

// routes/channels.php

use App\Models\Conversation;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('conversation.{conversationId}', function ($user, $conversationId) {
    return Conversation::where('id', $conversationId)
        ->whereHas('users', function ($query) use ($user) {
            $query->where('users.id', $user->id);
        })
        ->exists();
});

Cette petite portion de code joue un rôle de sécurité énorme. Elle empêche un utilisateur curieux de s’abonner à une conversation qui ne lui appartient pas. Dans une application de chat, la sécurité des canaux est aussi importante que la sécurité des routes. Les messages sont souvent personnels, parfois sensibles, et ils doivent rester visibles uniquement par les bonnes personnes.

Écouter les messages côté frontend

Pour que l’interface se mette à jour automatiquement, il faut écouter les événements côté frontend. On peut utiliser Laravel Echo pour se connecter au canal et afficher les nouveaux messages dès leur arrivée. Peu importe que vous utilisiez Blade, Alpine, Vue ou React, le principe reste le même.

Voici un exemple simple en JavaScript.

import Echo from 'laravel-echo';

window.Echo.private(`conversation.${conversationId}`)
    .listen('.message.sent', (event) => {
        console.log('Nouveau message reçu', event);

        const messagesContainer = document.getElementById('messages');
        const messageElement = document.createElement('div');
        messageElement.className = 'message';

        messageElement.innerHTML = `
            <strong>${event.user.name}</strong>
            <p>${event.body}</p>
            <small>${event.created_at}</small>
        `;

        messagesContainer.appendChild(messageElement);
    });

Si vous utilisez un canal de présence, vous pouvez aussi écouter les événements liés aux membres qui rejoignent ou quittent la conversation. Cela permet d’afficher “en ligne”, “en train d’écrire”, ou d’autres états interactifs. Pour une application de chat, ces détails donnent tout de suite une sensation de produit abouti.

Afficher une liste de conversations

La colonne de gauche d’une application de chat affiche souvent la liste des conversations disponibles. Cette zone est stratégique, parce qu’elle conditionne la navigation et la rapidité d’accès aux échanges. L’objectif est de montrer clairement avec qui on parle, ce qu’il s’est passé récemment, et si quelque chose de nouveau attend l’utilisateur.

Dans le contrôleur, on peut récupérer les conversations de l’utilisateur connecté avec leurs derniers messages.

// app/Http/Controllers/ConversationController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ConversationController extends Controller
{
    public function index()
    {
        $conversations = auth()->user()
            ->conversations()
            ->with(['users', 'messages' => function ($query) {
                $query->latest()->limit(1);
            }])
            ->get();

        return view('conversations.index', compact('conversations'));
    }
}

Dans la vue Blade :

{{-- resources/views/conversations/index.blade.php --}}

@extends('layouts.app')

@section('content')
<div class="max-w-6xl mx-auto px-4 py-6 grid grid-cols-1 md:grid-cols-3 gap-6">
    <div class="bg-white rounded-lg shadow p-4">
        <h2 class="text-lg font-semibold mb-4">Conversations</h2>

        <div class="space-y-3">
            @foreach ($conversations as $conversation)
                <a href="{{ route('conversations.show', $conversation) }}" class="block p-3 rounded hover:bg-gray-100">
                    <div class="font-medium">
                        {{ $conversation->title ?? $conversation->users->where('id', '!=', auth()->id())->first()?->name ?? 'Conversation' }}
                    </div>
                    <div class="text-sm text-gray-500">
                        {{ $conversation->messages->first()?->body ?? 'Aucun message' }}
                    </div>
                </a>
            @endforeach
        </div>
    </div>

    <div class="md:col-span-2 bg-white rounded-lg shadow p-4">
        <p class="text-gray-500">Sélectionnez une conversation pour commencer.</p>
    </div>
</div>
@endsection

Ce type d’interface reste simple, mais il crée déjà une structure claire pour l’utilisateur. Une application de chat réussie ne cherche pas à tout montrer en même temps. Elle guide doucement la personne vers la bonne conversation, puis lui laisse l’espace nécessaire pour lire et répondre confortablement.

Afficher les messages dans une conversation

La vue de conversation est la partie la plus émotionnelle du produit, parce qu’elle contient réellement les échanges. Ici, on veut une mise en page agréable, des bulles distinctes selon l’auteur, un bon espacement, et un accès facile au formulaire d’envoi.

// app/Http/Controllers/ConversationController.php

public function show(\App\Models\Conversation $conversation)
{
    abort_unless(
        $conversation->users()->where('users.id', auth()->id())->exists(),
        403
    );

    $conversation->load([
        'users',
        'messages.user' => function ($query) {
            $query->orderBy('created_at');
        }
    ]);

    return view('conversations.show', compact('conversation'));
}

Et dans la vue Blade :

{{-- resources/views/conversations/show.blade.php --}}

@extends('layouts.app')

@section('content')
<div class="max-w-6xl mx-auto px-4 py-6">
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="border-b p-4">
            <h1 class="text-xl font-semibold">
                {{ $conversation->title ?? 'Conversation privée' }}
            </h1>
        </div>

        <div id="messages" class="h-[500px] overflow-y-auto p-4 space-y-4 bg-gray-50">
            @foreach ($conversation->messages as $message)
                <div class="flex {{ $message->user_id === auth()->id() ? 'justify-end' : 'justify-start' }}">
                    <div class="max-w-lg px-4 py-2 rounded-lg shadow
                        {{ $message->user_id === auth()->id() ? 'bg-blue-600 text-white' : 'bg-white text-gray-800' }}">
                        <div class="text-sm font-semibold mb-1">
                            {{ $message->user->name }}
                        </div>
                        <div>{{ $message->body }}</div>
                        <div class="text-xs mt-1 opacity-75">
                            {{ $message->created_at->format('H:i') }}
                        </div>
                    </div>
                </div>
            @endforeach
        </div>

        <div class="border-t p-4">
            <form id="message-form" action="{{ route('conversations.messages.store', $conversation) }}" method="POST">
                @csrf
                <div class="flex gap-3">
                    <input
                        type="text"
                        name="body"
                        id="body"
                        class="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring"
                        placeholder="Écrire un message..."
                        autocomplete="off"
                    >
                    <button class="px-5 py-2 rounded-lg bg-blue-600 text-white">
                        Envoyer
                    </button>
                </div>
            </form>
        </div>
    </div>
</div>
@endsection

La première chose que l’on remarque ici, c’est l’opposition visuelle entre les messages de l’utilisateur et ceux des autres. C’est un détail très important, car un chat se lit presque comme une conversation réelle : on a besoin de reconnaître rapidement qui parle, sans relire chaque nom à chaque bulle. Une interface bien pensée fait gagner du temps et de l’énergie mentale.

Envoyer un message sans recharger la page

Le vrai confort d’une application de chat vient de l’impression de fluidité. L’utilisateur écrit son message, appuie sur Entrée ou clique sur envoyer, puis le message apparaît immédiatement. Pour cela, on peut intercepter le formulaire avec JavaScript et envoyer la requête en AJAX.

document.getElementById('message-form').addEventListener('submit', async function (e) {
    e.preventDefault();

    const input = document.getElementById('body');
    const body = input.value.trim();

    if (!body) return;

    const formData = new FormData(this);

    const response = await fetch(this.action, {
        method: 'POST',
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value,
        },
        body: formData,
    });

    if (response.ok) {
        input.value = '';
    }
});

On peut ensuite améliorer ce comportement pour afficher le message immédiatement dans la liste, sans attendre la réponse du serveur. C’est ce qu’on appelle souvent un affichage optimiste. On ajoute le message dans l’interface, puis on laisse le backend confirmer sa persistance. Cette approche donne une sensation de rapidité très agréable, à condition de bien gérer les erreurs.

Gérer les messages non lus

Une application de chat moderne ne se limite pas à l’envoi et à la réception. Il faut aussi aider l’utilisateur à repérer ce qu’il n’a pas encore lu. Dans une conversation active, cette fonctionnalité améliore énormément l’ergonomie. On peut utiliser le champ last_read_at de la table pivot pour stocker le dernier moment où l’utilisateur a consulté la conversation.

Lorsque l’utilisateur ouvre une conversation, on met à jour cette valeur.

public function show(\App\Models\Conversation $conversation)
{
    abort_unless(
        $conversation->users()->where('users.id', auth()->id())->exists(),
        403
    );

    $conversation->users()->updateExistingPivot(auth()->id(), [
        'last_read_at' => now(),
    ]);

    $conversation->load(['users', 'messages.user']);

    return view('conversations.show', compact('conversation'));
}

Ensuite, pour calculer les messages non lus, on peut comparer la date du dernier message à last_read_at. Cette logique peut être raffinée selon les besoins, mais elle donne déjà une base utile pour afficher des badges et des compteurs.

Ajouter l’indicateur de saisie

L’indicateur “en train d’écrire” n’est pas indispensable au fonctionnement, mais il change vraiment la perception de l’application. On a l’impression d’un système plus vivant, plus humain. Pour cela, le canal de présence est très utile. Quand un utilisateur commence à écrire, on peut envoyer un petit événement, puis l’afficher chez les autres membres de la conversation.

let typingTimeout = null;

const input = document.getElementById('body');

input.addEventListener('input', function () {
    clearTimeout(typingTimeout);

    window.Echo.private(`conversation.${conversationId}`)
        .whisper('typing', {
            user_id: currentUserId,
            name: currentUserName,
        });

    typingTimeout = setTimeout(() => {
        // On pourrait envoyer un whisper "stop typing" ici
    }, 1000);
});

Et côté écoute :

window.Echo.private(`conversation.${conversationId}`)
    .listenForWhisper('typing', (event) => {
        const typingIndicator = document.getElementById('typing-indicator');
        typingIndicator.textContent = `${event.name} est en train d’écrire...`;
        typingIndicator.classList.remove('hidden');

        clearTimeout(window.typingIndicatorTimeout);
        window.typingIndicatorTimeout = setTimeout(() => {
            typingIndicator.classList.add('hidden');
            typingIndicator.textContent = '';
        }, 2000);
    });

Ce petit ajout donne de la chaleur à l’application. Un chat, ce n’est pas seulement un système technique, c’est aussi un espace où les gens se sentent présents les uns aux autres. Plus l’interface respecte cette sensation, plus le produit paraît naturel.

Préparer les validations et la sécurité

Un projet de chat peut vite devenir une porte d’entrée vers des problèmes de sécurité si l’on néglige les validations. Les messages doivent être limités en longueur, le contenu doit être traité avec prudence, et les accès doivent être strictement contrôlés. Laravel aide beaucoup sur ces points, mais il faut rester rigoureux. Les règles de validation simples ne suffisent pas toujours ; il faut aussi réfléchir au comportement du système en cas d’abus, de spam, d’injection de contenu ou de requêtes répétées.

Pour une première protection, on peut utiliser une Form Request dédiée.

// app/Http/Requests/StoreMessageRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreMessageRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'body' => ['required', 'string', 'min:1', 'max:5000'],
        ];
    }
}

Puis, dans le contrôleur :

public function store(StoreMessageRequest $request, Conversation $conversation)
{
    abort_unless(
        $conversation->users()->where('users.id', auth()->id())->exists(),
        403
    );

    $message = Message::create([
        'conversation_id' => $conversation->id,
        'user_id' => auth()->id(),
        'body' => $request->validated()['body'],
    ]);

    broadcast(new MessageSent($message->load('user')))->toOthers();

    return response()->json([
        'message' => $message->load('user'),
    ]);
}

On peut également ajouter un rate limiting sur l’envoi de messages pour réduire le spam. Cette étape devient importante dès que l’application sort d’un usage purement personnel. En production, les limites servent autant à protéger le système qu’à préserver une expérience agréable pour les autres utilisateurs.

Structurer le projet pour qu’il reste propre

Un projet de chat peut s’enrichir très vite. Aujourd’hui on veut seulement des conversations privées ; demain on ajoute les groupes, les images, les réactions aux messages, la recherche, les notifications push, le statut de lecture, et peut-être des appels audio ou vidéo. Si l’on n’organise pas bien le code dès le départ, l’application devient vite difficile à faire évoluer. Il est donc préférable de séparer les responsabilités : les contrôleurs reçoivent et valident les requêtes, les services orchestrent la logique métier, les modèles décrivent les relations, les événements diffusent les changements, et les vues ou composants frontend gèrent l’affichage.

Cette discipline peut sembler un peu exigeante au départ, mais elle devient un vrai soulagement ensuite. Quand on revient sur le projet plusieurs semaines plus tard, on retrouve plus facilement ce que fait chaque fichier. C’est l’un des grands avantages de Laravel : on peut avancer vite, mais on peut aussi construire proprement si l’on respecte une architecture cohérente.

Ajouter des pièces jointes

Un chat moderne permet souvent d’envoyer des images ou d’autres fichiers. Ajouter cette fonctionnalité demande un peu plus de travail, mais la logique reste accessible. Il faut accepter le fichier, le stocker dans storage, conserver le chemin dans la base de données, puis l’afficher proprement dans le frontend. On peut enrichir la table messages avec un champ attachment_path et éventuellement attachment_type.

Schema::table('messages', function (Blueprint $table) {
    $table->string('attachment_path')->nullable()->after('body');
    $table->string('attachment_type')->nullable()->after('attachment_path');
});

Ensuite, dans le contrôleur :

if ($request->hasFile('attachment')) {
    $path = $request->file('attachment')->store('attachments', 'public');

    $message->attachment_path = $path;
    $message->attachment_type = $request->file('attachment')->getMimeType();
    $message->save();
}

Cette fonctionnalité donne immédiatement plus de richesse au chat. On peut envoyer une capture d’écran, un document, une photo, ou un autre fichier utile au contexte de la conversation. Là encore, il faut bien contrôler les types de fichiers autorisés et la taille maximale.

Gérer les notifications

Une application de chat digne de ce nom ne se contente pas d’afficher les nouveaux messages dans la fenêtre ouverte. Elle doit aussi prévenir l’utilisateur lorsqu’une nouvelle activité arrive ailleurs. Laravel facilite énormément ce genre de logique grâce aux notifications. On peut notifier un utilisateur lorsqu’il reçoit un message dans une conversation inactive, puis afficher un badge ou envoyer une notification email ou in-app selon le besoin.

// app/Notifications/NewMessageNotification.php

namespace App\Notifications;

use App\Models\Message;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\BroadcastMessage;

class NewMessageNotification extends Notification
{
    use Queueable;

    public function __construct(public Message $message)
    {
    }

    public function via($notifiable): array
    {
        return ['broadcast'];
    }

    public function toBroadcast($notifiable): BroadcastMessage
    {
        return new BroadcastMessage([
            'message_id' => $this->message->id,
            'conversation_id' => $this->message->conversation_id,
            'body' => $this->message->body,
        ]);
    }
}

Ce système devient très utile si l’utilisateur navigue sur une autre page au moment où le message arrive. Il peut ainsi être averti, puis revenir vers la conversation au bon moment. Une bonne notification n’interrompt pas inutilement ; elle attire l’attention seulement quand c’est pertinent.

Tester la logique

Il ne faut jamais négliger les tests, surtout dans une application de chat où l’on manipule des accès utilisateurs, des événements et des relations entre plusieurs tables. Les tests permettent de s’assurer qu’un message est bien enregistré, qu’un utilisateur ne peut pas écrire dans une conversation à laquelle il n’appartient pas, et qu’un événement est bien déclenché après l’envoi.

Exemple de test simple :

public function test_user_can_send_message_in_own_conversation(): void
{
    $user = User::factory()->create();
    $other = User::factory()->create();

    $conversation = Conversation::create(['is_group' => false]);
    $conversation->users()->attach([$user->id, $other->id]);

    $this->actingAs($user);

    $response = $this->postJson(route('conversations.messages.store', $conversation), [
        'body' => 'Bonjour, comment ça va ?',
    ]);

    $response->assertOk();
    $this->assertDatabaseHas('messages', [
        'conversation_id' => $conversation->id,
        'user_id' => $user->id,
        'body' => 'Bonjour, comment ça va ?',
    ]);
}

Les tests donnent une assurance très concrète. Dans un projet de chat, où les interactions se multiplient vite, ils évitent de casser une fonctionnalité en travaillant sur une autre. Ce sont eux qui donnent la stabilité nécessaire pour faire évoluer l’application avec confiance.

Améliorer l’expérience utilisateur

La technique compte beaucoup, mais l’expérience utilisateur compte autant. Un chat réussi doit être rapide, lisible, rassurant et agréable. On peut ajouter le défilement automatique vers le dernier message, un champ de saisie qui grandit au fur et à mesure, des timestamps discrets mais accessibles, des séparateurs de date, des avatars d’utilisateurs, et des animations légères pour accompagner l’arrivée des messages. Il ne faut pas chercher l’effet spectaculaire pour lui-même ; il faut surtout réduire la friction. Une bonne application de chat disparaît presque derrière la conversation elle-même.

On peut par exemple faire défiler automatiquement vers le bas quand un nouveau message arrive :

function scrollToBottom() {
    const container = document.getElementById('messages');
    container.scrollTop = container.scrollHeight;
}

window.Echo.private(`conversation.${conversationId}`)
    .listen('.message.sent', (event) => {
        appendMessage(event);
        scrollToBottom();
    });

Ce genre de détail paraît mineur, mais il participe énormément au confort global. Quand une application réagit de manière fluide, on a immédiatement l’impression qu’elle est bien conçue. Et dans un chat, cette impression fait toute la différence.

Passer à l’échelle

Lorsqu’une application de chat commence à accueillir un vrai trafic, il faut penser à la montée en charge. Certains messages peuvent être diffusés à de nombreux participants, certaines conversations peuvent contenir des volumes importants d’historique, et l’activité temps réel peut solliciter fortement le serveur. Laravel offre plusieurs leviers pour gérer cela : les queues pour déporter le broadcasting, le cache pour accélérer certaines lectures, l’indexation des requêtes fréquentes, et la séparation des responsabilités entre stockage des données et diffusion des événements.

Il est aussi judicieux de réfléchir à la pagination des messages. Charger toute l’historique d’une conversation d’un coup n’est pas toujours nécessaire. Une stratégie fréquente consiste à charger les derniers messages puis à proposer un chargement progressif lorsque l’utilisateur remonte la conversation. Cela améliore la rapidité initiale et réduit la charge inutile. Plus le chat devient grand, plus ces optimisations deviennent précieuses.

Ajouter des salons de groupe

Une fois le chat privé en place, les salons de groupe deviennent une évolution naturelle. Heureusement, notre modèle initial l’a déjà anticipée grâce au champ is_group et à la table pivot conversation_user. Pour créer un groupe, il suffit de créer une conversation avec un titre, d’ajouter plusieurs utilisateurs, puis d’afficher les messages dans le même espace.

$conversation = Conversation::create([
    'title' => 'Équipe produit',
    'is_group' => true,
]);

$conversation->users()->attach([1, 2, 3, 4]);

À partir de là, vous pouvez ajouter des rôles internes au groupe, comme administrateur ou simple membre, des invitations, des expulsions, ou même une image de groupe. La base technique reste la même, ce qui est très rassurant. C’est précisément la force d’une architecture bien pensée : elle permet de grandir sans tout recommencer.

Quelques bonnes pratiques à retenir

Même si chaque projet a ses contraintes, certaines habitudes reviennent presque toujours dans les applications de chat. Il faut valider chaque entrée utilisateur. Il faut protéger les canaux de diffusion. Il faut éviter de dupliquer les conversations privées. Il faut garder un œil sur les performances dès que le volume monte. Il faut aussi penser aux cas réels, pas seulement aux parcours idéaux. Que se passe-t-il si le message échoue ? Si la connexion coupe ? Si l’utilisateur ouvre la conversation sur plusieurs appareils ? Si deux requêtes arrivent en même temps ? Ce sont ces questions qui transforment une démo en produit fiable.

L’autre bon réflexe, c’est d’écrire du code lisible. Les applications de chat deviennent vite émotionnelles et rapides, donc on a parfois envie de tout traiter dans un seul fichier pour aller plus vite. C’est tentant, mais rarement une bonne idée. Mieux vaut garder des services clairs, des contrôleurs légers, des composants bien nommés, et des migrations explicites. Ce qui est clair aujourd’hui restera maintenable demain.

Conclusion

Créer une application de chat avec Laravel est un projet enthousiasmant parce qu’il réunit plusieurs aspects très concrets du développement web : la gestion d’utilisateurs, la modélisation des données, le temps réel, l’interface interactive, la sécurité, les notifications, et la montée en charge. C’est aussi un excellent exercice pour apprendre à penser au-delà de la simple page web. Un chat doit donner l’impression d’un espace vivant, instantané, fiable et agréable. Laravel fournit une base solide pour atteindre cet objectif sans s’épuiser dans la complexité technique.

En suivant une structure propre, en séparant bien les responsabilités, en soignant le broadcasting et l’expérience utilisateur, vous pouvez construire une vraie application de chat capable d’évoluer vers des fonctionnalités plus riches. Ce type de projet est particulièrement formateur, parce qu’il oblige à relier l’architecture backend à la sensation frontend. À la fin, le plus satisfaisant n’est pas seulement de voir les messages s’afficher. C’est de sentir que tout s’enchaîne naturellement, comme si l’application comprenait le rythme de la conversation.

#Laravel chat app #application de chat Laravel #chat en temps réel Laravel #broadcasting Laravel #Laravel Reverb #Laravel Echo #messages temps réel #Laravel Blade #notifications Laravel #Eloquent relations #chat privé Laravel #salon de groupe Laravel

Abonnez-vous à notre newsletter

12k+

Abonnés

Hebdomadaire

Fréquence

Gratuit

Toujours