NEXT-GEN CONTENT MANAGEMENT FRAMEWORK
From a Content Management System
"Bulletproof"
Enterprise Application Framework
A world-class, optimized, secure, and extensible foundation that bridges plugin-rich but cumbersome CMS platforms and frameworks that require starting everything from scratch.
Every modern web project starts on a double-edged sword. CPalius was built to close the gap between these two worlds.
Packed with plugins and themes, yet burdened by cumbersome database schemas, mountains of technical debt, and security weaknesses.
An approach that wastes time by rebuilding core components such as authentication, ACL, file management, and admin panels for every project.
Writing your own framework may sound romantic, but it hides a massive iceberg of security (CSRF, XSS, SQLi), dependency injection, and HTTP-layer concerns.
An architecture built on Symfony 7.4 LTS's proven security and performance foundation, free from technical debt.
Core components such as authentication, ACL, file management, localization, and an admin panel are included from day one. No more reinventing the wheel.
More than a CMS: an application platform for car dealerships, travel agencies, CRM, and ERP systems — all running on the same foundation.
Errors from user modules never bring down the system. Four layers of active defense and quarantine keep the core running.
As third-party packages and recipes accumulate, a standard Symfony project root becomes cluttered. CPalius follows a "Pristine Root" policy.
CPalius/ │ ├── cp-core/ # KERNEL & SYSTEM SPACE │ ├── bin/ # Console command tools │ ├── config/ # Core configuration │ ├── migrations/ # Database migrations │ ├── src/ # PHP classes (App\) │ └── var/ # Cache, logs, SQLite │ ├── cp-includes/ # DEPENDENCIES SPACE │ └── vendor/ # Composer packages │ ├── cp-content/ # USER & DEVELOPER SPACE │ ├── config/ # Config Sync area │ ├── modules/ # Independent modules │ ├── themes/ # UI themes │ └── translations/ # Translations (tr, en) │ ├── public/ # WEB ROOT │ ├── index.php # Front Controller │ └── assets/ # Frontend assets │ ├── .env └── composer.json
Kernel & System SpaceCore PHP classes, configuration, console tools, and database migrations. Symfony Flex recipes are guaranteed to install directly here.
Dependencies SpaceComposer packages and Symfony libraries live in this isolated space. The vendor-dir has been reconfigured.
User & Developer SpaceModules, themes, translations, and Config Sync live here. Developer code never mixes with framework system files.
Web RootThe only public folder. The Front Controller (index.php) and compiled frontend assets live here. This is the security boundary.
A four-layer active defense and quarantine line that prevents bad or broken user code from locking down the entire system.
When Symfony boots, it reads config/bundles.php. At this stage the database connection, service container, and autoloader are not fully ready. Trying to read module status from the database would lock the system before it can boot.
Solution: When a module is activated or deactivated, CPalius generates a static, PHP opcache-friendly array file named active_modules.php. bundles.php only reads that file — it never touches the database.
// Core always stays up $bundles = [ FrameworkBundle::class => ['all' => true], TwigBundle::class => ['all' => true], DoctrineBundle::class => ['all' => true], CoreBundle::class => ['all' => true], ]; // Load active modules from file $activeModules = require 'active_modules.php'; foreach ($activeModules as $moduleClass) { if (class_exists($moduleClass)) { $bundles[$moduleClass] = ['all' => true]; } }
A class existing on disk (class_exists) does not mean its code runs safely. A TypeError or RuntimeException thrown inside a module's boot() method can crash the entire system.
Solution: Kernel::boot() is overridden so every module-level boot process is wrapped in try/catch (\Throwable). Faulty modules are quarantined automatically while the core keeps running.
Invalid YAML syntax or a bad DI definition in a plugin can make the Symfony container uncompilable. In that state, even terminal commands fail.
Solution: Before activation, an isolated subprocess is started with symfony/process. It runs cache:clear → lint:yaml → lint:container. On failure, activation is cancelled and the module is permanently quarantined. Broken state is never written to disk.
In standard Symfony, a syntax error in a module's routes.yaml file crashes the entire system.
Solution: SafeModuleRouteLoader loads each module's routes inside an isolated try/catch block. The faulty module is skipped while the rest of the system stays 100% available.
foreach ($this->moduleRegistry->getHealthyModuleBundles() as $moduleClass) {
try {
$imported = $this->import($routesFile, 'yaml');
$collection->addCollection($imported);
} catch (\Throwable $e) {
$this->logger?->error('Skipping broken module routes', [
'module' => $moduleClass,
'error' => $e->getMessage(),
]);
}
}
A hybrid data model, high-performance indexing, and localization are built into the core from day one.
Frequently queried fields (ID, Title, Slug, Status) live in real columns; flexible fields (body, SEO, gallery) live in a single JSON column. Escape EAV JOIN hell and WordPress meta chaos.
Query JSON data in a database-agnostic way. NodeFieldIndex plus a Doctrine Event Listener build a typed index table. The same DQL works on SQLite, MySQL, and Postgres.
Multilingual support is baked into the core from day one. Composite uniqueness guarantees UNIQUE(slug, locale) and UNIQUE(translation_group_id, locale). Language is not a late polish layer.
Drupal's config sync approach, combined with Symfony TreeBuilder. Roles and capability matrices live in YAML and travel with Git. User data stays in the database.
Every module runs as an independent Symfony Bundle with its own routes, services, templates, and migrations. Full isolation under the Modules\ namespace.
Developer and admin panels use AssetMapper + Standalone Tailwind CSS. Zero Node.js dependency. No build step — develop and deploy directly.
Per Manifesto Law 5.3, every rich-text value destined for the database is sterilized at the core level via RichTextSanitizer.
Automatic JSON-LD (Schema.org) generation (BlogPosting, WebPage) from hybrid JSON-based data through SchemaOrgBuilder.
A reactive messaging backbone built without violating YAGNI — asynchronous and decoupling-focused. Cross-layer dependencies and hook triggers (event subscribers) are minimized with "Golden Ratio" precision.
CPalius is more than a CMS; it is a flexible application platform for car dealerships, travel agencies, personnel management, and CRM/ERP systems.
| Feature | Content Entities (Node) | Business Records (Resource) |
|---|---|---|
| Conceptual Equivalent | Pages, Articles, Listing Showcases, Blogs | Vehicles, Invoices, Reservations, Personnel |
| Slug | Yes — SEO-friendly URL generation | No — Internal management record |
| Localization | Yes — with translation_group_id | No — Monolingual record |
| Publication Status | Yes — draft / published / archived | No — State machine (workflow) |
| SEO | Yes — Metadata in JSON data | No — Not public |
| Common Ground | The same Capability model, Twig components, CLI management, and Config Sync | |
To reduce business-process development to seconds, a custom PHP Attribute was designed. With a single annotation, a plain Doctrine class is connected to the full power of the platform.
With this single attribute:
#[CpResource( name: 'vehicle', module: 'oto-galeri', capabilities: [ 'create','edit', 'delete','view' ], auditable: true, multiTenant: true, workflow: 'vehicle_lifecycle' )] #[ORM\Entity] class Vehicle { private ?int $id; private ?string $plate; private ?string $brand; private ?int $price; }
Every insert, update, and delete of an entity marked #[CpResource(auditable: true)] or #[Auditable] is written automatically to the cp_audit_logs table by a fully isolated Doctrine listener that never touches the audited entity itself.
onFlush / postFlush Listener
Updates and deletes are captured in onFlush — identifiers are already known — and inserted into the same flush via UnitOfWork::computeChangeSet(). Creates are held until postFlush so the generated primary key can be recorded, then persisted with a single guarded re-flush.
The current user comes from TokenStorageInterface; the diff is stored as {field: [oldValue, newValue]} JSON. Creates use [null, newValue], deletes use [oldValue, null].
cp_audit_logs
Columns: resource_name, resource_id (VARCHAR — composite and UUID keys fit too), user_id, action (create/update/delete), changes (JSON), and created_at.
AuditLogRepository exposes findForResource(), findRecent(), and findByUser(). Records are never edited or deleted through the application — append-only by design.
#[AsDoctrineListener(event: Events::onFlush)]
#[AsDoctrineListener(event: Events::postFlush)]
final class AuditLogListener
{
public function onFlush(OnFlushEventArgs $args): void
{
$unitOfWork = $args->getObjectManager()->getUnitOfWork();
$auditable = $this->resourceRegistry->getAuditableEntityClasses();
foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
if (!$this->isAuditable($entity, $auditable)) {
continue;
}
// {field: [oldValue, newValue]} — computed into the same flush
$changes = $this->buildUpdateChanges($unitOfWork, $entity);
}
}
}
Since the first whitepaper release, four new extension backbones have been added to the core: REST API Gateway, an isolated Hook System, a unified Cron/Automation Engine, and a module-independent Plugin layer. Each carries the same "Core Never Dies" armor.
A single /api/{path} wildcard route matches services annotated with #[CpApi] at compile time via ApiRegistrationPass. Authentication uses the X-CP-API-KEY header, verified with SHA-256 hashing and the timing-attack-safe hash_equals(), in a fail-closed manner — if the key is invalid, the target method is never called. Each endpoint is isolated in its own try/catch armor; a crashing endpoint only affects itself.
$providedHash = hash('sha256', $providedKey);
foreach ($this->loadAll() as $apiKey) {
if (!$apiKey->active) {
continue;
}
// Timing-attack safe compare — never early-return on length alone.
if (hash_equals($apiKey->hash, $providedHash)) {
return true;
}
}
return false;
Cotonti-style flat-file Hooks/{hook_point}.php files run inside an isolated Closure scope — variable leakage into outer scope is impossible. Symfony-style services annotated with #[CpHook] are collected via HookRegistrationPass. A crashing hook never turns the page into a 500; it is written to the quarantine log.
// Cotonti-style: include inside a bound Closure — no $this leak.
$runner = static function (array $context) use ($file): void {
include $file;
};
try {
$runner($context);
} catch (\Throwable $e) {
$this->quarantineHook($file, $e);
}
// Attribute lane: #[CpHook('blog.render.sidebar')] on a service method
CronManager merges three parallel sources — database cp_cron_jobs, services marked with #[CpCronJob], and flat-file Hooks/cron.{job}.php files — into a single list. Jobs run in isolated subprocesses allowed only for cp:*-prefixed commands via CronCommandWhitelist.
// 1) DB rows in cp_cron_jobs
// 2) #[CpCronJob] attribute methods
// 3) Hooks/cron.{name}.php flat files
foreach ($this->collectJobs() as $job) {
$this->processFactory->runIsolated($job);
}
A second extension point apart from modules: services marked with PluginInterface are collected by PluginRegistry, and activation state is stored in the database via PluginToggleRepository. A module can toggle optional sub-features such as widgets/sidebars independently of the module itself.
The N+1 protection announced as a concept in the whitepaper now runs as a real Doctrine DBAL Middleware chain: QueryCounterConnection, Driver, Statement, and TableParser together keep a per-table query counter and throw MaxQueriesExceededException when the limit is exceeded.
The symfony/messenger package is installed and its status is visible in the AACP System Monitor. The foundation for future email, notification, and bulk-processing queues is already in place; no active transport is wired yet.
A settings infrastructure managed with #[CpSetting] and loaded from the database in a single query only when requested (SettingsRegistry). It stays faithful to the performance budget.
The Safe Mode / Recovery Console promised in the whitepaper roadmap is now real: including a token-based recovery interface that stays up even if the database is completely down, AACP has become a full system management center.
The /aacp/recovery endpoint is intentionally left public (PUBLIC_ACCESS) in security.yaml; it authorizes itself by comparing the AACP_RECOVERY_TOKEN from .env with hash_equals() (timing-safe). It runs no Doctrine queries — it remains operational even when the database is completely down. If the token is empty, the door stays fully closed (fail-safe).
The /aacp/system and /aacp/system/metrics JSON endpoints report load average, memory, OPcache hit-rate, database connection status, and Messenger queue status live in an htop-like fashion. For components that are not installed, it honestly returns "not installed".
/aacp/quarantine — a read-only panel that reads and lists the module_quarantine.log file. /aacp/system/cache-rebuild/* triggers Symfony cache, OPcache reset, and Tailwind asset rebuild operations through three separate AJAX endpoints that share the same CSRF token.
TranslationManager lets you edit core and module messages+intl-icu.{tr,en}.yaml files from the web with an atomic write guarantee (.tmp + rename()). A separate Performance Management screen runs live connection tests for Redis, Memcached, Varnish, and Nginx PageSpeed — the "active" flag is set only when the test succeeds.
Key generation and lifecycle management for the REST Gateway.
A unified list of tasks from DB, attribute, and flat-file sources.
A panel that discovers all hook points and attached listeners.
Active/passive/default language settings — a single source of truth.
The Blog, Media, and Menu modules are reference implementations that exercise every platform extension point (API, Hook, Cron, Plugin, Settings) end to end.
A full blog system built on Node::type='post'. Category/tag management, scheduled publishing (PublishScheduledPostsTask), a GET /api/blog/posts REST endpoint, a sidebar hook, and Schema.org (BlogPosting) JSON-LD generation — all together.
A media library plus image picker admin UI built on AssetManager and an independent Asset entity. SHA-256-based deduplication on a Flysystem storage layer.
WordPress-style drag-and-drop frontend menu management. Menu/MenuItem entities bind to Node with a loose reference (not an FK) for soft-delete compatibility.
The Forum module proves the platform can carry a full community suite — hierarchical boards, thread prefixes, a moderation queue, and a rank/badge system — on the same Node tree and Capability model as the rest of CPalius.
Boards nest to any depth on the same Node tree used for content. Sub-forums, categories, and private sections inherit permissions from their parent while each board keeps its own last-post cache for a single-query index render.
Per-board prefixes such as [Announcement], [Solved], and [Question] with their own colour, permissions, and filters — stored on the topic and rendered into the title and the listing chips.
Members report posts with a reason; reports land in a dedicated queue where moderators move, merge, lock, soft-delete, or dismiss. Every action is a Capability check, and the whole flow is CSRF-guarded.
Post-count and reputation thresholds promote members through configurable ranks; manual badges and a per-user reputation ledger (with reasons) drive the profile card and the activity panel.
The post view splits 20% author card / 80% message body — the Golden Ratio proportion used across CPalius — so long threads stay readable on every viewport without a fixed sidebar width.
Hierarchical boards, thread prefixes, and the moderation queue are production-ready — jump from the showcase into the Forum Engine.
.forum-postbit { display: grid; grid-template-columns: 20% 80%; gap: var(--sp-md); }
.forum-postbit__author { /* rank, badges, reputation */ }
.forum-postbit__body { /* message + reactions */ }
A GD-based service resizes and crops uploaded images to any requested dimension on demand, caches every derivative under public/uploads/cache/, and exposes it to templates through a single Twig filter.
ImageProcessor::thumbnail($source, $width, $height, $mode) generates a crop (cover + centre-crop) or fit (contain, no upscale) derivative, preserves transparency for PNG/GIF/WebP, and is fail-soft — a single image that cannot be processed returns the original URL instead of breaking the page.
Derivatives are written to public/uploads/cache/<w>x<h>-<mode>/ and served directly by the web server on every later request. A cache hit is a single filemtime() comparison against the source; purge() drops every size of one asset when it is replaced.
{# Any Asset, asset id, storage key or /uploads/... URL #}
<img src="{{ asset|cp_thumb(300, 200) }}" alt="{{ asset.originalName }}">
{# 'fit' keeps the whole image inside the box without upscaling #}
<img src="{{ featuredImageUrl|cp_thumb(800, 600, 'fit') }}" alt="">
The cp_thumb Twig filter accepts an Asset, an asset id, a storage key, or a /uploads/... URL and returns the resized derivative URL — for example {{ asset|cp_thumb(300, 200) }} or {{ url|cp_thumb(800, 600, 'fit') }}.
Content rows bind vertically through a translation_group_id UUID; LocaleSwitchService never 404s when a sibling is missing; translation files are written with atomic .tmp + rename() so a power cut cannot corrupt them.
Each content row lives in its own locale and links to siblings through a translation_group_id UUID. At the DB layer, UNIQUE(translation_group_id, locale) enforces at most one row per locale per group — even under concurrent requests.
The language switcher builds the counterpart URL for the current page. If the sibling slug is missing or route generation fails, it falls back to that locale's homepage — never a 404. AACP has no URL prefix; panel locale uses the cp_locale cookie.
TranslationManager writes YAML to a .tmp-* file first, then atomically replaces the target with an OS-level rename(). Even a power cut cannot leave a half-written translation file.
try {
$params = $this->withTranslatedParams($request, $params, $targetLocale);
$params['_locale'] = $targetLocale;
return $this->urlGenerator->generate($route, $params);
} catch (RoutingException|Throwable) {
// Missing sibling or broken route → locale home, never 404.
return $this->homeUrl($targetLocale);
}
$tmpPath = $filePath . '.tmp-' . bin2hex(random_bytes(4)); $this->filesystem->dumpFile($tmpPath, $yaml); // OS-level rename is atomic on the same filesystem. $this->filesystem->rename($tmpPath, $filePath, true);
Security and performance are not a finishing polish added at the end of a project; they are the system's fundamental building blocks.
Dynamically generated capabilities are checked instead of relying on the standard ROLE_ADMIN approach. Role names are never checked anywhere in the code.
denyAccessUnlessGranted('system.module.manage')
Roles = Config (YAML): Travel with Git.
Users = Content (DB): Never exported.
The biggest security risk in multi-tenant (SaaS) systems is a developer forgetting to write WHERE tenant_id = ?.
In CPalius, this possibility has been eliminated at the platform level. Doctrine SQLFilter automatically injects a tenant constraint into every query. No developer intervention is required.
A Doctrine Listener that blocks N+1 query errors — one of the most common causes of database slowness — and triggers in development.
If the query count for the same table exceeds the limit in one HTTP request, MaxQueriesExceededException is thrown. Developers cannot ship until the error is fixed locally.
QueryScopeApplier eliminates PHP-based Voter checks on list screens (and the N+1 memory crisis they cause).
A user's .own or .any capabilities are converted into SQL WHERE conditions on the Doctrine QueryBuilder before the query ever hits the database.
A strict-type DTO (Data Transfer Object) architecture that trusts no HTTP payload from the outside world. Data is autonomously sterilized with Symfony Validator before it reaches controllers.
Malicious manipulation attempts are rejected immediately, protecting core integrity with "Golden Ratio" precision.
An Optimistic Locking backbone that relegates traditional table-lock crises to history. Thousands of concurrent data mutations are isolated with atomic transactions without deadlocks.
Stripped of complexity through the YAGNI philosophy, this infrastructure defers server bottlenecks to pure hardware limits.
Five foundational architectural principles and deep code-level features that form the enterprise backbone of CPalius CMF.
Two-Class Entity and JSON-Relational Hybrid Structure: Completely rejecting the cumbersome EAV (Entity-Attribute-Value) model of traditional CMS platforms, CPalius manages fields in two separate classes. Business records (#[CpResource]) live in strict-typed SQL columns, while content records (Node) store all dynamic data in a single JSON column for flexibility.
Performance Genius (Flat-Field Index Engine): Because searching JSON columns with WHERE conditions is slow, CPalius uses an autonomous Doctrine Event Listener-based flattening engine. Fields marked queryable: true are reflected into the node_field_index table. The ProcessWire-inspired findNodesBySelector API simplifies complex queries.
// Super-fluent Selector API usage
$nodes = $nodeRepository->findNodesBySelector(
'type=post, status=published, is_featured=1, limit=5, sort=createdAt:desc'
);
// Flat-Field engine snippet running in the background
// The dynamic 'is_featured' field from JSON is JOINed at high speed
// via 'value_int' in the index table.
Core Never Dies (Unbreakable Core): A massive isolation layer sits between the user space (cp-content/modules) and the core (cp-core). A faulty third-party module cannot crash the entire application.
Quarantine Armor and Emergency Valve: When a module is activated, lint:container and lint:yaml tests run in an isolated subprocess via symfony/process. At runtime, entry points are armored with try/catch. A crashing module is immediately ignored and written to module_quarantine.log.
// Real-time quarantine isolation in ApiGatewayController
try {
$service = $this->serviceLocator->get($serviceId);
$result = $service->$methodName($request, ...$endpoint['pathParameters']);
return $result instanceof JsonResponse ? $result : new JsonResponse($result);
} catch (\Throwable $e) {
// A crashing module never takes the whole system down with a 500.
// It is isolated, logged, and the Gateway keeps running.
$this->quarantineApiFailure($serviceId, $methodName, $e);
return new JsonResponse(['error' => 'Internal API Error'], 500);
}
Enterprise Multilingual Topology: Language support is part of the core. Each piece of content is produced as a vertical table row (Node) and linked via a translationGroupId UUID. It is physically protected at the DB layer.
Plug-and-Play Translation Compilation: Modules keep their own translations in their own directories. TranslationFileLocator finds them and injects them into the framework.translator.paths array via a Symfony Compiler Pass. Translations are written to disk with an atomic write algorithm.
// Atomic file-write guarantee inside TranslationManager
$dir = dirname($filePath);
if (!is_dir($dir)) {
$this->filesystem->mkdir($dir);
}
// 1. First write safely to a temporary .tmp file
$yaml = Yaml::dump($data, 2, 2);
$tmpPath = $filePath . '.tmp-' . bin2hex(random_bytes(4));
$this->filesystem->dumpFile($tmpPath, $yaml);
// 2. Atomically replace the target via OS-level rename().
// Even if power is cut, the file can never be left half-written.
$this->filesystem->rename($tmpPath, $filePath, true);
Unified Cron Infrastructure: Legacy database crons meet modern architecture. Virtual jobs can be created with the #[CpCronJob] attribute, or via Hooks/cron.*.php files.
Isolated Triggering and System Oversight: CronManager runs jobs as isolated subprocesses (Process) to prevent memory leaks. Administrators can live-monitor jobs from the /aacp/cron cyber-console and trigger them with "Run Now" AJAX signals.
// cp-content/modules/Blog/Cron/PublishScheduledPostsTask.php
final class PublishScheduledPostsTask
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly NodeRepository $nodeRepository,
) {}
// Pure attribute-based cron registration — no DB record required
#[CpCronJob(schedule: '*/5 * * * *', name: 'blog.publish_scheduled')]
public function execute(): string
{
$dueNodes = $this->nodeRepository->findDueScheduledNodes();
if ($dueNodes === []) {
return 'No content due for publication.';
}
foreach ($dueNodes as $node) {
$node->publish($node->getPublishedAt());
}
$this->entityManager->flush();
return sprintf('%d content items published.', count($dueNodes));
}
}
An End to Routing Chaos: Instead of routing files, module developers open endpoints by placing a #[CpApi] attribute on the method. Everything is managed centrally through ApiGatewayController.
Cryptographic Security Backbone: The X-CP-API-KEY header is checked via cp_settings. ApiKeyService performs SHA-256 verification with the timing-attack-safe hash_equals function.
// 1. A clean endpoint inside the module
#[CpApi(path: '/blog/posts', methods: ['GET'], public: false)]
public function getPosts(Request $request): JsonResponse
{
// Business logic...
}
// 2. Fail-closed armor in ApiGatewayController
// On public:false routes, a missing API key means the target
// method is NEVER called.
if (!$endpoint['definition']['public'] && !$this->hasValidApiKey($request)) {
return new JsonResponse(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
}
Of the items promised in the first whitepaper, Safe Mode, the on-demand image pipeline, and the audit log are complete. The following systems will be built in upcoming development sprints.
A mechanism that manages transitions for business records such as invoices, vehicles, and reservations (draft → preparation → sold) through YAML definitions and connects each transition to an automatic audit log. The CpResource::$workflow field is already declared — the next step is wiring the real transition/guard engine.
Shipped. ImageProcessor (GD) resizes/crops on demand and caches under public/uploads/cache/; the cp_thumb Twig filter exposes it to every theme.
Shipped. The cp_audit_logs table and an isolated onFlush/postFlush listener record every create/update/delete of an auditable entity. Marking any entity #[Auditable] switches it on.
symfony/messenger is installed and visible in the AACP System Monitor; the next step is wiring a real transport (Doctrine/Redis) and moving email and notification work onto an asynchronous queue.
CPalius will become a stronger application framework through the power of its community. We look forward to your ideas, feedback, and architectural suggestions.