· Admin

Laravel'de Gerçek Zamanlı Bildirimler: Socket.IO + Redis

Kullanıcı bir form gönderdi, admin panelde anında bildirim düştü — sayfa yenilemeden. Bunu yapmanın en temiz yolu Laravel Broadcasting + Socket.IO + Redis üçlüsü.

Bu yazıda sıfırdan kurulumu anlatacağım. Pusher gibi ücretli servislere gerek yok, kendi sunucunuzda çalışan açık kaynak bir stack kuracağız.

Mimari: Kim Kimle Konuşuyor?

Akış şöyle işliyor:

Kullanıcı form gönderdi
    → Laravel event dispatch etti
        → Event Redis'e publish edildi
            → Node.js server Redis'i dinliyor, mesajı aldı
                → Socket.IO ile bağlı olan tarayıcılara push etti
                    → Admin panelde bildirim göründü

Laravel tarafı event'i ateşliyor ve işi bırakıyor. Node.js tarafı Redis'ten mesajı alıp WebSocket üzerinden client'lara iletiyor. İki taraf birbirinden bağımsız çalışıyor.

Gereksinimler

  • Laravel 11+ (12 de çalışır)
  • Redis server (lokal veya remote)
  • Node.js (Socket.IO server için)
  • predis/predis veya phpredis extension

Adım 1: Redis ve Broadcasting Kurulumu

Önce gerekli paketleri yükleyelim:

composer require predis/predis
php artisan install:broadcasting
npm install --save laravel-echo socket.io-client

.env dosyasında broadcast driver'ı Redis olarak ayarlayın:

BROADCAST_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

Önemli bir detay: Redis config'inde prefix boş olmalı, yoksa channel isimleri uyuşmaz. config/database.php'de Redis options kısmında:

'redis' => [
    'client' => env('REDIS_CLIENT', 'predis'),
    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => '',  // Bu boş olmalı!
    ],
    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_DB', '0'),
    ],
],

prefix ayarını boş bırakmazsanız Laravel laravel_database_posts diye publish eder ama Node.js tarafı posts channel'ını dinler — birbirini bulamaz. Bu hatayı debug etmek can sıkıcı.

Adım 2: Event Oluşturma

Yeni bir post oluşturulduğunda tetiklenecek event'i yazalım:

php artisan make:event PostCreated
namespace App\Events;

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

class PostCreated implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public $post
    ) {}

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

    public function broadcastAs(): string
    {
        return 'created';
    }

    public function broadcastWith(): array
    {
        return [
            'id' => $this->post->id,
            'title' => $this->post->title,
            'message' => "Yeni yazı: '{$this->post->title}'",
            'time' => $this->post->created_at->format('H:i'),
        ];
    }
}

Birkaç not:

  • ShouldBroadcastNow kullanıyoruz, ShouldBroadcast değil. Fark şu: ShouldBroadcast event'i queue'ya atar, ShouldBroadcastNow anında gönderir. Development'ta anında göndermek debug'u kolaylaştırır. Production'da queue kullanmanızı öneririm.
  • broadcastAs() ile event ismini kendimiz belirliyoruz. Belirlemezseniz Laravel class isminin tamamını (namespace dahil) kullanır ki client tarafında eşleştirmek zor olur.
  • broadcastWith() ile sadece gerekli veriyi gönderiyoruz. Tüm model'i göndermek gereksiz veri transferi ve potansiyel güvenlik açığı.

Adım 3: Controller'dan Event Tetikleme

namespace App\Http\Controllers;

use App\Events\PostCreated;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'body' => 'required',
        ]);

        $post = Post::create([
            'user_id' => auth()->id(),
            'title' => $validated['title'],
            'body' => $validated['body'],
        ]);

        event(new PostCreated($post));

        return back()->with('success', 'Yazı oluşturuldu.');
    }
}

event() helper'ı çağırdığımız anda Laravel bu event'i Redis'e publish ediyor. Buraya kadar olan kısım tamamen Laravel tarafı.

Adım 4: Node.js Socket.IO Server

Şimdi Redis'ten gelen mesajları WebSocket'e aktaracak Node.js server'ı yazacağız. Proje kök dizininde:

npm install ioredis socket.io

server.js dosyası:

import { createServer } from 'http';
import { Redis } from 'ioredis';
import { Server } from 'socket.io';

const httpServer = createServer();

const io = new Server(httpServer, {
    cors: { origin: '*' }
});

