تنبيهات أبل: إرسال التنبيهات

دفع التنبيهات إلى أجهزة iOS عن طريق Laravel

Alhoqbaniمنذ 5 سنوات

بسم الله الرحمن الرحيم

 

سوف نستخدم PHP وLaravel لبناء موقع يقوم بإرسال التنبيهات إلى مستخدمي تطبيق الـiOS 

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

كما سوف نستخدم composer لإنشاء مشروع لارافال، وإعداد قاعدة بيانات للمشروع. تأكد من أن جهازك يحتوي على php, composer, and MYSQL

تجربة الإرسال بواسطة PHP

قبل البدء في بناء مشروع Laravel يمكنك تجربة الإرسال باستخدام الكود أدناه. قم بإضافة الـDevice Token الخاص بالآيفون، وشهادة المصادقة من النوع PEM والتي أصدرناها في المقال السابق.

<?php

    // قم بإضافة الرقم الخاص بجهازك لإرسال التنبيه
    $deviceToken = '';

    // عدل المسار إلى موقع شهادة المصادقة
    $pathToCertificate = '/Users/hamoud/Desktop/Certificates/Certificates.pem';

    // محتوى الرسالة
    $payload = json_encode([
        'aps' => [
            'alert' => [
                'title' => 'لديك رسالة جديدة',
                'body' => 'مرحبا بك في تنبيهات أبل.'
            ],
            'badge' => 10,
            'sound' => 'default'
        ]
    ]);

    // رابط خدمة أبل لدفع التنبيهات
    $url = 'tls://gateway.sandbox.push.apple.com:2195';
    // $url = tls://gateway.push.apple.com:2195; // for production.

    // فتح اتصال مع الخدمة
    $streamContext = stream_context_create(['ssl' =>
        ['local_cert' => $pathToCertificate]
    ]);
    $socket = stream_socket_client($url, $errorNumber, $errorString,
        60, STREAM_CLIENT_CONNECT, $streamContext);

    // إرسال الرسالة.
    $message = chr(0) . pack('n', 32) . pack('H*', $deviceToken);
    $message .= pack('n', strlen($payload)) . $payload;
    $written = (int)@fwrite($socket, $message);

    // إغلاق الاتصال
    fclose($socket);

    echo $written . ' bytes were sent.';

مشروع لارافال

سوف نبدأ بتجهيز مشروع لارافال لإرسال التنبيهات، ابدأ بإنشاء مشروع جديد، وسنقوم بإضافة قاعدة البيانات لاستخدامها لاحقاً. كما سنقوم بتوليد صفحات نظام المصادقة (ِAuthentication) وذلك لاستخدام ملفات العرض الخاصة به اختصارًا للوقت.

# إنشاء مشروع جديد
composer create-project laravel/laravel ApplePushNotifications
cd ApplePushNotifications

# توليد ملفات نظام التسجيل
php artisan make:auth

# أضف إعدادات قاعدة البيانات إلى الملف .env

# توليد جداول قاعدة البيانات
php artisan migrate

# تشغيل السيرفر
php artisan serve

سيحتوي الموقع على جزئين مهمين، الأول هو ارسال التنبيهات لجميع الأجهزة المسجلة في قاعدة البيانات وذلك عن طريق نموذج يتم من خلاله تعبئة عنوان الرسالة ومحتواها، والجزء الثاني هو استقبال طلبات تسجيل الـDevice Token والتي سيتم إرسالها من الأجهزة التي يعمل عليها تطبيق الـiOS.

إرسال التنبيهات

سنبدأ بإعداد صفحة نستطيع من خلالها إرسال التنبيهات. وتحتوي الصفحة على نموذج بعد تعبئته سوف نرسل التنبيه على الجهاز المسجل مسبقًا والذي حصلنا على الـDevice Token الخاص به في المقال السابق. وفي الخطوة التالية سوف نضيف جميع الأجهزة المسجلة حتى نستطيع إرسال التنبيه لجميع الأجهزة دفعة واحده.

الكنترولر والرابط

سوف نقوم بإنشاء كنترولر خاص بإرسال التنبيهات، وأيضًا رابط Route لإستقبال البيانات من النموذج.

سنستخدم شهادة المصادقة Certificates.pem لإرسال التنبيهات. في هذا المثال قمت بنقل كامل المجلد Certificates الذي أنشأناه في المقال السابق إلى داخل مشروع لارافال على المسار storage/app/Certificates تأكد من تعديل المسار في الأمثلة وتوجيهه إلى موقع الشهادة على جهازك.

