فصل ۱۰: پروژه‌های پیشرفته و مطالعات موردی

در این فصل پایانی، به بررسی پروژه‌های پیشرفته و مطالعات موردی برای استفاده از Laravel Reverb در نسخه ۱۲ لاراول می‌پردازیم. ابتدا نمونه‌هایی از پروژه‌های واقعی مانند داشبورد تحلیلی real-time، ویرایش مشارکتی (collaborative editing)، و مزایده‌های زنده (live auctions) را ارائه می‌دهیم. سپس نحوه انجام تست‌های end-to-end برای WebSocketها با استفاده از ابزارهایی مانند Pest یا Laravel Dusk را توضیح می‌دهیم. در ادامه، به عیب‌یابی مشکلات رایج مانند قطعی اتصالات و مسائل مقیاس‌پذیری پرداخته و به‌روزرسانی‌های مربوط به لاراول ۱۲ را بررسی می‌کنیم. در نهایت، نگاهی به آینده Reverb و ادغام آن با ابزارهای جدید لاراول و روندهای real-time خواهیم داشت. تمام کدها بر اساس مستندات رسمی لاراول ۱۲.x تنظیم شده‌اند.

مثال‌های واقعی: داشبورد تحلیلی Real-Time، Collaborative Editing، و Live Auctions

۱. داشبورد تحلیلی Real-Time

یک داشبورد تحلیلی real-time داده‌هایی مانند معیارهای فروش، رفتار کاربران، یا معیارهای سرور را به‌صورت زنده نمایش می‌دهد. در این مثال، یک داشبورد ساده برای نمایش تعداد کاربران آنلاین و فعالیت‌های اخیر آن‌ها پیاده‌سازی می‌کنیم.

تنظیم Backend

ایجاد یک Event برای ارسال معیارهای کاربران:

php artisan make:event AnalyticsUpdate

فایل app/Events/AnalyticsUpdate.php:

<?php

namespace App\Events;

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

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

    public $data;

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

    public function broadcastOn()
    {
        return new Channel('analytics');
    }

    public function broadcastAs()
    {
        return 'analytics.updated';
    }
}

ایجاد یک Job برای جمع‌آوری داده‌های تحلیلی:

php artisan make:job CollectAnalytics

فایل app/Jobs/CollectAnalytics.php:

<?php

namespace App\Jobs;

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

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

    public function handle()
    {
        $data = [
            'online_users' => User::where('last_activity', '>=', now()->subMinutes(5))->count(),
            'recent_actions' => User::whereNotNull('last_action')->take(10)->get(['id', 'name', 'last_action'])->toArray(),
        ];

        event(new AnalyticsUpdate($data));
    }
}

تنظیم یک Command برای اجرای دوره‌ای Job:

php artisan make:command RunAnalytics

فایل app/Console/Commands/RunAnalytics.php:

<?php

namespace App\Console\Commands;

use App\Jobs\CollectAnalytics;
use Illuminate\Console\Command;

class RunAnalytics extends Command
{
    protected $signature = 'analytics:run';
    protected $description = 'Collect and broadcast analytics data';

    public function handle()
    {
        CollectAnalytics::dispatch();
        $this->info('Analytics data collected and broadcasted.');
    }
}

اضافه کردن Command به Scheduler در app/Console/Kernel.php:

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('analytics:run')->everyMinute();
    }
}

تنظیم Frontend (React)

ایجاد یک کامپوننت React برای نمایش داشبورد:

import React, { useState, useEffect } from 'react';

const AnalyticsDashboard = () => {
    const [analytics, setAnalytics] = useState({ online_users: 0, recent_actions: [] });

    useEffect(() => {
        window.Echo.channel('analytics')
            .listen('.analytics.updated', (e) => {
                setAnalytics(e.data);
            });

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

    return (
        <div>
            <h1>Real-Time Analytics Dashboard</h1>
            <p>Online Users: {analytics.online_users}</p>
            <h2>Recent Actions</h2>
            <ul>
                {analytics.recent_actions.map(action => (
                    <li key={action.id}>{action.name}: {action.last_action}</li>
                ))}
            </ul>
        </div>
    );
};

export default AnalyticsDashboard;

فایل resources/js/app.jsx:

import React from 'react';
import { createRoot } from 'react-dom/client';
import AnalyticsDashboard from './components/AnalyticsDashboard';

const root = createRoot(document.getElementById('app'));
root.render(<AnalyticsDashboard />);

فایل resources/views/analytics.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>Analytics Dashboard</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    @vite(['resources/js/app.jsx'])
</head>
<body>
    <div id="app"></div>
</body>
</html>

اجرا

  1. سرور Reverb را اجرا کنید: php artisan reverb:start
  2. سرور لاراول را اجرا کنید: php artisan serve
  3. Scheduler را اجرا کنید: php artisan schedule:work
  4. فایل‌های جاوااسکریپت را کامپایل کنید: npm run dev
  5. به http://localhost:8000 بروید و داشبورد را مشاهده کنید.

۲. Collaborative Editing

یک سیستم ویرایش مشارکتی مانند Google Docs که کاربران می‌توانند به‌صورت real-time یک سند را ویرایش کنند.

تنظیم Backend

ایجاد مدل و migration برای سند:

php artisan make:model Document -m

فایل database/migrations/*_create_documents_table.php:

<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('documents', function (Blueprint $table) {
            $table->id();
            $table->text('content');
            $table->timestamps();
        });
    }

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

ایجاد Event برای به‌روزرسانی سند:

php artisan make:event DocumentUpdated

فایل app/Events/DocumentUpdated.php:

<?php

namespace App\Events;

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

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

    public $documentId;
    public $content;

    public function __construct($documentId, $content)
    {
        $this->documentId = $documentId;
        $this->content = $content;
    }

    public function broadcastOn()
    {
        return new PresenceChannel('document.' . $this->documentId);
    }

    public function broadcastAs()
    {
        return 'document.updated';
    }
}

ایجاد Route‌ها در routes/web.php:

<?php

use App\Events\DocumentUpdated;
use App\Models\Document;
use Illuminate\Support\Facades\Route;

Route::get('/document/{id}', function ($id) {
    $document = Document::findOrFail($id);
    return view('document', compact('document'));
});

Route::middleware('auth')->post('/document/{id}/update', function ($id) {
    $document = Document::findOrFail($id);
    $content = request('content');
    $document->update(['content' => $content]);
    event(new DocumentUpdated($id, $content));
    return response()->json(['status' => 'Document updated']);
});

تنظیم Frontend (Vue.js)

ایجاد کامپوننت Vue در resources/js/components/DocumentEditor.vue:

<template>
    <div>
        <h1>Collaborative Document Editor</h1>
        <textarea v-model="content" @input="updateDocument" rows="10" cols="50"></textarea>
    </div>
</template>

<script>
export default {
    props: ['initialContent', 'documentId'],
    data() {
        return {
            content: this.initialContent,
        };
    },
    mounted() {
        window.Echo.join(`document.${this.documentId}`)
            .listen('.document.updated', (e) => {
                if (e.content !== this.content) {
                    this.content = e.content;
                }
            });
    },
    methods: {
        updateDocument() {
            fetch(`/document/${this.documentId}/update`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                    'Authorization': `Bearer ${localStorage.getItem('token')}`
                },
                body: JSON.stringify({ content: this.content })
            });
        }
    }
};
</script>

فایل resources/views/document.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>Document Editor</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    @vite(['resources/js/app.js'])
</head>
<body>
    <div id="app">
        <document-editor :initial-content="{{ json_encode($document->content) }}" :document-id="{{ $document->id }}"></document-editor>
    </div>
</body>
</html>

اجرا

  1. Migration را اجرا کنید: php artisan migrate
  2. یک سند نمونه ایجاد کنید:
php artisan tinker
\App\Models\Document::create(['content' => 'Initial content']);
  1. سرور Reverb و لاراول را اجرا کنید و به /document/1 بروید.

۳. Live Auctions

یک سیستم مزایده زنده که کاربران می‌توانند پیشنهادات خود را به‌صورت real-time ثبت کنند.

تنظیم Backend

ایجاد مدل و migration برای مزایده:

php artisan make:model Auction -m

فایل database/migrations/*_create_auctions_table.php:

<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('auctions', function (Blueprint $table) {
            $table->id();
            $table->string('item');
            $table->decimal('current_bid', 8, 2)->default(0);
            $table->unsignedBigInteger('highest_bidder_id')->nullable();
            $table->timestamps();

            $table->foreign('highest_bidder_id')->references('id')->on('users')->onDelete('set null');
        });
    }

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

ایجاد Event برای به‌روزرسانی مزایده:

php artisan make:event AuctionBid

فایل app/Events/AuctionBid.php:

<?php

namespace App\Events;

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

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

    public $auctionId;
    public $bid;
    public $bidder;

    public function __construct($auctionId, $bid, $bidder)
    {
        $this->auctionId = $auctionId;
        $this->bid = $bid;
        $this->bidder = $bidder;
    }

    public function broadcastOn()
    {
        return new Channel('auction.' . $this->auctionId);
    }

    public function broadcastAs()
    {
        return 'auction.bid';
    }
}

ایجاد Route‌ها در routes/web.php:

<?php

use App\Events\AuctionBid;
use App\Models\Auction;
use Illuminate\Support\Facades\Route;

Route::get('/auction/{id}', function ($id) {
    $auction = Auction::findOrFail($id);
    return view('auction', compact('auction'));
});

Route::middleware('auth')->post('/auction/{id}/bid', function ($id) {
    $auction = Auction::findOrFail($id);
    $bid = request('bid');
    if ($bid > $auction->current_bid) {
        $auction->update([
            'current_bid' => $bid,
            'highest_bidder_id' => auth()->id()
        ]);
        event(new AuctionBid($id, $bid, auth()->user()));
        return response()->json(['status' => 'Bid placed']);
    }
    return response()->json(['error' => 'Bid too low'], 422);
});

تنظیم Frontend (Vanilla JS)

فایل resources/views/auction.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>Live Auction</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    @vite(['resources/js/app.js'])
</head>
<body>
    <h1>Auction: {{ $auction->item }}</h1>
    <p>Current Bid: $<span id="current-bid">{{ $auction->current_bid }}</span></p>
    @auth
        <form id="bid-form">
            <input type="number" name="bid" step="0.01" min="{{ $auction->current_bid + 0.01 }}" required>
            <button type="submit">Place Bid</button>
        </form>
    @endauth
    <ul id="bid-history"></ul>

    <script>
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });

        $('#bid-form').submit(function(e) {
            e.preventDefault();
            $.post('/auction/{{ $auction->id }}/bid', $(this).serialize(), () => {
                $(this)[0].reset();
            }).fail((xhr) => {
                alert(xhr.responseJSON.error);
            });
        });

        window.Echo.channel('auction.{{ $auction->id }}')
            .listen('.auction.bid', (e) => {
                $('#current-bid').text(e.bid);
                $('#bid-history').prepend(`<li>${e.bidder.name} bid $${e.bid}</li>`);
            });
    </script>
</body>
</html>

اجرا

  1. Migration را اجرا کنید: php artisan migrate
  2. یک مزایده نمونه ایجاد کنید:
   php artisan tinker
   \App\Models\Auction::create(['item' => 'Antique Vase', 'current_bid' => 100]);
  1. سرور Reverb و لاراول را اجرا کنید و به /auction/1 بروید.

تست End-to-End با Pest یا Dusk برای WebSocketها

برای اطمینان از عملکرد صحیح اپلیکیشن‌های real-time، باید تست‌های end-to-end (E2E) انجام دهید. ابزارهای Pest و Laravel Dusk برای این منظور مناسب هستند.

تست با Pest

Pest یک فریم‌ورک تست ساده و مدرن برای لاراول است. برای تست WebSocketها، می‌توانید از یک کلاینت WebSocket مانند ratchet/pawl استفاده کنید.

  1. نصب وابستگی‌ها:
   composer require --dev pestphp/pest ratchet/pawl
   php artisan pest:install
  1. ایجاد تست برای مزایده:
<?php

use App\Models\Auction;
use App\Models\User;
use Ratchet\Client\WebSocket;
use function Pest\Laravel\actingAs;

it('broadcasts auction bids', function () {
    $user = User::factory()->create();
    $auction = Auction::factory()->create(['current_bid' => 100]);

    $received = false;
    $message = null;

    \Ratchet\Client\connect('ws://localhost:8080/app/my-reverb-app?protocol=7&client=js')->then(function (WebSocket $conn) use ($auction, &$received, &$message) {
        $conn->send(json_encode([
            'command' => 'subscribe',
            'identifier' => json_encode(['channel' => 'auction.' . $auction->id])
        ]));

        $conn->on('message', function ($msg) use (&$received, &$message) {
            $data = json_decode($msg, true);
            if (isset($data['message']['data'])) {
                $message = json_decode($data['message']['data'], true);
                $received = true;
            }
        });
    });

    actingAs($user)->post('/auction/' . $auction->id . '/bid', ['bid' => 150]);

    // انتظار برای دریافت پیام
    $timeout = now()->addSeconds(5);
    while (!$received && now()->lessThan($timeout)) {
        usleep(100000); // 100ms صبر
    }

    expect($received)->toBeTrue();
    expect($message['bid'])->toBe(150);
    expect($message['bidder']['id'])->toBe($user->id);
});
  1. اجرای تست:
   php artisan reverb:start
   php artisan serve
   php artisan test

تست با Laravel Dusk

Dusk برای تست‌های مرورگر مناسب است و می‌تواند تعاملات WebSocket را در یک محیط واقعی تست کند.

  1. نصب Dusk:
   composer require --dev laravel/dusk
   php artisan dusk:install
  1. ایجاد تست Dusk:
<?php

namespace Tests\Browser;

use App\Models\Auction;
use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class AuctionDuskTest extends DuskTestCase
{
    public function testAuctionBidUpdates()
    {
        $user = User::factory()->create();
        $auction = Auction::factory()->create(['current_bid' => 100]);

        $this->browse(function (Browser $browser) use ($user, $auction) {
            $browser->loginAs($user)
                    ->visit('/auction/' . $auction->id)
                    ->type('bid', '150')
                    ->press('Place Bid')
                    ->waitForText('$150', 5)
                    ->assertSeeIn('#bid-history', 'bid $150');
        });
    }
}
  1. اجرای تست:
   php artisan reverb:start
   php artisan serve
   php artisan dusk

عیب‌یابی رایج (Disconnections, Scaling Issues) و به‌روزرسانی‌ها در Laravel 12

مشکلات رایج و راه‌حل‌ها

  1. قطعی اتصالات (Disconnections):

  2. علت: مشکلات شبکه، تنظیمات نادرست reverse proxy، یا محدودیت‌های سرور.

  3. راه‌حل:

    • بررسی لاگ‌های Reverb با --debug: sh php artisan reverb:start --debug
    • اطمینان از تنظیم صحیح هدرهای Upgrade و Connection در reverse proxy.
    • استفاده از keep-alive برای WebSocketها در فایل config/reverb.php: php 'ping_interval' => 30, // ارسال ping هر 30 ثانیه 'ping_timeout' => 120, // قطع اتصال پس از 120 ثانیه
  4. مشکلات مقیاس‌پذیری:

  5. علت: تعداد زیاد اتصالات یا پیام‌ها که سرور را تحت فشار قرار می‌دهد.

  6. راه‌حل:

    • استفاده از Redis برای توزیع پیام‌ها (فصل نهم).
    • فعال‌سازی ext-uv برای event loop (فصل هشتم).
    • تنظیم capacity در config/reverb.php برای محدود کردن اتصالات.
  7. مشکلات احراز هویت:

  8. علت: خطا در تنظیمات Sanctum یا Passport.

  9. راه‌حل:
    • بررسی endpoint /broadcasting/auth با: sh curl -H "Authorization: Bearer <token>" http://localhost:8000/broadcasting/auth
    • اطمینان از فعال بودن middleware EnsureFrontendRequestsAreStateful.

به‌روزرسانی‌های Laravel 12

  • Reverb به‌عنوان درایور پیش‌فرض: در لاراول ۱۲، Reverb به‌عنوان درایور broadcasting پیش‌فرض معرفی شده و جایگزین Pusher برای بسیاری از پروژه‌ها شده است.
  • پشتیبانی از ext-uv: بهبود عملکرد event loop با استفاده از libuv.
  • ادغام با Pulse: امکان نظارت پیشرفته‌تر بر اتصالات و پیام‌ها.
  • بهبودهای امنیتی: اضافه شدن قابلیت‌های rate limiting و رمزنگاری داخلی برای پیام‌ها.

آینده Reverb: ادغام با ابزارهای جدید لاراول و روندهای Real-Time

ادغام با ابزارهای جدید لاراول

  • Laravel Pulse: انتظار می‌رود در نسخه‌های آینده، Pulse کارت‌های سفارشی بیشتری برای Reverb ارائه دهد، مانند تجزیه‌وتحلیل الگوهای پیام یا مصرف منابع.
  • Laravel Octane: ادغام عمیق‌تر Reverb با Octane برای بهبود عملکرد در اپلیکیشن‌های real-time با استفاده از سرورهای Swoole یا RoadRunner.
  • Laravel Scout: امکان استفاده از Reverb برای به‌روزرسانی‌های real-time در نتایج جستجو.

روندهای Real-Time

  • WebSocketهای مقیاس‌پذیر: با افزایش استفاده از اپلیکیشن‌های real-time (مانند چت، گیمینگ، و IoT)، Reverb احتمالاً قابلیت‌های بیشتری برای مقیاس‌پذیری افقی ارائه خواهد داد.
  • پشتیبانی از پروتکل‌های جدید: ممکن است Reverb در آینده از پروتکل‌های پیشرفته‌تر مانند WebTransport پشتیبانی کند.
  • ادغام با AI: استفاده از Reverb برای اپلیکیشن‌های مبتنی بر AI که نیاز به پاسخ‌های real-time دارند، مانند چت‌بات‌های هوشمند یا تحلیل داده‌های زنده.

نکات و بهترین شیوه‌ها

  • تست جامع: همیشه تست‌های E2E را برای سناریوهای real-time اجرا کنید تا از عملکرد صحیح اطمینان حاصل کنید.
  • مانیتورینگ: از Pulse و ابزارهای خارجی مانند Prometheus برای نظارت مداوم استفاده کنید.
  • به‌روزرسانی مداوم: مستندات لاراول را برای ویژگی‌های جدید Reverb بررسی کنید.

تمرین پیشنهادی

  • یک سیستم اعلان برای داشبورد تحلیلی اضافه کنید که تغییرات مهم (مانند افزایش ناگهانی کاربران آنلاین) را گزارش دهد.
  • قابلیت undo/redo را به ویرایشگر مشارکتی اضافه کنید.
  • یک تست Dusk برای مزایده زنده بنویسید که چندین پیشنهاد را به‌صورت همزمان تست کند.

منابع

  • مستندات رسمی لاراول: laravel.com/docs/12.x/reverb
  • مستندات Pulse: laravel.com/docs/12.x/pulse
  • مستندات Pest: pestphp.com
  • مستندات Dusk: laravel.com/docs/12.x/dusk

این فصل پایان مجموعه آموزشی Laravel Reverb است. شما اکنون ابزارها و دانش لازم برای ساخت، تست، و استقرار اپلیکیشن‌های real-time پیشرفته را دارید!