Чат и Сообщения
Раз у нас есть мэтчи, людям нужно общаться! При мэтче мы будем создавать “Комнату/Диалог” (Conversation), а внутри неё уже писать сообщения (Messages).
1. Создаем миграции:
php artisan make:migration create_conversations_table
php artisan make:migration create_messages_table
2.Открой миграцию ..._create_conversations_table.php:
public function up(): void
{
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->foreignId('user1_id')->constrained('users')->onDelete('cascade');
$table->foreignId('user2_id')->constrained('users')->onDelete('cascade');
$table->timestamp('last_message_at')->nullable(); // Для сортировки диалогов по последнему сообщению
$table->timestamps();
// Уникальный индекс: диалог между двумя юзерами может быть только один!
$table->unique(['user1_id', 'user2_id']);
});
}
3. Открой миграцию ..._create_messages_table.php:
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
$table->foreignId('sender_id')->constrained('users')->onDelete('cascade');
$table->text('body'); // Текст сообщения
$table->boolean('is_read')->default(false); // Прочитано или нет
$table->timestamps();
});
}
4. Накатываем миграции:
php artisan migrate
Как только таблицы создадутся, мы обновим SwipeController, чтобы он автоматически создавал диалог при мэтче, и напишем API для отправки сообщений но сначала создадим модели !
Создаем модели для чата
Когда происходит Мэтч (взаимный лайк), нам нужно автоматически создать пустой диалог (Conversation), чтобы они могли сразу начать переписку.
Выполни в терминале:
php artisan make:model Conversation
php artisan make:model Message
1. Открой app/Models/Conversation.php и настроим связи:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Conversation extends Model
{
protected $fillable = ['user1_id', 'user2_id', 'last_message_at'];
// Участник 1
public function user1()
{
return $this->belongsTo(User::class, 'user1_id');
}
// Участник 2
public function user2()
{
return $this->belongsTo(User::class, 'user2_id');
}
// Сообщения в диалоге
public function messages()
{
return $this->hasMany(Message::class);
}
// Последнее сообщение (удобно для списка диалогов)
public function lastMessage()
{
return $this->hasOne(Message::class)->latestOfMany();
}
// Преобразование дат
protected function casts(): array
{
return [
'last_message_at' => 'datetime',
];
}
}
Открой app/Models/Message.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $fillable = ['conversation_id', 'sender_id', 'body', 'is_read'];
// Кто отправил
public function sender()
{
return $this->belongsTo(User::class, 'sender_id');
}
protected function casts(): array
{
return [
'is_read' => 'boolean',
];
}
}
Обновляем SwipeController (Создаем диалог при Мэтче!)
Теперь идем в app/Http/Controllers/SwipeController.php. Нам нужно добавить логику создания Conversation, если is_match стал true.
Важный нюанс: чтобы не было дублей диалогов (например, Алекс создал диалог с Анной, а Анна потом пытается создать второй), мы будем сохранять ID юзеров по возрастанию: кто первый, кто второй — не важно, диалог между ними один!
Замени блок внутри store() где у нас проверка на мэтч:
$isMatch = false;
if ($request->action === 'like' || $request->action === 'superlike') {
$reverseSwipe = Swipe::where('swiper_id', $swipedId)
->where('swiped_id', $swiperId)
->whereIn('action', ['like', 'superlike'])
->exists();
if ($reverseSwipe) {
$isMatch = true;
// НОВОЕ: Создаем диалог при мэтче!
// Чтобы не было дублей, меньший ID всегда на первом месте
$smallerId = min($swiperId, $swipedId);
$largerId = max($swiperId, $swipedId);
Conversation::firstOrCreate(
['user1_id' => $smallerId, 'user2_id' => $largerId],
['last_message_at' => now()]
);
}
}
Создаем ChatController (Отправка и чтение)
Теперь сделаем API для списка диалогов и отправки сообщений.
php artisan make:controller ChatController
Открой app/Http/Controllers/ChatController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Conversation;
use App\Models\Message;
use Illuminate\Http\Request;
class ChatController extends Controller
{
// 1. Получить список всех диалогов юзера
public function index(Request $request)
{
$userId = $request->user()->id;
// Ищем диалоги, где юзер является либо user1, либо user2
$conversations = Conversation::where('user1_id', $userId)
->orWhere('user2_id', $userId)
->with(['user1.profile', 'user2.profile', 'lastMessage']) // Подтягиваем профили и последнее сообщение
->orderBy('last_message_at', 'desc') // Самые свежие сверху
->get()
->map(function ($conv) use ($userId) {
// Определяем, с кем мы общаемся (если я user1, то собеседник user2)
$companion = $conv->user1_id === $userId ? $conv->user2 : $conv->user1;
return [
'conversation_id' => $conv->id,
'companion' => [
'id' => $companion->id,
'name' => $companion->profile->name,
// 'avatar' => $companion->profile->avatar, (когда добавим фото)
],
'last_message' => $conv->lastMessage ? $conv->lastMessage->body : null,
'updated_at' => $conv->last_message_at,
];
});
return response()->json($conversations);
}
// 2. Получить сообщения конкретного диалога
public function messages(Request $request, $conversationId)
{
$conversation = Conversation::findOrFail($conversationId);
// Проверка безопасности: юзер должен быть участником этого диалога
if ($request->user()->id !== $conversation->user1_id && $request->user()->id !== $conversation->user2_id) {
return response()->json(['message' => 'Доступ запрещен'], 403);
}
return Message::where('conversation_id', $conversationId)
->with('sender.profile')
->orderBy('created_at', 'asc') // Старые внизу, новые вверху (как в WhatsApp)
->get();
}
// 3. Отправить сообщение
public function send(Request $request, $conversationId)
{
$conversation = Conversation::findOrFail($conversationId);
if ($request->user()->id !== $conversation->user1_id && $request->user()->id !== $conversation->user2_id) {
return response()->json(['message' => 'Доступ запрещен'], 403);
}
$request->validate([
'body' => 'required|string|max:1000',
]);
$message = Message::create([
'conversation_id' => $conversationId,
'sender_id' => $request->user()->id,
'body' => $request->body,
]);
// Обновляем время последнего сообщения в диалоге (чтобы он поднялся в списке)
$conversation->update(['last_message_at' => now()]);
return response()->json([
'message' => 'Сообщение отправлено',
'data' => $message->load('sender.profile')
], 201);
}
}
Добавляем роуты чата
В routes/api.php внутри защищенной группы добавь:
// Чат
Route::get('/chats', [ChatController::class, 'index']);
Route::get('/chats/{conversationId}/messages', [ChatController::class, 'messages']);
Route::post('/chats/{conversationId}/send', [ChatController::class, 'send']);
ТЕСТИРОВАНИЕ ЧАТА!
У нас Алекс и Анна уже залайкали друг друга на прошлом шаге, значит в базе уже должен был создаться диалог (если ты обновил SwipeController и засвайпал их заново. Если нет, давай просто получим их токены и они сделают тестовый свайп по новой, либо мы посмотрим диалог).
1. Проверяем список диалогов Алекса:
curl http://127.0.0.1:8000/api/chats -H "Authorization: Bearer ТУТ_ТОКЕН_АЛЕКСА" -H "Accept: application/json"
Там должна отобразиться Анна в качестве собеседника.
2. Алекс пишет Анне сообщение (предположим ID диалога = 1):
curl -X POST http://127.0.0.1:8000/api/chats/1/send -H "Authorization: Bearer ТУТ_ТОКЕН_АЛЕКСА" -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"body\":\"Hello Anna\"}"
3. Читаем переписку (сообщения диалога):
curl http://127.0.0.1:8000/api/chats/1/messages -H "Authorization: Bearer ТУТ_ТОКЕН_АЛЕКСА" -H "Accept: application/json"
Брат, мы собрали полноценный бэкенд дейтинга: Регистрация -> Профиль -> Геолокация -> Лента -> Свайпы -> Мэтч -> Чат!
Как работают чаты в современном вебе (Связка REST + WebSocket)
То, что мы написали в ChatController — это REST API. Оно нужно для сохранения истории сообщений в базу данных и их прочтения. Но если мы оставим только его, Анне придется обновлять страницу (или тянуть приложение вниз), чтобы увидеть сообщение от Алекса. Это не чат, это почта!
Для реал-тайма (чтобы сообщения появлялись мгновенно, как в Telegram/WhatsApp) нам нужен WebSocket.
Давай разберем, как это будет работать в нашей архитектуре.
Чат всегда работает по двум каналам:
- HTTP (REST) — для надежности. Когда Алекс нажимает “Отправить”, его приложение делает
POST /api/chats/1/send. Это гарантирует, что сообщение сохранится в БД, даже если интернет плохой. - WebSocket — для скорости. Как только Laravel сохранил сообщение в БД, он тут же “кричит” по WebSocket-соединению: “Эй, все, кто в этом чате, новое сообщение!”. Приложение Анны слушает этот канал и мгновенно отрисовывает пузырек с текстом.
⚙️ Наш стек для WebSocket: Laravel Reverb
В Laravel 11 появился родной, мощный и бесплатный WebSocket-сервер — Laravel Reverb. Раньше приходилось использовать сторонние сервисы типа Pusher (которые дерут деньги), а теперь всё свое!
Как мы это внедрим (План действий):
1. Установка Reverb: Мы выполним команду php artisan install:reverb. Она настроит WebSocket-сервер и установит фронтенд-библиотеку Laravel Echo, которая будет слушать эти события.
2. Создание Event (События): Мы создадим событие NewMessageSent, которое будет реализовывать интерфейс ShouldBroadcast. Это магический интерфейс Laravel.
3. Изменение ChatController: В методе send мы не просто вернем JSON, а вызовем наше событие:
// Сохраняем в БД
$message = Message::create([...]);
// ТРАНСЛИРУЕМ ПО WEBSOCKET!
broadcast(new NewMessageSent($message))->toOthers();
4. Авторизация каналов (Безопасность): Мы не хотим, чтобы кто попало читал чужие переписки. Мы настроим систему, чтобы Анна могла подключиться только к WebSocket-каналу chat.1, если она является участницей диалога №1.
5. Слушатель на Фронтенде (Next.js / React Native): Когда мы перейдем к написанию фронтенда, мы напишем там примерно такой код:
Echo.private(`chat.${conversationId}`)
.listen('NewMessageSent', (e) => {
// e.message - это новое сообщение от Алекса
// Мгновенно добавляем его в UI чата без перезагрузки!
addMessageToChat(e.message);
});
Установка Laravel Reverb
Настроить бэкенд так, чтобы он сразу стрелял событиями в WebSocket — это значит, что когда мы дойдем до фронтенда, нам останется просто “подключить уши” и всё будет летать в реал-тайме.
Laravel Reverb — это встроенный сервер WebSocket в Laravel 11+. Он поднимается локально и позволяет серверу “разговаривать” с клиентами мгновенно.
Погнали настраивать!
В терминале выполни команду:
php artisan install:broadcasting --reverb
Установщик задаст несколько вопросов. Отвечай так:
- Do you want to install Reverb’s first-party Node dependencies? — Yes (он установит Laravel Echo для будущего фронтенда).
- Другие вопросы (про хост/порт) — просто жми Enter (оставим дефолтные значения).
Что он сделал:
- Скачал пакет
laravel/reverb. - Обновил файл
.env(добавил настройки Reverb). - Опубликовал конфиг
config/reverb.php.
если выкинет ошибку из за того что не успел установить зависимости по таймауту просто сделай вручную:
npm install --save-dev laravel-echo pusher-js
npm run build
Создаем Событие (Event)
Когда отправляется сообщение, мы должны “выкрикнуть” об этом в WebSocket.
Создаем событие:
php artisan make:event NewMessageSent
Открой файл app/Events/NewMessageSent.php и замени его содержимое на это:
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
// ВАЖНО: Используем ShouldBroadcastNow, чтобы событие отправлялось мгновенно
// без настройки очередей (Redis Queue), что удобно для разработки
class NewMessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public function __construct(Message $message)
{
// Передаем само сообщение, уже с подтянутым профилем отправителя
$this->message = $message->load('sender.profile');
}
// Указываем, в какой канал WebSocket мы будем стрелять
public function broadcastOn(): array
{
// Используем ПРИВАТНЫЙ канал для конкретного диалога (для безопасности!)
return [
new PrivateChannel('chat.' . $this->message->conversation_id),
];
}
// Указываем, какие данные полетят в WebSocket (чтобы не слать всю базу)
public function broadcastWith(): array
{
return [
'message' => $this->message
];
}
}
Безопасность каналов (Authorization)
Мы создали PrivateChannel. Это значит, что Анна не сможет просто так подключиться к чужому чату. Нам нужно объяснить Laravel, кто имеет право слушать этот канал.
Открой файл routes/channels.php. Добавь туда следующий код:
<?php
use Illuminate\Support\Facades\Broadcast;
use App\Models\Conversation;
// Авторизация приватного канала чата
Broadcast::channel('chat.{conversationId}', function ($user, $conversationId) {
// Находим диалог
$conversation = Conversation::find($conversationId);
// Если диалога нет - доступ закрыт
if (!$conversation) {
return false;
}
// Разрешаем слушать канал ТОЛЬКО если юзер является участником этого диалога
return $conversation->user1_id === $user->id || $conversation->user2_id === $user->id;
});
Обновляем ChatController (Стреляем событием!)
Теперь идем в app/Http/Controllers/ChatController.php. В методе send мы вызовем наше событие.
Найди метод send и обнови его:
public function send(Request $request, $conversationId)
{
$conversation = Conversation::findOrFail($conversationId);
if ($request->user()->id !== $conversation->user1_id && $request->user()->id !== $conversation->user2_id) {
return response()->json(['message' => 'Доступ запрещен'], 403);
}
$request->validate([
'body' => 'required|string|max:1000',
]);
$message = Message::create([
'conversation_id' => $conversationId,
'sender_id' => $request->user()->id,
'body' => $request->body,
]);
$conversation->update(['last_message_at' => now()]);
// МАГИЯ REVERB: Транслируем событие по WebSocket!
broadcast(new \App\Events\NewMessageSent($message))->toOthers();
return response()->json([
'message' => 'Сообщение отправлено',
'data' => $message->load('sender.profile')
], 201);
}
Метод ->toOthers() означает: отправить всем в канале, КРОМЕ того, кто отправил запрос (его же сообщение мы уже вернули в JSON ответе, незачем дублировать).
Как тестировать? (Новая жизнь сервера)
Теперь твой локальный сервер должен работать в двух режимах: обычный HTTP для API и WebSocket для реал-тайма.
Запускай в терминале:
php artisan reverb:start
Ты увидишь красивый логотип Reverb, который говорит, что WebSocket-сервер крутится на порту 8080. Не закрывай это окно!
Открой второе окно терминала и запусти обычный API:
php artisan serve
Теперь отправь сообщение от Алекса в чат (как мы делали в прошлом шаге):
curl -X POST http://127.0.0.1:8000/api/chats/1/send -H "Authorization: Bearer ТУТ_ТОКЕН_АЛЕКСА" -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"body\":\"Hello from WebSocket\"}"
В окне, где крутится php artisan reverb:start, ты увидишь, как проскочила строчка: кто-то подключился к приватному каналу, и наше событие NewMessageSent было транслировано!
Но это будет работать при условии если есть подключенный клиент на фронте, если клиента нет то ничего вы не увидите. Можно проверить службу заменив в .env BROADCAST_CONNECTION=reverb на BROADCAST_CONNECTION=log , но тут нужно учесть что этих строки в файле 2 и менять нужноо ту что последняя. Тогда для теста бродкасты будуть лететь в storage/logs/laravel.log
Бэкенд полностью готов к реал-тайму. Когда мы будем писать Next.js, мы просто подключимся туда, и сообщения будут прилетать сами!