İleri
uygulama-iskeleti/php-baslangic-sablonu
PHP Başlangıç Şablonu (CY PHP Starter)
- PHP 8.1
- PDO
- MySQL
- MariaDB
- +4
309 inceleme 150 canlı açılış 6 beğeni 2 yorum
Uygulama İskeleti İleri
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.
Veritabanı şeması projedeki database.sql dosyasında.
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:
Ç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?
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
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)
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ım | Nasıl |
|---|---|
| VAPID JWT | {aud, exp, sub} → base64url → openssl_sign (ES256) → DER imzayı ham 64 bayta çevir |
| Ortak sır | Geçici ECDH anahtar çifti + abonenin p256dh açık anahtarı |
| Anahtar türetme | HKDF ile içerik anahtarı (16 bayt) ve nonce (12 bayt) |
| Şifreleme | openssl_encrypt · AES-128-GCM · RFC 8188 gövde biçimi |
Bu, PHP'nin openssl eklentisi dışında hiçbir şey gerektirmez.
aes128gcm — eski aesgcm değilWeb 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',
.env'de değil, dosyadaAçı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.
// Abonelikler kullanıcıya bağlıdır; gönderim de öyle.
WHERE user_id = :idKendi 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.
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.
fetch olayında gezinme ile varlık isteğini ayırırHer 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.
notificationclick yeni sekme açmaz — var olanı öne getirirclients.matchAll({ type: 'window', includeUncontrolled: true })focus() edilir; yoksa yenisi açılır.
start_url olarak dashboard verirAna 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.
Web Push (saf PHP)
TTL, Content-Encoding ve Authorization: vapid başlıkları404/410'da silme, geçici hatada sayaçbin/vapid-keys.php)PWA kabuğu
service-worker.js — install · activate · fetch · push · notificationclick · pushsubscriptionchangemanifest.webmanifest — ana ekrana eklenebiliroffline.html — ağ yokken gezinme yanıtıPanel
Ortak altyapı
X-Frame-Options: DENY, nosniffService 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:
| Olay | Ne yapar |
|---|---|
install | Çevrimdışı kabuğu önbelleğe alır (offline.html, logo) |
activate | Eski sürümün önbelleklerini siler — bayat dosya kalmaz |
fetch | Yalnızca gezinme isteklerinde: ağ yoksa offline.html |
push | Gelen veriyi ayrıştırır ve bildirimi gösterir |
notificationclick | Açık sekmeyi öne getirir, yoksa yenisini açar |
pushsubscriptionchange | Tarayı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, "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.phpprime256v1) eğrisinde bir ECDSA çifti üretir:
storage/vapid.pem (web'e kapalı klasör).env içindeki VAPID_PUBLIC_KEY satırına yapıştırınVAPID_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'taopenssl.cnfbulunamıyor hatası alırsanız betik gerekli seçenekleriWebPush::keygenOptions()üzerinden zaten veriyor; yine de sorun sürersephp.iniiçindekiopenssl.cafileveOPENSSL_CONFortam 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.
| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| Özel anahtar sızıntısı | PEM'i assets/ altına koymak | storage/vapid.pem — klasör .htaccess ile web'e kapalı |
| İçeriğin push servisinde okunması | Şifresiz gönderim | aes128gcm ile uçtan uca şifreleme; servis yalnızca şifreli baytları taşır |
| Sahte gönderim | İmzasız istek | VAPID / 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önderme | Kısıtsız "toplu gönder" | Gönderim yalnızca giriş yapan kullanıcının kendi cihazlarına |
| Ölü aboneliklerin birikmesi | Hiç temizlememek | 404/410 yanıtında satır hemen silinir; geçici hatalarda yalnızca sayaç artar |
| CLI betiğine web'den erişim | bin/ klasörü açık | bin/.htaccess ile kapalı ve betikte PHP_SAPI === 'cli' kontrolü |
| SQL enjeksiyonu | "... WHERE endpoint = '$e'" | Tüm sorgular hazır ifade; ATTR_EMULATE_PREPARES = false |
| CSRF | Gizli alan yok | Her POST'ta jeton; hash_equals() |
| XSS | echo $row['user_agent'] | Sunucuda e(); ayrıca CSP script-src 'self' |
| Hata sızıntısı | Canlıda display_errors=On | APP_DEBUG ortamdan türetilir |
| Güvensiz bağlam | http:// üzerinden push denemek | Tarayıcı zaten engeller; belgeler HTTPS'i zorunlu olarak işaretler |
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].env'de değildir; storage/vapid.pem dosyasındadır.
| Değer | Yeri | Varsayılan | Ne yapar |
|---|---|---|---|
WebPush::TTL | app/Core/WebPush.php | 86400 | Push servisi mesajı kaç saniye saklasın (cihaz kapalıysa) |
| Şifreleme | WebPush::send() | aes128gcm | RFC 8188 içerik şifreleme biçimi |
CACHE | service-worker.js | 'cy-pwa-v1' | Önbellek sürüm adı; değiştirince activate eskileri temizler |
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/
push_subscriptions| Sütun | Tip | İşi |
|---|---|---|
id | INT UNSIGNED | Birincil anahtar |
user_id | INT UNSIGNED | Abonelik kime ait (ON DELETE CASCADE) |
endpoint | VARCHAR(500) | Push servisinin verdiği cihaza özel adres |
p256dh | VARCHAR(255) | Tarayıcının açık anahtarı — şifreleme bununla yapılır |
auth | VARCHAR(255) | Kimlik doğrulama sırrı (şifrelemenin ikinci girdisi) |
user_agent | VARCHAR(255) | "Hangi cihazım bu?" sorusuna cevap verecek kadarı |
failures | SMALLINT UNSIGNED | Geçici hata sayacı; 404/410 gelirse satır zaten silinir |
created_at · updated_at | DATETIME | Abone olma ve son yenilenme anı |
| Karar | Neden |
|---|---|
endpoint benzersiz | Aynı 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ıyor | Cihazı tanımaya yetecek kadarı yeterli; parmak izi çıkarmaya gerek yok |
failures yalnızca geçici hatalar için | 404/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 |
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:
localhost hariç).env içinde VAPID_PUBLIC_KEY dolu mu? Kontrol panelindeki kart size söyler.storage/vapid.pem dosyası var mı ve PHP onu okuyabiliyor mu?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.
.env içinde APP_DEBUG=false (veya satırı tümüyle silin)VAPID_PUBLIC_KEY dolu, storage/vapid.pem yerindestorage/vapid.pem dosyasını yedekleyin — kaybederseniz tüm abonelikler geçersiz olurstorage/ 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| Belirti | Sebep | Çözüm |
|---|---|---|
| "Bildirimleri aç" hiçbir şey yapmıyor | HTTPS yok | localhost kullanın veya sertifika kurun |
| İzin bir daha sorulmuyor | Kullanıcı bir kez reddetti | Site ayarlarından elle açın |
Gönderim 401 / 403 dönüyor | VAPID imzası geçersiz | VAPID_PUBLIC_KEY ile storage/vapid.pem aynı çiftten mi? |
Gönderim 404 / 410 dönüyor | Abonelik ölmüş | Normaldir; failures artar, eşik aşılınca silinir |
| Bildirim geliyor ama içerik boş | Şifreleme adımı başarısız | openssl eklentisi açık mı? |
openssl.cnf bulunamadı (Windows) | XAMPP yapılandırması | OPENSSL_CONF ortam değişkenini ayarlayın |
| Service worker güncellenmiyor | Eski sürüm sekmeyi kontrol ediyor | CACHE değerini artırın; DevTools'tan "Unregister" |
| Çevrimdışıyken beyaz sayfa | Service worker kayıtlı değil | DevTools → Application → Service Workers'tan durumu kontrol edin |
| iOS'ta izin istenmiyor | Ana ekrana eklenmemiş | Paylaş → "Ana Ekrana Ekle" |
actions) örneğiUrgency başlığı desteği ve panelden seçilebilmesiHata bildirimi ve öneriler için issue açabilirsiniz.
MIT — ticari projelerinizde de özgürce kullanabilirsiniz.
İncelemek istediğiniz dosyayı ağaçtan seçin.
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).
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.
Bu örneğin daha gelişmişi projenizde mi lazım?
İhtiyacınızı anlatın; bu yapıyı sizin sisteminize uyarlayalım ya da size özel geliştirelim.
Henüz yorum yok. Takıldığınız bir yer varsa ilk soruyu siz sorun.
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.
İleri
uygulama-iskeleti/php-baslangic-sablonu
309 inceleme 150 canlı açılış 6 beğeni 2 yorum
Orta
uygulama-iskeleti/php-scheduler-cron
178 inceleme 50 canlı açılış
Orta
uygulama-iskeleti/php-multi-language-i18n
150 inceleme 48 canlı açılış