إرسال Push Notifications إلى Android وiOS في Laravel باستخدام Firebase Cloud Messaging FCM

تُعتبر الإشعارات الفورية Push Notifications من أهم المكونات في تطبيقات الهواتف الذكية، فهي تسمح للـ Backend بإرسال رسائل وتنبيهات إلى أجهزة المستخدمين حتى عندما لا يكون التطبيق مفتوحاً.

إذا كنت تستخدم Laravel كـ Backend لتطبيق Android أو iOS، فيمكن الاعتماد على Firebase Cloud Messaging - FCM لإرسال الإشعارات إلى التطبيق.

في هذا الدليل سنركز على جانب Laravel، بداية من إعداد Firebase والحصول على بيانات المصادقة، مروراً بتخزين FCM Tokens، وحتى إنشاء Service احترافي لإرسال الإشعارات باستخدام FCM HTTP v1 API.

لن نتطرق بالتفصيل إلى كود استقبال الإشعارات في Flutter أو Android أو iOS؛ فهذا الجزء يقع ضمن مسؤولية تطبيق الهاتف، بينما سيكون تركيزنا هنا على إعداد الـ Backend.

ما هو Firebase Cloud Messaging؟

Firebase Cloud Messaging أو FCM هي خدمة من Google تسمح بإرسال Push Notifications ورسائل Data إلى تطبيقات:

  • Android.
  • iOS.
  • Web.
  • منصات أخرى مدعومة بواسطة Firebase.

في السيناريو المعتاد يكون التسلسل بالشكل التالي:

Laravel Backend
       │
       ▼
Firebase Cloud Messaging
       │
       ├────► Android Device
       │
       ├────► iPhone
       │
       └────► Other Devices

Laravel لا يتصل بالهاتف مباشرة، بل يقوم بإرسال الرسالة إلى FCM، ثم تتولى Firebase عملية إيصالها إلى الجهاز المستهدف.

كيف يعمل النظام؟

بشكل مبسط:

  1. يقوم تطبيق الهاتف بالتسجيل لدى Firebase.
  2. يحصل التطبيق على FCM Registration Token خاص بنسخة التطبيق المثبتة على الجهاز.
  3. يرسل تطبيق الهاتف هذا Token إلى Laravel Backend.
  4. يقوم Laravel بتخزينه في قاعدة البيانات.
  5. عندما نريد إرسال إشعار، يرسل Laravel رسالة إلى FCM HTTP v1 API.
  6. FCM تقوم بإرسال الإشعار إلى الجهاز المقصود.
Mobile App
   │
   │ FCM Token
   ▼
Laravel API
   │
   │ Store Token
   ▼
Database


Laravel
   │
   │ HTTP v1
   ▼
Firebase FCM
   │
   ▼
Mobile Device

FCM HTTP v1 وليس Legacy API

قد تجد العديد من الشروحات القديمة تستخدم:

https://fcm.googleapis.com/fcm/send

وتضع Header بالشكل:

Authorization: key=SERVER_KEY

هذه الطريقة تنتمي إلى Legacy FCM API وليست الطريقة التي ينبغي بناء تطبيق جديد عليها.

الطريقة الحديثة هي:

POST
https://fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send

وتستخدم:

Authorization: Bearer ACCESS_TOKEN

والـ Access Token يتم إنشاؤه باستخدام Service Account عبر OAuth 2.0.

1. إنشاء مشروع Firebase

توجه إلى:

Firebase Console

ثم:

  1. اضغط على Create a project.
  2. اختر اسم المشروع.
  3. أكمل إعداد المشروع.
  4. أضف تطبيق Android أو iOS الخاص بك إلى المشروع.

من المهم أن يكون تطبيق الهاتف وLaravel يستخدمان نفس Firebase Project المخصص للنظام.

2. الحصول على Firebase Project ID

من Firebase Console توجه إلى:

Project Settings
→ General

وستجد:

Project ID

مثلاً:

my-mobile-app-12345

