• Push Notification dengan Firebase dan Laravel

    Push Notification dengan Firebase dan Laravel

    Dalam era aplikasi modern, komunikasi real-time menjadi kebutuhan utama. Salah satu cara efektif untuk menjaga interaksi dengan pengguna adalah melalui push notification. Pada tulisan ini, kita akan membahas bagaimana mengintegrasikan Firebase dengan Laravel untuk mengirim notifikasi yang cepat, andal, dan mudah dikelola.

    Nah, sebelum kita mulai ngoding, ada satu hal penting yang harus disiapkan dulu: akun Firebase. Dari sinilah kita akan mendapatkan “kunci” yang dibutuhkan Laravel untuk mengirim notifikasi ke perangkat. Caranya cukup mudah: buat project baru di Firebase Console, lalu simpan data seperti Server Key dan Sender ID yang nanti akan kita gunakan di Laravel.

    Caranya gampang kok. Tinggal buka Firebase Console, klik Add Project, kasih nama sesuai kebutuhan, lalu ikuti langkah-langkah sampai selesai. Begitu project jadi, masuk ke menu Project Settings dan pilih tab Cloud Messaging. Di sana kamu bakal nemuin Server Key dan Sender ID yang jadi “senjata” utama kita untuk menghubungkan Firebase dengan Laravel.

    Di step nomor 1 pilih project setting. Lanjut ke step 2 untuk membuat aplikasi yang akan menggunakan firebase ini. Di step 2 ini terdapat config yang akan dipakai di aplikasi. Lanjut ke step 3 untuk mendapatkan key server. Ini akan mendapatkan file json. Ke step terakhir di firebase yaitu mendapatkan Key pair dimana untuk mendapatkan FCM token dari sisi client nya nanti. Key pair ini akan digunakan sebagai Vapid Key di sisi client.

    Nah sekarang langsung ke bagian Laravelnya ya. Tinggal install laravelnya saja

    composer create-project laravel/laravel firebase-notification
    
    cd firebase-notification

    1) Install & konfigurasi dasar

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

    Letakkan file credentials JSON (service account) di storage/app/firebase/firebase_credentials.json mis. dan tambahkan di .env:

    FIREBASE_CREDENTIALS=/full/path/to/storage/app/firebase/firebase_credentials.json
    

    2) Migration & Model (simpan token perangkat)

    Buat migration:

    php artisan make:migration create_fcm_tokens_table --create=fcm_tokens
    

    database/migrations/xxxx_xx_xx_create_fcm_tokens_table.php

    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    class CreateFcmTokensTable extends Migration
    {
        public function up()
        {
            Schema::create('fcm_tokens', function (Blueprint $table) {
                $table->id();
                $table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade');
                $table->string('token')->unique();
                $table->string('platform')->nullable(); // android, ios, web
                $table->timestamp('last_seen_at')->nullable();
                $table->boolean('is_active')->default(true);
                $table->timestamps();
            });
        }
    
        public function down()
        {
            Schema::dropIfExists('fcm_tokens');
        }
    }
    

    Model app/Models/FcmToken.php:

    <?php
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class FcmToken extends Model
    {
        protected $fillable = ['user_id', 'token', 'platform', 'last_seen_at', 'is_active'];
    
        public function user()
        {
            return $this->belongsTo(\App\Models\User::class);
        }
    }
    

    3) PushNotificationService (core — gunakan Kreait DI)

    Buat service: app/Services/PushNotificationService.php

    <?php
    
    namespace App\Services;
    
    use Kreait\Firebase\Contract\Messaging;
    use Kreait\Firebase\Messaging\CloudMessage;
    use Kreait\Firebase\Messaging\Notification;
    use Kreait\Firebase\Exception\MessagingException;
    use App\Models\FcmToken;
    use Illuminate\Support\Facades\Log;
    
    class PushNotificationService
    {
        protected Messaging $messaging;
    
        public function __construct(Messaging $messaging)
        {
            $this->messaging = $messaging;
        }
    
        protected function normalizeData(array $data): array
        {
            // all keys/values must be strings for FCM data payload
            return array_map(fn($v) => (string)$v, $data);
        }
    
        public function sendToToken(string $token, string $title, string $body, array $data = []): array
        {
            $message = CloudMessage::withTarget('token', $token)
                ->withNotification(Notification::create($title, $body))
                ->withData($this->normalizeData($data));
    
            try {
                $this->messaging->send($message);
                return ['ok' => true];
            } catch (MessagingException $e) {
                Log::warning('FCM sendToToken error', ['token' => $token, 'e' => $e->getMessage()]);
                return ['ok' => false, 'error' => $e->getMessage()];
            } catch (\Throwable $e) {
                Log::error('FCM unknown error', ['e' => $e->getMessage()]);
                return ['ok' => false, 'error' => $e->getMessage()];
            }
        }
    
        /**
         * sendMulticast: kirim ke banyak token. Chunk tokens untuk menghindari limit.
         * Perhatikan limit / performa — chunk default 500.
         */
        public function sendMulticast(array $tokens, string $title, string $body, array $data = [], int $chunkSize = 500): array
        {
            $message = CloudMessage::new()
                ->withNotification(Notification::create($title, $body))
                ->withData($this->normalizeData($data));
    
            $result = [
                'success' => 0,
                'failure' => 0,
                'invalid_tokens' => [],
                'unknown_tokens' => [],
            ];
    
            $chunks = array_chunk(array_values($tokens), $chunkSize);
            foreach ($chunks as $chunk) {
                try {
                    $report = $this->messaging->sendMulticast($message, $chunk);
                    $result['success'] += $report->successes()->count();
                    $result['failure'] += $report->failures()->count();
    
                    // collect tokens to cleanup if needed
                    $result['invalid_tokens'] = array_merge($result['invalid_tokens'], $report->invalidTokens());
                    $result['unknown_tokens'] = array_merge($result['unknown_tokens'], $report->unknownTokens());
                } catch (\Throwable $e) {
                    Log::error('FCM sendMulticast error', ['e' => $e->getMessage()]);
                }
            }
    
            // cleanup tokens from DB if invalid/unknown
            $bad = array_unique(array_merge($result['invalid_tokens'], $result['unknown_tokens']));
            if (!empty($bad)) {
                FcmToken::whereIn('token', $bad)->delete();
            }
    
            return $result;
        }
    
        public function sendToTopic(string $topic, string $title, string $body, array $data = []): array
        {
            $message = CloudMessage::withTarget('topic', $topic)
                ->withNotification(Notification::create($title, $body))
                ->withData($this->normalizeData($data));
    
            try {
                $this->messaging->send($message);
                return ['ok' => true];
            } catch (\Throwable $e) {
                Log::error('FCM sendToTopic error', ['topic' => $topic, 'e' => $e->getMessage()]);
                return ['ok' => false, 'error' => $e->getMessage()];
            }
        }
    
        // Topic management (subscribe/unsubscribe)
        public function subscribeTokensToTopic(string $topic, array $tokens): array
        {
            try {
                $res = $this->messaging->subscribeToTopic($topic, $tokens);
                return ['ok' => true, 'result' => $res];
            } catch (\Throwable $e) {
                Log::error('FCM subscribeToTopic error', ['topic' => $topic, 'e' => $e->getMessage()]);
                return ['ok' => false, 'error' => $e->getMessage()];
            }
        }
    
        public function unsubscribeTokensFromTopic(string $topic, array $tokens): array
        {
            try {
                $res = $this->messaging->unsubscribeFromTopic($topic, $tokens);
                return ['ok' => true, 'result' => $res];
            } catch (\Throwable $e) {
                Log::error('FCM unsubscribeFromTopic error', ['topic' => $topic, 'e' => $e->getMessage()]);
                return ['ok' => false, 'error' => $e->getMessage()];
            }
        }
    }
    

    4) Controller & routes

    Controller app/Http/Controllers/FcmController.php

    <?php
    
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    use App\Models\FcmToken;
    use App\Services\PushNotificationService;
    use Illuminate\Support\Facades\Auth;
    
    class FcmController extends Controller
    {
        public function storeToken(Request $req)
        {
            $req->validate(['token' => 'required|string', 'platform' => 'nullable|string']);
    
            $userId = Auth::id();
    
            $token = $req->input('token');
            $platform = $req->input('platform', 'web');
    
            FcmToken::updateOrCreate(
                ['token' => $token],
                ['user_id' => $userId, 'platform' => $platform, 'last_seen_at' => now(), 'is_active' => true]
            );
    
            return response()->json(['ok' => true]);
        }
    
        // simple admin blade for testing
        public function adminForm()
        {
            return view('notifications.send'); // nanti buat blade di resources/views/notifications/send.blade.php
        }
    
        public function send(Request $req, PushNotificationService $push)
        {
            $req->validate([
                'target_type' => 'required|in:token,tokens,topic,all',
                'title' => 'required|string',
                'body' => 'required|string',
                'token' => 'nullable|string',
                'tokens' => 'nullable|string', // comma separated
                'topic' => 'nullable|string',
            ]);
    
            $title = $req->title;
            $body = $req->body;
    
            if ($req->target_type === 'token') {
                return $push->sendToToken($req->token, $title, $body);
            }
    
            if ($req->target_type === 'tokens') {
                $tokens = array_filter(array_map('trim', explode(',', $req->tokens)));
                return $push->sendMulticast($tokens, $title, $body);
            }
    
            if ($req->target_type === 'topic') {
                return $push->sendToTopic($req->topic, $title, $body);
            }
    
            if ($req->target_type === 'all') {
                $tokens = \App\Models\FcmToken::pluck('token')->toArray();
                return $push->sendMulticast($tokens, $title, $body);
            }
    
            return ['ok' => false, 'error' => 'unknown_target'];
        }
    }
    

    Routes (tambahkan di routes/web.php atau routes/api.php sesuai kebutuhan):

    use App\Http\Controllers\FcmController;
    
    Route::post('/fcm/token', [FcmController::class, 'storeToken']); // client calls to register token
    Route::get('/admin/notifications', [FcmController::class, 'adminForm'])->middleware('auth'); 
    Route::post('/admin/notifications/send', [FcmController::class, 'send'])->middleware('auth');
    

    5) Blade (admin) — contoh sederhana

    @extends('layouts.app')
    
    @section('content')
    <div class="container">
      <h3>Kirim Notifikasi</h3>
    
      <form id="sendForm" method="POST" action="{{ url('/admin/notifications/send') }}">
        @csrf
        <div>
          <label>Target</label>
          <select name="target_type" id="target_type">
            <option value="topic">Topic</option>
            <option value="token">Single Token</option>
            <option value="tokens">Multiple Tokens (comma)</option>
            <option value="all">All (DB)</option>
          </select>
        </div>
    
        <div id="tokenField" style="display:none">
          <input name="token" placeholder="device token">
        </div>
    
        <div id="tokensField" style="display:none">
          <textarea name="tokens" placeholder="token1,token2,..."></textarea>
        </div>
    
        <div id="topicField">
          <input name="topic" placeholder="topic name (e.g. news)">
        </div>
    
        <div><input name="title" placeholder="Judul" required></div>
        <div><textarea name="body" placeholder="Isi pesan" required></textarea></div>
    
        <button type="submit">Kirim</button>
      </form>
    </div>
    
    <script>
    document.getElementById('target_type').addEventListener('change', function() {
      const t = this.value;
      document.getElementById('tokenField').style.display = t === 'token' ? 'block' : 'none';
      document.getElementById('tokensField').style.display = t === 'tokens' ? 'block' : 'none';
      document.getElementById('topicField').style.display = t === 'topic' ? 'block' : 'none';
    });
    </script>
    @endsection
    

    6) Client-side (Web) — contoh mendapatkan token & mendaftarkan ke server

    1. Siapkan firebase-messaging-sw.js di public/:
      public/firebase-messaging-sw.js
    importScripts('https://www.gstatic.com/firebasejs/9.22.2/firebase-app-compat.js');
    importScripts('https://www.gstatic.com/firebasejs/9.22.2/firebase-messaging-compat.js');
    
    firebase.initializeApp({
      apiKey: "YOUR_API_KEY",
      authDomain: "PROJECT_ID.firebaseapp.com",
      projectId: "PROJECT_ID",
      messagingSenderId: "SENDER_ID",
      appId: "APP_ID"
    });
    
    const messaging = firebase.messaging();
    
    messaging.onBackgroundMessage(function(payload) {
      const { title, body } = payload.notification || {};
      self.registration.showNotification(title || 'Notification', {
        body: body || '',
        // icon: '/icon.png'
      });
    });
    
    1. Di halaman (blade/client) untuk mendapatkan token:
    <!-- include module or compat scripts -->
    <script type="module">
      import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.2/firebase-app.js";
      import { getMessaging, getToken, onMessage } from "https://www.gstatic.com/firebasejs/9.22.2/firebase-messaging.js";
    
      const firebaseConfig = {
        apiKey: "YOUR_API_KEY",
        authDomain: "PROJECT_ID.firebaseapp.com",
        projectId: "PROJECT_ID",
        messagingSenderId: "SENDER_ID",
        appId: "APP_ID"
      };
    
      const app = initializeApp(firebaseConfig);
      const messaging = getMessaging(app);
    
      // ambil VAPID key dari Firebase Console (Web push certificates)
      const VAPID_KEY = "YOUR_PUBLIC_VAPID_KEY";
    
      async function registerTokenToServer() {
        try {
          const currentToken = await getToken(messaging, { vapidKey: VAPID_KEY });
          if (currentToken) {
            // kirim ke server
            await fetch('/fcm/token', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
              },
              credentials: 'same-origin',
              body: JSON.stringify({ token: currentToken, platform: 'web' })
            });
            console.log('FCM token registered:', currentToken);
          } else {
            console.log('No registration token available. Request permission to generate one.');
          }
        } catch (err) {
          console.error('An error occurred while retrieving token. ', err);
        }
      }
    
      // panggil saat halaman siap / setelah user memberi izin notifikasi
      registerTokenToServer();
    
      // handle pesan foreground
      onMessage(messaging, (payload) => {
        console.log('Message received. ', payload);
        // tampilkan notifikasi custom jika mau
      });
    </script>
    

    Untuk mendapatkan VAPID public key: buka Firebase Console → Project settings → Cloud Messaging → Web configuration → Web Push certificates → Generate Key Pair (atau ambil key existing).

    7) Chunking / skala / topic tips

    • sendMulticast useful untuk banyak token, namun jangan kirim terlalu banyak sekaligus — chunk ke batch (umumnya 500) untuk menghindari error. Jika audiens besar, lebih efisien pakai topic. Firebase Admin SDK for PHP+1
    • Untuk subscribe/unsubscribe topic kamu bisa gunakan subscribeToTopic / unsubscribeFromTopic dari SDK. Per satu request dapat mendaftarkan sampai ~1.000 devices (per request). Untuk jumlah besar, gunakan chunking. Firebase+1

    8) Membersihkan token yang invalid

    Pada response sendMulticast SDK memberikan invalidTokens() dan unknownTokens() — sebaiknya hapus/dinonaktifkan token-token itu dari DB. Contoh di service sudah men-delete token invalid/unknown.


    9) (Opsional tapi sangat direkomendasikan) Jalankan pengiriman di Queue

    Untuk scale / UI responsiveness, dispatch sebuah Job (Laravel queue) yang memanggil PushNotificationService::sendMulticast(...). Contoh singkat job:

    php artisan make:job SendPushNotificationJob

    app/Jobs/SendPushNotificationJob.php:

    // in handle(): $this->pushService->sendMulticast($this->tokens, $this->title, $this->body, $this->data);
    

    Jalankan worker: php artisan queue:work (atau supervisor di production).


    10) Testing & debugging

    • Test endpoint /fcm/token dari client web/mobile untuk memastikan token tersimpan.
    • Kirim notifikasi single token lalu cek device.
    • Lihat logs jika ada MessagingException — SDK mengubah error FCM jadi exception yang bisa kamu inspect.

  • Troubleshooting Server

    Troubleshooting merupakan salah satu service yang kami berikan. Jika anda kewalahan untuk menangani server sendiri bisa hubungi kami. Namun jika anda mau mencoba memperbaiki sendiri kami akan memberi sedikit apa saja yang harus dilakukan.

    Ada beberapa kasus di server. Nah berikut beberapa kasus dan cara menanganinya.

    1. Server Tidak Dapat Diakses

    Gejala: Tidak bisa ping, tidak merespon koneksi remote.

    Penyebab umum: Kabel jaringan terputus, IP berubah, atau firewall memblokir koneksi.

    Solusi:

    • Periksa koneksi fisik (kabel & port switch/router).

    • Pastikan konfigurasi IP sesuai.

    • Cek firewall atau security group.

    1. Server Lambat

    Gejala: Respon aplikasi menurun, proses lama.

    Penyebab umum: Beban CPU tinggi, RAM penuh, atau disk I/O overload.

    Solusi:

    • Monitor penggunaan CPU, RAM, dan disk.

    • Optimalkan aplikasi dan database.

    • Tambahkan sumber daya atau upgrade hardware.

    1. Kegagalan Booting

    Gejala: Server tidak menyala atau berhenti di POST/bootloader.

    Penyebab umum: Kerusakan sistem operasi, konfigurasi BIOS salah, atau media boot hilang.

    Solusi:

    • Periksa urutan boot di BIOS/UEFI.

    • Gunakan media recovery OS.

    • Lakukan restore dari backup.

    1. Layanan Tidak Berfungsi

    Gejala: Website, email, atau database down.

    Penyebab umum: Service mati, port diblokir, atau konfigurasi salah.

    Solusi:

    • Restart service terkait.

    • Cek log error untuk penyebab spesifik.

    • Verifikasi konfigurasi service.

    1. Serangan atau Malware

    Gejala: Trafik tidak wajar, file berubah sendiri, atau akun diretas.

    Penyebab umum: Celah keamanan, patch OS tidak terpasang.

    Solusi:

    • Isolasi server dari jaringan.

    • Scan malware & lakukan pembersihan.

    • Perbarui patch keamanan.

    Nah di atas sudah dijelaskan beberapa kasus dan cara menanganinya. Selamat mencoba untuk troubleshoot sendiri. Tetapi tenang saja jika anda masih kewalahan untuk troubleshoot. Kami siap melayani anda. Silakan hubungi kami.

  • Wose

    Wose

    Apa itu Wose?

    Wose berasal dari bahasa Jawa “wose tembung” yang berarti inti dari obrolan. Adalah salah satu produk penerapan AI yang dikembangkan oleh Fortis Solution.

    Tujuan utama aplikasi

    Dirancang untuk merangkum isi grup WhatsApp secara otomatis menggunakan integrasi API WhatsApp dan teknologi AI.

    Fitur Utama Wose

    • Integrasi dengan API WhatsApp
      Memungkinkan aplikasi membaca isi grup WhatsApp secara terstruktur (dengan izin).
    • Penggunaan AI
      Menggunakan teknologi AI untuk memahami konten dan kemudian menyajikan highlighted summary—intisari percakapan penting, keputusan, atau to-do list.
    • Keamanan & privasi
      Walau Meta menyatakan summary mereka diproses dalam enclave tepercaya tanpa akses pihak ketiga, penting memastikan Wose memiliki proteksi data dan model serupa jika cloud-processed.

    Bagaimana Cara Kerja Wose?

    • Pengguna menghubungkan Daftar dan Masuk ke Wose
    • Daftarkan akun WhatsApp.
    • AI memproses percakapan: mengidentifikasi topik penting dan poin diskusi.
    • Hasil ringkasan dikirim kembali ke pengguna, bisa berupa bullet points, highlight, atau ringkasan naratif.

    Manfaat Wose

    • Menghemat waktu saat membaca percakapan panjang.
    • Cocok untuk manajemen proyek, grup pendidikan atau komunitas besar yang aktif.
    • Membantu menindaklanjuti keputusan, tugas, atau topik utama secara efisien.

  • Download Audio Youtube

    Pernah putar video musik di youtube? Pengen simpan audio untuk diputar offline? Pastinya kamu akan mencari situs yang bisa mengkonversi video youtube menjadi mp3 kan. Situs memang ada yang gratis tapi memiliki kualitas audio yang standar. Pengen unduk mp3 dari youtube dengan kualitas audio yang bagus? Ya pakai aplikasi berbayar dong.

    Tetapi tenang jika kamu bisa mengoperasikan komputer, kamu bakalan bisa unduh mp3 kualitas bagus tanpa bayar. Pengalaman saya menggunakan OS Ubuntu untuk mengunduh mp3 dari Youtube berikut ini memang manjur loh. Mau tahu caranya? Simak penjelasan berikut ini.

    Pertama buka terminal ubuntunya ya, ini perintahnya

    sudo wget -O /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp
    
    sudo chmod a+rx /usr/local/bin/yt-dlp

    Selanjutnya pilih url Youtube targetnya

    /usr/local/bin/yt-dlp -x --audio-format mp3 "https://www.youtube.com/watch?v=KG18xKK5CWI"

    Nah kode di atas akan mengkonversi ke audio kualitas standar. Untuk menjadikan kualitas yang bagus memakai sintak ini.

    /usr/local/bin/yt-dlp -x --audio-format mp3 --audio-quality 0 "https://www.youtube.com/watch?v=KG18xKK5CWI"

    –audio-quality 0 artinya best quality (320kbps).

    Nah tadi itu adalah cara untuk mengunduh audio dari Youtube. Selamat mencoba.

  • Bedakah Developer vs Engineer?

    Jika kalian menemukan iklan loker IT pasti tidak asing dengan istilah developer dan engineer ini. Tapi tahukah kalian apa itu developer? apa itu engineer? bedanya apa? Ini merupakan pertanyaan umum yang sering ditemukan dikalangan pencari kerja fresh graduate. Lalu apa jawabanya? mari kita simak penjelasan berikut ini.

    👨‍💻 Engineer di bidang IT itu apa?

    ✅ Secara umum, Engineer berarti orang yang bekerja membuat, membangun, dan memelihara sistem/software di bidang teknologi.
    ✅ Di dunia IT, kata “Engineer” sering dipakai sebagai pengganti kata “Developer” atau “Programmer” tapi dengan kesan lebih profesional.

    Contoh:

    • Software Engineer = Developer yang membuat aplikasi/software.
    • Backend Engineer = Developer khusus backend.
    • Frontend Engineer = Developer khusus frontend.
    • Full Stack Engineer = Developer yang mengerjakan backend + frontend.

    📌 Perbedaan Developer vs Engineer

    🔥 Developer🚀 Engineer
    Fokus ngoding fiturFokus merancang + membangun sistem
    Biasanya task-levelBisa system-level (arsitektur)
    Bisa lebih spesifikLebih umum dan sering multi-skill

    Singkatnya:
    Developer = fokus coding
    Engineer = coding + design sistem + thinking lebih luas

    Tapi di banyak perusahaan kecil → Developer dan Engineer sering dianggap sama.

    🏷️ Jadi kalau iklan cuma tulis “Engineer”

    ✅ Bisa berarti:

    • Posisi umum untuk Software Engineer
    • Levelnya mid-level (tidak junior, tidak senior)
    • Di perusahaan kecil/startup kadang berarti full stack developer
    • Di perusahaan besar kadang berarti bagian tim (bukan lead)

    ✅ Contoh:

    • Backend Engineer = Fokus server, API, database
    • Frontend Engineer = Fokus UI/UX di browser
    • Data Engineer = Fokus olah data & pipeline
    • DevOps Engineer = Fokus deployment & server
    • QA Engineer = Fokus testing otomatis/manual