const redis = new Redis();

// Redis'ten gelen mesajları dinle
redis.subscribe('posts', (err, count) => {
    if (err) {
        console.error('Redis subscribe hatası:', err.message);
        return;
    }
    console.log(`${count} channel dinleniyor`);
});

// Mesaj geldiğinde tüm bağlı client'lara ilet
redis.on('message', (channel, message) => {
    const event = JSON.parse(message);
    console.log(`[${channel}] ${event.event}:`, event.data);
    io.emit(event.event, channel, event.data);
});

// Bağlantı logları
io.on('connection', (socket) => {
    console.log(`Client bağlandı: ${socket.id}`);
    socket.on('disconnect', () => {
        console.log(`Client ayrıldı: ${socket.id}`);
    });
});

httpServer.listen(6001, () => {
    console.log('Socket.IO server: http://localhost:6001');
});

Bu server Redis'in posts channel'ını subscribe ediyor. Laravel bir event publish ettiğinde Redis bu server'a iletiyor, server da bağlı tüm tarayıcılara push ediyor.

Çalıştırmak için:

node server.js

Terminalde "1 channel dinleniyor" mesajını görmelisiniz.

Adım 5: Frontend — Laravel Echo

resources/js/echo.js dosyasında Echo'yu Socket.IO ile yapılandırın:

import Echo from 'laravel-echo';
import io from 'socket.io-client';

window.io = io;

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});

Sonra Blade template'inizde bildirimleri dinleyin:

<div id="notifications"></div>

<script type="module">
    window.Echo.channel('posts')
        .listen('.created', (data) => {
            const container = document.getElementById('notifications');
            const alert = document.createElement('div');
            alert.className = 'alert alert-info';
            alert.innerHTML = `<strong>${data.time}</strong> — ${data.message}`;
            container.prepend(alert);
        });
</script>

.created başındaki nokta önemli — broadcastAs() ile custom isim verdiğimizde Echo'da noktayla başlatmamız gerekiyor, yoksa Laravel namespace prefix'i ekler.

Test Etme

Üç terminal açın:

# Terminal 1: Laravel server
php artisan serve

# Terminal 2: Socket.IO server
node server.js

# Terminal 3: Vite (frontend build)
npm run dev

Bir tarayıcıda admin olarak giriş yapıp posts sayfasını açın. Başka bir tarayıcıda (veya incognito'da) normal kullanıcı olarak giriş yapıp yeni bir post oluşturun. Admin tarafında sayfa yenilenmeden bildirim düşecek.

Production İçin Notlar

Development'ta her şey localhost'ta çalışıyor, production'da birkaç şeye dikkat edin:

PM2 ile Node.js server'ı ayakta tutun:

npm install -g pm2
pm2 start server.js --name socket-server
pm2 save
pm2 startup

Nginx reverse proxy ile 6001 portunu gizleyin:

location /socket.io/ {
    proxy_pass http://127.0.0.1:6001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Bu sayede client tarafında port belirtmenize gerek kalmaz, Echo config'inde host: window.location.hostname yeterli olur.

ShouldBroadcastNow yerine ShouldBroadcast kullanın ve queue worker çalıştırın. Broadcast işlemi ana request'i yavaşlatmasın:

php artisan queue:work redis --queue=default

Private channel kullanmayı düşünün. Bu örnekte public channel kullandık — herkes dinleyebilir. Admin-only bildirimler için PrivateChannel kullanıp auth middleware ekleyin. Ama bu durumda Node.js server yerine Laravel Reverb veya Soketi gibi auth destekli bir WebSocket server'a geçmeniz gerekir.

Alternatifler

Bu yazıda Socket.IO + Redis stack'ini anlattım ama başka seçenekler de var:

  • Laravel Reverb — Laravel'in resmi WebSocket server'ı. PHP tabanlı, Node.js gerektirmiyor. Private/presence channel desteği var. Yeni projelerde benim ilk tercihim bu olurdu.
  • Pusher — Hosted servis, kurulum derdi yok ama ücretsiz planın limitleri dar. Küçük projelerde iş görür.
  • Soketi — Pusher API uyumlu, self-hosted. Pusher client kütüphaneleriyle çalışır.

Hangisini seçerseniz seçin, temel konsept aynı: Laravel event dispatch eder, bir WebSocket server client'lara push eder. Bir kez anladığınızda araç değiştirmek kolay.