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

Pratik Senaryo: Tek index.php Dosyasından Çıkmak
Çoğu özel CMS projesi tek birindex.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ı

Önerilen Dizin Yapısı
Dizin yapısı sade tutulmalı. Web kökü yalnızca tek bir giriş dosyası içermeli; tüm hassas kodlarpublic 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

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 |
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ğrulamapassword_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/uploadsdışına yazılmamalı. - Hatalar production ortamında ekrana basılmamalı;
storage/logsaltı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/cachealtında dosya tabanlı önbelleğe al. - Üretimde
opcacheaçı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:- Çekirdeğin modüllere doğrudan bağımlı olması: Çekirdek hiçbir modülün ismini bilmemeli. Aksi halde modülerlik bozulur.
- 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.
- Aynı işi yapan iki yardımcı fonksiyon: Farklı modüllerin kendi
e()veyaescape()yardımcılarını yazması çakışmaya yol açar. Yardımcılarıcore/helpers.phpaltında toplayın. - Yetersiz hata loglama: Üretimde “beyaz ekran” en kötü hata türüdür.
error_logveset_exception_handlerile tüm hatalar dosyaya yazılmalı. - 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çinsession_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ünmigrations/ 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.
- PHP PDO MySQL CMS mimarisi: Giriş: Neden Modüler Bir CMS Mimarisi?
- Pratik Senaryo: Tek index.php Dosyasından Çıkmak
- Mimarinin Genel Yapısı
- PDO ile Veritabanı Katmanı
- Router ve Modül Yükleyici
- Çekirdek Tablolar ve Veri Modeli
- Güvenlik Notları
- Performans Notları
- Yaygın Hatalar ve Çözümleri
- Sonuç ve Sonraki Adım
- Sıkça Sorulan Sorular
- PHP PDO MySQL CMS mimarisi için pratik kontrol listesi
- SSS: PHP PDO MySQL ile Modüler CMS Mimarisi Nasıl Kurulur
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.
Bir Cevap Yaz
E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir.