Laravel Reverb

Reverb — это отдельный пакет с открытым исходным кодом, который является сервером WebSocket для приложений Laravel. Он помогает облегчить взаимодействие в реальном времени между клиентом и сервером.

До этого нового пакета Laravel имел вещание событий, но по сути не имел встроенного способа настроить сервер WebSocket с собственным хостингом. К счастью, Reverb теперь дает нам такую ​​возможность.

Laravel Reverb имеет несколько ключевых особенностей: он написан на PHP, он быстрый и масштабируемый. Он был разработан специально для горизонтального масштабирования.

Reverb в основном позволяет вам запускать приложение на одном сервере – но если приложение начинает перерастать этот сервер, вы можете добавить несколько дополнительных серверов. Затем все эти серверы могут взаимодействовать друг с другом, чтобы распределять сообщения между собой.

В этой статье вы узнаете, как создать приложение чата в реальном времени с помощью Laravel Reverb. Это позволит вам легко реализовать WebSocket-коммуникации между вашим бэкендом и фронтендом.

В качестве фронтенд-технологии вы можете использовать все, что захотите, но в данном случае мы будем использовать React.js с инструментом сборки Vite.js.

К концу этой статьи на вашем локальном компьютере будет полнофункциональное приложение реального времени, которое будет работать следующим образом:

Изображение Демонстрация приложения, демонстрирующая обмен сообщениями между двумя вошедшими в систему пользователями

Предпосылки

Для приложения, которое мы создадим в этой статье, вам понадобятся следующие инструменты:

  • PHP : версия 8.2 или выше (запустите php -v для проверки версии)
  • Composer (запустите, composer чтобы проверить, существует ли он)
  • Node.js : версия 20 или выше (запустите node -v, чтобы проверить версию)
  • MySQL : версия 5.7 или выше (запустите mysql --version, чтобы проверить, существует ли она)

Общие шаги

Основными шагами в этой статье будут:

  • Установка Laravel 11.
  • Добавление к нему потока аутентификации (структура аутентификации). Laravel предоставляет базовую отправную точку для этого, используя Bootstrap с React / Vue.
  • Установка Reverb.
  • Компоненты React.js и прослушивание событий во внешнем интерфейсе.

Как установить Laravel

Для начала установите Laravel 11 с помощью команды composer:

composer create-project laravel/laravel:^11.0 laravel-reverb-react-chat && cd laravel-reverb-react-chat/

На этом этапе вы можете проверить приложение, выполнив serveкоманду:

php artisan serve

Как создать модель и миграцию

Вы можете создать модель и миграцию для сообщений, используя эту единственную команду:

php artisan make:model -m Message

Затем вам нужно будет настроить модель сообщения с помощью следующего кода:

<?php

namespace App\Models;

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

class Message extends Model
{
    use HasFactory;

    public $table = 'messages';
    protected $fillable = ['id', 'user_id', 'text'];

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

    public function getTimeAttribute(): string {
        return date(
            "d M Y, H:i:s",
            strtotime($this->attributes['created_at'])
        );
    }
}

Как вы видите, есть getTimeAttribute() аксессор, который форматирует временную метку создания сообщения в понятный человеку формат даты и времени. Он будет показывать ее в верхней части каждого сообщения в окне чата.

Далее настройте миграцию для messages таблицы базы данных с помощью этого кода:

<?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('user_id')->constrained();
            $table->text('text')->nullable();
            $table->timestamps();
        });
    }

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

Эта миграция создает messages таблицу в базе данных. Таблица содержит столбцы для автоинкрементного первичного ключа ( id), внешнего ключа ( user_id), ссылающегося на id столбец таблицы users, text столбец для хранения содержимого сообщения и timestamps для автоматического отслеживания времени создания и изменения каждой записи.

Миграция также включает метод отката ( down()) для удаления messages таблицы при необходимости.

В этой статье мы будем использовать базу данных MySQL, но вы можете использовать SQLite в качестве базы данных по умолчанию, если предпочитаете. Просто убедитесь, что правильно настроили учетные данные базы данных в .envфайле:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=username
DB_PASSWORD=password

