PHP İLE VERİTABANI BAĞLANTISI VE GÜVENLİ SORGULAMA REHBERİ
PDO, MySQLi, Prepared Statements, Güvenlik, Performans ve En İyi Pratikler

Merhaba, bugün PHP ile veritabanı bağlantısı kurma, güvenli sorgulama yapma ve veritabanı
işlemlerini profesyonelce yönetme konularını derinlemesine inceleyeceğiz. PHP, web
geliştirme dünyasında en yaygın kullanılan dillerden biridir ve veritabanı işlemleri,
hemen hemen her web uygulamasının temelini oluşturur. PHP ile MySQL, PostgreSQL,
SQLite, Oracle gibi birçok farklı veritabanı sistemine bağlanabilir ve işlemler
gerçekleştirebilirsiniz.

Bu rehberde, PHP'de veritabanı bağlantısı kurmanın iki ana yöntemini (MySQLi ve PDO)
detaylıca inceleyeceğiz. Özellikle PDO (PHP Data Objects) üzerinde duracağız çünkü
PDO, birden fazla veritabanı sistemini desteklemesi ve hazırlanmış ifadeler
(prepared statements) ile güvenli sorgulama imkanı sunması nedeniyle en çok tercih
edilen yöntemdir.

Ayrıca, SQL enjeksiyon (SQL Injection) saldırılarından korunma yöntemlerini,
veritabanı işlemlerinde performans optimizasyonunu, hata yönetimini ve en iyi
pratikleri detaylıca ele alacağız. Hazırsanız, PHP ile veritabanı işlemlerinin
derinliklerine dalalım!


PHP'de veritabanına bağlanmak için temel olarak iki farklı yöntem kullanılır.
Bu yöntemlerin her birinin kendine özgü avantajları ve kullanım alanları vardır.
Hangi yöntemin kullanılacağı, projenizin ihtiyaçlarına, kullanacağınız veritabanı
sistemine ve kişisel tercihlerinize bağlıdır.


MySQLi, PHP'nin MySQL veritabanı için özel olarak geliştirilmiş bir uzantısıdır.
MySQLi, hem nesne yönelimli hem de prosedürel kullanımı destekler. MySQLi'nin
en büyük avantajı, MySQL ile çalışırken en iyi performansı sunmasıdır. Ancak
dezavantajı, sadece MySQL veritabanları ile çalışabilmesidir. Eğer projenizde
sadece MySQL kullanacaksanız, MySQLi iyi bir seçenektir.



<?php

$sunucu = "localhost";
$kullaniciAdi = "root";
$sifre = "";
$veritabaniAdi = "ornek_site";

$baglanti = new mysqli($sunucu, $kullaniciAdi, $sifre, $veritabaniAdi);

if ($baglanti->connect_error) {
die("Bağlantı hatası: " . $baglanti->connect_error);
}

echo "MySQLi ile bağlantı başarılı!";

$baglanti->close();


MySQLi'nin prosedürel kullanımı da mümkündür. Aşağıdaki örnek, prosedürel
yaklaşımı göstermektedir:



<?php

$baglanti = mysqli_connect("localhost", "root", "", "ornek_site");

if (!$baglanti) {
die("Bağlantı hatası: " . mysqli_connect_error());
}

echo "Bağlantı başarılı!";

mysqli_close($baglanti);


MySQLi'nin nesne yönelimli kullanımı, daha modern ve düzenli bir kod yapısı
sunduğu için genellikle tercih edilir. Prosedürel kullanım ise, daha eski
projelerde veya hızlı prototipleme yaparken kullanılabilir. Ancak, büyük ve
sürdürülebilir projeler için nesne yönelimli yaklaşım önerilir.


PDO (PHP Data Objects), PHP'de veritabanı işlemleri için en modern ve güvenli
yöntemdir. PDO, farklı veritabanı sistemleri (MySQL, PostgreSQL, SQLite, Oracle,
Microsoft SQL Server vb.) için tek bir arayüz sunar. Bu sayede, projenizde
veritabanı sistemini değiştirmeniz gerektiğinde, kodunuzun büyük bir kısmını
değiştirmek zorunda kalmazsınız.

PDO'nun en önemli özelliği, hazırlanmış ifadeler (prepared statements) ile
SQL enjeksiyon saldırılarına karşı güvenli sorgulama yapabilmesidir. Ayrıca,
PDO, hata yönetimi, işlemler (transactions) ve veri türü dönüşümleri gibi
gelişmiş özellikleri de destekler.

PDO kullanımı, modern PHP projelerinde standart haline gelmiştir. Özellikle
Laravel, Symfony gibi framework'ler, PDO üzerine inşa edilmiştir.



<?php

