comez.dev
tr
~/project/cpalius-cmf-2Tüm projeler

proje

CPalius CMF

teknolojiler

SymfonyTailwind

eklenme
14 Tem 2026

web sitesi kaynak

Ayrıntılar

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 :)

Aradan geçen zamanda proje ilk günkü taslağından epey yol katetti, o yüzden bu yazıyı da güncel haliyle yeniden yazma ihtiyacı hissettim. İlk whitepaper'da "yapılacaklar" listesinde duran bazı maddeler (Safe Mode / Kurtarma Konsolu gibi) artık gerçek kod, üstüne de whitepaper'da hiç bahsi geçmeyen REST API Gateway, Hook Sistemi, Cron/Otomasyon Motoru ve Plugin katmanı gibi koca koca bölümler eklendi. Aşağıda projenin bugünkü, güncel halini bulacaksınız.

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.1 (Güncel)
  • 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

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:

{ "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 artık dört kademeli aktif savunma ve karantina hattı geliştirmiştir — ilk taslakta üç kademeydi, dördüncüsü rota yükleme katmanına eklendi.

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:

// 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:

// 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!

4. Savunma Hattı (Yeni): İzole Route Yükleyicisi

Standart Symfony'de bir modülün routes.yaml dosyasındaki tek bir syntax hatası tüm sistemi çökertir — üçüncü savunma hattı bunu aktivasyon anında yakalar ama sonradan bozulan bir dosyayı yakalamaz. Bu boşluğu kapatmak için SafeModuleRouteLoader eklendi: her modülün rotasını kendi izole try/catch bloğunda yükler. Hatalı modül sessizce atlanır, sistemin geri kalanı %100 ayakta kalır.

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. queryable: true işaretlenen her alan otomatik olarak node_field_index tablosuna yansıtılır, ve ProcessWire'dan ilham alınan akıcı bir Selector API'siyle sorgulanır:

// Süper Akıcı (Fluent) Selector API Kullanımı
$nodes = $nodeRepository->findNodesBySelector( 'type=post, status=published, is_featured=1, limit=5, sort=createdAt:desc'
);

Bu tek satır arka planda tip-uygun kolonlara (value_string, value_int, value_decimal, value_datetime) sahip bir indeks tablosu üzerinden çalışır — hangi veritabanı motorunu kullanırsanız kullanın aynı DQL çalışır.

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).

Ayrıca modüllerin kendi çevirilerini kendi dizinlerinde barındırabilmesi için TranslationFileLocator eklendi; bu sınıf dosyaları bulur ve bir Symfony Compiler Pass ile framework.translator.paths dizisine enjekte eder. Admin panelinden yapılan çeviri düzenlemeleri de atomik dosya yazma garantisiyle (.tmp dosyasına yazıp rename() ile değiştirme) diske işlenir, elektrik kesintisinde bile dosya yarım kalmaz.

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)

ÖzellikContent Entities (Node)Business Records (Resource)
Kavramsal KarşılıkSayfa, Yazı, İlan Vitrini, BlogAraç, Fatura, Rezervasyon, Personel
ÖzellikleriSlug var, çoklu dil var, yayın durumu var, SEO varSlug yok, dil yok, yayın yok, durum makinesi (workflow) var
Ortak PaydaAynı 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:

#[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.

Not: Bu altyapı (registry, capability üretimi, multiTenant/auditable bayrakları) tamamen hazır ama henüz somut bir business entity ile canlıda kullanılmıyor — yol haritasındaki bir sonraki adım bu.

6. BÖLÜM (Yeni): Platform Genişletilebilirlik Katmanı

İlk whitepaper'dan bu yana çekirdeğe dört yeni genişletme omurgası eklendi; hepsi aynı "Core Never Dies" zırhını taşıyor:

REST API Gateway

Tek bir /api/{path} joker route'u, #[CpApi] attribute'lu servis metotlarını derleme zamanında (ApiRegistrationPass) eşleştirir. Kimlik doğrulama X-CP-API-KEY header'ı ile fail-closed çalışır — anahtar geçersizse hedef metot hiç çağrılmaz. Her endpoint kendi try/catch zırhında izole edilir; çöken bir uç yalnızca kendini etkiler.

// Modül içerisindeki tertemiz uç nokta
#[CpApi(path: '/blog/posts', methods: ['GET'], public: false)]
public function getPosts(Request $request): JsonResponse
{ // İş mantığı...
} // ApiGatewayController'daki fail-closed zırhı
if (!$endpoint['definition']['public'] && !$this->hasValidApiKey($request)) { return new JsonResponse(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
}

İzole Hook Sistemi

Cotonti tarzı flat-file Hooks/{hook_point}.php dosyaları izole bir Closure içinde çalışır — dış scope'a değişken sızıntısı imkansızdır. Symfony tarzı #[CpHook] attribute'lu servisler ise HookRegistrationPass ile toplanır. Çöken hook sayfayı asla 500'e düşürmez, karantina günlüğüne yazılır.

Birleşik Cron / Otomasyon Motoru

CronManager üç paralel kaynağı — veritabanındaki cp_cron_jobs, #[CpCronJob] işaretli servisler ve flat-file Hooks/cron.{job}.php dosyalarını — tek listede birleştirir. Görevler CronCommandWhitelist ile yalnızca cp:* önekli komutlara izin veren izole alt-süreçlerde çalıştırılır.

// cp-content/modules/Blog/Cron/PublishScheduledPostsTask.php
final class PublishScheduledPostsTask
{ public function __construct( private readonly EntityManagerInterface $entityManager, private readonly NodeRepository $nodeRepository, ) {} #[CpCronJob(schedule: '*/5 * * * *', name: 'blog.publish_scheduled')] public function execute(): string { $dueNodes = $this->nodeRepository->findDueScheduledNodes(); if ($dueNodes === []) { return 'Yayın zamanı gelmiş içerik yok.'; } foreach ($dueNodes as $node) { $node->publish($node->getPublishedAt()); } $this->entityManager->flush(); return sprintf("%d içerik yayına alındı.", count($dueNodes)); }
}

Modülden Bağımsız Plugin Katmanı

Modüllerden ayrı ikinci bir genişletme noktası: PluginInterface işaretli servisler PluginRegistry tarafından toplanır, aktiflik durumu PluginToggleRepository ile veritabanında tutulur. Bir modül kendi içinde w

#cpalius#cpalius nedir

Yorumlar0

Henüz yorum yok. İlk yorumu siz yazın.

Yorum yaz

İlgili

proje

CPalius CMF

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ğa…

proje

CloudPanel Forum prjesi - Cloudpanelforum.org

Merhabalar efenim. Bir süredir Cloudpanel kullanıyorum oldukça kaliteli ve güzele sadece performansa odaklanmış bir control paneli yeni yeni gelişmey…

proje

Ağasar.org Kültür portalı projesi

Ağasar.org - Ağasar kültür portalı projesini Php / Laravel ile hazırladım, Projenin amacı Ağasar yöresine ait tüm içerikleri tek bir portalda paylaşm…

Benzer bir iş üzerinde mi çalışıyorsunuz?
Mimariyi incelemekten teslimata kadar yardımcı olmaktan memnuniyet duyarım.

iletişim