После настройки переменных среды оптимизируйте кэш:

php artisan optimize

Запустите миграции, чтобы заново создать таблицы базы данных, а также добавить таблицу messages:

php artisan migrate:fresh

Как добавить аутентификацию

Теперь вы можете добавить аутентификацию scaffolding в свое приложение. Вы можете использовать пакет UI Laravel для импорта некоторых файлов ресурсов. Сначала вам нужно установить соответствующий пакет:

composer require laravel/ui

Затем импортируйте в приложение ресурсы, связанные с React:

php artisan ui react --auth

Может потребоваться перезапись app/Http/Controllers/Controller.php, и вы можете разрешить это:

The [Controller.php] file already exists. Do you want to replace it? (yes/no) [no]

Это сделает все необходимые для аутентификации скаффолдинги скомпилированными и установленными, включая маршруты, контроллеры, представления, конфигурации vite и простой пример, специфичный для React.

На этом этапе вы всего в одном шаге от того, чтобы приложение было готово к запуску.

ПРИМЕЧАНИЕ: Убедитесь, что у вас установлен Node.jsnpm ) версии 20 или выше. Вы можете проверить это, выполнив node -vкоманду. В противном случае просто установите его с официальной страницы .

npm install && npm run build

Команда выше установит пакеты NPM и создаст frontend assets. Теперь вы можете запустить приложение Laravel и проверить полностью готовый пример приложения:

php artisan optimize && php artisan serve

Изображение Скриншот страницы регистрации

Также важно отметить, что вы можете запустить dev команду отдельно, а не использовать build каждый раз, когда вносите изменения в файлы интерфейса:

npm run dev

Подробности смотрите в package.jsonфайле, в scriptsполе.

Как настроить маршруты

В этом приложении для чата в реальном времени вам понадобится несколько маршрутов:

  • home для домашней страницы (уже должно быть добавлено)
  • message для добавления нового сообщения
  • messages чтобы получить все существующие сообщения

В файле будут такие маршруты web.php:

<?php

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;

Route::get('/', function () { return view('welcome'); });

Auth::routes();

Route::get('/home', [HomeController::class, 'index'])
    ->name('home');
Route::get('/messages', [HomeController::class, 'messages'])
    ->name('messages');
Route::post('/message', [HomeController::class, 'message'])
    ->name('message');

После настройки этих маршрутов давайте воспользуемся преимуществами Laravel Events и Queue Jobs.

Как настроить событие Laravel

Вам необходимо создать GotMessage событие для прослушивания определенного события:

php artisan make:event GotMessage
События Laravel предоставляют простую реализацию шаблона наблюдателя, позволяя вам подписываться и прослушивать различные события, происходящие в вашем приложении. Классы событий обычно хранятся в app/Eventsкаталоге.

Настройте частный канал WebSocket в broadcastOn методе для всех аутентифицированных пользователей, чтобы получать сообщения в режиме реального времени. В этом случае мы назовем его "channel_for_everyone", но вы также можете сделать его динамическим, в зависимости от пользователя, например "App.Models.User.{$this->message['user_id']}".

<?php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

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

    public function __construct(public array $message) {
        //
    }

    public function broadcastOn(): array {
        // $this->message is available herereturn [
            new PrivateChannel("channel_for_everyone"),
        ];
    }
}

Как видите, $massage в качестве аргумента конструктора есть публичное свойство, поэтому вы можете получить информацию о сообщении во внешнем интерфейсе.

Мы уже использовали имя канала в файле каналов, и мы будем использовать его во внешнем интерфейсе для обновления сообщений в реальном времени.

Не забудьте реализовать ShouldBroadcast интерфейс в классе события.

Как настроить очередь заданий Laravel

Теперь пришло время создать SendMessage задание по отправке сообщений:

php artisan make:job SendMessage
Laravel позволяет вам легко создавать поставленные в очередь задания, которые могут обрабатываться в фоновом режиме. Перемещая длительные задачи в очередь, ваше приложение может отвечать на веб-запросы с молниеносной скоростью и предоставлять лучший пользовательский опыт для ваших клиентов.
<?php