سنحتاج هذه القيمة لإنشاء عنوان HTTP v1 الخاص بإرسال الرسائل:

https://fcm.googleapis.com/v1/projects/my-mobile-app-12345/messages:send

3. إنشاء Service Account

بدلاً من Server Key القديم، تستخدم FCM HTTP v1 بيانات اعتماد Service Account.

من Firebase Console انتقل إلى:

Project Settings
→ Service Accounts

ومن هناك يمكنك إنشاء Private Key بصيغة JSON للحساب الخدمي.

سيكون الملف مشابهاً للبنية التالية:

{
    "type": "service_account",
    "project_id": "my-project",
    "private_key_id": "...",
    "private_key": "...",
    "client_email": "...",
    "client_id": "..."
}

4. تخزين Service Account داخل Laravel

يمكن مثلاً إنشاء:

storage/app/firebase/firebase-service-account.json

ثم وضع ملف Service Account بداخله.

لا تضع الملف داخل:

public/

لأن أي ملف داخل Public قد يصبح متاحاً عبر الويب إذا أخطئ إعداد الخادم.

5. إعداد ملف .env

أضف Firebase Project ID:

FIREBASE_PROJECT_ID=my-mobile-app-12345

ويمكن كذلك تحديد مسار ملف Service Account:

FIREBASE_CREDENTIALS=/full/path/to/firebase-service-account.json

أو الاحتفاظ به داخل Storage واستخدام المسار من ملف Configuration.

6. إضافة إعدادات Firebase إلى Laravel

داخل:

config/services.php

أضف:

'firebase' => [

    'project_id' => env('FIREBASE_PROJECT_ID'),

    'credentials' => env(
        'FIREBASE_CREDENTIALS',
        storage_path('app/firebase/firebase-service-account.json')
    ),

],

بهذه الطريقة لا نحتاج إلى استدعاء env() مباشرة داخل Services أو Controllers.

7. تثبيت Google Auth Library

تحتاج FCM HTTP v1 إلى OAuth 2.0 Access Token.

يمكن استخدام مكتبة Google الرسمية لـ PHP:

composer require google/auth

هذه المكتبة مسؤولة عن قراءة Service Account وإنشاء Access Token يمكن استخدامه للوصول إلى Firebase Messaging API.

8. ما هو FCM Token؟

عند تثبيت التطبيق وتشغيل Firebase Messaging، يحصل التطبيق على Registration Token يمكن استخدامه لاستهداف نسخة التطبيق على الجهاز.

مثال تقريبي:

fV8D7nPq4R....:APA91b...

تطبيق الهاتف هو المسؤول عن الحصول على هذا Token وإرساله إلى Laravel API.

لا يقوم Laravel بإنشاء FCM Token الخاص بالجهاز.

9. لماذا لا يفضل تخزين token داخل users مباشرة؟

أحد الأخطاء الشائعة هو إضافة:

users.fcm_token

هذا يعمل إذا كان كل مستخدم يمتلك جهازاً واحداً فقط.

لكن في الواقع قد يسجل المستخدم الدخول من:

  • هاتف Android.
  • iPhone.
  • Tablet.
  • هاتف إضافي.

وبالتالي قد يمتلك المستخدم عدة FCM Tokens.

لهذا السبب من الأفضل إنشاء جدول منفصل للأجهزة.

10. إنشاء جدول user_devices

نفذ:

php artisan make:model UserDevice -m

ثم داخل Migration:

Schema::create('user_devices', function (Blueprint $table) {

    $table->id();

    $table->foreignId('user_id')
        ->constrained()
        ->cascadeOnDelete();

    $table->text('fcm_token')->unique();

    $table->string('platform')->nullable();

    $table->string('device_name')->nullable();

    $table->timestamp('last_seen_at')->nullable();

    $table->timestamps();

});

ثم:

php artisan migrate

11. لماذا نستخدم جدول أجهزة منفصلاً؟

يمكن أن تصبح العلاقة:

