PHP PDO MySQL ile Modüler CMS Mimarisi Nasıl Kurulur

Google News Google News Flipboard Flipboard Sesli oku Yazıyı beğen Favorilere Ekle 0 Yorumlar
Daha fazla

PHP PDO MySQL CMS mimarisi: Giriş: Neden Modüler Bir CMS Mimarisi?

PHP PDO MySQL CMS mimarisi: Giriş: Neden Modüler Bir CMS Mimarisi?
PHP PDO MySQL CMS mimarisi: Giriş: Neden Modüler Bir CMS Mimarisi?
Hazır CMS’ler (WordPress, Drupal) çoğu projede işi görür. Ancak iç süreçleri farklı işleyen bir blog, müşteri paneli, çok dilli içerik aracı veya kendine özgü URL yapısı olan bir site söz konusu olduğunda hazır eklenti yığınlarıyla mücadele etmek geliştirme süresini şişirir. Bu noktada PHP + PDO + MySQL ile yazılmış, sade ve modüler bir çekirdek genelde daha sürdürülebilir bir seçim olur. Modüler mimarinin temel vaadi sade: çekirdek (core) küçük kalır, her yeni özellik kendi klasörü ve dosyalarıyla ayrı bir modül olarak eklenir. Bir modülü silmek diğer modülleri etkilemez. Bu yazıda Hostinger benzeri paylaşımlı hostingde de sorunsuz çalışan, framework bağımlılığı olmayan bir modüler CMS iskeletinin nasıl kurulacağını adım adım göreceksiniz.

Pratik Senaryo: Tek index.php Dosyasından Çıkmak

Çoğu özel CMS projesi tek bir index.php ile başlar. Önce header, footer ve birkaç sayfa dahil edilir. Sonra blog modülü eklenir, ardından admin paneli, derken iletişim formu… Birkaç ay sonra index.php yüzlerce satıra ulaşır, include zinciri karışır, aynı SQL sorgusu üç farklı dosyada tekrar eder ve yeni bir özellik eklemek korkutucu hale gelir. Modüler yapı bu sorunu kökünden çözer. Çekirdek bir router yazar, modüller kendi rotalarını ve şablonlarını tanımlar, PDO tek bir bağlantı üzerinden tüm modüllere hizmet verir. Hata olduğunda hangi katmanda olduğu nettir.

Mimarinin Genel Yapısı

Mimarinin Genel Yapısı
Mimarinin Genel Yapısı
<!– image: yazi-ici-1 | heading: Mimarinin Genel Yapısı | alt: PHP modüler CMS dizin yapısı şeması –> İyi bir modüler CMS’in temelinde üç katman bulunur: çekirdek (core), modüller (modules) ve şablonlar (templates). Çekirdek; konfigürasyon, otomatik yükleyici, router, veritabanı bağlantısı ve yardımcı sınıfları içerir. Modüller bağımsız klasörlerde bulunur ve hem ön yüz (public) hem yönetim panel (admin) tarafına rota tanımlayabilir.

Önerilen Dizin Yapısı

Dizin yapısı sade tutulmalı. Web kökü yalnızca tek bir giriş dosyası içermeli; tüm hassas kodlar public dışında olmalı:
/cms-root
├── public/                  (web kökü)
│   ├── index.php           (front controller)
│   ├── assets/
│   └── uploads/
├── core/
│   ├── Database.php
│   ├── Router.php
│   ├── ModuleLoader.php
│   ├── View.php
│   └── helpers.php
├── modules/
│   ├── blog/
│   │   ├── module.php
│   │   ├── controllers/
│   │   ├── views/
│   │   └── migrations/
│   ├── pages/
│   └── contact/
├── templates/
│   ├── default/
│   └── admin/
├── config/
│   └── app.php
└── storage/
    ├── cache/
    └── logs/
Burada kritik nokta: public dışına çıkan tüm dosyalar web’den doğrudan erişilemez. Bu güvenlik açısından önemli; .htaccess veya nginx konfigürasyonunda web kökünü public klasörüne sabitlemeniz gerekir.

Katmanlar Arası Sorumluluk Dağılımı

Her katmanın işi net olmalı. Çekirdek; modüllerin nasıl yükleneceğini, hangi URL’nin hangi modüle ait olduğunu ve veritabanı bağlantısının nasıl sağlanacağını bilir, ama bir modülün iç işleyişine karışmaz. Modüller ise yalnızca kendi rotalarını, kontrolcülerini ve görünümlerini tanımlar. Şablonlar görsel sunumdan sorumludur ve modüllerden gelen veriyi render eder.