namespace App\Jobs;

use App\Events\GotMessage;
use App\Models\Message;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendMessage implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public Message $message) {
        //
    }

    public function handle(): void {
        GotMessage::dispatch([
            'id' => $this->message->id,
            'user_id' => $this->message->user_id,
            'text' => $this->message->text,
            'time' => $this->message->time,
        ]);
    }
}

Задание очереди SendMessage.php отвечает за отправку GotMessage события с информацией о новом отправленном сообщении. Оно получает Message объект при построении, представляющий сообщение для отправки.

В своем handle() методе он отправляет GotMessage событие с такими подробностями, как идентификатор сообщения, идентификатор пользователя, текст и временная метка. Это задание предназначено для постановки в очередь для асинхронной обработки, что позволяет эффективно обрабатывать задачи отправки сообщений в фоновом режиме.

Как видите, $massage в качестве аргумента конструктора имеется публичное свойство, которое мы будем использовать для присоединения информации о сообщении к заданию очереди.

Как написать методы контроллера

Для определенных маршрутов приведены соответствующие методы контроллера:

<?php

namespace App\Http\Controllers;

use App\Jobs\SendMessage;
use App\Models\Message;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function __construct() {
        $this->middleware('auth');
    }

    public function index() {
        $user = User::where('id', auth()->id())->select([
            'id', 'name', 'email',
        ])->first();

        return view('home', [
            'user' => $user,
        ]);
    }

    public function messages(): JsonResponse {
        $messages = Message::with('user')->get()->append('time');

        return response()->json($messages);
    }

    public function message(Request $request): JsonResponse {
        $message = Message::create([
            'user_id' => auth()->id(),
            'text' => $request->get('text'),
        ]);
        SendMessage::dispatch($message);

        return response()->json([
            'success' => true,
            'message' => "Message created and job dispatched.",
        ]);
    }
}
  • В home методе мы получим данные вошедшего в систему пользователя из базы данных с помощью Userмодели и отправим их в представление Blade.
  • В messages методе мы извлечем все сообщения из базы данных с помощью Messageмодели, прикрепим userк ней данные о взаимосвязи, добавим timeполе (метод доступа) к каждому элементу и отправим все это в представление.
  • В message методе с использованием модели в таблице базы данных будет создано новое сообщение Message, и SendMessage будет отправлено задание очереди.

Как установить Laravel Reverb

Теперь мы подошли к самому важному моменту: пришло время установить Reverb в ваше приложение Laravel.

Это так просто. Все необходимые настройки упаковки и конфигурации можно выполнить с помощью одной команды:

php artisan install:broadcasting

Вам будет предложено установить Laravel Reverb, а также установить и собрать зависимости Node, необходимые для трансляции. Просто нажмите Enter, чтобы продолжить.

После выполнения команды убедитесь, что вы автоматически добавили в .envфайл переменные среды, специфичные для Reverb, например:

BROADCAST_CONNECTION=reverb

###

REVERB_APP_ID=795051
REVERB_APP_KEY=s3w3thzezulgp5g0e5bs
REVERB_APP_SECRET=gncsnk3rzpvczdakl6pz
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

В каталоге также появятся два новых файла конфигурации config:

  • reverb.php
  • broadcasting.php

Как настроить каналы WebSocket

Наконец, вам нужно будет добавить канал в channels.phpфайл. Он должен быть уже создан после установки Reverb.

<?php

use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('channel_for_everyone', function ($user) {
    return true;
});

У вас будет только один канал. Вы можете изменить название канала и сделать его динамичным — решать вам. При закрытии канала мы всегда будем возвращать true, но вы можете изменить его позже, чтобы сделать некоторые ограничения относительно подписки на канал.

Оптимизируем кэши еще раз:

php artisan optimize

Как настроить View Laravel

На этом этапе ваш бэкэнд должен быть готов, поэтому вы можете переключиться на фронтэнд.

Прежде чем работать над React-вещями, вам нужно настроить *.blade.phpпредставления Laravel. В home представлении blade убедитесь, что у вас есть корневой div с идентификатором , mainчтобы отобразить там все компоненты React.

