PWA ve Web Push Bildirim Sistemi

Tarayıcı kapalıyken bile bildirim gönderin — kütüphane kullanmadan, saf PHP ile. VAPID/ES256 imzası, ECDH + HKDF anahtar türetme ve aes128gcm uçtan uca şifreleme; çevrimdışı çalışan PWA kabuğuyla birlikte.

PHP 8 PDO MySQL Bootstrap 5 Ajax Oturum Girişi
Seviye
İleri
Dosya
85
Kod satırı
~19.547
Proje boyutu
1.9 MB
Veritabanı
cy_pwa_push
Lisans
MIT
İnceleme
7
Beğeni
0
Yayın

Ekran Görüntüleri 5 görsel

Bu Örnek Ne Yapıyor?

  • Hiçbir push kütüphanesi kullanılmadı — VAPID ve şifreleme saf PHP'de
  • VAPID JWT üretimi ve ES256 imzası; DER imza ham 64 bayta çevrilir
  • ECDH ortak sır + HKDF ile içerik anahtarı ve nonce türetme
  • aes128gcm (RFC 8188) ile uçtan uca şifreleme — push servisi içeriği okuyamaz
  • Service worker altı olayı dinler; sekme kapalıyken de bildirim gelir
  • Bildirime tıklayınca yeni sekme açılmaz, açık sekme öne getirilir
  • Çevrimdışı gezinme yanıtı ve ana ekrana eklenebilir manifest
  • 404/410 kesin ölüm sayılır ve satır silinir; geçici hatalarda sayaç artar
  • Özel anahtar web'e kapalı storage/ klasöründe tutulur

Nerede İşe Yarar?

  • Sitesine bildirim eklemek isteyip FCM SDK'sından kaçınanlar
  • VAPID'in ne olduğunu ve neden gerektiğini öğrenmek isteyenler
  • Web push şifrelemesinin adım adım nasıl yapıldığını görmek isteyenler
  • Uygulamasını PWA'ya çevirip ana ekrana eklenebilir hâle getirecekler

Gereksinimler

  • PHP 8.0+ · openssl eklentisi (zorunlu) · MySQL 5.7+ / MariaDB 10.3+ · pdo_mysql · mbstring · HTTPS (localhost hariç)

Nasıl Kurulur?

  1. Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
  2. database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
  3. .env.example dosyasını .env adıyla kopyalayın
  4. php bin/vapid-keys.php çalıştırıp açık anahtarı .env içine yapıştırın
  5. HTTPS üzerinden açın (localhost tek istisnadır) ve bildirimleri açın

Veritabanı şeması projedeki database.sql dosyasında.

Nasıl Çalışıyor?

Çılgın Yazılım

PWA ve Web Push Bildirim Sistemi

PHP 8 · PDO · MySQL · Service Worker · VAPID · aes128gcm · Çevrimdışı Çalışma · Çılgın Yazılım Tasarım Kalıbı

Tarayıcı kapalıyken bile bildirim gönderin — kütüphane kullanmadan, saf PHP ile.

PHP
MySQL
PWA
VAPID
License

🇹🇷 Türkçe · 🇬🇧 English

▶ Canlı Demo · Kaynak Kütüphanesi · cilginyazilim.com


Canlı Demo

Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.

Canlı Demoyu Aç
Kaynak Kodu İncele
ZIP İndir

PWA push bildirim canlı demo önizlemesi

▲ Görsele tıklayarak demoyu açabilirsiniz

Demo hesapları

RolE-postaParola
Yönetici[email protected]Admin1234
Kullanıcı[email protected]Demo1234

Demoda 60 saniyede neleri deneyebilirsiniz?

#Şunu deneyinPerde arkasında ne oluyor?
1Push Bildirimler sayfasında "Bildirimleri aç" deyin ve tarayıcı iznini verinTarayıcı push servisinden (Google/Mozilla/Apple) size özel bir abonelik adresi alır ve sunucuya kaydeder. Bu adres cihaza özeldir; sunucu üretemez
2"Kendi cihazlarıma gönder" düğmesine basınPHP, VAPID ile imzalı ve uçtan uca şifreli bir istek hazırlayıp push servisine yollar. Bildirim tarayıcınıza servis üzerinden ulaşır
3Bildirimi gönderdikten sonra sekmeyi kapatın ve tekrar gönderinBildirim yine gelir. Onu gösteren şey sayfa değil, arka planda çalışan service worker'dır
4Gelen bildirime tıklayınUygulama açılır. Zaten açık bir sekme varsa yenisi açılmaz, var olan sekme öne getirilir (clients.matchAll)
5Kayıtlı Cihazlar tablosuna bakınCihaz adı, kullanıcı ve push servisi (fcm.googleapis.com, updates.push.services.mozilla.com, web.push.apple.com…) ayrı ayrı görünür
6Son satırdaki "3 hata" rozetine bakınO aboneliğe üç kez geçici hata alındı. Kesin ölüm (404/410) olsaydı satır zaten silinmiş olurdu; sayaç, ikisini ayırt etmek içindir
7Tarayıcının çevrimdışı modunu açıp sayfayı yenileyinBeyaz hata sayfası yerine offline.html gelir. Service worker gezinme isteklerine ağ yokken bu sayfayla yanıt verir
8Adres çubuğundaki "Uygulamayı yükle" simgesine basınUygulama ana ekrana eklenir ve adres çubuğu olmadan açılır (display: standalone)
9Kontrol panelindeki PWA Kurulum Durumu kartına bakınService worker kapsamı, manifest, VAPID açık anahtarının tanımlı olup olmadığı ve çevrimdışı sayfası tek bakışta
10Telefonunuzdan açınAlt navigasyon devreye girer; sayfa gövdesinde yatay kaydırma yoktur
Neden localhost'ta çalışıyor ama http:// bir sunucuda çalışmıyor? Service worker ve push güvenli bağlam ister. Tarayıcılar localhost'u tek istisna olarak güvenli sayar; başka her yerde HTTPS zorunludur.

Demo alanı hakkında bilinmesi gerekenler

KonuDurum
Verilerdatabase.sql içindeki 51 kullanıcı + 4 örnek abonelik. Gerçek kişi verisi yoktur.
Örnek aboneliklerBaşka kullanıcılara aittir (#3, #7, #12, #20). "Kendi cihazlarıma gönder" yalnızca sizin kendi tarayıcınızdan oluşturduğunuz aboneliğe gönderir; örnek satırlar gönderiminizi etkilemez.
SıfırlamaDemo veritabanı düzenli aralıklarla başlangıç hâline döner; aboneliğiniz kalıcı değildir.
Bildirim izniTarayıcınız soracaktır. Reddederseniz sayfa çalışmaya devam eder, yalnızca gönderim yapılamaz.
APP_DEBUGCanlıda kendiliğinden false — sunucu adından türetilir.
BağımlılıkSıfır. web-push-php yok, Composer yok, npm yok, CDN yok. VAPID imzası ve şifreleme saf PHP'de.

Bu Proje Nedir?

Web push, ilk bakışta "bir istek at, bildirim gitsin" gibi görünür. Gerçekte üç ayrı parçanın uyum içinde çalışması gerekir ve üçü de tarayıcı tarafından zorunlu kılınmıştır:

  1. Service worker — sayfadan bağımsız çalışan bir betik. Bildirimi gösteren odur; sekme kapalıyken bile ayaktadır.
  2. VAPID imzası — push servisi (Google, Mozilla, Apple) size ait olmayan bir sunucudur ve "bu isteği gerçekten o site mi yolladı?" diye sorar. Cevabı ES256 ile imzalanmış bir JWT verir.
  3. Uçtan uca şifreleme — push servisi mesajın içeriğini okuyamaz. İçerik, tarayıcının abonelikte verdiği anahtarlarla şifrelenir; sunucu ile tarayıcı arasında araya giren servis yalnızca şifreli baytları taşır.

Çoğu PHP örneği bu üçünü composer require minishlink/web-push ile geçiştirir. Bu proje hiçbir kütüphane kullanmadan yazıyor: WebPush sınıfı VAPID JWT'sini üretip ES256 ile imzalar, ECDH anahtar değişimini ve HKDF türetmesini yapar, içeriği aes128gcm (RFC 8188) ile şifreler ve isteği kendisi gönderir.

Sonuç: nasıl çalıştığını gerçekten görebileceğiniz, üretime hazır ve bağımlılıksız bir uygulama — üstelik çevrimdışı çalışan bir PWA kabuğuyla birlikte.

Kimler için uygun?

  • Sitesine bildirim eklemek isteyip "FCM SDK'sı olmadan olur mu?" diye soranlar
  • VAPID'in ne olduğunu ve neden gerektiğini öğrenmek isteyenler
  • Web push şifrelemesinin adım adım nasıl yapıldığını görmek isteyenler
  • Uygulamasını PWA'ya çevirip ana ekrana eklenebilir hâle getirecekler
  • Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir panel kalıbı arayanlar

Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.


İçindekiler


Ekran Görüntüleri

Push bildirimler

Projenin konusu bu sayfadadır. "Bildirimleri aç" düğmesi tarayıcıdan izin ister ve push servisinden (Google, Mozilla, Apple) cihaza özel bir abonelik adresi alıp sunucuya kaydeder — o adresi sunucu üretemez. Kayıtlı Cihazlar tablosu cihaz adını, kullanıcıyı ve hangi push servisinin kullanıldığını ayrı ayrı gösterir. Satırdaki hata rozeti geçici hataları sayar; kesin ölüm (404/410) olsaydı satır zaten silinmiş olurdu.

Push bildirimler sayfası: abonelik düğmesi ve kayıtlı cihazlar tablosu

Kontrol paneli

PWA Kurulum Durumu kartı dört şeyi tek bakışta verir: service worker kapsamı, manifest, VAPID açık anahtarının tanımlı olup olmadığı ve çevrimdışı sayfası. Bir bildirimin gitmemesinin sebebi neredeyse her zaman bu dörtten biridir.

Kontrol paneli: sayaç şeridi ve PWA kurulum durumu kartı

Giriş ekranı

Demo hesapları tek tıkla doldurulur. Giriş denemeleri hız sınırına tabidir; art arda başarısız denemeden sonra hesap geçici olarak kilitlenir.

Giriş ekranı: demo hesapları tek tıkla doldurulur

Koyu tema

Tema tarayıcıda değil kullanıcı hesabında saklanır; başka bir cihazdan girdiğinizde de aynı gelir.

Koyu tema görünümü

Mobil görünüm

390px genişlikte alt navigasyon devreye girer. Sayfa gövdesinde yatay kaydırma yoktur. Uygulama ana ekrana eklendiğinde adres çubuğu olmadan açılır (display: standalone).

390px genişlikte mobil görünüm

Bir bildirimin yolculuğu

 ABONE OLMA (bir kez)
 ────────────────────
 Tarayıcı                          Sunucu (PHP)
    │  "Bildirimleri aç"              │
    │  Notification.requestPermission()│
    │                                  │
    │  pushManager.subscribe({          │
    │    applicationServerKey: VAPID_PUBLIC })
    │        │                          │
    │        ▼                          │
    │   PUSH SERVİSİ (Google/Mozilla/Apple)
    │        │                          │
    │   endpoint + p256dh + auth        │
    │        │                          │
    │        └──── POST /api/push/subscribe ──►  push_subscriptions
    │                                            (kullanıcıya bağlı)

 GÖNDERME (her seferinde)
 ────────────────────────
 Sunucu (PHP)                                    Tarayıcı
    │
    │ 1) VAPID JWT üret: {aud, exp, sub}
    │    ES256 ile imzala (P-256 özel anahtar)
    │
    │ 2) İçeriği ŞİFRELE (aes128gcm / RFC 8188)
    │    - geçici ECDH anahtar çifti üret
    │    - abonenin p256dh'si ile ortak sır türet
    │    - HKDF ile içerik anahtarı + nonce
    │    - AES-128-GCM ile şifrele
    │
    │ 3) POST <endpoint>
    │    Authorization: vapid t=<jwt>, k=<public>
    │    Content-Encoding: aes128gcm
    │    TTL: 86400
    │              │
    │              ▼
    │        PUSH SERVİSİ  ── içeriği OKUYAMAZ, sadece taşır
    │              │
    │              ▼
    │     service-worker.js  'push' olayı
    │              │  event.data.json()
    │              ▼
    │     showNotification(title, {body, icon, data})
    │              │
    │              ▼  kullanıcı tıklar
    │     'notificationclick' → açık sekme varsa ÖNE GETİR,
    │                           yoksa yeni sekme aç
    │
    └── Yanıt 404/410 ise → failures++ (ölü abonelik ayıklanır)

Kritik Kararlar

1. Hiçbir push kütüphanesi kullanılmadı

web-push-php iyi bir kütüphanedir ve üretimde kullanmakta hiçbir sakınca yoktur. Ama bu proje öğretmek için yazıldı: kütüphane çağrısı, VAPID'in ne olduğunu ve şifrelemenin neden gerektiğini gizler.

app/Core/WebPush.php işi baştan sona yapar:

AdımNasıl
VAPID JWT{aud, exp, sub} → base64url → openssl_sign (ES256) → DER imzayı ham 64 bayta çevir
Ortak sırGeçici ECDH anahtar çifti + abonenin p256dh açık anahtarı
Anahtar türetmeHKDF ile içerik anahtarı (16 bayt) ve nonce (12 bayt)
Şifrelemeopenssl_encrypt · AES-128-GCM · RFC 8188 gövde biçimi

Bu, PHP'nin openssl eklentisi dışında hiçbir şey gerektirmez.

2. aes128gcm — eski aesgcm değil

Web push'un iki şifreleme biçimi vardır. Eski aesgcm, tuz ve geçici anahtarı ayrı başlıklarda (Encryption, Crypto-Key) taşır ve uygulaması hem karmaşık hem hataya açıktır.

aes128gcm (RFC 8188) ikisini de gövdenin içine koyar. Sonuç: daha az başlık, daha az yanlış yapma ihtimali. Bütün güncel tarayıcılar destekler.

'Content-Encoding: aes128gcm',

3. VAPID özel anahtarı .env'de değil, dosyada

Açık anahtar .env içinde durur — zaten JavaScript'e gönderilir, gizli değildir.

Özel anahtar ise PEM dosyası olarak storage/vapid.pem içinde tutulur ve o klasör web'e kapalıdır. Sebep: PEM çok satırlıdır ve .env biçimi tek satır bekler; sıkıştırmaya çalışmak sessiz bozulmalara yol açar.

Anahtar çiftini kaybederseniz bütün abonelikler geçersiz olur — abonelikler açık anahtara bağlıdır. Kullanıcılar yeniden izin vermek zorunda kalır. storage/vapid.pem dosyasını yedekleyin.

4. Bildirim yalnızca giriş yapan kullanıcının kendi cihazlarına gider

// Abonelikler kullanıcıya bağlıdır; gönderim de öyle.
WHERE user_id = :id

"Herkese gönder" düğmesi bilerek yoktur. Bir demo uygulamasında herkese bildirim gönderebilen bir düğme, ilk ziyaretçinin oyuncağı olurdu.

Kendi projenizde toplu gönderim yaparken sırayla değil, kuyrukla gönderin: 10.000 aboneye tek istekte gönderim yapmaya çalışmak PHP'nin zaman aşımına takılmasıyla biter.

5. Ölü abonelik ile geçici hata ayrı ele alınır

Kullanıcı tarayıcı verilerini temizlerse veya uygulamayı kaldırırsa abonelik geçersiz olur ama sunucu bunu ancak gönderim denediğinde öğrenir. Push servisinin yanıt kodu ikisini birbirinden ayırır:

// app/Http/Controllers/Api/PushApiController.php
if (in_array($result['status'], [404, 410], true)) {
    $repository->deleteByEndpoint($subscription['endpoint']);  // kesin ölüm
} else {
    $repository->markFailure($subscription['endpoint']);        // geçici olabilir
}

404 / 410 kesin ölümdür: push servisi "böyle bir adres yok" diyor. Satır hemen silinir; beklemenin bir anlamı yoktur.

Diğer hata kodları (500, ağ zaman aşımı) geçici olabilir. Bunlarda satır silinmez, yalnızca failures sayacı artar. Tek bir geçici hata yüzünden çalışan bir aboneliği atmak, kullanıcıyı sessizce bildirimsiz bırakırdı.

Abonelik yeniden kaydedildiğinde sayaç sıfırlanır — ON DUPLICATE KEY UPDATE ... failures = 0.

6. Service worker fetch olayında gezinme ile varlık isteğini ayırır

Her isteği önbelleğe almak cazip görünür ama tehlikelidir: eski bir JavaScript dosyası önbellekte kalırsa uygulama açıklanamaz biçimde bozulur.

Bu yüzden service worker yalnızca gezinme isteklerinde (request.mode === 'navigate') devreye girer: ağ varsa ağdan, yoksa offline.html'den yanıt verir. Diğer istekler doğrudan ağa gider.

7. notificationclick yeni sekme açmaz — var olanı öne getirir

clients.matchAll({ type: 'window', includeUncontrolled: true })

Bildirime her tıklandığında yeni sekme açan bir uygulama, gün sonunda 14 sekme bırakır. Açık bir sekme varsa focus() edilir; yoksa yenisi açılır.

8. Manifest start_url olarak dashboard verir

Ana ekrana eklenen uygulama açıldığında giriş sayfasını değil, panele gider. Oturum yoksa uygulama zaten girişe yönlendirir; ama oturum varsa kullanıcı doğrudan işine başlar.


Neler Var?

Web Push (saf PHP)

  • VAPID JWT üretimi ve ES256 imzası
  • DER → ham imza dönüşümü
  • ECDH ortak sır + HKDF anahtar türetme
  • aes128gcm (RFC 8188) içerik şifreleme
  • TTL, Content-Encoding ve Authorization: vapid başlıkları
  • 404/410'da silme, geçici hatada sayaç
  • Anahtar üretici CLI (bin/vapid-keys.php)

PWA kabuğu

  • service-worker.js — install · activate · fetch · push · notificationclick · pushsubscriptionchange
  • manifest.webmanifest — ana ekrana eklenebilir
  • offline.html — ağ yokken gezinme yanıtı

Panel

  • Bildirimleri aç / kapat (izin akışıyla)
  • Kendi cihazlarına test bildirimi gönderme
  • Kayıtlı cihazlar: ad, kullanıcı, push servisi, hata sayacı
  • PWA kurulum durumu kartı (worker · manifest · VAPID · çevrimdışı)

Ortak altyapı

  • Oturum girişi, "beni hatırla", hız sınırı, CSRF
  • CSP, X-Frame-Options: DENY, nosniff
  • Açık / koyu tema, hesaba kayıtlı
  • Mobilde alt navigasyon, yatay kaydırma yok
  • Kullanıcılar sayfasında canlı filtre (JS kapalıysa da çalışır)
  • Sıfır bağımlılık

Service Worker

Service worker, sayfadan bağımsız çalışan bir betiktir. Sekme kapalıyken bile tarayıcı onu uyandırıp bir olay verebilir — push bildiriminin sekme kapalıyken gelmesinin sebebi budur.

Altı olay dinlenir:

OlayNe yapar
installÇevrimdışı kabuğu önbelleğe alır (offline.html, logo)
activateEski sürümün önbelleklerini siler — bayat dosya kalmaz
fetchYalnızca gezinme isteklerinde: ağ yoksa offline.html
pushGelen veriyi ayrıştırır ve bildirimi gösterir
notificationclickAçık sekmeyi öne getirir, yoksa yenisini açar
pushsubscriptionchangeTarayıcı aboneliği yenilerse sunucuya haber verir

Kapsam (scope) kuralı: service worker yalnızca kendi klasörü ve altındaki adresleri kontrol edebilir. Bu yüzden service-worker.js proje kökündedir; assets/js/ altına koysaydınız yalnızca o klasörü kontrol ederdi ve gezinme isteklerini hiç göremezdi.

Güncelleme: tarayıcı dosyayı bayt bayt karşılaştırır. Tek bir karakter değişirse yeni sürümü kurar. Bu yüzden dosyanın başındaki önbellek sürüm adını (var CACHE = 'cy-pwa-v1') her değişiklikte artırın; activate olayı eskileri temizler.


VAPID anahtarı üretmek

VAPID, "bu bildirimi gerçekten o site gönderdi" diyebilmek için kullanılan bir anahtar çiftidir. Bir kez üretilir ve değişmez.

php bin/vapid-keys.php

Betik P-256 (prime256v1) eğrisinde bir ECDSA çifti üretir:

  • Özel anahtarstorage/vapid.pem (web'e kapalı klasör)
  • Açık anahtar → ekrana yazılır; .env içindeki VAPID_PUBLIC_KEY satırına yapıştırın

VAPID_PUBLIC_KEY=BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQ...
VAPID_SUBJECT=mailto:[email protected]

VAPID_SUBJECT bir mailto: veya https: adresidir; push servisi bir sorun olduğunda kime ulaşacağını buradan öğrenir.

Windows/XAMPP'ta openssl.cnf bulunamıyor hatası alırsanız betik gerekli seçenekleri WebPush::keygenOptions() üzerinden zaten veriyor; yine de sorun sürerse php.ini içindeki openssl.cafile ve OPENSSL_CONF ortam değişkenini kontrol edin.
Anahtarı değiştirmeyin. Abonelikler açık anahtara bağlıdır; değiştirirseniz mevcut bütün abonelikler geçersiz olur ve kullanıcılar yeniden izin vermek zorunda kalır.

Güvenlik: Neyi, Nasıl Kapattık?

AçıkTipik hatalı kodBu projede
Özel anahtar sızıntısıPEM'i assets/ altına koymakstorage/vapid.pem — klasör .htaccess ile web'e kapalı
İçeriğin push servisinde okunmasıŞifresiz gönderimaes128gcm ile uçtan uca şifreleme; servis yalnızca şifreli baytları taşır
Sahte gönderimİmzasız istekVAPID / ES256 imzası; push servisi imzasız isteği reddeder
Başkasının aboneliğini silme (IDOR)DELETE WHERE endpoint = ?Sorguya user_id de katılır; kullanıcı yalnızca kendi aboneliğini silebilir
Herkese bildirim göndermeKısıtsız "toplu gönder"Gönderim yalnızca giriş yapan kullanıcının kendi cihazlarına
Ölü aboneliklerin birikmesiHiç temizlememek404/410 yanıtında satır hemen silinir; geçici hatalarda yalnızca sayaç artar
CLI betiğine web'den erişimbin/ klasörü açıkbin/.htaccess ile kapalı ve betikte PHP_SAPI === 'cli' kontrolü
SQL enjeksiyonu"... WHERE endpoint = '$e'"Tüm sorgular hazır ifade; ATTR_EMULATE_PREPARES = false
CSRFGizli alan yokHer POST'ta jeton; hash_equals()
XSSecho $row['user_agent']Sunucuda e(); ayrıca CSP script-src 'self'
Hata sızıntısıCanlıda display_errors=OnAPP_DEBUG ortamdan türetilir
Güvensiz bağlamhttp:// üzerinden push denemekTarayıcı zaten engeller; belgeler HTTPS'i zorunlu olarak işaretler

Kurulum

Gereksinimler

PHP8.0 veya üzeri · openssl eklentisi zorunlu
MySQL / MariaDB5.7+ / 10.3+
Web sunucusuApache (mod_rewrite) veya Nginx
BağlantıHTTPS (yalnızca localhost istisnadır)

Adımlar

git clone https://github.com/CilginYazilim/pwa-push-notification-system.git
cd pwa-push-notification-system

mysql -u root -p < database.sql
cp .env.example .env        # Windows: copy .env.example .env

php bin/vapid-keys.php      # açık anahtarı .env'e yapıştırın

Açın: http://localhost/pwa-push-notification-system/ · Giriş: [email protected] / Admin1234

Sonra Push Bildirimler sayfasından "Bildirimleri aç" deyin.


Yapılandırma

APP_DEBUG=true          # silerseniz: yerelde açık, canlıda kapalı
APP_URL=
APP_PRETTY_URLS=true

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=cy_pwa_push
DB_USER=root
DB_PASS=

# --- WEB PUSH (VAPID) ---
VAPID_PUBLIC_KEY=       # bin/vapid-keys.php çıktısı
VAPID_SUBJECT=mailto:[email protected]

Özel anahtar .env'de değildir; storage/vapid.pem dosyasındadır.

DeğerYeriVarsayılanNe yapar
WebPush::TTLapp/Core/WebPush.php86400Push servisi mesajı kaç saniye saklasın (cihaz kapalıysa)
ŞifrelemeWebPush::send()aes128gcmRFC 8188 içerik şifreleme biçimi
CACHEservice-worker.js'cy-pwa-v1'Önbellek sürüm adı; değiştirince activate eskileri temizler

Dosya Yapısı

pwa-push-notification-system/
│
├── index.php                     Ön denetleyici — TEK giriş noktası
├── service-worker.js             ★ KÖKTE olmalı (kapsam kuralı)
├── manifest.webmanifest          Ana ekrana ekleme künyesi
├── offline.html                  Ağ yokken gösterilen sayfa
├── database.sql                  Şema + 51 kullanıcı + 4 örnek abonelik
├── .env.example
│
├── bin/
│   ├── vapid-keys.php            ★ P-256 anahtar çifti üretici (CLI)
│   └── .htaccess                 Web erişimine kapalı
│
├── storage/
│   ├── vapid.pem                 ★ VAPID ÖZEL ANAHTARI — web'e kapalı
│   └── .htaccess
│
├── app/
│   ├── Core/
│   │   ├── WebPush.php           ★ VAPID JWT · ECDH · HKDF · aes128gcm · gönderim
│   │   ├── Auth.php · Session.php · Csrf.php · RateLimiter.php
│   │   ├── Database.php          PDO (EMULATE_PREPARES = false)
│   │   ├── Env.php               .env okuyucu + isLocalHost()
│   │   └── ...
│   │
│   ├── Http/Controllers/         Auth · Dashboard · Push · User · Api
│   ├── Repositories/PushSubscriptionRepository.php
│   └── Support/helpers.php
│
├── views/                        Düzenler, push sayfası
├── assets/                       css · js (push izin akışı) · images
├── config/config.php
├── routes/web.php
└── docs/screenshots/

Veritabanı Şeması

push_subscriptions

SütunTipİşi
idINT UNSIGNEDBirincil anahtar
user_idINT UNSIGNEDAbonelik kime ait (ON DELETE CASCADE)
endpointVARCHAR(500)Push servisinin verdiği cihaza özel adres
p256dhVARCHAR(255)Tarayıcının açık anahtarı — şifreleme bununla yapılır
authVARCHAR(255)Kimlik doğrulama sırrı (şifrelemenin ikinci girdisi)
user_agentVARCHAR(255)"Hangi cihazım bu?" sorusuna cevap verecek kadarı
failuresSMALLINT UNSIGNEDGeçici hata sayacı; 404/410 gelirse satır zaten silinir
created_at · updated_atDATETIMEAbone olma ve son yenilenme anı
KararNeden
endpoint benzersizAynı tarayıcı iki kez abone olursa yeni satır değil, var olan güncellenir; failures de sıfırlanır
Anahtarlar (p256dh, auth) saklanıyorŞifreleme her gönderimde bu iki değerle yapılır; onlarsız mesaj hazırlanamaz
user_agent tam saklanmıyorCihazı tanımaya yetecek kadarı yeterli; parmak izi çıkarmaya gerek yok
failures yalnızca geçici hatalar için404/410 kesin ölümdür ve hemen silinir; ama geçici bir ağ hatası yüzünden çalışan bir aboneliği atmak kullanıcıyı sessizce bildirimsiz bırakırdı
Kullanıcıya bağlı (CASCADE)Hesap silinince abonelikleri de gider; ölü kayıt kalmaz

SSS

HTTPS olmadan çalışır mı?

Hayır — localhost dışında çalışmaz ve bu bir tercih değil, tarayıcı kuralıdır.

Service worker ve push API'si güvenli bağlam ister. Sebebi açıktır: service worker sitenizin bütün isteklerini görebilir; araya giren biri onu enjekte edebilseydi tam kontrolü ele geçirirdi.

Geliştirme için localhost yeterlidir. Yerel ağdaki bir telefondan denemek isterseniz ngrok gibi bir tünel veya kendi imzaladığınız bir sertifika kullanın.

iOS'ta çalışıyor mu?

Evet, iOS 16.4 ve sonrasında — ama bir şartla: kullanıcının uygulamayı önce ana ekrana eklemesi gerekir. Safari sekmesinde açık dururken push izni istenemez.

Bu yüzden manifest ve "ana ekrana ekle" akışı yalnızca bir süs değil, iOS'ta push'un ön koşuludur.

Bildirim gitmiyor, hata da yok

Sırayla kontrol edin:

  1. HTTPS mi? (localhost hariç)
  2. Tarayıcı izni verilmiş mi? Adres çubuğundaki kilit simgesinden bakın; bir kez reddedildiyse sayfa bir daha soramaz, izni elle açmanız gerekir.
  3. .env içinde VAPID_PUBLIC_KEY dolu mu? Kontrol panelindeki kart size söyler.
  4. storage/vapid.pem dosyası var mı ve PHP onu okuyabiliyor mu?
  5. İşletim sistemi düzeyinde bildirimler kapalı olabilir (Windows'ta "Odak yardımı", macOS'ta "Rahatsız etmeyin").

Toplu bildirim nasıl gönderirim?

Döngüyle değil, kuyrukla. 10.000 aboneye tek istekte gönderim yapmaya çalışmak PHP'nin zaman aşımına takılmasıyla biter ve yarısı gitmiş, yarısı gitmemiş bir gönderim bırakır.

Doğru kalıp: her abonelik için kuyruğa bir iş bırakın, worker'lar paralel göndersin. Kuyruk örneği için İş Kuyruğu ve Worker Sistemi'ne bakın.

Neden web-push-php kullanmadınız?

Üretimde kullanabilirsiniz, iyi bir kütüphanedir. Ama bu proje VAPID'in ve şifrelemenin nasıl çalıştığını göstermek için yazıldı; tek satırlık bir kütüphane çağrısı bunu gizlerdi.

WebPush.php 471 satırdır ve her adımı yorumlarla açıklar. Okuduktan sonra kütüphaneye geçmek isterseniz, artık onun ne yaptığını biliyor olacaksınız.

Service worker'ı güncelledim ama eski sürüm çalışıyor

Tarayıcı yeni service worker'ı kurar ama eski sürüm hâlâ bir sekmeyi kontrol ediyorsa beklemeye alır.

Geliştirme sırasında: DevTools → Application → Service Workers → "Update on reload" işaretleyin veya "Unregister" deyip sayfayı yenileyin.

Üretimde: CACHE değerini her sürümde artırın (cy-pwa-v2 …); activate olayı eski önbellekleri temizler.


Canlı Ortama Alırken

  • [ ] HTTPS zorunlu olsun — push ve service worker bunsuz çalışmaz
  • [ ] .env içinde APP_DEBUG=false (veya satırı tümüyle silin)
  • [ ] VAPID_PUBLIC_KEY dolu, storage/vapid.pem yerinde
  • [ ] storage/vapid.pem dosyasını yedekleyin — kaybederseniz tüm abonelikler geçersiz olur
  • [ ] storage/ ve bin/ klasörlerinin web'den erişilemediğini doğrulayın (403)
  • [ ] service-worker.js proje kökünde mi?
  • [ ] manifest.webmanifest içindeki start_url ve simgeler doğru mu?
  • [ ] VAPID_SUBJECT gerçek bir iletişim adresi olsun
  • [ ] Toplu gönderim yapacaksanız kuyruk kullanın
  • [ ] Demo hesaplarının parolalarını değiştirin veya hesapları silin

Sorun Giderme

BelirtiSebepÇözüm
"Bildirimleri aç" hiçbir şey yapmıyorHTTPS yoklocalhost kullanın veya sertifika kurun
İzin bir daha sorulmuyorKullanıcı bir kez reddettiSite ayarlarından elle açın
Gönderim 401 / 403 dönüyorVAPID imzası geçersizVAPID_PUBLIC_KEY ile storage/vapid.pem aynı çiftten mi?
Gönderim 404 / 410 dönüyorAbonelik ölmüşNormaldir; failures artar, eşik aşılınca silinir
Bildirim geliyor ama içerik boşŞifreleme adımı başarısızopenssl eklentisi açık mı?
openssl.cnf bulunamadı (Windows)XAMPP yapılandırmasıOPENSSL_CONF ortam değişkenini ayarlayın
Service worker güncellenmiyorEski sürüm sekmeyi kontrol ediyorCACHE değerini artırın; DevTools'tan "Unregister"
Çevrimdışıyken beyaz sayfaService worker kayıtlı değilDevTools → Application → Service Workers'tan durumu kontrol edin
iOS'ta izin istenmiyorAna ekrana eklenmemişPaylaş → "Ana Ekrana Ekle"

Yol Haritası

  • [ ] Bildirim eylem düğmeleri (actions) örneği
  • [ ] Toplu gönderim için kuyruk entegrasyonu
  • [ ] Gönderim geçmişi tablosu (kime, ne zaman, sonuç)
  • [ ] Konu bazlı abonelik (topic) desteği
  • [ ] Urgency başlığı desteği ve panelden seçilebilmesi

Katkı

Hata bildirimi ve öneriler için issue açabilirsiniz.

Lisans

MIT — ticari projelerinizde de özgürce kullanabilirsiniz.


Kaynak Kod soldaki ağaçtan bir dosya seçin

  • app
    • Core
      • Auth.php 13.9 KB
      • Autoloader.php 2.2 KB
      • Config.php 1.9 KB
      • Csrf.php 2.8 KB
      • Database.php 3.5 KB
      • Env.php 4.3 KB
      • Flash.php 2.6 KB
      • Middleware.php 2.5 KB
      • Paginator.php 8.6 KB
      • RateLimiter.php 3.9 KB
      • Request.php 4.5 KB
      • Response.php 7 KB
      • Router.php 6.3 KB
      • Session.php 6.3 KB
      • Validator.php 8.8 KB
      • View.php 3.5 KB
      • WebPush.php 17.5 KB
    • Http
      • Controllers
        • Api
          • PreferenceApiController.php 2.5 KB
          • PushApiController.php 6.8 KB
        • AuthController.php 3.2 KB
        • DashboardController.php 1.4 KB
        • PushController.php 1.9 KB
        • UserController.php 3.1 KB
      • Controller.php 1.3 KB
    • Models
      • User.php 5.9 KB
    • Repositories
      • ActivityRepository.php 3.8 KB
      • PushSubscriptionRepository.php 5.5 KB
      • RememberTokenRepository.php 5.8 KB
      • UserRepository.php 6.9 KB
    • Support
      • helpers.php 12.3 KB
    • .htaccess 851 B
  • assets
    • css
      • admin.css 58.6 KB
      • bootstrap.min.css 227.5 KB
      • cilginyazilim.css 29.8 KB
      • feature.css 2.1 KB
    • images
      • logo.png 70.4 KB
    • js
      • app.js 20.2 KB
      • bootstrap.bundle.js 203.2 KB
      • jquery-3.7.0.js 278.3 KB
      • login.js 1.8 KB
      • push.js 10.5 KB
      • users.js 4.7 KB
  • bin
    • .htaccess 666 B
    • vapid-keys.php 4.4 KB
  • config
    • .htaccess 196 B
    • config.php 5 KB
    • menu.php 1 KB
  • docs
    • screenshots
      • 01-giris.png 377 KB
      • 02-kontrol-paneli.png 69.6 KB
      • 03-push-bildirimler.png 84 KB
      • 04-koyu-tema.png 83.8 KB
      • 05-mobil.png 55 KB
  • routes
    • .htaccess 197 B
    • web.php 3.3 KB
  • storage
    • .gitkeep 1 B
    • .htaccess 685 B
    • vapid.pem 241 B
  • views
    • auth
      • login.php 7.4 KB
    • dashboard
      • _feature.php 3.3 KB
      • index.php 4 KB
    • errors
      • 404.php 844 B
      • 500.php 565 B
    • layouts
      • admin.php 6.8 KB
      • auth.php 2.9 KB
      • plain.php 1.3 KB
    • partials
      • bottomnav.php 2.1 KB
      • head_extra.php 1.3 KB
      • pagination.php 4.9 KB
      • sidebar.php 2.7 KB
      • topbar.php 3.5 KB
    • push
      • index.php 8.1 KB
    • users
      • index.php 6.8 KB
    • .htaccess 262 B
  • .gitattributes 2.3 KB
  • .gitignore 1 KB
  • .htaccess 7.2 KB
  • CHANGELOG.md 7.6 KB
  • database.sql 19.2 KB
  • index.php 5.4 KB
  • LICENSE 1.1 KB
  • manifest.webmanifest 703 B
  • offline.html 3.9 KB
  • README.en.md 30.5 KB
  • README.md 31.4 KB
  • service-worker.js 9.3 KB
Dosya seçilmedi
İncelemek istediğiniz dosyayı soldaki ağaçtan seçin.

Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).

Sık Sorulan Sorular

Hayır, localhost dışında çalışmaz ve bu bir tercih değil tarayıcı kuralıdır. Service worker ve push API'si güvenli bağlam ister; sebebi açıktır, service worker sitenizin bütün isteklerini görebilir ve araya giren biri onu enjekte edebilseydi tam kontrolü ele geçirirdi. Geliştirme için localhost yeterlidir.

Evet, iOS 16.4 ve sonrasında — ama bir şartla: kullanıcının uygulamayı önce ana ekrana eklemesi gerekir. Safari sekmesinde açık dururken push izni istenemez. Bu yüzden manifest ve "ana ekrana ekle" akışı yalnızca bir süs değil, iOS'ta push'un ön koşuludur.

Üretimde kullanabilirsiniz, iyi bir kütüphanedir. Ama bu proje VAPID'in ve şifrelemenin nasıl çalıştığını göstermek için yazıldı; tek satırlık bir kütüphane çağrısı bunu gizlerdi. WebPush.php 471 satırdır ve her adımı yorumlarla açıklar.

Döngüyle değil, kuyrukla. On bin aboneye tek istekte gönderim yapmaya çalışmak PHP'nin zaman aşımına takılmasıyla biter ve yarısı gitmiş bir gönderim bırakır. Doğru kalıp: her abonelik için kuyruğa bir iş bırakın, worker'lar paralel göndersin.

Değiştirmeyin. Abonelikler açık anahtara bağlıdır; değiştirirseniz mevcut bütün abonelikler geçersiz olur ve kullanıcılar yeniden izin vermek zorunda kalır. storage/vapid.pem dosyasını mutlaka yedekleyin.

Sırayla: HTTPS mi (localhost hariç), tarayıcı izni verilmiş mi (bir kez reddedildiyse sayfa bir daha soramaz), .env içinde VAPID_PUBLIC_KEY dolu mu, storage/vapid.pem var mı ve PHP okuyabiliyor mu, işletim sistemi düzeyinde bildirimler kapalı mı. Kontrol panelindeki PWA kurulum kartı ilk üçünü size söyler.

Yorumlar 0 konuşma

Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.

Soru Sor veya Yorum Yaz

Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.

Yayınlanmaz; yalnızca yanıt bildirimi için kullanılır.
En az 10 karakter.

İlgili Kod Örnekleri