User
 │
 ├── Android Phone
 │      └── FCM Token
 │
 ├── iPhone
 │      └── FCM Token
 │
 └── Tablet
        └── FCM Token

وبالتالي نستطيع إرسال الإشعار إلى جميع أجهزة المستخدم بدلاً من جهاز واحد فقط.

12. إنشاء العلاقة داخل User Model

public function devices()
{
    return $this->hasMany(UserDevice::class);
}

وداخل:

app/Models/UserDevice.php

نضيف:

class UserDevice extends Model
{
    protected $fillable = [
        'user_id',
        'fcm_token',
        'platform',
        'device_name',
        'last_seen_at',
    ];

    protected function casts(): array
    {
        return [
            'last_seen_at' => 'datetime',
        ];
    }

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

13. API لتسجيل FCM Token

يجب أن يقوم تطبيق الهاتف بإرسال Token إلى Laravel بعد تسجيل الدخول أو عند تحديث Token.

مثلاً:

POST /api/device-token

والبيانات:

{
    "fcm_token": "TOKEN",
    "platform": "android",
    "device_name": "Samsung S25"
}

14. تخزين Token في Laravel

public function storeDeviceToken(Request $request)
{
    $validated = $request->validate([

        'fcm_token' => [
            'required',
            'string',
        ],

        'platform' => [
            'nullable',
            'in:android,ios',
        ],

        'device_name' => [
            'nullable',
            'string',
            'max:255',
        ],

    ]);

    UserDevice::updateOrCreate(

        [
            'fcm_token' => $validated['fcm_token'],
        ],

        [
            'user_id' => $request->user()->id,

            'platform' => $validated['platform'] ?? null,

            'device_name' => $validated['device_name'] ?? null,

            'last_seen_at' => now(),
        ]

    );

    return response()->json([
        'message' => 'Device token saved successfully',
    ]);
}

استخدام:

updateOrCreate()

يمنع إنشاء نسخ متكررة من Token نفسه.

15. إنشاء Firebase Messaging Service

بدلاً من وضع كود Firebase داخل كل Controller، الأفضل إنشاء Service مستقلة.

أنشئ:

app/Services/FirebaseMessagingService.php

16. إنشاء OAuth Access Token

نستخدم Google Auth Library:

use Google\Auth\Credentials\ServiceAccountCredentials;

والـ Scope المطلوب لـ Firebase Messaging هو:

https://www.googleapis.com/auth/firebase.messaging

ثم يمكن إنشاء Access Token:

private function getAccessToken(): string
{
    $credentialsPath = config(
        'services.firebase.credentials'
    );

    if (!file_exists($credentialsPath)) {

        throw new RuntimeException(
            'Firebase service account file not found.'
        );

    }

    $serviceAccount = json_decode(
        file_get_contents($credentialsPath),
        true,
        512,
        JSON_THROW_ON_ERROR
    );

    $credentials = new ServiceAccountCredentials(
        [
            'https://www.googleapis.com/auth/firebase.messaging'
        ],
        $serviceAccount
    );

    $token = $credentials->fetchAuthToken();

    if (!isset($token['access_token'])) {

        throw new RuntimeException(
            'Unable to generate Firebase access token.'
        );

    }

    return $token['access_token'];
}

17. لماذا لا نخزن Access Token بشكل دائم؟

الـ OAuth Access Token الخاص بـ FCM HTTP v1 قصير العمر.

وهذا أحد الاختلافات الأمنية المهمة عن Server Key القديم.

الـ Service Account يحتفظ بالمفتاح الأساسي، ثم يقوم Backend بالحصول على Access Token مؤقت عند الحاجة.

Service Account
      │
      ▼
OAuth 2.0
      │
      ▼
Short-lived Access Token
      │
      ▼
FCM HTTP v1

18. إنشاء دالة إرسال Notification

نستخدم Laravel HTTP Client بدلاً من كتابة cURL يدوياً.

use Illuminate\Support\Facades\Http;

ثم:

public function sendToToken(
    string $token,
    string $title,
    string $body,
    array $data = []
): array {

    $accessToken = $this->getAccessToken();

    $projectId = config(
        'services.firebase.project_id'
    );

    $url = sprintf(
        'https://fcm.googleapis.com/v1/projects/%s/messages:send',
        $projectId
    );

    $data = collect($data)
        ->mapWithKeys(function ($value, $key) {
            return [$key => (string) $value];
        })
        ->all();

    $response = Http::withToken($accessToken)
        ->acceptJson()
        ->post($url, [

            'message' => [

                'token' => $token,

                'notification' => [
                    'title' => $title,
                    'body' => $body,
                ],

                'data' => $data,

                'android' => [
                    'priority' => 'high',
                ],

                'apns' => [

                    'headers' => [
                        'apns-priority' => '10',
                    ],

                    'payload' => [

                        'aps' => [
                            'sound' => 'default',
                        ],

                    ],

                ],

            ],

        ]);

    $response->throw();

    return $response->json();
}

19. FirebaseMessagingService كاملاً

<?php

namespace App\Services;

use Google\Auth\Credentials\ServiceAccountCredentials;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class FirebaseMessagingService
{
    private function getAccessToken(): string
    {
        $credentialsPath = config(
            'services.firebase.credentials'
        );

        if (!file_exists($credentialsPath)) {

            throw new RuntimeException(
                'Firebase service account file not found.'
            );

        }

        $serviceAccount = json_decode(
            file_get_contents($credentialsPath),
            true,
            512,
            JSON_THROW_ON_ERROR
        );

        $credentials = new ServiceAccountCredentials(
            [
                'https://www.googleapis.com/auth/firebase.messaging'
            ],
            $serviceAccount
        );

        $token = $credentials->fetchAuthToken();

        if (!isset($token['access_token'])) {

            throw new RuntimeException(
                'Unable to generate Firebase access token.'
            );

        }

        return $token['access_token'];
    }


    public function sendToToken(
        string $token,
        string $title,
        string $body,
        array $data = []
    ): array {

        $accessToken = $this->getAccessToken();

        $projectId = config(
            'services.firebase.project_id'
        );

        $url = sprintf(
            'https://fcm.googleapis.com/v1/projects/%s/messages:send',
            $projectId
        );

        $data = collect($data)
            ->mapWithKeys(function ($value, $key) {

                return [
                    $key => (string) $value
                ];

            })
            ->all();


        $response = Http::withToken($accessToken)
            ->acceptJson()
            ->timeout(10)
            ->retry(2, 500)
            ->post($url, [

                'message' => [

                    'token' => $token,

                    'notification' => [

                        'title' => $title,

                        'body' => $body,

                    ],

                    'data' => $data,

                    'android' => [

                        'priority' => 'high',

                    ],

                    'apns' => [

                        'headers' => [

                            'apns-priority' => '10',

                        ],

                        'payload' => [

                            'aps' => [

                                'sound' => 'default',

                            ],

                        ],

                    ],

                ],

            ]);

        $response->throw();

        return $response->json();
    }
}

20. ما الفرق بين notification وdata؟

FCM تسمح بإرسال Notification Payload:

'notification' => [

    'title' => 'خبر جديد',

    'body' => 'تم إضافة مقال جديد',

]

وData Payload:

'data' => [

    'screen' => 'article',

    'article_id' => '150',

]

الـ Notification يحتوي عادة على المعلومات التي ستظهر للمستخدم.

أما Data فيحتوي معلومات يستخدمها التطبيق لتنفيذ منطق معين.

مثلاً عند الضغط على الإشعار يمكن لتطبيق الهاتف قراءة:

screen = article
article_id = 150

ثم فتح المقال رقم 150.

21. إرسال إشعار إلى مستخدم

داخل Controller يمكن حقن Service:

use App\Services\FirebaseMessagingService;

class NotificationController extends Controller
{
    public function send(
        Request $request,
        FirebaseMessagingService $firebase
    ) {

        $user = User::with('devices')
            ->findOrFail($request->user_id);

        foreach ($user->devices as $device) {

            $firebase->sendToToken(

                $device->fcm_token,

                $request->title,

                $request->body,

                [
                    'screen' => 'notifications',
                ]

            );

        }

        return response()->json([
            'message' => 'Notification sent successfully',
        ]);
    }
}

22. إرسال إشعار عند إضافة مقال جديد

لنفترض أننا نريد إرسال إشعار بعد إضافة Article.

public function store(
    Request $request,
    FirebaseMessagingService $firebase
) {

    $article = Article::create([

        'title_ar' => $request->title_ar,

        'text_ar' => $request->text_ar,

    ]);


    UserDevice::query()
        ->select('id', 'fcm_token')
        ->chunkById(
            500,
            function ($devices) use ($firebase, $article) {

                foreach ($devices as $device) {

                    $firebase->sendToToken(

                        $device->fcm_token,

                        'مقال جديد',

                        $article->title_ar,

                        [
                            'screen' => 'article',

                            'article_id' => $article->id,
                        ]

                    );

                }

            }
        );


    return redirect('/admin/article')
        ->with(
            'success',
            'تم إضافة المقال بنجاح'
        );
}

23. لا ترسل آلاف الإشعارات داخل HTTP Request

المثال السابق يشرح المبدأ، لكنه ليس التصميم الأفضل عندما يكون لديك عدد كبير من المستخدمين.

إذا كان لديك مثلاً:

50,000 devices

ثم أرسلت الإشعارات داخل Controller، سيبقى HTTP Request يعمل لفترة طويلة وقد ينتهي بـ Timeout.

الحل الصحيح هو استخدام:

Laravel Queues

24. إنشاء Job للإشعارات

نفذ:

php artisan make:job SendPushNotification

مثلاً:

class SendPushNotification implements ShouldQueue
{
    use Queueable;

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