$host = 'localhost';
$veritabani = 'ornek_site';
$kullanici = 'root';
$sifre = '';
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$veritabani;charset=$charset";
$secenekler = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];

try {
$pdo = new PDO($dsn, $kullanici, $sifre, $secenekler);
echo "PDO ile bağlantı başarılı!";
} catch (PDOException $hata) {
echo "Bağlantı hatası: " . $hata->getMessage();
exit;
}


PDO bağlantısında kullanılan DSN (Data Source Name), hangi veritabanı
sistemine bağlanılacağını ve bağlantı parametrelerini belirtir. Örneğin,
MySQL için "mysql:host=localhost;dbname=ornek_site;charset=utf8mb4"
formatında bir DSN kullanılır. PostgreSQL için ise "pgsql:host=localhost;dbname=ornek_site"
formatında bir DSN kullanılır.

PDO bağlantısında kullanılan seçenekler, bağlantının nasıl davranacağını
belirler. Yukarıdaki örnekte, hata raporlamasının exception olarak ayarlanması
(ATTR_ERRMODE), varsayılan veri çekme modunun ilişkili dizi olarak ayarlanması
(ATTR_DEFAULT_FETCH_MODE) ve prepared statements'lerin emüle edilmemesi
(ATTR_EMULATE_PREPARES) gibi önemli ayarlar yapılmıştır.

Hata yönetimi için try-catch blokları kullanılması, olası bağlantı hatalarının
yakalanmasını ve uygun şekilde yönetilmesini sağlar. Bu sayede, uygulamanızın
beklenmedik hatalar nedeniyle çökmesi engellenir.


PDO ile veritabanı sorgulama işlemleri, hazırlanmış ifadeler (prepared statements)
kullanılarak gerçekleştirilir. Hazırlanmış ifadeler, SQL sorgularını ve verileri
birbirinden ayırarak SQL enjeksiyon saldırılarına karşı güvenlik sağlar.

Hazırlanmış ifadelerin çalışma prensibi şu şekildedir: Önce SQL sorgusu,
parametrelerin yerine "placeholder" (yer tutucu) konularak hazırlanır. Daha
sonra, bu sorgu veritabanına gönderilir ve veritabanı tarafından derlenir.
Son olarak, gerçek veriler bu derlenmiş sorguya gönderilir ve sorgu çalıştırılır.
Bu sayede, veriler SQL sorgusu ile karışmaz ve SQL enjeksiyonu engellenir.

Hazırlanmış ifadeler, iki farklı placeholder formatını destekler: isimli
(named) ve soru işaretli (positional). İsimli placeholder'lar, sorgunun
daha okunabilir olmasını sağlar. Soru işaretli placeholder'lar ise daha
kısa ve basittir.


PDO ile veritabanından veri çekmek için "query" veya "prepare/execute"
metotları kullanılabilir. Ancak, güvenlik açısından, özellikle kullanıcı
girdisi içeren sorgularda "prepare/execute" metotları tercih edilmelidir.
Aşağıdaki örnekler, farklı senaryolar için veri çekme işlemlerini göstermektedir.



<?php

$pdo = new PDO("mysql:host=localhost;dbname=ornek_site;charset=utf8mb4", "root", "");

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$kullaniciId = 5;

$sql = "SELECT id, ad, soyad, email FROM kullanicilar WHERE id = :id";

$ifade = $pdo->prepare($sql);

$ifade->execute([':id' => $kullaniciId]);

$kullanici = $ifade->fetch(PDO::FETCH_ASSOC);

if ($kullanici) {
echo "ID: " . $kullanici['id'] . "\n";
echo "Ad: " . $kullanici['ad'] . "\n";
echo "Soyad: " . $kullanici['soyad'] . "\n";
echo "Email: " . $kullanici['email'] . "\n";
} else {
echo "Kullanıcı bulunamadı!";
}


Yukarıdaki örnekte, isimli placeholder kullanılmıştır. execute metodu,
placeholder'ların değerlerini bir dizi olarak alır. Bu yöntem, sorguyu ve
verileri ayırdığı için güvenlidir. fetch metodu ise, sorgu sonucundan tek
bir satırı döndürür. fetchAll metodu ile tüm satırları alabilirsiniz.

Aşağıdaki örnek, tüm kullanıcıları listelemek için fetchAll metodunun
kullanımını göstermektedir:



<?php

$sql = "SELECT id, ad, soyad, email FROM kullanicilar WHERE aktif = :aktif";

$ifade = $pdo->prepare($sql);
$ifade->execute([':aktif' => 1]);

$kullanicilar = $ifade->fetchAll(PDO::FETCH_ASSOC);

