· Admin

Laravel 12 ile Push Notification: Firebase Cloud Messaging (FCM)

Kullanıcıya anlık bildirim göndermek istiyorsunuz ama kullanıcı sitenizde değil — tarayıcı kapalı, telefonda başka bir uygulama açık. İşte tam bu senaryo için Firebase Cloud Messaging (FCM) var.

Önceki yazıda Socket.IO + Redis ile gerçek zamanlı bildirim kurmuştuk. O sistem kullanıcı sitedeyken çalışıyor. FCM ise farklı: tarayıcı arka plandayken, hatta kapalıyken bile cihaza push notification gönderebiliyorsunuz. SMS gibi düşünün ama ücretsiz ve sınırsız.

Bu yazıda Laravel 12 + FCM entegrasyonunu sıfırdan kuracağız.

Socket.IO vs FCM: Hangisi Ne Zaman?

Bu iki teknolojiyi rakip olarak görmeyin, birbirini tamamlıyorlar:

Özellik Socket.IO + Redis FCM Push Notification
Kullanıcı sitede Anlık, gerçek zamanlı Gereksiz
Kullanıcı sitede değil Çalışmaz Cihaza push gönderir
Tarayıcı kapalı Çalışmaz Service Worker ile çalışır
Mobil uygulama Web only iOS + Android + Web
Maliyet Kendi sunucunuz Ücretsiz (Google)
Kurulum Redis + Node.js Firebase proje + Service Worker

Kendi projelerimde ikisini birlikte kullanıyorum: kullanıcı online'sa Socket.IO ile anlık güncelleme, offline'sa FCM ile push notification. Ama sadece biriyle başlamak istiyorsanız, FCM daha geniş kapsam sunuyor.

Hangi Paketi Kullanmalı?

FCM entegrasyonu için birkaç seçenek var:

Paket Avantaj Dezavantaj
kreait/laravel-firebase Resmi SDK wrapper, tam kontrol, tüm Firebase servisleri Kurulum biraz fazla
devkandil/notifire Hızlı kurulum, hazır migration ve endpoint, HasFcm trait Daha az esneklik
google/auth + manuel HTTP Sıfır bağımlılık, sadece HTTP v1 API Her şeyi kendin yazıyorsun

Bu yazıda kreait/laravel-firebase ile ilerleyeceğim — en çok kontrol sağlayan ve production'da güvenle kullanılan paket. Yazının sonunda Notifire ile hızlı kurulum alternatifini de göstereceğim.