    public function handle(
        FirebaseMessagingService $firebase
    ): void {

        $firebase->sendToToken(

            $this->token,

            $this->title,

            $this->body,

            $this->data

        );

    }
}

ثم بدل الإرسال المباشر:

$firebase->sendToToken(...);

استخدم:

SendPushNotification::dispatch(

    $device->fcm_token,

    'مقال جديد',

    $article->title_ar,

    [
        'screen' => 'article',

        'article_id' => $article->id,
    ]

);

25. لماذا Queues مهمة؟

بدلاً من:

User
 ↓
POST Article
 ↓
Send 10,000 Notifications
 ↓
Wait...
 ↓
HTTP Response

تصبح البنية:

User
 ↓
POST Article
 ↓
Save Article
 ↓
Dispatch Jobs
 ↓
HTTP Response


Queue Workers
      │
      ├── Send Notification
      ├── Send Notification
      ├── Send Notification
      └── ...

وبذلك لا ينتظر المستخدم انتهاء عملية إرسال جميع الإشعارات.

26. التعامل مع Token غير صالح

FCM Token ليس قيمة دائمة بالضرورة.

قد يصبح Token غير صالح عندما:

  • يقوم المستخدم بإزالة التطبيق.
  • يتم إعادة تثبيت التطبيق.
  • تتغير بيانات Firebase الخاصة بنسخة التطبيق.
  • يصبح تسجيل الجهاز قديماً أو منتهي الصلاحية.

لذلك يجب مراقبة رد FCM.

من الأخطاء المهمة:

UNREGISTERED

والذي يدل على أن التسجيل لم يعد صالحاً.

في هذه الحالة ينبغي حذف Token من قاعدة البيانات وعدم محاولة إرسال إشعارات إليه مستقبلاً.

27. الاحتفاظ بتاريخ آخر تحديث للجهاز

لهذا أضفنا:

last_seen_at

إلى جدول الأجهزة.

عندما يرسل التطبيق Token إلى Backend نقوم بتحديث:

'last_seen_at' => now()

وهذا يسمح بمعرفة الأجهزة التي ما زالت نشطة وتنظيف registrations القديمة حسب سياسة التطبيق.

28. Token Refresh

لا تفترض أن FCM Token لن يتغير.

يجب أن يكون تطبيق Android أو iOS قادراً على إرسال Token الجديد إلى Laravel عندما تقوم Firebase بتحديث Registration الخاص بنسخة التطبيق.

التدفق الصحيح:

FCM Registration Changed
        │
        ▼
Mobile Application
        │
        ▼
POST New Registration
        │
        ▼
Laravel
        │
        ▼
Update Database

29. إرسال إشعار إلى Topic

ليس من الضروري دائماً إرسال رسالة لكل Device Token بشكل منفصل.

تدعم Firebase ما يسمى:

Topics

مثلاً يمكن جعل المستخدمين يشتركون في:

news

ثم إرسال:

'message' => [

    'topic' => 'news',

    'notification' => [

        'title' => 'خبر جديد',

        'body' => 'تم نشر خبر جديد',

    ],

]

وبذلك تقوم Firebase بتوزيع الرسالة على الأجهزة المشتركة في Topic.

30. متى نستخدم Topic؟

Topics مناسبة لرسائل مثل:

  • الأخبار العامة.
  • تحديثات السوق.
  • العروض العامة.
  • التنبيهات المشتركة بين مجموعة كبيرة من المستخدمين.

لكن إذا كان الإشعار يحتوي بيانات خاصة بمستخدم محدد، فيجب استهداف جهاز المستخدم أو مستخدمي النظام وفق صلاحيات التطبيق بدلاً من Topic عام.

31. Android وiOS من نفس Laravel Backend

لا تحتاج لإنشاء Backend منفصل لكل منصة.

نفس FCM HTTP v1 Endpoint يستطيع إرسال رسائل إلى Android وiOS.

Laravel
  │
  ▼
FCM HTTP v1
  │
  ├──── Android
  │
  └──── iOS
          │
          ▼
         APNs

بالنسبة إلى iOS، تقوم Firebase بالتكامل مع خدمة Apple Push Notification Service أو APNs بعد إعداد بيانات Apple المناسبة داخل Firebase.

32. إعداد iOS

حتى تستطيع FCM إرسال Push Notifications إلى تطبيق iOS يجب إعداد تكامل APNs داخل Firebase.

عادة يتم ذلك من:

Firebase Console
→ Project Settings
→ Cloud Messaging
→ Apple app configuration

ومن ثم إضافة بيانات APNs المطلوبة من حساب Apple Developer.

هذه العملية تخص إعداد تطبيق iOS وFirebase أكثر من Laravel؛ Laravel سيستمر بإرسال الرسالة إلى FCM بنفس HTTP v1 API.

33. حماية Firebase Credentials

ملف Service Account يجب اعتباره Secret عالي الحساسية.

لا تقم أبداً بـ:

git add firebase-service-account.json

ولا تضعه داخل:

public/

ولا ترسله إلى تطبيق Android أو iOS.

أضف المسار إلى:

.gitignore

مثلاً:

/storage/app/firebase/*.json

34. لا تستخدم CURLOPT_SSL_VERIFYPEER = false

بعض الأكواد القديمة تستخدم:

curl_setopt(
    $ch,
    CURLOPT_SSL_VERIFYPEER,
    false
);

تعطيل التحقق من TLS Certificate ممارسة غير جيدة أمنياً ولا ينبغي استخدامها كحل عادي في Production.

استخدم Laravel HTTP Client بإعداداته الآمنة الافتراضية، وعالج مشكلة شهادات الخادم إذا كانت موجودة بدلاً من تعطيل التحقق.

35. لا تستخدم dd() بعد إرسال الإشعار

الكود:

dd($response);

مفيد فقط أثناء Debugging.

في Production يجب تحليل Response أو تسجيل الخطأ في Logs.

مثلاً:

if ($response->failed()) {

    Log::error(
        'FCM request failed',
        [
            'status' => $response->status(),

            'response' => $response->json(),
        ]
    );

}

36. إضافة Timeout وRetry

إرسال الإشعار يعتمد على خدمة خارجية، وبالتالي قد تحدث مشاكل اتصال مؤقتة.

يمكن استخدام:

Http::withToken($accessToken)
    ->timeout(10)
    ->retry(2, 500)
    ->post(...);

وبذلك لا يبقى الاتصال مفتوحاً إلى ما لا نهاية ويمكن إعادة محاولة بعض حالات الفشل المؤقتة.

37. لا تثق ببيانات Notification القادمة من المستخدم

إذا كانت لديك لوحة Admin تسمح بإرسال Notification، قم بعمل Validation.

$validated = $request->validate([

    'title' => [
        'required',
        'string',
        'max:255',
    ],

    'body' => [
        'required',
        'string',
        'max:1000',
    ],

]);

ويجب كذلك حماية Endpoint بصلاحية إدارية مناسبة.

38. البنية المقترحة للنظام

Mobile Application
        │
        │ Registration / Token
        ▼
Laravel API
        │
        ▼
user_devices
        │
        ▼
Database


Application Event
        │
        ▼
Laravel Controller / Event
        │
        ▼
Queue
        │
        ▼
SendPushNotification Job
        │
        ▼
FirebaseMessagingService
        │
        ▼
OAuth 2.0
        │
        ▼
FCM HTTP v1
        │
        ├────► Android
        │
        └────► iOS

39. الأخطاء الشائعة

استخدام Server Key القديم

استخدم HTTP v1 وOAuth 2.0 بدلاً من Legacy API.

وضع Token واحد داخل users

استخدم جدول Devices إذا كان المستخدم يستطيع تسجيل الدخول من أكثر من جهاز.

عدم تحديث Registration

يجب أن يقوم التطبيق بإرسال التسجيل الحديث إلى Backend عند تغيّره.

عدم حذف Registrations غير الصالحة

تراكم الأجهزة غير النشطة يؤدي إلى إرسال Requests غير مفيدة وتشويه إحصائيات التسليم.

الإرسال إلى آلاف الأجهزة داخل Controller

استخدم Laravel Queues.

وضع Firebase Credentials في Git

Service Account Secret ولا يجب أن يكون جزءاً من Source Code Repository.

تعطيل SSL Verification

لا تستخدم CURLOPT_SSL_VERIFYPEER = false في Production.

استخدام cURL يدوياً دون حاجة

Laravel يحتوي على HTTP Client واضح يدعم Authentication وTimeout وRetries ومعالجة الأخطاء.

40. الخلاصة

إرسال Push Notifications من Laravel إلى Android وiOS يعتمد على ثلاثة أجزاء أساسية:

Mobile Registration
       +
Laravel Backend
       +
Firebase Cloud Messaging

يحصل تطبيق الهاتف على Registration خاص به ويرسله إلى Laravel، ويقوم Laravel بتخزين بيانات الأجهزة، ثم يستخدم FCM HTTP v1 API لإرسال الرسائل إلى Firebase.

وفي التطبيقات الحديثة لا ينبغي الاعتماد على Server Key وLegacy FCM Endpoint، بل يجب استخدام Service Account وOAuth 2.0 للوصول إلى:

https://fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send

أما في الأنظمة الحقيقية التي تحتوي عدداً كبيراً من المستخدمين، فيفضل بناء النظام باستخدام:

  • جدول منفصل لأجهزة المستخدمين.
  • تحديث Registration بشكل مستمر.
  • إزالة registrations القديمة وغير الصالحة.
  • Laravel Queues للإرسال في الخلفية.
  • Firebase Topics عند إرسال الإشعارات العامة.
  • Service منفصلة للتعامل مع FCM.
  • Laravel HTTP Client بدلاً من cURL اليدوي.
  • Service Account وOAuth 2.0 بدلاً من Server Key القديم.

بهذه البنية يصبح نظام الإشعارات أكثر أماناً وقابلية للتوسع والصيانة، ويمكن استخدام نفس Laravel Backend لإدارة Push Notifications لكل من Android وiOS.