Kullanıcı bir sekmeyi açık bırakmış, birkaç dakika sonra geri dönmüş ve karşısında login ekranı. Ya da daha kötüsü: uzun bir form doldurmuş, kaydet butonuna basmış ve veriler uçup gitmiş. Bu sorunun arkasında birden fazla neden yatıyor. Benim daha önceleri amatör projelerimde saç baş yolduğum bir konu : )
PHP Session Neden Beklenmedik Şekilde Sona Erer?
PHP’nin varsayılan session süresi çok kısa
PHP’nin session.gc_maxlifetime ayarı varsayılan olarak 1440 saniye, yani tam olarak 24 dakika. Sunucuda bu değeri php.ini veya kod içinden değiştirebilirsiniz, ama shared hosting ortamlarında işler karmaşıklaşır.
Shared hosting’de kontrol elinizde değil
Shared hosting kullanıyorsanız, sunucu üzerindeki PHP ayarları hosting sağlayıcısı tarafından belirlenir. ini_set('session.gc_maxlifetime', 86400) yazsanız bile, gc (garbage collector) başka bir kullanıcının isteğiyle tetiklendiğinde sizin session dosyalarınızı da silip götürebilir. Bu, shared hosting’de oldukça yaygın bir durumdur.
Session dosyaları disk tabanlıdır
PHP varsayılan olarak session verilerini sunucunun /tmp dizinine dosya olarak kaydeder. Bu dosyalar:
- İşletim sistemi tarafından temizlenebilir,
- Birden fazla sunucu (load balancer) kullanıyorsanız paylaşılamaz,
- Disk dolduğunda sorun çıkarır.
Çözüm: Session’ları Veritabanında Saklayın
PHP, SessionHandlerInterface arayüzü sayesinde session’ların nerede saklanacağını tamamen özelleştirmenize olanak tanır. Veritabanını kullandığınızda:
- Session süresi üzerinde tam kontrol sizde olur,
- Shared hosting’in gc mekanizması sizi etkileyemez,
- Birden fazla sunucu aynı session’ı paylaşabilir,
- Session verilerini raporlayabilir, izleyebilirsiniz.
Adım 1: Veritabanı Tablosunu Oluşturun
Önce session verilerini tutacak tabloyu oluşturun:
CREATE TABLE IF NOT EXISTS php_sessions (
session_id VARCHAR(128) NOT NULL,
session_data MEDIUMTEXT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (session_id),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
MEDIUMTEXT kullanıyoruz çünkü session içinde büyük veriler (sepet içeriği, izinler vb.) bulunabilir. INDEX idx_expires ise süresi dolmuş kayıtları temizleme sorgusunu hızlandırır.
Adım 2: Session Handler Sınıfını Yazın
<?php
class DbSessionHandler implements SessionHandlerInterface
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
// Session başlatılırken çağrılır — veritabanı bağlantımız zaten hazır
public function open(string $path, string $name): bool
{
return true;
}
// Session kapatılırken çağrılır
public function close(): bool
{
return true;
}
// Session verisi okunur
public function read(string $id): string
{
$stmt = $this->db->prepare(
"SELECT session_data
FROM php_sessions
WHERE session_id = ? AND expires_at > NOW()"
);
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $row['session_data'] : '';
}
// Session verisi yazılır (her istek sonunda otomatik çağrılır)
public function write(string $id, string $data): bool
{
$expires = date('Y-m-d H:i:s', time() + SESSION_LIFETIME_SECONDS);
$stmt = $this->db->prepare(
"INSERT INTO php_sessions (session_id, session_data, expires_at)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
session_data = VALUES(session_data),
expires_at = VALUES(expires_at)"
);
return $stmt->execute([$id, $data, $expires]);
}
// Session yok edildiğinde (logout veya session_destroy())
public function destroy(string $id): bool
{
$stmt = $this->db->prepare(
"DELETE FROM php_sessions WHERE session_id = ?"
);
return $stmt->execute([$id]);
}
// Süresi dolmuş session'ları temizle
public function gc(int $max_lifetime): int|false
{
$stmt = $this->db->prepare(
"DELETE FROM php_sessions WHERE expires_at < NOW()"
);
$stmt->execute();
return $stmt->rowCount();
}
}
Bu sınıfın kilit noktası write() metodundaki ON DUPLICATE KEY UPDATE yapısıdır. Her istekte session verisi güncellenirken expires_at de yenilenir. Böylece kullanıcı aktif olduğu sürece oturumu hiçbir zaman sona ermez.
Adım 3: Handler’ı Aktif Edin
Session handler’ı session_start()‘tan önce kaydetmeniz şart:
<?php
// Sabit tanımla — tek yerden yönetin
if (!defined('SESSION_LIFETIME_SECONDS')) {
define('SESSION_LIFETIME_SECONDS', 30 * 24 * 60 * 60); // 30 gün
}
// Veritabanı bağlantısı
$db = new PDO(
"mysql:host=localhost;dbname=veritabani_adi;charset=utf8mb4",
"kullanici",
"sifre",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Handler'ı kaydet (session_start'tan ÖNCE!)
$handler = new DbSessionHandler($db);
session_set_save_handler($handler, true);
// Çerez ayarlarını yapılandır
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
'lifetime' => SESSION_LIFETIME_SECONDS,
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
// Artık session'ı başlatabilirsiniz
session_start();
Bu kadar. Bundan sonra $_SESSION ile her zamanki gibi çalışmaya devam edebilirsiniz.
Adım 4: Hareketsizlik Kontrolü (Opsiyonel ama Önerilen)
“30 gün boyunca oturum açık kalsın” derken iki farklı davranış söz konusu olabilir:
A) Kullanıcı aktif olduğu sürece süre uzasın, 30 gün boyunca hiç gelmezse kapansın:
Yukarıdaki write() metodu bunu zaten yapıyor. Her istekte expires_at güncelleniyor.
B) Kullanıcı X saat hiçbir şey yapmazsa kapansın:
Bunun için checkSession() fonksiyonuna hareketsizlik kontrolü ekleyin:
function checkSession(int $inactivity_limit = 3600): void
{
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header('Location: login.php');
exit();
}
$now = time();
$last_activity = (int)($_SESSION['last_activity'] ?? $now);
// Hareketsizlik süresi aşıldıysa oturumu kapat
if ($now - $last_activity > $inactivity_limit) {
session_unset();
session_destroy();
header('Location: login.php?msg=timeout');
exit();
}
// Her istekte aktivite zamanını güncelle
$_SESSION['last_activity'] = $now;
}
Kullanımı:
// Varsayılan 1 saat hareketsizlik kontrolü
checkSession();
// Ya da özel süre — 8 saat
checkSession(8 * 3600);
Adım 5: Süresi Dolmuş Session’ları Temizleme
PHP’nin gc() mekanizması otomatik çalışır ama production ortamında bunu kendiniz kontrol etmek daha güvenilirdir. Bir cron job ile günlük temizlik yapabilirsiniz:
<?php
// cleanup_sessions.php — günde bir kez çalıştırın
require_once 'db.php';
$stmt = $db->prepare("DELETE FROM php_sessions WHERE expires_at < NOW()");
$stmt->execute();
$silinen = $stmt->rowCount();
echo date('Y-m-d H:i:s') . " — $silinen süresi dolmuş session silindi.\n";
Cron tanımı (her gece 03:00’da):
0 3 * * * php /var/www/html/cleanup_sessions.php >> /var/log/session_cleanup.log
Dosya Yapısı ve Sıralama
Projedeki dosya yapısı şöyle olmalı:
sistem/
├── db.php ← Bağlantı + SESSION_LIFETIME_SECONDS sabiti
├── DbSessionHandler.php ← Session handler sınıfı
└── funcs.php ← checkSession() ve diğer fonksiyonlar
db.php içinde sıralama kritik — her adım bir öncekine bağlı:
<?php
// 1. Sabiti tanımla
define('SESSION_LIFETIME_SECONDS', 30 * 24 * 60 * 60);
// 2. Veritabanına bağlan
$db = new PDO(...);
// 3. Handler sınıfını yükle
require_once __DIR__ . '/DbSessionHandler.php';
// 4. Handler'ı kaydet (session_start ÖNCESİNDE!)
session_set_save_handler(new DbSessionHandler($db), true);
// 5. Çerez parametrelerini ayarla
session_set_cookie_params([...]);
funcs.php başında:
<?php
require_once __DIR__ . '/db.php'; // handler zaten kayıtlı
if (session_status() === PHP_SESSION_NONE) {
session_start(); // Artık veritabanından okuyup yazıyor
}
Bonus: Aktif Oturumları Listeleyin
Veritabanı kullandığınız için artık aktif oturumları sorgulayabilirsiniz:
function aktif_oturumlari_getir(PDO $db): array
{
$stmt = $db->query(
"SELECT session_id, expires_at, created_at
FROM php_sessions
WHERE expires_at > NOW()
ORDER BY expires_at DESC"
);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Bu, güvenlik paneli, “aktif kullanıcılar” ekranı veya şüpheli oturumları sonlandırma gibi özellikler için kapı açar. Kodlar PHP 8.1+ ile uyumludur.