Gereksinimler

  • Laravel 12
  • Firebase projesi (ücretsiz)
  • kreait/laravel-firebase paketi
  • HTTPS (FCM service worker'lar HTTPS gerektirir, localhost hariç)

Adım 1: Firebase Proje Kurulumu

Firebase Console'a gidin ve yeni proje oluşturun:

  1. Firebase Console > "Add project"
  2. Proje adı verin (örn: "projeman-notifications")
  3. Google Analytics opsiyonel, kapatabilirsiniz
  4. Proje oluşturulduktan sonra Project Settings > Cloud Messaging sekmesine gidin
  5. Web app ekleyin (</> ikonu), Firebase config bilgilerini kopyalayın
  6. Project Settings > Service Accounts > "Generate new private key" ile JSON dosyasını indirin

İndirdiğiniz JSON dosyasını Laravel projenizde storage/app/firebase-credentials.json olarak kaydedin.

Dikkat: Bu dosyayı .gitignore'a ekleyin, repository'ye push etmeyin:

echo "storage/app/firebase-credentials.json" >> .gitignore

Adım 2: Laravel Paketi Kurulumu

kreait/laravel-firebase paketi resmi Firebase Admin SDK'sının Laravel wrapper'ı:

composer require kreait/laravel-firebase
php artisan vendor:publish --provider="Kreait\Laravel\Firebase\ServiceProvider"

.env dosyasına Firebase credentials path'ini ekleyin:

FIREBASE_CREDENTIALS=storage/app/firebase-credentials.json

config/firebase.php otomatik oluşacak. Varsayılan ayarlar yeterli, değiştirmenize gerek yok.

Adım 3: Migration — FCM Token Saklama

Her kullanıcının tarayıcısı/cihazı için bir FCM token oluşur. Bu token'ları veritabanında saklamamız gerekiyor:

php artisan make:migration create_fcm_tokens_table
public function up(): void
{
    Schema::create('fcm_tokens', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->text('token');
        $table->string('device', 50)->default('web'); // web, android, ios
        $table->timestamps();

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

Bir kullanıcının birden fazla cihazı olabilir (telefon + laptop + tablet). Her cihaz ayrı token alır, hepsine ayrı bildirim gider.

Adım 4: FCM Token Kaydetme

Kullanıcı bildirim iznini verdikten sonra tarayıcı bir token üretir. Bu token'ı backend'e kaydetmemiz gerekiyor.

Route:

// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/fcm/token', [FcmController::class, 'storeToken']);
});

Controller:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FcmController extends Controller
{
    public function storeToken(Request $request)
    {
        $validated = $request->validate([
            'token' => 'required|string',
            'device' => 'nullable|string|in:web,android,ios',
        ]);

        $request->user()->fcmTokens()->updateOrCreate(
            ['token' => $validated['token']],
            ['device' => $validated['device'] ?? 'web']
        );

        return response()->json(['message' => 'Token kaydedildi.']);
    }
}

User model'ine relation ekleyin:

// App\Models\User
public function fcmTokens()
{
    return $this->hasMany(\App\Models\FcmToken::class);
}

FcmToken model:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class FcmToken extends Model
{
    protected $fillable = ['user_id', 'token', 'device'];

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

Adım 5: Bildirim Gönderme Servisi

Firebase Admin SDK ile bildirim gönderen bir servis yazalım:

namespace App\Services;

use Kreait\Firebase\Contract\Messaging;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Notification;
use Kreait\Firebase\Exception\MessagingException;

class FcmService
{
    public function __construct(
        private Messaging $messaging
    ) {}

    /**
     * Tek bir token'a bildirim gönder
     */
    public function sendToToken(string $token, string $title, string $body, array $data = []): bool
    {
        $message = CloudMessage::withTarget('token', $token)
            ->withNotification(Notification::create($title, $body))
            ->withData($data);

        try {
            $this->messaging->send($message);
            return true;
        } catch (MessagingException $e) {
            // Token geçersiz veya süresi dolmuşsa sil
            if ($this->isInvalidToken($e)) {
                \App\Models\FcmToken::where('token', $token)->delete();
            }
            report($e);
            return false;
        }
    }

    /**
     * Bir kullanıcının tüm cihazlarına bildirim gönder
     */
    public function sendToUser(int $userId, string $title, string $body, array $data = []): int
    {
        $tokens = \App\Models\FcmToken::where('user_id', $userId)
            ->pluck('token')
            ->toArray();

        if (empty($tokens)) {
            return 0;
        }

        return $this->sendToTokens($tokens, $title, $body, $data);
    }

    /**
     * Birden fazla token'a toplu bildirim gönder (multicast)
     */
    public function sendToTokens(array $tokens, string $title, string $body, array $data = []): int
    {
        $message = CloudMessage::new()
            ->withNotification(Notification::create($title, $body))
            ->withData($data);

        $report = $this->messaging->sendMulticast($message, $tokens);

        // Geçersiz token'ları temizle
        if ($report->hasFailures()) {
            $invalidTokens = [];
            foreach ($report->failures()->getItems() as $failure) {
                if ($this->isInvalidToken($failure->error())) {
                    $invalidTokens[] = $failure->target()->value();
                }
            }
            if (!empty($invalidTokens)) {
                \App\Models\FcmToken::whereIn('token', $invalidTokens)->delete();
            }
        }

        return $report->successes()->count();
    }

    /**
     * Tüm kullanıcılara bildirim gönder (topic kullanarak)
     */
    public function sendToTopic(string $topic, string $title, string $body, array $data = []): bool
    {
        $message = CloudMessage::withTarget('topic', $topic)
            ->withNotification(Notification::create($title, $body))
            ->withData($data);

        try {
            $this->messaging->send($message);
            return true;
        } catch (MessagingException $e) {
            report($e);
            return false;
        }
    }

    private function isInvalidToken(\Throwable $e): bool
    {
        $message = $e->getMessage();
        return str_contains($message, 'not-found')
            || str_contains($message, 'invalid-registration-token')
            || str_contains($message, 'registration-token-not-registered');
    }
}

Bu servis dört farklı gönderim destekliyor:

  • sendToToken: Tek bir cihaza
  • sendToUser: Bir kullanıcının tüm cihazlarına
  • sendToTokens: Belirli token listesine (multicast)
  • sendToTopic: Bir konuya abone olan herkese

Geçersiz token temizleme kritik. FCM token'ları kullanıcı tarayıcı verisini sildiğinde, uygulamayı kaldırdığında veya uzun süre kullanmadığında geçersiz olur. Temizlemezseniz her gönderimde hata alırsınız.

Adım 6: Laravel Notification Entegrasyonu

Laravel'in Notification sistemiyle entegre edelim ki $user->notify() ile kullanabilelim:

namespace App\Notifications;

use App\Services\FcmService;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

class PushNotification extends Notification
{
    use Queueable;

    public function __construct(
        private string $title,
        private string $body,
        private array $data = []
    ) {}

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

    public function toFcm($notifiable): array
    {
        return [
            'title' => $this->title,
            'body' => $this->body,
            'data' => $this->data,
        ];
    }
}

Custom notification channel:

namespace App\Channels;

use App\Services\FcmService;
use Illuminate\Notifications\Notification;

class FcmChannel
{
    public function __construct(private FcmService $fcm) {}

    public function send($notifiable, Notification $notification)
    {
        $data = $notification->toFcm($notifiable);

        $this->fcm->sendToUser(
            $notifiable->id,
            $data['title'],
            $data['body'],
            $data['data'] ?? []
        );
    }
}

AppServiceProvider'da channel'ı kaydedin:

use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Facades\Notification;

public function boot(): void
{
    Notification::resolved(function (ChannelManager $service) {
        $service->extend('fcm', function ($app) {
            return $app->make(\App\Channels\FcmChannel::class);
        });
    });
}

Artık herhangi bir yerde:

$user->notify(new PushNotification(
    title: 'Yeni Siparis',
    body: 'Siparis #1234 olusturuldu.',
    data: ['url' => '/admin/orders/1234']
));

Adım 7: Frontend — Service Worker

FCM'in tarayıcıda çalışması için bir Service Worker gerekiyor. Bu worker tarayıcı arka plandayken bile bildirim alabilir.

public/firebase-messaging-sw.js dosyası oluşturun (isim sabit, değiştirmeyin):

importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js');

firebase.initializeApp({
    apiKey: "YOUR_API_KEY",
    authDomain: "YOUR_PROJECT.firebaseapp.com",
    projectId: "YOUR_PROJECT_ID",
    messagingSenderId: "YOUR_SENDER_ID",
    appId: "YOUR_APP_ID"
});

const messaging = firebase.messaging();

// Arka plan bildirimi
messaging.onBackgroundMessage((payload) => {
    const { title, body } = payload.notification;
    self.registration.showNotification(title, {
        body: body,
        icon: '/icons/notification-icon.png',
        badge: '/icons/badge-icon.png',
        data: payload.data
    });
});

Ana sayfada (Blade template) izin isteme ve token alma:

<script type="module">
    import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js';
    import { getMessaging, getToken, onMessage } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging.js';

    const app = initializeApp({
        apiKey: "YOUR_API_KEY",
        authDomain: "YOUR_PROJECT.firebaseapp.com",
        projectId: "YOUR_PROJECT_ID",
        messagingSenderId: "YOUR_SENDER_ID",
        appId: "YOUR_APP_ID"
    });

    const messaging = getMessaging(app);

    // Bildirim izni iste
    async function requestPermission() {
        const permission = await Notification.requestPermission();
        if (permission !== 'granted') {
            console.log('Bildirim izni reddedildi');
            return;
        }

        // FCM token al
        const token = await getToken(messaging, {
            vapidKey: 'YOUR_VAPID_KEY'  // Firebase Console > Web Push certificates
        });

        // Token'i backend'e gonder
        await fetch('/api/fcm/token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
            },
            body: JSON.stringify({ token, device: 'web' })
        });

        console.log('FCM Token kaydedildi');
    }

    // Foreground bildirim (kullanici sitedeyken)
    onMessage(messaging, (payload) => {
        const { title, body } = payload.notification;

        // Tarayici notification API ile goster
        new Notification(title, {
            body: body,
            icon: '/icons/notification-icon.png'
        });

        // Veya kendi UI component'iniz ile gosterin
        // showToast(title, body);
    });

    // Sayfa yuklendiginde izin iste
    requestPermission();
