
Proje Detayı
Merhabalar efenim, bir süredir aklımda olan ancak hiç bir zaman başlamaya yeltenemediğim aslında gerek duymadığım ama dünyada benim de bir dikili ağacım çakılı çivim olsun isteyerek bir projeye kolları sıvadım.
Projemin amacı aslında şu anda piyasadaki en bilinen en büyük CMF ve CMS sistemlerinin hepsini detaylıca inceleyip kapsamlıca artılarını eksilerini not ederek çok daha geniş kapsamlı ancak çok daha dayanıklı bir içerik yönetim framework'ü yapmaktı ve yavaştan başlayayım bakalım neler çıkar diyerekten kolları sıvadım.
Bu süreçte bolca yapay zeka desteği aldım hatta projemin whitepaper dosyasını da sağolsun gemini benim yerime yazdı ki buraya ekleyeceğim aşağıdan detaylarını inceleyebilirsiniz o kadar uzun yazmaya halim vaktim var aslında ama üşengeçliğim el vermiyor :)
CPalius CMF: Bir İçerik Yönetim Sisteminden "Kurşun Geçirmez" Kurumsal Uygulama Framework'üne Evrim
Yayın Türü: Teknik İnceleme (Whitepaper) & Mimari Yol Haritası (RFC)
Sürüm: v1.0.0-draft
Hedef Kitle: Kıdemli PHP Geliştiricileri, Sistem Mimarları, Açık Kaynak Geliştiricileri ve Yapay Zeka Ajanları
Yazar / Kurucu: Ali Çömez (slaweally)
Teknoloji Yığını: PHP 8.2+, Symfony 7.4 LTS, Doctrine ORM, AssetMapper, Tailwind CSS Standalone Binary
Giriş: Teori, Pratik ve "Tekerleği Yeniden İcat Etme" Sancısı
Modern web ekosisteminde yeni bir projeye başlarken geliştiriciler kendilerini her zaman iki ucu keskin bir bıçağın üzerinde bulurlar. Bir yanda, hazır eklenti ve tema sistemleriyle donatılmış ancak hantal veritabanı şemaları, teknik borç (technical debt) dağları ve güvenlik zaafiyetleriyle boğuşan klasik CMS'ler (WordPress, Drupal) vardır. Diğer yanda ise her şeye sıfırdan başlamayı gerektiren, her projede auth, ACL, dosya yönetimi ve admin paneli gibi temel bileşenleri sıfırdan yazarak zaman kaybettiren saf modern framework'ler (Symfony, Laravel) bulunur.
Sıfırdan kendi MVC yapısını yazmak kulağa romantik gelse de günümüz standartlarında rasyonel değildir. Bir framework sadece yönlendirme (routing) ve kontrolcülerden (controllers) ibaret değildir; güvenlik (CSRF, XSS, SQLi engelleme), Dependency Injection (DI) Container yönetimi ve HTTP katmanı gibi devasa bir "görünmez buzdağı" barındırır.
İşte CPalius, bu iki dünya arasındaki köprüyü kurmak; geliştiriciyi tekerleği yeniden icat etme sancısından kurtarırken ona dünya standartlarında, optimize, güvenli ve genişletilebilir bir altyapı sunmak amacıyla doğdu. Bu döküman, CPalius’un en temiz skeleton kurulumundan, "çökmeyen" bir işletim sistemi mimarisine; ardından bir CMS'ten "Kurumsal Uygulama Framework'üne" (Enterprise Application Framework) uzanan teknik yolculuğunun en ince ayrıntısına kadar dökümüdür.
1. BÖLÜM: "Pristine Root" ve Klasör Mimarisi
Standart bir Symfony projesinde üçüncü parti paketler ve tarifler (recipes) yüklendikçe projenin ana dizini (root) dosya ve klasör çöplüğüne döner. Geliştiricinin kendi yazdığı kodlar ile framework'ün sistem dosyaları birbirine karışır. CPalius, projenin ilk gününde bu karmaşaya savaş açtı ve "Pristine Root" (Tertemiz Ana Dizin) politikasını benimsedi.
Ana dizinde sadece neyin nerede olduğunu gösteren 4 temel klasör, .env dosyası ve composer.json kalacak şekilde tüm mimariyi izole ettik.
Klasör Hiyerarşisi
Plaintext
CPalius/ (Ana Dizin)
│
├── cp-core/ # === KERNEL & SYSTEM SPACE ===
│ ├── bin/ # Konsol komut aracı (bin/console)
│ ├── config/ # Core konfigürasyonları (bundles, packages, routes)
│ ├── migrations/ # Çekirdek veritabanı göç dosyaları (Git'e tabi)
│ ├── src/ # Çekirdek PHP sınıfları (App\ namespace)
│ └── var/ # Cache, log ve SQLite dosyaları (Yazma alanı, gitignore)
│
├── cp-includes/ # === DEPENDENCIES SPACE ===
│ └── vendor/ # Composer paketleri (Symfony ve kütüphaneler)
│
├── cp-content/ # === USER & DEVELOPER SPACE ===
│ ├── config/ # Config Sync alanı (YAML olarak taşınan altyapı)
│ ├── modules/ # Bağımsız modüller/eklentiler (Modules\ namespace)
│ ├── themes/ # Kullanıcı arayüz temaları
│ └── translations/ # Global arayüz çevirileri (tr, en)
│
├── public/ # === WEB ROOT === (Dışarıya açık tek klasör)
│ ├── index.php # Front Controller (Tek giriş kapısı)
│ └── assets/ # Derlenmiş/Semlink edilmiş frontend varlıkları
│
├── .env # Ortam yapılandırmaları
└── composer.json # CPalius özel yol eşlemeleri
Symfony Flex'in İç Mekanizmalarını Bükmek
Bu yapıyı kurabilmek için Symfony Flex ve Composer'ın yapılandırma yeteneklerini en uç sınırlarına kadar zorladık. composer.json dosyasını Flex'e kendi klasör yapımızı dikte edecek şekilde yapılandırdık:
JSON
{ "config": { "vendor-dir": "cp-includes/vendor", "bin-dir": "cp-core/bin" }, "autoload": { "psr-4": { "App\\": "cp-core/src/", "Modules\\": "cp-content/modules/", "DoctrineMigrations\\": "cp-core/migrations/" } }, "extra": { "root-dir": ".", "bin-dir": "cp-core/bin", "config-dir": "cp-core/config", "src-dir": "cp-core/src", "var-dir": "cp-core/var", "public-dir": "public", "runtime": { "project_dir": ".", "dotenv_path": ".env" } }
}
Teknik Not: Sadece config.vendor-dir değiştirmek yetersizdir. Symfony Flex'in içindeki komutlar (cache:clear, assets:install), yolları hesaplarken composer'ın standart konfigürasyonlarından değil, extra bloğu altındaki parametrelerden beslenir. Bu parametreler eklenmediğinde sistem derleme aşamasında çöküyordu. Bu sayede tüm Flex tariflerinin doğrudan cp-core altına kurulmasını garanti altına aldık.
2. BÖLÜM: "Core Never Dies" (Çökmeyen Çekirdek) Mimarisi
Bir CMF geliştirirken en büyük kabus, kullanıcı alanından (cp-content/modules) gelebilecek kötü yazılmış veya bozuk kodların tüm sistemi kilitlemesidir. PHP'de gerçek bir işletim sistemi seviyesinde "sandbox" olmadığı için, CPalius Üç Kademeli Aktif Savunma ve Karantina Hattı geliştirmiştir.
1. Savunma Hattı: Tavuk-Yumurta Probleminin Çözümü (bundles.php)
Symfony boot edilirken ilk olarak config/bundles.php dosyası okunur. Bu aşamada henüz veritabanı bağlantısı, servis konteyneri (container) veya autoloader tam olarak ayağa kalkmamıştır. Eğer modüllerin aktiflik durumunu doğrudan veritabanından okumaya çalışırsak, sistem daha boot edilemeden kilitlenir.
Çözüm: CPalius, bir modül aktif veya pasif edildiğinde cp-core/config/active_modules.php adında statik, PHP opcache dostu bir dizi (array) dosyası üretir. bundles.php sadece bu dosyayı okur:
PHP
// cp-core/config/bundles.php
$bundles = [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], App\Core\CoreBundle::class => ['all' => true], // Çekirdek her zaman ayakta!
]; $activeModulesFile = __DIR__ . '/active_modules.php';
if (file_exists($activeModulesFile)) { $activeModules = require $activeModulesFile; foreach ($activeModules as $moduleClass) { if (class_exists($moduleClass)) { $bundles[$moduleClass] = ['all' => true]; } }
}
return $bundles;
2. Savunma Hattı: Çalışma Zamanı İzolasyonu (Kernel::boot Override)
Sınıfın fiziksel olarak var olması (class_exists), o modülün kodunun hatasız çalıştığı anlamına gelmez. Modülün kendi boot() metodu içinde fırlatacağı bir TypeError veya RuntimeException tüm sistemi çökertebilir.
Bunu engellemek için Kernel::boot() metodunu override ederek modül seviyesindeki tüm boot süreçlerini try/catch (\Throwable) zırhıyla kuşattık:
PHP
// cp-core/src/Kernel.php
public function boot(): void
{ parent::boot(); foreach ($this->getBundles() as $bundle) { if (str_starts_with($bundle->getNamespace(), 'Modules\\')) { try { // Modülleri izole olarak boot et $bundle->boot(); } catch (\Throwable $e) { // Çekirdeği koru, hatayı karantina günlüğüne yaz $this->quarantineModule($bundle, $e); } } }
}
3. Savunma Hattı: Compile-Time Kilitlenme Koruması & Dry-Run
Eklentideki bir services.yaml dosyasında geçersiz bir YAML sözdizimi varsa veya yanlış bir Dependency Injection (autowire) tanımı yapıldıysa, Symfony container'ı derlenemez (compile-time error). Bu durumda sistem çöker ve terminalden cp:module:deactivate komutunu bile çalıştıramayız.
Çözüm: ModuleActivateCommand sınıfına izole bir Dry-Run (Önizleme) kontrolü entegre ettik. Bir modül aktive edilmek istendiğinde şu akış izlenir:
- Modül sınıfı geçici olarak active_modules.php dosyasına yazılır.
- symfony/process bileşeni kullanılarak arka planda tamamen izole bir alt süreç (subprocess) başlatılır.
- Bu alt süreçte sırasıyla cache:clear --no-warmup -> lint:yaml -> lint:container komutları koşturulur.
- Eğer alt süreç 0 dışında bir exit code ile dönerse (hata varsa), ana süreç finally bloğunda dosyayı anında eski haline geri getirir, aktivasyonu iptal eder ve modülü kalıcı olarak karantinaya alır. Dosya asla bozuk haliyle kaydedilmez!
3. BÖLÜM: Hibrit Veri Modeli ve Yüksek Performanslı Flat Index Sistemi
İçerik yönetiminde iki klasik yaklaşım da sorunludur:
- EAV (Entity-Attribute-Value) Modeli (Drupal): Her yeni alan için yeni bir veritabanı tablosu açılır. 15 özel alanı olan bir sayfayı çekmek için 15 JOIN sorgusu atılır; veritabanı kilitlenir.
- Postmeta/Serialized Modeli (WordPress): Tüm özel alanlar tek bir metatablosunda satır satır tutulur. Filtreleme ve sıralama yapmak tam bir performans felaketidir.
CPalius Hibrit Veri Modeli: Sık sorgulanan, filtrelenen ve indekslenen temel alanları (ID, Başlık, Slug, Tür, Durum, Dil, Tarihler) gerçek veritabanı sütunları olarak tutuyoruz. İçeriğin kendisine ait tüm dinamik, esnek ve her projede değişebilecek alanları (Gövde metni, öne çıkan görsel, galeri alanları, SEO verileri) ise tek bir SQL json sütununda (data) saklıyoruz.
SQLite, MySQL ve Postgres Uyumluluğunda İndeksleme Çıkmazı
Dinamik JSON verilerini veritabanı düzeyinde sorgulamak isterseniz, SQLite, MySQL ve Postgres arasında tamamen farklı SQL sözdizimleri (syntax) kullanmanız gerekir. Ayrıca generated columns (üretilmiş kolonlar) üzerinde indeks oluşturmak Doctrine ORM şema araçlarıyla çalışırken son derece kırılgandır; Doctrine her şema güncellemesinde bu indeksleri silmeye çalışır.
Çözüm: NodeFieldIndex ve Dinamik İndeksleme Motoru
CPalius, bu sorunu Flat Field Index (Düz Alan Endeksi) tablosu ve bir Doctrine Event Listener ile çözer.
1. Düzleştirilmiş Yardımcı Tablo Şeması
Sorgulanacak dinamik alanlar için tip-uygun kolonlara sahip bağımsız bir indeks tablosu tasarladık:
PHP
#[ORM\Entity]
#[ORM\Index(columns: ['field_name', 'value_string'])]
#[ORM\Index(columns: ['field_name', 'value_int'])]
#[ORM\Index(columns: ['field_name', 'value_decimal'])]
#[ORM\Index(columns: ['field_name', 'value_datetime'])]
class NodeFieldIndex { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\ManyToOne(targetEntity: Node::class, onDelete: 'CASCADE')] private ?Node $node = null; #[ORM\Column(length: 100)] private ?string $fieldName = null; #[ORM\Column(length: 255, nullable: true)] private ?string $valueString = null; #[ORM\Column(nullable: true)] private ?int $valueInt = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] private ?string $valueDecimal = null; #[ORM\Column(type: 'datetime_immutable', nullable: true)] private ?\DateTimeImmutable $valueDatetime = null;
}
2. Doctrine Event Listener (NodeIndexListener)
Geliştiricinin belirlediği QueryableFieldsRegistry yapılandırmasına göre, bir Node kaydedildiğinde veya güncellendiğinde, listener otomatik olarak JSON (data) sütunundaki verileri parse eder, eski indeksleri temizler ve idempotent şekilde bu yardımcı tabloya yazar:
PHP
public function postPersist(PostPersistEventArgs $args): void
{ $entity = $args->getObject(); if (!$entity instanceof Node) return; $this->syncIndex($entity, $args->getObjectManager());
}
3. Akıllı ve Taşınabilir Sorgulama Metodu (NodeRepository)
Artık hangi veritabanı motorunu kullanırsak kullanalım, tek bir standart SQL/DQL sorgusuyla JSON verilerini indeksler üzerinden ışık hızında sorgulayabiliyoruz:
PHP
// "product" tipinde, "price" değeri 1500'den küçük olan ve aktif dildeki tüm node'ları getir
$nodes = $nodeRepository->findByIndexedField( type: 'product', locale: 'tr', fieldName: 'price', value: '1500.00', valueColumn: 'valueDecimal', operator: '<'
);
4. BÖLÜM: Çok Dilli Yapı ve Kompozit Benzersizlik Kısıtları
Çok dilli (i18n) yapı projelere sonradan eklendiğinde tüm veri modelini çökertir. CPalius, çoklu dili ilk günden çekirdeğin hücrelerine işlemiştir.
Kompozit Unique Constraints (Benzersizlik Güvencesi)
Çok dilli bir yapıda, klasik unique: true kısıtlamaları sistemi kilitler. Örneğin, Türkçe /tr/hakkimizda ile İngilizce /en/hakkimizda sayfalarının aynı anda var olabilmesi gerekir. Global tek bir slug unique kısıtlaması bunu engeller.
CPalius Çözümü: Veritabanı şemamızda kompozit (composite) benzersizlik kısıtları kullandık:
- Yönlendirme Benzersizliği: Aynı slug sadece aynı dilde unique olmalıdır: UNIQUE(slug, locale).
- Çeviri Grubu Benzersizliği: Aynı çeviri grubunda (translation group) aynı dilden sadece bir adet içerik bulunabilir: UNIQUE(translation_group_id, locale).
PHP
#[ORM\Entity]
#[ORM\UniqueConstraint(name: 'uniq_node_slug_locale', columns: ['slug', 'locale'])]
#[ORM\UniqueConstraint(name: 'uniq_node_translation_locale', columns: ['translation_group_id', 'locale'])]
class Node { // ...
}
5. BÖLÜM: "CMS"ten "Uygulama Framework'üne" Geçiş
CPalius sadece bir içerik yönetim sistemi (CMS) değildir; arkasında Oto Galeri, Turizm Acentası, Personel Yönetimi veya CRM/ERP sistemlerinin çalışabileceği esnek bir uygulama platformudur. Bu dönüşümü sağlamak için iki sınıf varlık tanımladık:
İki Sınıf Varlık (Content vs Business)
| Özellik | Content Entities (Node) | Business Records (Resource) |
|---|---|---|
| Kavramsal Karşılık | Sayfa, Yazı, İlan Vitrini, Blog | Araç, Fatura, Rezervasyon, Personel |
| Özellikleri | Slug var, çoklu dil var, yayın durumu var, SEO var. | Slug yok, dil yok, yayın yok, durum makinesi (workflow) var. |
| Ortak Payda | Aynı Yetenek (Capability) modeli, aynı Twig bileşenleri, aynı CLI yönetimi, aynı Config Sync. |
#[CpResource] Devrimi
Geliştiricinin iş süreçlerini kodlamasını saniyeler seviyesine indirmek için özel bir PHP Attribute yapısı tasarladık. Tek bir anotasyon ile veritabanındaki ham bir Doctrine sınıfını platformun tüm güçleriyle birleştiriyoruz:
PHP
#[CpResource( name: 'vehicle', module: 'oto-galeri', capabilities: ['create', 'edit', 'delete', 'view'], auditable: true, multiTenant: true, workflow: 'vehicle_lifecycle'
)]
#[ORM\Entity]
class Vehicle { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 20)] private ?string $plate = null; #[ORM\Column(length: 50)] private ?string $brand = null; #[ORM\Column(type: 'integer')] private ?int $price = 0; // Kuruş bazında saklanır (Float asla!)
}
Bu tek nitelik (#[CpResource]) sayesinde:
- Rol ve yetenek matrisine (vehicle.create, vehicle.edit vb.) dinamik yetenekler otomatik kaydolur.
- Otomatik CRUD formları ve liste ekranları bu tanıma göre dinamik olarak türetilir.
- SaaS projeleri için çoklu kiracı (multiTenant: true) izolasyonu arka planda otomatik uygulanır.
- Entity üzerinde yapılan tüm değişiklikler anlık olarak sürüm geçmişine (audit log) kaydedilir.
6. BÖLÜM: Güvenlik ve Performans Anayasası
Güvenlik ve performans, projenin son aşamasında "üzerine eklenen" birer cila değildir; sistemin en temel yapı taşlarıdır.
1. Yetenek Tabanlı Erişim Kontrolü (CBAC)
Standart Symfony ROLE_ADMIN veya ROLE_USER yaklaşımları hantaldır ve sonradan genişletilemez. CPalius'ta kodun hiçbir yerinde rol ismi kontrol edilmez; her zaman dinamik olarak üretilen yetenekler (capabilities) kontrol edilir:
PHP
// Doğru Yaklaşım: $this->denyAccessUnlessGranted('system.module.manage'); ve
// Yanlış Yaklaşım:if ($this->getUser()->getRole() === 'ROLE_ADMIN') { ... }
- Roller = Config (YAML): Rol tanımları ve bunların yetenek matrisleri cp-content/config/sync/ altında YAML dosyalarında tutulur ve Git ile taşınır.
- Kullanıcılar = Content (DB): Gerçek kullanıcı kayıtları ise veritabanında kalır, asla dışa aktarılmaz (export edilmez).
2. SaaS Veri Sızıntısı Koruması (Automatic Tenant SQLFilter)
Çok kiracılı (SaaS) sistemlerde en büyük güvenlik açığı, yazılımcının bir sorgunun sonuna WHERE tenant_id = ? yazmayı unutmasıyla yaşanır. CPalius'ta bu ihtimal platform seviyesinde yok edilmiştir:
PHP
// cp-core/src/Core/Database/TenantFilter.php
class TenantFilter extends SQLFilter
{ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string { // Eğer entity multi-tenant ise otomatik olarak tenant kısıtını sorguya enjekte et if ($targetEntity->getReflectionClass()->hasAttribute(CpResource::class)) { return sprintf('%s.tenant_id = %s', $targetTableAlias, $this->getParameter('current_tenant_id')); } return ''; }
}
3. N+1 Sorgu Muhafızı (Dev-Mode N+1 Guard)
Veritabanı şişmelerinin ve yavaşlıklarının en yaygın sebebi olan N+1 sorgu hatalarını engellemek için sadece dev ortamında tetiklenen bir Doctrine Listener geliştirdik. Tek bir HTTP isteğinde aynı tabloya atılan sorgu sayısı belirlenen limiti (örn: 10 sorgu) aşarsa, sistem sessizce çalışmaya devam etmek yerine doğrudan MaxQueriesExceededException fırlatır. Geliştirici bu hatayı lokalde çözmeden kodu canlıya alamaz!
7. BÖLÜM: Gelecek Yol Haritası ve Teknik İstişare (RFC)
CPalius'un çekirdek güvenlik, performans ve mimari omurgasını tamamlamış bulunuyoruz. Sıradaki geliştirme sprintlerimizde aşağıdaki sistemleri inşa edeceğiz:
- AACP Safe Mode / Kurtarma Konsolu: cp-content altındaki tüm modüller çöktüğünde veya silindiğinde bile ayakta kalabilen, bağımsız varlıkları olan ve sistem yöneticisinin sisteme müdahale edip bozuk modülleri kapatmasını sağlayan izole web konsolu.
- Workflow & State Machine: Fatura, araç, rezervasyon gibi iş kayıtlarının geçiş süreçlerini (draft -> preparation -> sold) YAML tanımlarıyla yöneten ve her geçişi otomatik audit log'a ve bildirim kuyruğuna bağlayan mekanizma.
- Pimcore Tarzı Bağımsız Asset Sistemi: Medyayı Node'un bir alt tipi yapmaktan kurtarıp, Flysystem (S3, MinIO) ve LiipImagineBundle entegrasyonu ile URL üzerinden anlık görsel türetme sunan modern dosya kütüphanesi.
💬 Soru ve Görüşleriniz (Topluluk İstişaresi)
Bu mimariyi daha da kusursuzlaştırmak için siz değerli geliştiricilerden şu konularda geri bildirim bekliyoruz:
- Flat Field Index Modeli: SQLite, MySQL ve Postgres uyumluluğu için tasarladığımız bu model sizce devasa veri hacimlerinde nasıl bir performans gösterir? İndeks tablosunun partition edilmesi gerekir mi?
- Config Sync Yaklaşımı: Drupal'ın config sync sistemini Symfony'nin TreeBuilder yapısıyla taklit etme fikrimiz hakkındaki düşünceleriniz nelerdir?
- Zero Node.js Israrı: Geliştirici ve admin panelinde AssetMapper + Standalone Tailwind kullanma kararımız, gelecekte çok karmaşık frontend bileşenleri yazarken bizi kısıtlar mı?
Fikirlerinizi, eleştirilerinizi ve mimari önerilerinizi heyecanla bekliyoruz! CPalius, topluluğun gücüyle en sağlam uygulama framework'üne dönüşecek.
Ekran Görüntüleri






comments[] (0)
Henüz yorum yok. İlk yorumu siz yazın.
Yorum Yaz