@extends('layouts.app')

@section('content')
    <div class="container">
        <div id="main" data-user="{{ json_encode($user) }}"></div>
    </div>
@endsection

Элемент div с идентификатором mainполучает свойство data для хранения $userинформации, отправленной из метода контроллера home.

Я не буду размещать здесь весь resources/views/welcome.blade.phpконтент, но вы можете внести в него следующие небольшие изменения:

  • Заменить url('/dashboard')на url('/home');
  • Заменить Dashboardна Home;
  • Удалить mainи footerразделы.

Работа над фронтендом

В Reverb трансляция событий осуществляется серверным драйвером вещания, который транслирует ваши события Laravel, чтобы интерфейс мог получать их в клиентском браузере.

В front-end Laravel Echo выполняет эту работу под капотом. Echo — библиотека JavaScript, которая упрощает подписку на каналы и прослушивание событий, транслируемых вашим серверным драйвером вещания.

Настройки конфигураций WebSocket с Echo можно найти в rources/js/echo.js файле, но для этого проекта вам ничего делать не нужно.

Давайте создадим несколько компонентов React, чтобы проект стал более читаемым и переработанным.

Создайте Main.jsx компонент в новой components папке:

import React from 'react';
import ReactDOM from 'react-dom/client';
import '../../css/app.css';
import ChatBox from "./ChatBox.jsx";

if (document.getElementById('main')) {
    const rootUrl = "http://127.0.0.1:8000";

    ReactDOM.createRoot(document.getElementById('main')).render(
        <React.StrictMode><ChatBox rootUrl={rootUrl} /></React.StrictMode>
    );
}

Здесь мы проверим, есть ли элемент с id 'main'. Если он существует, он переходит к рендерингу приложения React.

Как вы видите, есть ChatBox компонент. Скоро мы узнаем о нем больше.

Удалите resources/js/components/Example.jsxфайл и импортируйте Main.jsxкомпонент в app.js:

import './bootstrap';
import './components/Main.jsx';

Создайте файлы Message.jsx и MessageInput.jsx, чтобы вы могли использовать их в ChatBoxкомпоненте.

Компонент Messageполучит userIdаргументы message(поля) для отображения каждого сообщения в окне чата.

import React from "react";

const Message = ({ userId, message }) => {
    return (
        <div className={`row ${
        userId === message.user_id ? "justify-content-end" : ""
        }`}><div className="col-md-6"><small className="text-muted"><strong>{message.user.name} | </strong></small><small className="text-muted float-right">
                    {message.time}
                </small><div className={`alert alert-${
                userId === message.user_id ? "primary" : "secondary"
                }`} role="alert">
                    {message.text}
                </div></div></div>
    );
};

export default Message;

Компонент Message.jsxотображает отдельные сообщения в интерфейсе чата. Он получает userId и message props. В зависимости от того, соответствует ли отправитель сообщения текущему пользователю, он выравнивает сообщение по соответствующей стороне экрана.

Каждое сообщение включает в себя имя отправителя, временную метку и само содержимое сообщения, оформленное по-разному в зависимости от того, отправлено ли сообщение текущим пользователем или другим пользователем.

Компонент MessageInput позаботится о создании нового сообщения:

import React, { useState } from "react";

const MessageInput = ({ rootUrl }) => {
    const [message, setMessage] = useState("");

    const messageRequest = async (text) => {
        try {
            await axios.post(`${rootUrl}/message`, {
                text,
            });
        } catch (err) {
            console.log(err.message);
        }
    };

    const sendMessage = (e) => {
        e.preventDefault();
        if (message.trim() === "") {
            alert("Please enter a message!");
            return;
        }

        messageRequest(message);
        setMessage("");
    };

    return (
        <div className="input-group"><input onChange={(e) => setMessage(e.target.value)}
                   autoComplete="off"
                   type="text"
                   className="form-control"
                   placeholder="Message..."
                   value={message}
            />
            <div className="input-group-append"><button onClick={(e) => sendMessage(e)}
                        className="btn btn-primary"
                        type="button">Send</button></div></div>
    );
};