PDO ile Veritabanı Katmanı

PDO’yu doğrudan kullanmak kodu çoğu mikro-ORM’den daha sade tutar, ancak tekrar eden bağlantı oluşturma ve hata yönetimi mantığını tek bir yere toplamak gerekir. Singleton kalıbı bu iş için en uygun çözümdür; uygulama yaşam döngüsünde tek bir PDO örneği oluşturulur.
<?php
namespace Core;

use PDO;
use PDOException;

class Database
{
    private static ?PDO $instance = null;

    public static function connection(): PDO
    {
        if (self::$instance instanceof PDO) {
            return self::$instance;
        }

        $config = require __DIR__ . '/../config/app.php';
        $db = $config['database'];

        $dsn = "mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4";

        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];

        try {
            self::$instance = new PDO($dsn, $db['user'], $db['pass'], $options);
        } catch (PDOException $e) {
            error_log('[DB] ' . $e->getMessage());
            http_response_code(500);
            exit('Veritabanı bağlantı hatası.');
        }

        return self::$instance;
    }
}
Burada üç ayar üretimde kritiktir: hata modunun istisna fırlatması, varsayılan fetch modunun associative array olması ve emülasyonlu sorguların kapatılması. Emülasyonu kapatmak gerçek hazırlanmış sorguları kullanmanızı sağlar; bu da SQL Injection’a karşı en güçlü doğal koruma katmanıdır. Modül içinden veritabanına erişim her zaman parametreli sorgu üzerinden yapılmalı:
$pdo = \Core\Database::connection();

$stmt = $pdo->prepare(
    "SELECT id, title, slug
     FROM posts
     WHERE status = :status
     ORDER BY published_at DESC
     LIMIT :lim"
);
$stmt->bindValue(':status', 'published', PDO::PARAM_STR);
$stmt->bindValue(':lim', 10, PDO::PARAM_INT);
$stmt->execute();

$posts = $stmt->fetchAll();
LIMIT gibi sayısal değerlerde PARAM_INT kullanmazsanız emülasyon kapalıyken sorgu hatası alırsınız. Bu küçük ayrıntı projelerde sık yapılan bir hatadır.

Router ve Modül Yükleyici

Router ve Modül Yükleyici
Router ve Modül Yükleyici
<!– image: yazi-ici-2 | heading: Router ve Modül Yükleyici | alt: PHP front controller ve modül yükleyici akışı –> Front controller olan public/index.php, gelen isteği temizledikten sonra Router’a teslim eder. Router, yüklenmiş modüllerin tanımladığı rotalar arasında eşleşme arar. Modül yükleyici uygulama açılışında modules klasöründeki her module.php dosyasını çalıştırır ve modülün kendini Router’a tanıtmasına izin verir.

Basit Bir Router İskeleti

<?php
namespace Core;

class Router
{
    private array $routes = [];

    public function add(string $method, string $path, callable $handler): void
    {
        $this->routes[strtoupper($method)][$path] = $handler;
    }

    public function dispatch(string $method, string $uri): void
    {
        $path   = parse_url($uri, PHP_URL_PATH) ?: '/';
        $method = strtoupper($method);

        $handler = $this->routes[$method][$path] ?? null;

        if (!$handler) {
            http_response_code(404);
            echo '404 - Sayfa bulunamadı';
            return;
        }

        echo $handler();
    }
}
Bu iskelet gerçek bir projede regex tabanlı parametre desteği (örnek: /blog/{slug}) ve middleware katmanıyla genişletilir. Ancak ilk sürüm için bu yapı yeterlidir ve eklenti benzeri modül kaydı için gereken sözleşmeyi sağlar. Modül kaydı şu basitlikte olur:
// modules/blog/module.php
$router->add('GET', '/blog', function () {
    require __DIR__ . '/controllers/PostListController.php';
    return (new \Modules\Blog\PostListController())->index();
});
Çekirdek modülleri tek tek bilmez. Yeni bir modül eklemek için yalnızca modules altında bir klasör açıp module.php dosyası eklemek yeterlidir.

Çekirdek Tablolar ve Veri Modeli