foreach ($kullanicilar as $kullanici) {
echo $kullanici['ad'] . " " . $kullanici['soyad'] . " - " . $kullanici['email'] . "\n";
}


Soru işaretli placeholder'lar da kullanabilirsiniz. Bu durumda, execute
metoduna sıralı bir dizi göndermeniz gerekir. Soru işaretli placeholder'lar,
daha kısa ve basit sorgular için uygundur. Ancak, birden fazla parametre
olduğunda hangi parametrenin hangi sırada olduğunu takip etmek zorlaşabilir.
Bu nedenle, isimli placeholder'lar genellikle daha okunabilir ve yönetilebilirdir.



<?php

$sql = "SELECT id, ad, soyad, email FROM kullanicilar WHERE id = ?";

$ifade = $pdo->prepare($sql);
$ifade->execute([$kullaniciId]);

$kullanici = $ifade->fetch(PDO::FETCH_ASSOC);



PDO ile veritabanına yeni veri eklemek için yine prepared statements kullanılır.
Bu yöntem, SQL enjeksiyonuna karşı güvenli bir şekilde veri eklemenizi sağlar.
Ayrıca, eklenen kaydın ID'sini almak için "lastInsertId" metodu kullanılabilir.
Bu metod, özellikle otomatik artan (auto-increment) birincil anahtarı olan
tablolarda çok kullanışlıdır.



<?php

$ad = "Mehmet";
$soyad = "Demir";
$email = "[email protected]";
$sifre = password_hash("guvenli_sifre", PASSWORD_DEFAULT);
$yas = 28;

$sql = "INSERT INTO kullanicilar (ad, soyad, email, sifre, yas, aktif, kayit_tarihi)
VALUES (:ad, :soyad, :email, :sifre, :yas, :aktif, :kayit_tarihi)";

$ifade = $pdo->prepare($sql);

$ifade->execute([
':ad' => $ad,
':soyad' => $soyad,
':email' => $email,
':sifre' => $sifre,
':yas' => $yas,
':aktif' => 1,
':kayit_tarihi' => date('Y-m-d H:i:s')
]);

$eklenenId = $pdo->lastInsertId();

echo "Yeni kullanıcı eklendi. ID: " . $eklenenId;


INSERT işlemlerinde, sütun isimleri ve değerler doğru eşleştirilmelidir.
Ayrıca, şifre gibi hassas bilgiler, hash'lenerek veritabanına kaydedilmelidir.
password_hash fonksiyonu, şifreleri güvenli bir şekilde hash'lemek için
kullanılır. Bu fonksiyon, otomatik olarak tuz (salt) ekler ve güvenli
hash algoritmaları kullanır.

execute metodu, başarılı veya başarısız olduğuna bağlı olarak true veya
false döndürür. Bu sayede, işlemin başarılı olup olmadığını kontrol
edebilirsiniz. Hata yönetimi için try-catch blokları kullanmak, olası
sorunları yakalamanıza ve kullanıcıya anlamlı mesajlar göstermenize
olanak tanır.


PDO ile veri güncelleme işlemleri, INSERT işlemlerine benzer şekilde
prepared statements kullanılarak gerçekleştirilir. UPDATE sorgularında,
hangi kaydın güncelleneceğini belirlemek için WHERE koşulu kullanılır.
WHERE koşulunda da prepared statements kullanarak güvenliği sağlamalısınız.



<?php

$kullaniciId = 5;
$yeniEmail = "[email protected]";
$yeniYas = 31;

$sql = "UPDATE kullanicilar
SET email = :email, yas = :yas
WHERE id = :id";

$ifade = $pdo->prepare($sql);

$sonuc = $ifade->execute([
':email' => $yeniEmail,
':yas' => $yeniYas,
':id' => $kullaniciId
]);

if ($sonuc) {
$etkilenenSatir = $ifade->rowCount();
echo $etkilenenSatir . " satır güncellendi.";
} else {
echo "Güncelleme başarısız!";
}


UPDATE işlemlerinde, rowCount metodu ile kaç satırın güncellendiğini
öğrenebilirsiniz. Bu, özellikle güncelleme işleminin başarılı olup
olmadığını kontrol etmek için kullanışlıdır. rowCount, güncellenen
satır sayısını döndürür. Eğer WHERE koşuluna uyan hiçbir kayıt yoksa,
rowCount 0 döndürür.

Güncelleme işlemlerinde, sadece değişmesi gereken alanları güncellemek
daha iyi bir uygulamadır. Gereksiz yere tüm alanları güncellemek,
performansı olumsuz etkileyebilir. Ayrıca, hassas alanların (örneğin şifre)
güncellenmesi gerektiğinde, özel işlemler yapılmalıdır.