</script>

VAPID key'i Firebase Console > Project Settings > Cloud Messaging > Web Push certificates altında bulabilirsiniz. "Generate key pair" ile oluşturun.

Adım 8: Pratik Kullanım Ornekleri

Siparis bildirimi:

// Yeni siparis geldiginde admin'e bildir
$admins = User::where('role', 'admin')->get();

Notification::send($admins, new PushNotification(
    title: 'Yeni Siparis!',
    body: "Siparis #{$order->id} - {$order->total} TL",
    data: ['url' => route('admin.orders.show', $order)]
));

Toplu bildirim (topic):

// Tum kullanicilara kampanya bildirimi
app(FcmService::class)->sendToTopic(
    topic: 'promotions',
    title: 'Yaz Indirimi Basladi!',
    body: 'Tum urunlerde %30 indirim. Kacirmayin!',
    data: ['url' => '/kampanyalar']
);

Zamanlanmis bildirim (queue ile):

// 1 saat sonra gonderilecek hatirlatma
PushNotification::dispatch($user, 'Sepetinizi unuttunuz!', 'Sepetinizdeki urunler sizi bekliyor.')
    ->delay(now()->addHour());

Test Etme

Firebase Console'da test bildirimi gonderebilirsiniz:

  1. Firebase Console > Messaging > "Create your first campaign"
  2. "Firebase Notification messages" secin
  3. Baslik ve metin girin
  4. "Send test message" > FCM token'i yapiştirin
  5. Gonder

