İçeriğe geç
Educora
İleri25 dk10 / 10

PDO ile veritabanları ve ziyaretçi defteri projesi

PDO ile MySQL ve SQLite'a bağlan, hazırlanmış ifadelerle SQL enjeksiyonunu önle, CRUD işlemlerini yap, bir ziyaretçi defteri geliştir ve Laravel'in aynı işi nasıl yaptığını gör.

Kendini test et
Bu derste öğreneceklerin
  • PDO ile bir veritabanına bağlanmak ve sonuçları farklı getirme modlarında okumak
  • Hazırlanmış ifadelerle CRUD işlemleri yapmak ve SQL enjeksiyonunu önlemek
  • PDO, SQLite, oturumlar ve kaçışlama ile eksiksiz bir ziyaretçi defteri geliştirmek
  • Laravel'in rotalarını, denetleyicilerini, Blade'i ve Eloquent'i genel hatlarıyla açıklamak

Bir betik işini bitirir bitirmez bütün değişkenleri yok olur. Oysa ziyaretçi defterindeki yazılar, kullanıcılar ve siparişler yıllarca saklanmalıdır; bunun için bir veritabanı gerekir. PHP'de veritabanlarıyla çalışmanın modern yolu PDO'dur (PHP Data Objects): aynı kod MySQL, PostgreSQL, SQLite ve diğer veritabanlarıyla çalışır. Bu son derste PDO'yu öğrenecek ve kurs boyunca öğrendiklerini birleştiren bir ziyaretçi defteri geliştireceksin.

PDO ile bağlanma

Bağlanmak için bir DSN dizesi gerekir: veritabanının türünü, adresini ve adını belirtir. MySQL için mutlaka charset=utf8mb4 yaz; böylece Azerbaycanca, Rusça ve Türkçe harfler, hatta emojiler bile doğru saklanır. SQLite için sunucu gerekmez: veritabanının tamamı tek bir dosyadır, sqlite::memory: ise veritabanını yalnızca bellekte oluşturur; denemeler için idealdir.

PHP
<?php

$pdo = new PDO(
    'mysql:host=localhost;dbname=educora;charset=utf8mb4',
    'educora_app',
    getenv('DB_PASSWORD'),
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

// SQLite: a file or memory, no server needed
$sqlite = new PDO('sqlite:' . __DIR__ . '/app.sqlite');
Parolayı koda yazma; onu bir ortam değişkeninden (getenv) ya da bir yapılandırma dosyasından oku.
  • ERRMODE_EXCEPTION — hatalı bir sorgu PDOException fırlatır (PHP 8'de bu varsayılandır ama açıkça yazmak iyidir).
  • FETCH_ASSOC — satırlar ['name' => ...] gibi ilişkisel diziler olarak döner.
  • EMULATE_PREPARES => false — MySQL'de gerçek hazırlanmış ifadeler kullanılır.

SQL enjeksiyonu ve hazırlanmış ifadeler

Kullanıcının yazdığını doğrudan SQL metnine yapıştırırsan sorgunun kendisini değiştirebilir. Örneğin e-posta yerine ' OR '1'='1 yazılırsa koşul her zaman doğru olur ve sorgu bütün kullanıcıları döndürür. Buna SQL enjeksiyonu denir; web sitelerinin en tehlikeli açıklarından biridir. Hazırlanmış ifadede (prepared statement) SQL metni ve veriler sunucuya ayrı ayrı gönderilir; bu yüzden veriler asla SQL olarak çalıştırılmaz.

Savunmasız: SQL enjeksiyonu
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$user = $pdo->query($sql)->fetch();
Güvenli: hazırlanmış ifade
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch();
? bir yer tutucudur: veri execute'a ayrıca verilir. :email gibi adlandırılmış yer tutucular da vardır: execute(['email' => $email]).

CRUD ve sonuçları okuma

CRUD dört temel işlemi ifade eder: oluşturma (Create — INSERT), okuma (Read — SELECT), güncelleme (Update — UPDATE) ve silme (Delete — DELETE). Aşağıdaki program bir yapılacaklar listesini bellekteki bir SQLite veritabanında tutar ve terminalde olduğu gibi çalışır. Bir kez hazırlanan bir ifade farklı verilerle defalarca çalıştırılabilir.

PHP
<?php

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

$pdo->exec('CREATE TABLE tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    done INTEGER NOT NULL DEFAULT 0
)');

$insert = $pdo->prepare('INSERT INTO tasks (title) VALUES (:title)');
foreach (['Learn PDO', 'Build a guestbook', 'Read about Laravel'] as $title) {
    $insert->execute(['title' => $title]);
}
echo "Last id: ", $pdo->lastInsertId(), "\n";

$pdo->prepare('UPDATE tasks SET done = 1 WHERE id = ?')->execute([1]);

$delete = $pdo->prepare('DELETE FROM tasks WHERE id = ?');
$delete->execute([3]);
echo "Deleted rows: ", $delete->rowCount(), "\n";

foreach ($pdo->query('SELECT id, title, done FROM tasks ORDER BY id') as $row) {
    $mark = $row['done'] ? 'x' : ' ';
    echo "[$mark] {$row['id']}. {$row['title']}\n";
}
Beklenen çıktı
Last id: 3
Deleted rows: 1
[x] 1. Learn PDO
[ ] 2. Build a guestbook
PHP
<?php

$pdo = new PDO('sqlite::memory:');
$pdo->exec("CREATE TABLE scores (name TEXT, score INTEGER)");
$pdo->exec("INSERT INTO scores VALUES ('Aysel', 92), ('Murad', 78), ('Leyla', 95)");

$count = $pdo->query('SELECT COUNT(*) FROM scores')->fetchColumn();
echo "Students: $count\n";

$best = $pdo->query('SELECT * FROM scores ORDER BY score DESC')->fetch(PDO::FETCH_OBJ);
echo "Best: {$best->name} ({$best->score})\n";

$pairs = $pdo->query('SELECT name, score FROM scores ORDER BY name')
    ->fetchAll(PDO::FETCH_KEY_PAIR);
print_r($pairs);
Beklenen çıktı
Students: 3
Best: Leyla (95)
Array
(
    [Aysel] => 92
    [Leyla] => 95
    [Murad] => 78
)
Metot / modNe döndürür
fetch()sonraki satırı; satır kalmadıysa false
fetchAll()tüm satırları dizi olarak
fetchColumn()sonraki satırın ilk sütununu — COUNT(*) için kullanışlıdır
PDO::FETCH_OBJsatırı nesne olarak: $row->name
PDO::FETCH_KEY_PAIRiki sütundan anahtar => değer dizisi
rowCount()bir UPDATE/DELETE'in değiştirdiği satır sayısı

Proje: ziyaretçi defteri

Şimdi her şeyi birleştirelim: ziyaretçi adını ve mesajını yazar, PHP bunları doğrular, bir SQLite veritabanına kaydeder ve son 20 yazıyı gösterir. Projede oturum, CSRF belirteci, hazırlanmış ifade, PRG yönlendirmesi ve htmlspecialchars yer alır. Projenin tamamı tek bir index.php dosyasıdır; onu iki parça hâlinde gösteriyoruz.

PHP
<?php
declare(strict_types=1);
session_start();

$pdo = new PDO('sqlite:' . __DIR__ . '/guestbook.sqlite');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE IF NOT EXISTS entries (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    message TEXT NOT NULL,
    created_at TEXT NOT NULL
)');

function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf'], (string) ($_POST['csrf'] ?? ''))) {
        http_response_code(403);
        exit('Invalid CSRF token');
    }
    $name = trim((string) ($_POST['name'] ?? ''));
    $message = trim((string) ($_POST['message'] ?? ''));
    if ($name === '' || mb_strlen($name) > 50) {
        $errors[] = 'Name must be 1-50 characters';
    }
    if ($message === '' || mb_strlen($message) > 500) {
        $errors[] = 'Message must be 1-500 characters';
    }
    if ($errors === []) {
        $stmt = $pdo->prepare('INSERT INTO entries (name, message, created_at) VALUES (?, ?, ?)');
        $stmt->execute([$name, $message, date('Y-m-d H:i')]);
        header('Location: /');
        exit;
    }
}

$entries = $pdo->query('SELECT * FROM entries ORDER BY id DESC LIMIT 20')->fetchAll();
?>
index.php, 1. bölüm: veritabanı, doğrulama ve yazının kaydedilmesi
PHP
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Guestbook</title></head>
<body>
<h1>Guestbook</h1>

<?php foreach ($errors as $error): ?>
    <p style="color: red"><?= e($error) ?></p>
<?php endforeach; ?>

<form method="post">
    <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
    <input name="name" placeholder="Your name" maxlength="50" required>
    <textarea name="message" placeholder="Your message" maxlength="500" required></textarea>
    <button>Sign the guestbook</button>