PDO ile veri silme işlemleri, diğer işlemlere benzer şekilde prepared
statements kullanılarak gerçekleştirilir. DELETE sorgularında, hangi
kaydın silineceğini belirlemek için WHERE koşulu kullanılır. Silme
işlemleri kalıcı olduğu için dikkatli olunmalıdır. Bu nedenle, silme
işlemlerinden önce mutlaka kullanıcı onayı alınmalı veya silinecek
verinin yedeği alınmalıdır.



<?php

$kullaniciId = 5;

$sql = "DELETE FROM kullanicilar WHERE id = :id";

$ifade = $pdo->prepare($sql);

$sonuc = $ifade->execute([':id' => $kullaniciId]);

if ($sonuc) {
$etkilenenSatir = $ifade->rowCount();
if ($etkilenenSatir > 0) {
echo "Kullanıcı başarıyla silindi.";
} else {
echo "Silinecek kullanıcı bulunamadı.";
}
} else {
echo "Silme işlemi başarısız!";
}


DELETE işlemlerinde, soft delete (yumuşak silme) yaklaşımı kullanmak
genellikle daha iyidir. Soft delete, veriyi kalıcı olarak silmek yerine,
bir "silindi" sütunu ile işaretleyerek verinin gizlenmesini sağlar.
Bu sayede, silinen veriler gerektiğinde geri getirilebilir ve veri
bütünlüğü korunur. Soft delete için, tabloya "silindi_tarihi" gibi
bir sütun eklenir ve silme işlemi bu sütunu güncelleyerek yapılır.

Silme işlemlerinde, ilişkili tablolardaki verilerin de silinmesi
gerekebilir (CASCADE DELETE). Ancak, bu işlemler veritabanı düzeyinde
yapılabileceği gibi, uygulama düzeyinde de yönetilebilir.


SQL enjeksiyonu, bir web uygulamasına yapılan en yaygın ve en tehlikeli
saldırı türlerinden biridir. Saldırgan, uygulamaya gönderilen veriler
aracılığıyla SQL sorgusuna müdahale ederek, veritabanından yetkisiz
veri çekebilir, verileri silebilir veya değiştirebilir.

Örneğin, aşağıdaki gibi bir sorgu, SQL enjeksiyonuna açıktır:



<?php

// TEHLİKELİ! ASLA KULLANMAYIN!
$kullaniciAdi = $_POST['kullanici_adi'];
$sifre = $_POST['sifre'];

$sql = "SELECT * FROM kullanicilar WHERE kullanici_adi = '$kullaniciAdi' AND sifre = '$sifre'";

$sonuc = $baglanti->query($sql);


Eğer saldırgan, kullanici_adi alanına "admin' OR '1'='1" gibi bir değer
gönderirse, sorgu şu hale gelir:



SELECT * FROM kullanicilar WHERE kullanici_adi = 'admin' OR '1'='1' AND sifre = 'sifre'


Bu sorgu, "1'='1" koşulu her zaman doğru olduğu için, tüm kullanıcıları
döndürür. Saldırgan, bu şekilde sisteme giriş yapabilir. SQL enjeksiyonu
saldırıları, bu basit örneğin çok ötesine geçebilir. Saldırgan, veritabanı
şemasını öğrenebilir, tabloları silebilir veya hatta veritabanı sunucusunu
ele geçirebilir.

SQL enjeksiyonundan korunmak için en etkili yöntem, prepared statements
kullanmaktır. PDO ve MySQLi, prepared statements'i destekler. Ayrıca,
kullanıcı girdilerini filtrelemek ve doğrulamak da ek bir güvenlik
katmanı sağlar.




<?php

// PDO ile güvenli sorgu
$kullaniciAdi = $_POST['kullanici_adi'];
$sifre = $_POST['sifre'];

$sql = "SELECT * FROM kullanicilar WHERE kullanici_adi = :kullanici_adi";
$ifade = $pdo->prepare($sql);
$ifade->execute([':kullanici_adi' => $kullaniciAdi]);

$kullanici = $ifade->fetch(PDO::FETCH_ASSOC);

if ($kullanici && password_verify($sifre, $kullanici['sifre'])) {
// Giriş başarılı
} else {
// Giriş başarısız
}


Yukarıdaki örnekte, kullanıcı adı prepared statement ile güvenli bir
şekilde sorguya eklenmiştir. Şifre doğrulaması ise, veritabanından
çekilen hash ile password_verify fonksiyonu kullanılarak yapılmıştır.
Bu yöntem, hem SQL enjeksiyonuna hem de şifre saldırılarına karşı
güvenlidir.

Aşağıdaki örnek, kullanıcı girdilerini filtreleme ve doğrulama
yöntemlerini göstermektedir:



<?php

$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);

if ($email === false) {
die("Geçersiz email adresi!");
}

$yas = filter_var($_POST['yas'], FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 150]
]);

if ($yas === false) {
die("Geçersiz yaş değeri!");
}

$sql = "INSERT INTO kullanicilar (email, yas) VALUES (:email, :yas)";
$ifade = $pdo->prepare($sql);
$ifade->execute([':email' => $email, ':yas' => $yas]);


Bu örnekte, email ve yaş değerleri filtrelenmiş ve doğrulanmıştır.
Bu sayede, veritabanına geçersiz veya tehlikeli verilerin gönderilmesi
engellenmiştir. Filtreleme ve doğrulama, prepared statements ile
birlikte kullanıldığında, çok güçlü bir güvenlik sağlar.


Veritabanı işlemleri, web uygulamalarının performansını doğrudan etkiler.
İyi optimize edilmemiş sorgular, uygulamanın yavaşlamasına ve hatta
çökmesine neden olabilir. Aşağıdaki bölümlerde, veritabanı işlemlerinde
performansı artırmak için kullanılabilecek yöntemleri inceleyeceğiz.


İndeksler, veritabanı sorgularının hızlanmasını sağlayan en önemli
araçlardan biridir. WHERE, JOIN, ORDER BY gibi koşullarda kullanılan
sütunlara indeks eklenmesi, sorgu performansını önemli ölçüde artırır.
Ancak, indekslerin de bir maliyeti vardır; veri ekleme ve güncelleme
işlemlerini yavaşlatabilir. Bu nedenle, indeksler dikkatli bir şekilde
kullanılmalıdır.



-- Kullanıcı adı sütununa indeks ekleme
CREATE INDEX idx_kullanici_adi ON kullanicilar (kullanici_adi);

-- Email sütununa benzersiz indeks ekleme
CREATE UNIQUE INDEX idx_email ON kullanicilar (email);

-- Tarih aralığı sorguları için bileşik indeks
CREATE INDEX idx_kayit_tarihi ON kullanicilar (kayit_tarihi, aktif);



Sık tekrarlanan sorgular, önbelleğe alınarak veritabanı yükü azaltılabilir.
Önbelleğe alma için Redis, Memcached gibi araçlar veya PHP'nin kendi
APCu extension'ı kullanılabilir. Önbelleğe alınan veriler, belirli
bir süre sonra otomatik olarak temizlenir veya manuel olarak
temizlenebilir.



<?php

// APCu ile önbelleğe alma örneği
$anahtar = "kullanici_listesi";
$kullanicilar = apcu_fetch($anahtar);

if ($kullanicilar === false) {
$sql = "SELECT id, ad, soyad, email FROM kullanicilar WHERE aktif = 1";
$kullanicilar = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

// 5 dakika boyunca önbellekte tut
apcu_store($anahtar, $kullanicilar, 300);
}

foreach ($kullanicilar as $kullanici) {
echo $kullanici['ad'] . " " . $kullanici['soyad'] . "\n";
}



Çok sayıda veri ekleme veya güncelleme işlemi yapılacaksa, toplu işlemler
kullanmak performansı artırır. Tek tek sorgu göndermek yerine, toplu
insert veya transaction kullanarak veritabanına tek seferde işlem
yapılabilir.



<?php

// Toplu insert örneği
$veriler = [
['ad' => 'Ali', 'soyad' => 'Yılmaz', 'email' => '[email protected]'],
['ad' => 'Veli', 'soyad' => 'Demir', 'email' => '[email protected]'],
['ad' => 'Can', 'soyad' => 'Kaya', 'email' => '[email protected]'],
];

$sql = "INSERT INTO kullanicilar (ad, soyad, email) VALUES (?, ?, ?)";
$ifade = $pdo->prepare($sql);

foreach ($veriler as $veri) {
$ifade->execute([$veri['ad'], $veri['soyad'], $veri['email']]);
}

// Transaction ile toplu işlem
$pdo->beginTransaction();

try {
foreach ($veriler as $veri) {
$ifade->execute([$veri['ad'], $veri['soyad'], $veri['email']]);
}
$pdo->commit();
echo "Tüm veriler başarıyla eklendi!";
} catch (PDOException $hata) {
$pdo->rollBack();
echo "Hata: " . $hata->getMessage();
}



SELECT * kullanmak, gereksiz veri transferine neden olur. Sadece ihtiyacınız
olan sütunları çekmek, hem ağ trafiğini hem de bellek kullanımını
azaltır. Ayrıca, veritabanı sorgusunun daha hızlı çalışmasını sağlar.



-- YAVAŞ (Tüm sütunları çeker)
SELECT * FROM kullanicilar WHERE aktif = 1;