Veya Laravel Tinker ile:

php artisan tinker
app(App\Services\FcmService::class)->sendToUser(
    userId: 1,
    title: 'Test Bildirimi',
    body: 'Bu bir test bildirimidir.',
    data: ['url' => '/admin/dashboard']
);

Production Notlari

Token yonetimi onemli. FCM token'lari surekli degisebilir. Token refresh mekanizmasi kurun:

// Frontend'de token degistiginde guncelle
messaging.onTokenRefresh(async () => {
    const newToken = await getToken(messaging);
    await fetch('/api/fcm/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: newToken, device: 'web' })
    });
});

Queue kullanin. FCM API'ye her istek 100-300ms surer. Bildirim gonderimini queue'ya atin ki kullanici isteği yavaşlamasın:

// ShouldQueue implement edin
class PushNotification extends Notification implements ShouldQueue
{
    use Queueable;
    // ...
}

Rate limit'e dikkat. FCM'in gunluk limiti cok yuksek (topic basina 1000/saniye) ama yine de toplu gonderimde multicast kullanin. sendMulticast() tek istekte 500 token'a kadar gonderebilir.

HTTP v1 API kullanin. Eski "legacy" API Haziran 2024'te kaldirildi. kreait/firebase-php v7+ zaten HTTP v1 kullaniyor, endise etmeyin.

Socket.IO + FCM Birlikte Kullanmak

En iyi deneyim icin ikisini birlikte kullanabilirsiniz:

// Event listener'da her iki kanali da kullan
class SendOrderNotification
{
    public function handle(OrderCreated $event)
    {
        $admin = User::find(1);

        // 1. Socket.IO ile anlik bildirim (kullanici sitedeyse)
        broadcast(new OrderNotificationEvent($event->order));

        // 2. FCM ile push bildirim (kullanici sitede degilse)
        $admin->notify(new PushNotification(
            title: 'Yeni Siparis',
            body: "#{$event->order->id} - {$event->order->total} TL"
        ));
    }
}

Frontend'de duplicate bildirimi onleyin: Socket.IO'dan mesaj geldiyse FCM bildirimini suppress edin.

Bu ikili yaklaşımı MesajBox projesinde kullanıyorum ve çok iyi çalışıyor. Kullanıcı admin paneldeyse anlık güncelleme görüyor, değilse telefonuna push düşüyor. Hiç bildirim kaçmıyor.

Alternatif: Notifire ile Hızlı Kurulum

Eğer tüm bu adımları tek tek yazmak istemiyorsanız, devkandil/notifire paketi her şeyi hazır sunuyor:

composer require devkandil/notifire
php artisan vendor:publish --provider="DevKandil\NotiFire\FcmServiceProvider"
php artisan migrate

Paket otomatik olarak fcm_tokens migration'ı, /fcm/token API endpoint'i (Sanctum korumalı) ve HasFcm trait'ini sağlıyor. User model'ine trait'i ekleyin:

use DevKandil\NotiFire\Traits\HasFcm;

class User extends Authenticatable
{
    use HasFcm;
}

Bildirim göndermek için:

use DevKandil\NotiFire\Facades\Fcm;

// Facade ile
Fcm::withTitle('Yeni Sipariş')
    ->withBody('Sipariş #1234 oluşturuldu.')
    ->withTokens($user->getFcmToken())
    ->sendNotification();

// Veya Laravel Notification ile
$user->notify(new OrderNotification($order));

Notification class'ında toFcm() metodu ile FcmMessage döndürürsünüz — priority, sound, click_action gibi detayları da ayarlayabilirsiniz.

Notifire hızlı prototip ve küçük projeler için ideal. Ama birden fazla cihaz yönetimi, topic bazlı gönderim veya multicast gibi ileri seviye ihtiyaçlarınız varsa, yazının başında anlattığım kreait/laravel-firebase yaklaşımı daha esnek.

Dikkat: Legacy API Kullanmayın

Google, FCM Legacy HTTP API'yi Haziran 2024'te tamamen kapattı. Eğer internette https://fcm.googleapis.com/fcm/send adresine istek atan eski örnekler görüyorsanız, bunlar artık çalışmıyor.

Yeni API endpoint'i: https://fcm.googleapis.com/v1/projects/{project_id}/messages:send

kreait/firebase-php v7+ ve devkandil/notifire zaten HTTP v1 API kullanıyor, bu konuda endişelenmenize gerek yok. Ama sıfırdan manuel entegrasyon yapıyorsanız, mutlaka HTTP v1 kullanın ve google/auth ile OAuth2 service account token'ı alın.