export default MessageInput;

Компонент MessageInput предоставляет поле ввода формы для ввода сообщений пользователями и отправки их в интерфейсе чата. При нажатии кнопки он запускает функцию отправки сообщения на сервер через запрос Axios POST на указанный rootUrl, который он получил от родительского ChatBoxкомпонента. Он также обрабатывает проверку, чтобы гарантировать, что пользователи не могут отправлять пустые сообщения. Вы можете настроить его позже, если захотите.

Теперь создайте ChatBox.jsx компонент, чтобы подготовить интерфейс:

import React, { useEffect, useRef, useState } from "react";
import Message from "./Message.jsx";
import MessageInput from "./MessageInput.jsx";

const ChatBox = ({ rootUrl }) => {
    const userData = document.getElementById('main')
        .getAttribute('data-user');

    const user = JSON.parse(userData);
    // `App.Models.User.${user.id}`;const webSocketChannel = `channel_for_everyone`;

    const [messages, setMessages] = useState([]);
    const scroll = useRef();

    const scrollToBottom = () => {
        scroll.current.scrollIntoView({ behavior: "smooth" });
    };

    const connectWebSocket = () => {
        window.Echo.private(webSocketChannel)
            .listen('GotMessage', async (e) => {
                // e.messageawait getMessages();
            });
    }

    const getMessages = async () => {
        try {
            const m = await axios.get(`${rootUrl}/messages`);
            setMessages(m.data);
            setTimeout(scrollToBottom, 0);
        } catch (err) {
            console.log(err.message);
        }
    };

    useEffect(() => {
        getMessages();
        connectWebSocket();

        return () => {
            window.Echo.leave(webSocketChannel);
        }
    }, []);

    return (
        <div className="row justify-content-center"><div className="col-md-8"><div className="card"><div className="card-header">Chat Box</div><div className="card-body"style={{height: "500px", overflowY: "auto"}}>
                        {
                            messages?.map((message) => (
                                <Message key={message.id}userId={user.id}message={message}
                                />
                            ))
                        }
                        <span ref={scroll}></span></div><div className="card-footer"><MessageInput rootUrl={rootUrl} /></div></div></div></div>
    );
};

export default ChatBox;

Компонент ChatBox управляет интерфейсом чата в приложении. Он извлекает и отображает сообщения с сервера с помощью WebSocket и HTTP-запросов.

Компонент отображает список сообщений, поле ввода сообщений и автоматически прокручивает список вниз при поступлении новых сообщений.

Он определяет канал WebSocket для обновлений сообщений в реальном времени. Вам нужно настроить этот канал, используя то же имя, которое было написано в routes/hannels.phpи в app/Events/GotMessage.phpзадании очереди.

Также leave() функция вызывается в useEffectфункции очистки для отмены подписки на канал WebSocket при размонтировании компонента. Это предотвращает утечки памяти и ненужные сетевые соединения, останавливая прослушивание компонентом обновлений на канале WebSocket после того, как он больше не нужен.

Запуск приложения

Теперь все готово и пора проверить приложение. Следуйте этим инструкциям:

Изображение Скриншот из терминала со всеми необходимыми командами

  • Сборка интерфейсных ресурсов (это не «вечно» работающая команда):
  • npm run build
  • Начните прослушивать события Laravel:
  • php artisan queue:listen
  • Запустите сервер WebSocket:
  • php artisan reverb:start
  • Запустите сервер (вы можете использовать альтернативу для своего приложения, например, локально запущенный сервер):
  • php artisan serve

После выполнения всех необходимых команд вы можете проверить приложение, перейдя по URL-адресу по умолчанию: http://127.0.0.1:8000.

Для тестирования вы можете зарегистрировать двух разных пользователей, позволить им войти в систему, отправлять сообщения от имени каждого из них и видеть окно чата.

Заключение

Теперь вы знаете, как создавать приложения реального времени с Laravel Reverb в новой версии Laravel. Благодаря этому вы можете реализовать коммуникации WebSocket в вашем полнофункциональном приложении и избежать использования дополнительных сторонних сервисов (вроде Pusher и Socket.io).