-- HIZLI (Sadece gerekli sütunları çeker)
SELECT id, ad, soyad, email FROM kullanicilar WHERE aktif = 1;



Veritabanı işlemlerinde hata yönetimi, uygulamanın güvenliği ve kararlılığı
için kritik öneme sahiptir. Hatalar, kullanıcıya doğrudan gösterilmemeli,
log dosyalarına kaydedilmeli ve kullanıcıya genel bir hata mesajı
gösterilmelidir. Ayrıca, hata durumlarında veritabanı işlemleri geri
alınabilmelidir.



<?php

class VeritabaniIslemleri
{
private PDO $pdo;

public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}

public function kullaniciEkle(array $veri): int
{
try {
$sql = "INSERT INTO kullanicilar (ad, soyad, email, sifre)
VALUES (:ad, :soyad, :email, :sifre)";

$ifade = $this->pdo->prepare($sql);
$ifade->execute($veri);

return (int) $this->pdo->lastInsertId();

} catch (PDOException $hata) {
// Hatayı log dosyasına yaz
error_log("Veritabanı hatası: " . $hata->getMessage());

// Kullanıcıya genel bir hata mesajı göster
throw new RuntimeException("Kayıt işlemi sırasında bir hata oluştu.");
}
}

public function kullaniciGuncelle(int $id, array $veri): bool
{
try {
$sql = "UPDATE kullanicilar SET
ad = :ad,
soyad = :soyad,
email = :email
WHERE id = :id";

$veri['id'] = $id;
$ifade = $this->pdo->prepare($sql);

return $ifade->execute($veri);

} catch (PDOException $hata) {
error_log("Güncelleme hatası: " . $hata->getMessage());
throw new RuntimeException("Güncelleme işlemi sırasında bir hata oluştu.");
}
}

public function kullaniciSil(int $id): bool
{
try {
// Soft delete - silindi sütununu güncelle
$sql = "UPDATE kullanicilar SET silindi_tarihi = NOW() WHERE id = :id";
$ifade = $this->pdo->prepare($sql);
return $ifade->execute([':id' => $id]);

} catch (PDOException $hata) {
error_log("Silme hatası: " . $hata->getMessage());
throw new RuntimeException("Silme işlemi sırasında bir hata oluştu.");
}
}
}


Yukarıdaki örnekte, tüm veritabanı işlemleri try-catch blokları ile
sararak hataların yakalanması ve uygun şekilde yönetilmesi sağlanmıştır.
Hata mesajları, error_log ile log dosyasına yazılmış ve kullanıcıya
teknik detaylar içermeyen genel bir hata mesajı gösterilmiştir.

Ayrıca, soft delete yaklaşımı kullanılarak, verilerin kalıcı olarak
silinmesi engellenmiştir. Bu sayede, yanlışlıkla silinen veriler
geri getirilebilir ve veri bütünlüğü korunur.


Transaction (işlem), birden fazla veritabanı işlemini tek bir birim olarak
ele almanızı sağlar. Tüm işlemler başarılı olursa, değişiklikler kalıcı
hale gelir. Eğer herhangi bir işlemde hata oluşursa, tüm değişiklikler
geri alınır (rollback). Transaction'lar, veri bütünlüğünü korumak için
çok önemlidir.



<?php

class SiparisIslemleri
{
private PDO $pdo;

public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}

public function siparisOlustur(int $kullaniciId, array $urunler, float $toplamTutar): int
{
try {
$this->pdo->beginTransaction();

// 1. Sipariş kaydı oluştur
$sqlSiparis = "INSERT INTO siparisler (kullanici_id, toplam_tutar, durum, olusturma_tarihi)
VALUES (:kullanici_id, :toplam_tutar, 'beklemede', NOW())";

$ifadeSiparis = $this->pdo->prepare($sqlSiparis);
$ifadeSiparis->execute([
':kullanici_id' => $kullaniciId,
':toplam_tutar' => $toplamTutar
]);

$siparisId = (int) $this->pdo->lastInsertId();

// 2. Sipariş detaylarını ekle
$sqlDetay = "INSERT INTO siparis_detaylar (siparis_id, urun_id, adet, birim_fiyat, toplam_fiyat)
VALUES (:siparis_id, :urun_id, :adet, :birim_fiyat, :toplam_fiyat)";

$ifadeDetay = $this->pdo->prepare($sqlDetay);

foreach ($urunler as $urun) {
$ifadeDetay->execute([
':siparis_id' => $siparisId,
':urun_id' => $urun['urun_id'],
':adet' => $urun['adet'],
':birim_fiyat' => $urun['birim_fiyat'],
':toplam_fiyat' => $urun['adet'] * $urun['birim_fiyat']
]);
}

// 3. Stok güncelle
$sqlStok = "UPDATE urunler SET stok = stok - :adet WHERE id = :urun_id AND stok >= :adet";
$ifadeStok = $this->pdo->prepare($sqlStok);

foreach ($urunler as $urun) {
$ifadeStok->execute([
':adet' => $urun['adet'],
':urun_id' => $urun['urun_id']
]);

if ($ifadeStok->rowCount() == 0) {
throw new RuntimeException($urun['urun_id'] . " ID'li ürün için yeterli stok yok!");
}
}

// Tüm işlemler başarılı - commit
$this->pdo->commit();

return $siparisId;

} catch (Exception $hata) {
// Herhangi bir hata - rollback
$this->pdo->rollBack();
error_log("Sipariş oluşturma hatası: " . $hata->getMessage());
throw new RuntimeException("Sipariş oluşturulamadı: " . $hata->getMessage());
}
}
}


Yukarıdaki örnekte, bir sipariş oluşturma işlemi transaction ile
yapılmıştır. Sipariş kaydı oluşturma, sipariş detaylarını ekleme ve
stok güncelleme işlemleri tek bir transaction içinde yapılmıştır.
Eğer herhangi bir işlem başarısız olursa (örneğin yetersiz stok),
tüm işlemler geri alınır ve veritabanı eski haline döner.

Transaction'lar, özellikle bankacılık, e-ticaret, rezervasyon sistemleri
gibi veri bütünlüğünün kritik olduğu uygulamalarda kullanılmalıdır.
Transaction'lar, ACID (Atomicity, Consistency, Isolation, Durability)
özelliklerini sağlayarak veritabanı işlemlerinin güvenilirliğini artırır.


Yüksek trafikli uygulamalarda, her istek için yeni bir veritabanı bağlantısı
oluşturmak performansı olumsuz etkiler. Bağlantı havuzu (connection pool),
önceden oluşturulmuş bağlantıları yeniden kullanarak bu sorunu çözer.
PHP'de PDO doğrudan connection pool desteği sunmasa da, MySQL ve diğer
veritabanları connection pool özelliğine sahiptir.

PHP uygulamalarında connection pool için aşağıdaki yaklaşımlar kullanılabilir:


-MySQL'deki "persistent connections" (kalıcı bağlantılar) özelliğini kullanmak
-Redis veya Memcached ile önbellekleme yaparak veritabanı yükünü azaltmak
-ORM (Object-Relational Mapping) araçları (Doctrine, Eloquent) ile bağlantı yönetimini optimize etmek

-PHP'nin OPcache extension'ı ile kod optimizasyonu yapmak


Veritabanı işlemlerinde güvenli ve performanslı kod yazmak için aşağıdaki
kontrol listesini kullanabilirsiniz:


[TABLE width="100%"]
[TR]
[th]Pratik[/th]
[th]Açıklama[/th]
[/TR]
[TR]
[td]Her zaman PDO veya MySQLi kullanın[/td][td]Eski mysql_* fonksiyonları güvensizdir ve kullanılmamalıdır.[/td]
[/TR]
[TR]
[td]Prepared statements kullanın[/td][td]SQL enjeksiyonuna karşı en etkili koruma yöntemidir.[/td]
[/TR]
[TR]
[td]Kullanıcı girdilerini filtreleyin ve doğrulayın[/td][td]Gelen verilerin tipini, uzunluğunu ve formatını kontrol edin.[/td]
[/TR]
[TR]
[td]Hata mesajlarını loglayın, kullanıcıya göstermeyin[/td][td]Teknik detaylar saldırganlara bilgi verebilir.[/td]
[/TR]
[TR]
[td]Şifreleri hash'leyerek saklayın[/td][td]password_hash ve password_verify fonksiyonlarını kullanın.[/td]
[/TR]
[TR]
[td]Veritabanı bağlantı bilgilerini .env dosyasında saklayın[/td][td]Kod içinde düz metin olarak bulundurmayın.[/td]
[/TR]
[TR]
[td]İndeksleri doğru kullanın[/td][td]Sorgu performansını artırmak için indeks ekleyin.[/td]
[/TR]
[TR]
[td]Gereksiz sütunları çekmeyin[/td][td]SELECT * yerine ihtiyacınız olan sütunları belirtin.[/td]
[/TR]
[TR]
[td]Transaction kullanın[/td][td]Veri bütünlüğü gerektiren işlemlerde transaction kullanın.[/td]
[/TR]
[TR]
[td]SQL sorgularını optimize edin[/td][td]JOIN, WHERE, ORDER BY koşullarını optimize edin.[/td]
[/TR]
[TR]
[td]Düzenli yedekleme yapın[/td][td]Veritabanınızı düzenli olarak yedekleyin.[/td]
[/TR]
[/TABLE]


[TABLE width="100%"]
[TR]
[th]İşlem[/th]
[th]PDO Kodu[/th]
[/TR]
[TR]
[td]Bağlantı[/td]
[td]

$pdo = new PDO("mysql:host=localhost;dbname=test", "root", "");
[/td]
[/TR]
[TR]
[td]SELECT (tek satır)[/td]
[td]

$ifade = $pdo->prepare("SELECT * FROM tablo WHERE id = ?"); $ifade->execute([$id]); $satir = $ifade->fetch();
[/td]
[/TR]
[TR]
[td]SELECT (tüm satırlar)[/td]
[td]

$ifade = $pdo->prepare("SELECT * FROM tablo"); $ifade->execute(); $satirlar = $ifade->fetchAll();
[/td]
[/TR]
[TR]
[td]INSERT[/td]
[td]

$ifade = $pdo->prepare("INSERT INTO tablo (kolon) VALUES (?)"); $ifade->execute([$deger]); $id = $pdo->lastInsertId();
[/td]
[/TR]
[TR]
[td]UPDATE[/td]
[td]

$ifade = $pdo->prepare("UPDATE tablo SET kolon = ? WHERE id = ?"); $ifade->execute([$deger, $id]); $count = $ifade->rowCount();
[/td]
[/TR]
[TR]
[td]DELETE[/td]
[td]

$ifade = $pdo->prepare("DELETE FROM tablo WHERE id = ?"); $ifade->execute([$id]); $count = $ifade->rowCount();
[/td]
[/TR]
[TR]
[td]Transaction[/td]
[td]

$pdo->beginTransaction(); try { /* işlemler */ $pdo->commit(); } catch (Exception $e) { $pdo->rollBack(); }
[/td]
[/TR]
[TR]
[td]Hata yakalama[/td]
[td]

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); try { /* sorgu */ } catch (PDOException $e) { /* hata yönetimi */ }
[/td]
[/TR]
[/TABLE]


PHP ile veritabanı işlemleri, modern web uygulamalarının vazgeçilmez bir
parçasıdır. Bu rehberde, PHP'de veritabanına bağlanmanın iki ana yöntemini
(MySQLi ve PDO) detaylıca inceledik. Özellikle PDO'nun prepared statements
ile güvenli sorgulama, çoklu veritabanı desteği ve gelişmiş özellikleri
nedeniyle en iyi seçenek olduğunu gördük.

Önemli Çıkarımlar:


  • PDO, modern PHP projelerinde veritabanı işlemleri için standarttır
  • Prepared statements, SQL enjeksiyonuna karşı en etkili koruma yöntemidir
  • Kullanıcı girdileri her zaman filtrelenmeli ve doğrulanmalıdır
  • Hata mesajları loglanmalı, kullanıcıya gösterilmemelidir
  • Transaction'lar veri bütünlüğünü korur
  • İndeksler ve sorgu optimizasyonu performansı artırır
  • [*]Şifreler asla düz metin olarak saklanmamalıdır


Güvenli, performanslı ve sürdürülebilir veritabanı işlemleri için PDO kullanmayı
alışkanlık haline getirin. Her zaman prepared statements kullanın, kullanıcı
girdilerini doğrulayın ve hata yönetimini ihmal etmeyin.

Unutmayın, veritabanı güvenliği, uygulama güvenliğinin temelidir. SQL enjeksiyonu,
web uygulamalarına yapılan en yaygın saldırılardan biridir ve bu rehberdeki
önlemleri alarak bu tür saldırılara karşı kendinizi koruyabilirsiniz.


[TABLE width="100%"]
[TR]
[th]#[/th][th]Kaynak[/th][th]Açıklama[/th]
[/TR]
[TR]
[td]1[/td][td]PHP Resmi Dokümantasyonu - PDO[/td][td]PDO ile ilgili resmi PHP dokümantasyonu[/td]
[/TR]
[TR]
[td]2[/td][td]PHP Resmi Dokümantasyonu - MySQLi[/td][td]MySQLi ile ilgili resmi PHP dokümantasyonu[/td]
[/TR]
[TR]
[td]3[/td][td]OWASP SQL Injection[/td][td]SQL enjeksiyonu ve korunma yöntemleri[/td]
[/TR]
[TR]
[td]4[/td][td]PHP The Right Way[/td][td]PHP en iyi pratikler rehberi[/td]
[/TR]
[TR]
[td]5[/td][td]Laravel Database Documentation[/td][td]Eloquent ORM ve veritabanı işlemleri[/td]
[/TR]
[/TABLE]