Çekirdek tablolar her CMS’in temelidir. Modüllere bırakılmayan, sistem genelinde kullanılan veriler burada tutulur. Aşağıdaki tablo başlangıç için yeterli bir settir:
Tablo Amaç Önemli Kolonlar
users Kullanıcı ve admin hesapları id, email, password_hash, role, created_at
settings Site geneli ayarlar key, value, type
modules Yüklü ve aktif modül kayıtları slug, name, is_active, version
posts Blog modülünün içeriği id, title, slug, body, status, published_at
pages Statik sayfalar id, slug, title, body, is_published
media Yüklenen dosyaların kayıtları id, path, mime, size, alt
Modüller kendi tablolarını migration dosyalarıyla kurmalı ve hangi tabloları oluşturduklarını belgelemelidir. Bu, modülün silinmesi gerektiğinde tabloların da temiz şekilde kaldırılmasını mümkün kılar.

Güvenlik Notları

Modüler bir CMS’de güvenlik çekirdeğin sorumluluğundadır. Modüller bu temeli kullanır. Aşağıdaki kontrolleri yayın öncesi listenizden çıkarmadan bırakmayın:
  • Tüm SQL sorguları PDO prepared statement ile yazılmış olmalı; doğrudan string concatenation kullanılmamalı.
  • Şifreler password_hash() ile saklanmalı; doğrulama password_verify() üzerinden yapılmalı.
  • Admin tarafında her formda CSRF token üretilmeli ve sunucuda doğrulanmalı.
  • Görünüm katmanında tüm dinamik veri htmlspecialchars() ile kaçırılmalı.
  • Yüklenen dosyalar MIME tipine ve uzantıya göre çift kontrolden geçirilmeli; public/uploads dışına yazılmamalı.
  • Hatalar production ortamında ekrana basılmamalı; storage/logs altına yazılmalı.
  • Oturum açma sonrası session_regenerate_id() mutlaka çağrılmalı.

Performans Notları

Paylaşımlı hostingde CMS performansını etkileyen üç nokta vardır: gereksiz veritabanı sorguları, sayfa başına PHP dosya yükleme sayısı ve önbellek yokluğu. Şu uygulamalar pratik fayda sağlar:
  • Çekirdeği sade tut; gereksiz framework parçaları yükleme.
  • Veritabanı bağlantısını singleton olarak tek seferlik kur.
  • Sık değişmeyen veriyi (menü, ayarlar) storage/cache altında dosya tabanlı önbelleğe al.
  • Üretimde opcache açık olmalı; Hostinger gibi sağlayıcılarda PHP sürümü 8.1+ tercih edilmeli.
  • Composer autoload yerine basit projelerde PSR-4 uyumlu kendi otomatik yükleyicini yazabilirsin.

Yaygın Hatalar ve Çözümleri

Modüler CMS yazarken sık karşılaşılan hatalar şunlardır:
  1. Çekirdeğin modüllere doğrudan bağımlı olması: Çekirdek hiçbir modülün ismini bilmemeli. Aksi halde modülerlik bozulur.
  2. Veritabanı erişiminin görünüm dosyalarına sızması: View dosyalarında SQL çalıştırmak hata ayıklamayı zorlaştırır. Sorguları kontrolcü katmanında tut.
  3. Aynı işi yapan iki yardımcı fonksiyon: Farklı modüllerin kendi e() veya escape() yardımcılarını yazması çakışmaya yol açar. Yardımcıları core/helpers.php altında toplayın.
  4. Yetersiz hata loglama: Üretimde “beyaz ekran” en kötü hata türüdür. error_log ve set_exception_handler ile tüm hatalar dosyaya yazılmalı.
  5. Bağımlılık sürümlerinin kayıt altına alınmaması: Composer kullanılmıyorsa bile modüllerin gerektirdiği PHP sürümü, eklenti veya MySQL özellikleri belgelenmeli.

Sonuç ve Sonraki Adım

PHP PDO ve MySQL ile modüler bir CMS kurmak göründüğünden daha sade bir iştir. Önemli olan çekirdeği hafif, modülleri bağımsız ve veritabanı katmanını tek noktadan yönetilebilir tutmaktır. Bu yapıyı kurduktan sonra üzerine kullanıcı yönetimi, içerik düzenleyici, SEO modülü veya çok dilli içerik desteği gibi katmanları rahatlıkla ekleyebilirsiniz. Sonraki adım olarak basit bir blog modülü yazıp router üzerinden /blog ve /blog/{slug} rotalarını ayağa kaldırmanız önerilir. Yapı bir kez oturduktan sonra yeni modüller saatler değil dakikalar içinde devreye girer.

Sıkça Sorulan Sorular

Framework yerine ham PHP + PDO ile CMS yazmak mantıklı mı?

Küçük ve orta ölçekli, kendine özgü kuralları olan projelerde mantıklıdır. Framework’lerin sağladığı hazır yapı kazanımı, projenin iş kuralları framework’ün varsayımlarıyla çakıştığında yerini bakım yüküne bırakabilir. Çekirdeği sade tutan modüler bir PDO yapısı, ihtiyaç duydukça büyütülebilir ve paylaşımlı hostingde daha az kaynak tüketir. Büyük takım veya karmaşık alanlı projelerde ise olgun bir framework hâlâ daha güvenli bir tercihtir.

Neden PDO’da emülasyonu kapatmak ve singleton kullanmak önerilir?

PDO::ATTR_EMULATE_PREPARES = false ayarı gerçek hazırlanmış sorguların kullanılmasını sağlar; bu sayede parametreler her zaman tip kontrolünden geçer ve SQL Injection riski doğal olarak azalır. Singleton ise her sayfada tek bir veritabanı bağlantısı kurulmasını garanti eder. Bu, hem performans hem de hata ayıklama açısından önemli bir farktır.

Bu yapı WordPress eklenti yapısından nasıl farklı?

WordPress eklentileri merkezi bir hook (action/filter) sistemine kayıt olur. Burada anlatılan modüler yapı ise rotalara doğrudan kaydolur, çekirdek hook altyapısı zorunlu değildir. Daha küçük bir öğrenme eğrisi sunar, ancak WordPress kadar geniş bir ekosisteme sahip değildir.

Çok kullanıcılı bir admin paneli için neye dikkat etmek gerekir?

Rol tabanlı erişim kontrolü (RBAC) çekirdek tarafında tanımlanmalı, modüller kendi rotalarını eklerken hangi rolün erişebileceğini bildirmelidir. Oturum güvenliği için session_regenerate_id(), CSRF token doğrulaması ve yanlış giriş denemesi limiti gibi temel kontroller başlangıçtan itibaren olmalıdır.

Modüler CMS’de migration sistemini nasıl basit tutarım?

Her modülün migrations/ klasöründe sıralı SQL dosyaları tutmak yeterlidir. Çekirdekte bir migration runner yazıp schema_migrations tablosunda çalıştırılan dosyaların adını saklayabilirsiniz. Bu yaklaşım Laravel veya Phinx kadar gelişmiş değildir, ancak çoğu özel CMS için fazlasıyla yeterlidir.

PHP PDO MySQL CMS mimarisi için pratik kontrol listesi

PHP PDO MySQL CMS mimarisi konusunda uygulamaya geçmeden önce mevcut yedeği doğrulayın, değişikliği küçük adımlarla test edin ve sonucu canlı sitede tekrar kontrol edin. Benzer konular için PHP PDO MySQL Hostinger kurulumu ve Laravel shared hosting rehberi rehberlerine de bakabilirsiniz. Resmi kaynak olarak PHP PDO dokümantasyonu sayfasını incelemek faydalı olur.

SSS: PHP PDO MySQL ile Modüler CMS Mimarisi Nasıl Kurulur

PHP PDO MySQL ile Modüler CMS Mimarisi Nasıl Kurulur için ilk kontrol ne olmalı?
Önce sorunun kapsamını belirleyin. Sadece tek sayfada mı, tüm sitede mi, yoksa belirli kullanıcı veya cihazlarda mı göründüğünü kontrol edin.

Bu işlem SEO performansını etkiler mi?
Evet, teknik sorunlar kullanıcı deneyimini ve tarama kalitesini etkileyebilir. Bu nedenle değişiklikten sonra Search Console verilerini takip etmek gerekir.

Canlı sitede işlem yapmadan önce yedek almak gerekir mi?
Kesinlikle gerekir. Dosya ve veritabanı yedeği olmadan yapılan canlı değişiklikler, küçük bir hatada geri dönüşü zorlaştırabilir.

Değişiklikten sonra cache temizlemek şart mı?
Çoğu durumda evet. WordPress cache eklentisi, CDN ve tarayıcı cache’i eski çıktıyı göstermeye devam edebilir.

Sorun devam ederse nasıl ilerlemeliyim?
Son yapılan değişikliği geri alın, hata loglarını kontrol edin ve eklenti/tema çakışmasını izole edin. Gerekirse küçük adımlarla yeniden test edin.

Yazar Hakkında

Benzer Yazılar

Bir Cevap Yaz

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir.

0/30 karakter