</form>

<?php foreach ($entries as $entry): ?>
    <article>
        <strong><?= e($entry['name']) ?></strong> · <?= e($entry['created_at']) ?>
        <p><?= nl2br(e($entry['message'])) ?></p>
    </article>
<?php endforeach; ?>
</body>
</html>
index.php, 2. bölüm: şablon — foreach (...): ve endforeach; HTML içinde kullanışlı bir yazımdır
  1. 1
    Bir klasör oluştur

    Bir guestbook klasörü oluştur ve iki bölümü art arda tek bir index.php dosyasına yaz.

  2. 2
    SQLite'ı denetle

    php -m listesinde pdo_sqlite bulunmalıdır. Windows'ta gerekirse php.ini içindeki extension=pdo_sqlite satırının başındaki ; işaretini sil.

  3. 3
    Sunucuyu başlat

    Klasörde php -S localhost:8000 komutunu çalıştır ve http://localhost:8000 adresini aç. guestbook.sqlite dosyası ilk istekte kendiliğinden oluşur.

  4. 4
    Dene ve bozmaya çalış

    Ad yerine <b>Hi</b> yaz: kalın değil, düz metin olarak görünmelidir. Formu gönderdikten sonra sayfayı yenile; yazı ikinci kez eklenmemelidir.

Sonraki adım: Laravel

Gerçek projelerde bunların hepsi her seferinde elle yazılmaz; bir çatı (framework) kullanılır. En popüler PHP çatısı Laravel'dir. Yeni bir proje composer create-project laravel/laravel guestbook komutuyla oluşturulur, php artisan serve ise geliştirme sunucusunu başlatır. Laravel'de rotalar (routes) bir adresi bir denetleyici metoduna bağlar, denetleyici (controller) isteği işler, Blade şablonları HTML'i oluşturur, Eloquent ise veritabanı tablolarını PHP sınıfları olarak sunar.

PHP
// routes/web.php
Route::get('/', [EntryController::class, 'index']);
Route::post('/entries', [EntryController::class, 'store']);

// app/Http/Controllers/EntryController.php
class EntryController extends Controller
{
    public function index()
    {
        return view('entries', ['entries' => Entry::latest()->take(20)->get()]);
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'name' => 'required|max:50',
            'message' => 'required|max:500',
        ]);
        Entry::create($data);
        return redirect('/');
    }
}
Aynı ziyaretçi defteri Laravel'de: doğrulama, CSRF koruması ve hazırlanmış ifadeler çatının içinde hazırdır (Entry modelinin $fillable özelliğinde name ve message listelenir)
HTML
<form method="post" action="/entries">
    @csrf
    <input name="name">
    <textarea name="message"></textarea>
    <button>Send</button>
</form>

@foreach ($entries as $entry)
    <p><strong>{{ $entry->name }}</strong>: {{ $entry->message }}</p>
@endforeach
resources/views/entries.blade.php: {{ }} metni otomatik olarak kaçışlar, @csrf ise gizli belirteç alanını ekler
GörevSaf PHPLaravel
Yönlendirme$_SERVER['REQUEST_METHOD']Route::get(...), Route::post(...)
Kaçışlamahtmlspecialchars(){{ $value }}
CSRFrandom_bytes + hash_equals@csrf
VeritabanıPDO::prepare()Entry::create($data)
Tablo oluşturmaCREATE TABLEmigration'lar: php artisan migrate
Bir çatı sihir değildir: bu kursta elle yazdığın fikirlerin aynısını otomatikleştirir.

Önemli noktalar

  • PDO birçok veritabanı için tek bir arayüz sunar; bağlantı bir DSN dizesiyle yapılır, MySQL için charset=utf8mb4 eklenir.
  • Kullanıcı verisi SQL'e yalnızca prepare ve execute ile, yer tutucular (?, :name) aracılığıyla girer; bu, SQL enjeksiyonunu önler.
  • fetch, fetchAll, fetchColumn ve FETCH_ASSOC, FETCH_OBJ, FETCH_KEY_PAIR modları sonucu gereken biçimde verir.
  • Güvenli bir web uygulaması; CSRF belirteci, sunucu tarafı doğrulama, hazırlanmış ifadeler, PRG yönlendirmesi ve kaçışlanmış çıktıyı birleştirir.
  • Laravel aynı fikirleri rotalar, denetleyiciler, Blade ve Eloquent ile otomatikleştirir.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Hazırlanmış ifadelerin temel yararı nedir?