قم بتنفيذ الأمر التالي لإنشاء الكنترولر:

# إنشاء كنترولر في المسار التالي:
# app/Http/Controllers/APNsController.php
php artisan make:controller APNsController

قم فتح الملف app/Http/Controllers/APNsController.php وأضف عليه الدالة التالية والتي ستقوم بمعالجة الطلب وإرسال التنبيه للجهاز المرتبط بالـDevice Token:

     /**
     * Send notifications to iOS devices.
     *
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(Request $request)
    {
        $data = $request->validate([
            'title' => ['required', 'max:255'],
            'body' => ['required', 'max:255'],
        ]);

        $deviceToken = '';
        $pathToCertificate = storage_path('app/Certificates/Certificates.pem');
        $payload = json_encode([
            'aps' => [
                'alert' => [
                    'title' => $data['title'],
                    'body' => $data['body'],
                ],
                'badge' => 10,
                'sound' => 'default'
            ]
        ]);

        $url = 'tls://gateway.sandbox.push.apple.com:2195';
        $streamContext = stream_context_create(['ssl' =>
            ['local_cert' => $pathToCertificate]
        ]);
        $socket = stream_socket_client($url, $errorNumber, $errorString,
            60, STREAM_CLIENT_CONNECT, $streamContext);

        $message = chr(0) . pack('n', 32) . pack('H*', $deviceToken);
        $message .= pack('n', strlen($payload)) . $payload;
        $written = (int)@fwrite($socket, $message);

        fclose($socket);

        return back()->with(['status' => 'تم إرسال الرسالة بنجاح']);
    }

تأكد من إضافة الـDevice Token الخاص بك إلى المتغير deviceToken، وأيضًا من تعديل موقع شهادة المصادقة.

ثم نقوم بإضافة الـRoute الخاص بإرسال التنبيهات.

بداخل الملف routes/web.php أضف السطر التالي: 

Route::post('/apns', 'APNsController@store')->middleware('auth');

نموذج الإرسال

سوف نضيف نموذج لإرسال التنبيهات، وبدلًا من إنشاء صفحة مستقلة بالنموذج، سنستخدم الصفحة الافتراضية التي تم توليدها من نظام المصادقة الخاص بلارافال والتي يمكن الدخول إليها من الرابط http://localhost:8001/home

وبالطبع للوصول إلى هذه الصفحة يجب عليك تسجيل الدخول أولاً.

هذا الصفحة يتم عرض محتواها من الملف resources/views/home.blade.php سوف نقوم بتعديل هذا الملف وإضافة نموذج الإرسال.

قم باستبدال محتوى الملف resources/views/home.blade.php بالكود التالي:

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">Apple Push Notifications Service</div>

                    <div class="card-body">
                        @if (session('status'))
                            <div class="alert alert-success">
                                {{ session('status') }}
                            </div>
                        @endif

                        <form method="POST" action="/apns" novalidate>
                            @csrf

                            <div class="form-group row">
                                <label for="title"
                                       class="col-sm-4 col-form-label text-md-right">Title</label>

                                <div class="col-md-6">
                                    <input id="title" type="text"
                                           class="form-control{{ $errors->has('title') ? ' is-invalid' : '' }}"
                                           name="title" value="{{ old('title') }}" required autofocus>

                                    @if ($errors->has('title'))
                                        <span class="invalid-feedback">
                                        <strong>{{ $errors->first('title') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>

                            <div class="form-group row">
                                <label for="body"
                                       class="col-sm-4 col-form-label text-md-right">Body</label>

                                <div class="col-md-6">
                                    <textarea id="body" type="text"
                                              class="form-control{{ $errors->has('body') ? ' is-invalid' : '' }}"
                                              name="body" required>{{ old('body') }}</textarea>

                                    @if ($errors->has('body'))
                                        <span class="invalid-feedback">
                                        <strong>{{ $errors->first('body') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>

                            <div class="form-group row mb-0">
                                <div class="col-md-8 offset-md-2">
                                    <button type="submit" class="btn btn-primary btn-block">
                                        Send
                                    </button>
                                </div>
                            </div>
                        </form>

                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

والآن نقوم بزيارة الصفحة على الرابط  http://localhost:8001/home وتجربة الإرسال.

تسجيل أجهزة الـiOS

في هذه الخطوة سوف نقوم بإرسال الـDevice Token من جهاز المستخدم إلى موقعنا وحفظها في قاعدة البيانات لنتمكن من استخدامها لاحقًا لإرسال التنبيهات.

وسيكون عبر خطوتين الأولى يتم تنفيذها في مشروع لارافال لإستقبال الطلبات، والثانية من مشروع xcode لإرسالها.

استقبال الطلبات

سنبدأ بإضافة جدول إلى قاعدة البيانات لحفظ الـDevice Tokens، وذلك بإنشاء Model جديد، كما سنقوم بإنشاء كنترولر خاص بهذا الـModel

سوف نقوم بحفظ الـDevice Token في قاعدة البيانات بالإضافة إلى معرف رقمي للجهاز لكي لا يتكرر معنا تسجيل الـToken لنفس الجهاز.

إبدأ بتنفيذ هذا الأمر:

# إنشاء مودل جديد مع الكنترولر الخاص به
php artisan make:model DeviceToken -a

بداخل المجلد database/migrations ستجد الملف الخاص بإعداد الجدول device_tokens والذي سيكون اسمه مشابهًا لـ 2018_06_12_083219_create_device_tokens_table.php مع اختلاف الأرقام بحسب التوقيت الذي قمت فيه بإنشاء الملف.

سنقوم بالتعديل على هذا الملف لإضافة أعمدة جديدة للجدول.

قم بتعديل الدالة up لتصبح بالشكل التالي:

    public function up()
    {
        Schema::create('device_tokens', function (Blueprint $table) {
            $table->increments('id');
            $table->string('device_id')->unique();
            $table->string('token', 100);
            $table->string('device_type')->default('ios');
            $table->timestamps();
        });
    }

كذلك تعديل الكلاس DeviceToken والموجود في الملف app/DeviceToken.php وإضافة السطر التالي بداخله:

    protected $fillable = ['token', 'device_id'];

قم بتنفيذ هذا الأمر لإضافة الجدول الجديد لقاعدة البيانات:

php artisan migrate

وبذلك يمكننا إنشاء عناصر جديدة في الجدول وحفظ الأرقام الخاصة بأجهزة الـiOS في العمود token.

بعد ذلك نقوم بتعديل الكنترولر DeviceToken لإستقبال الطلب وحفظ البيانات في قاعدة البيانات.

قم بفتح الملف app/Http/Controllers/DeviceTokenController.php وتعديل الدالة store على النحو التالي:

    public function store(Request $request)
    {
        // التحقق من إرفاق البيانات مع الطلب
        $data = $request->validate([
            'token' => ['required', 'max:100'],
            'device_id' => ['required', 'max:100'],
        ]);
        
        // نقوم بتحديث التوكن إن كان الجهاز مسجلًا، أو تسجيل جهاز جديد
        DeviceToken::updateOrCreate(['device_id' => $data['device_id']], $data);

        return response()->json(['message' => 'Token was stored'], 201);
    }

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

وأخيرًا سنقوم بإضافة رابط للموقع يتم عن طريقه إرسال الـtoken.

ولتفادي متطلب الـcsrf من لارافال، سنقوم بإضافة هذا الرابط في الملف api.php بدلاً من web.php.

قم بفتح الملف routes/api.php  وإضافة السطر التالي:

// Route: /api/device-token
Route::post('/device-token', 'DeviceTokenController@store');

وبذلك يمكننا إرسال الطلبات إلى الرابط http:/localhost:8001/api/device-token

ويمكنك تجربة الرابط عن طريق أي من برامج الـHttp Client مثل Postman. ولكن لاتنسى إضافة الـHeader التالي للطلب: Accept: application/json

الإرسال من أجهزة الـiOS

ملاحظة: سنقوم بإرسال طلب HTTP Request من تطبيق الـiOS  إلى موقع لارافال الموجود على localhost. وحيث أنه لا يمكن استخدام المحاكي للحصول على الـDevice Token فيجب استخدام جهاز ايفون يتم توصيله بالكمبيوتر. فسنواجه مشكلتين: الاولى: أن localhost لا يمكن الوصول إليه من الآيفون، والثانية أن أبل بشكل افتراضي تمنع الوصول إلى المواقع باستخدام HTTP فلابد أن يكون الاتصال بواسطة  HTTPS وفي ما يلي طريقة تفادي المشكلتين.

localhost من الآيفون

يمكن الدخول إلى السيرفر الموجود على جهاز الماك باستخدم العنوان الرقمي الـIP Address للماك شريطة أن يكون الآيفون والماك متصلين بشبكة واحدة.

تحتاج إلى العنوان الداخلي للماك والذي يمكن الحصول عليه باستخدام الأمر ipconfig getifaddr en0 أو من إعدادات الشبكة. في هذا المثال سأستخدم الرقم 192.168.0.2

ويمكنك مباشرة الدخول باستخدام المتصفح في الآيفون على هذا الرقم مع إضافة رقم المنفذ، فلو كنت تستطيع الوصول إلى مشروع لارافال باستخدام الرابط http://localhost:8000، فيمكن الوصول إليه من الآيفون باستخدام http://192.168.0.2:8000 وسيكون هذا الرابط هو ما نستخدمه في مشروع xcode لإرسال الطلب.

في حال لم تعمل الطريقة السابقة، تأكد من أن الأيفون والماك متصلين بشبكة واحدة، ثم قم باستخدام السيرفر الداخلي لـphp لتشغيل مشروع لارافال مع تحديد رقم العنوان للماك كاسم المستضيف. ويمكن تنفيذ ذلك بالأمر:

php artisan serve --port 8000 --host 192.168.0.2

استخدام HTTP في iOS

تشترط سياسة أبل App Transport Security (ATS) على تطبيقات الـiOS أن يتم استخدام الروابط التي تعمل على البروتوكول HTTPS، ويمكن تجاوز هذا الشرط بتعديل الملف info.plist الخاص بالمشروع وتعطيل هذه السياسية. مع العلم أن هذه الطريقة يمكن استخدامها أثناء تطوير التطبيق فقط، ويجب إزالة هذا التعديل قبل تقديم التطبيق للمتجر وإلا سيتعرض تطبيقك للرفض في حال لم يكن لديك مسوغات لاستخدامه.

 قم بفتح الملف info.plist بالضغط عليه بالزر الأيمن واختيار Open As ثم Source Code.

وقم بإضافة هذا الكود قبل نهاية الملف كما هو موضح:

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

# الكود أعلاه يتم وضعه في آخر الملف قبل أخر سطرين في الملف
</dict>
</plist>

ارسال الطلب

عندما يقوم المستخدم بتشغيل تطبيقك على جهازه لأول مرة، سوف يتم طلب موافقته على استخدام تطبيقك للتنبيهات. وفي حال موافقة المستخدم على التنبيهات، سيقوم النظام بتزويد تطبيقك بالـDevice Token عبر استدعاء الدالة application(_:didRegisterForRemoteNotificationsWithDeviceToken:) من الكلاس AppDelegate.

هذه الدالة هي الفرصة التي من خلالها نستطيع إرسال الـDevice Token إلى موقعنا وحفظها في قاعدة البيانات. ولكن هذه الدالة يتم استدعائها في كل مرة يقوم المستخدم بتشغيل التطبيق. وبالتالي فإننا يجب أن لا نقوم بإرسال الطلب إلا مرة واحدة فقط أو عند تغيير الـDevice Token، ويكمن أن نقوم بذلك بحفظ الرقم عند إرساله في الـUserDefaults ومقارنته بما تم إرساله عند تشغيل التطبيق.

سنقوم بإضافة دالة جديدة sendTokenToServer(_:) للـAppDelegate، والتي ستقوم بإرسال البيانات إلى موقعنا.

    
    private func sendTokenToServer(_ token: String) -> Void {
        // قم بتغيير الرابط بحسب العنوان الداخلي لجهازك
        let url = URL(string: "http://192.168.0.2:8000/api/device-token")!
        
        // تجهيز الطلب وإرفاق البيانات المطلوبة
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try! JSONEncoder().encode([
            "token": token,
            "device_id": UIDevice.current.identifierForVendor!.uuidString // الرقم التعريفي الخاص بالجهاز (يتغير هذا الرقم عند إعادة تثبيت التطبيق على الجهاز)
            ])
        
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            // خطأ في إرسال الطلب
            guard error == nil else {
                print(">>> Error in sending the request: \(error?.localizedDescription ?? "")")
                
                return
            }
            
            // عند الفشل في إضافةالبيانات على السيرفر
            guard let statusCode = (response as? HTTPURLResponse)?.statusCode, (200..<300).contains(statusCode) else {
                // نقوم بطباعة الرسالة
                if let data = data, let message = String(data: data, encoding: .utf8) {
                    print(">>> Request to send DeviceToken Failed: \(message)")
                    
                    return
                }
                
                print(">>> Request to send DeviceToken Failed.")
                return
            }
            
            // حفظ التوكن لتفادي ارسالها مرة أخرى وطباعة رسالة نجاح الطلب 
            if let data = data, let message = String(data: data, encoding: .utf8) {

                UserDefaults.standard.set(token, forKey: "apns-device-token")
                print(">>> Response: \(message)")
            }
            
        }
        
        // إرسال الطلب
        task.resume()
    }

وسنقوم بتعديل الدالة application(_:didRegisterForRemoteNotificationsWithDeviceToken:) لاستدعاء دالة إرسال الـDevice Token للسيرفر، بعد التأكد من عدم إرسالها مسبقًأ.

قم بتعديل الدالة لتصبح بالشكل التالي:

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        
        // تحويل الرقم إلى نص حتى نتمكن من استخدامه وإرساله إلى السيرفر
        let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
        
        print(">>> Token: \(token)")
        
        // التأكد من عدم إرسال التوكن مسبقًا، ومقارنة ما تم إرساله بالتوكن الجديد
        let defaults = UserDefaults.standard
        if let oldToken = defaults.object(forKey: "apns-device-token") as? String, token == oldToken {
            
            print(">>> Token was already sent to server.")

            return
        }
        
        // إرسال التوكن وحفظه لتفادي إرساله مرة أخرى
        sendTokenToServer(token)
    }

إرسال التنبيهات لجميع الأجهزة

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

وكل ما نحتاج فعله هو تعديل الدالة store من الكنترولر APNsController لجلب البيانات من قاعدة البيانات، والإرسال لجميع الأجهزة دفعة واحدة.

وسنقوم بذلك في دورة اتصال واحد مع APNs بحيث أننا لن نقوم بإلغاء الاتصال حتى الانتهاء من الإرسال.

قم تعديل الملف app/Http/Controllers/APNsController.php لتكون الدالة store كالآتي:

    public function store(Request $request)
    {
// التحقق من إرسال البيانات
        $data = $request->validate([
            'title' => ['required', 'max:255'],
            'body' => ['required', 'max:255'],
        ]);
// جلب البينات من قاعدة البينات
        $devices = \App\DeviceToken::all();
// قم بتعديل مسار شهادة المصادقة
        $pathToCertificate = storage_path('app/Certificates/Certificates.pem');
        $payload = json_encode([
            'aps' => [
                'alert' => [
                    'title' => $data['title'],
                    'body' => $data['body'],
                ],
                'badge' => 10,
                'sound' => 'default'
            ]
        ]);
// فتح الاتصال مع سيرفرات أبل
        $url = 'tls://gateway.sandbox.push.apple.com:2195';
        $streamContext = stream_context_create(['ssl' =>
            ['local_cert' => $pathToCertificate]
        ]);
        $socket = stream_socket_client($url, $errorNumber, $errorString,
            60, STREAM_CLIENT_CONNECT, $streamContext);
// إرسال الرسالة لجميع الأجهزة
        $devices->each(function ($device) use ($socket, $payload) {

            $message = chr(0) . pack('n', 32) . pack('H*', $device->token);
            $message .= pack('n', strlen($payload)) . $payload;
            $written = (int)@fwrite($socket, $message);

        });
// إغلاق الاتصال
        fclose($socket);

        return back()->with(['status' => 'تم إرسال الرسالة بنجاح']);
    }

والآن تستطيع إرسال الرسال لجميع الأجهزة التي تم ثبيت تطبيقك عليها.

يرجى ملاحظة أننا لا نقوم بالتحقق من بيانات الرسالة، حيث أن هناك بعض المتطلبات من أبل مثل أن لا يزيد حجم الرسالة عن 4KB، كما أننا لا نقوم بالتأكد من إتمام إرسال للرسالة. والكود أعلاه بحاجة إلى إعادة ترتتيب حتى نستطيع اعادة استخدامه. وهذا ما سنقوم به في المقالات القادمة بإذن الله.

 

يمكن الاطلاع على الملفات الكاملة للمشرعين من خلال الروابط التالية: 

مشروع لارافال

مشروع iOS

كلمات دليلية: apns ios laravel swift
6
إعجاب
2682
مشاهدات
2
مشاركة
3
متابع
متميز
محتوى رهيب

التعليقات (2)

Moath saleh:

يمكنك اختصار الكثير من الوقت من خلال استخدام Onesignal مجانا و كذالك يوجد بكج جاهز تفضل

 

https://github.com/moathdev/laravel-onesignal

 

Alhoqbani:

شكرًا على لك أخي معاذ على مرورك وعلى الإضافة، وقد ذكرت في الجزء الأول من هذه السلسلة أن هناك طرق أسهل لاستخدام التنبيهات مثل Google FCM و Pusher. والتركيز هنا على إرسال التنبيهات من دون استخدام مكتبات خارجية.

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !