ECMS9 architecture
Overall layout
app/
├── Config/ # core configuration (Routes, Filters, Events, Autoload,
│ │ # ModuleExtensions, Toolbar, Boot/production.php)
├── Commands/ # core spark commands (cms9:*)
├── Controllers/ # base core controllers (BaseFront, ...)
├── Database/ # core migrations (baseline + incremental)
├── Filters/ # core filters (CSRF, LsCache, AdminDebugToolbar, ...)
├── Helpers/ # helpers (modules, cms, shop_render, widget)
├── Language/ # core language files
├── Libraries/ # core libraries (Template, RenderPoint, Scheduler, JobQueue, updaters)
├── Models/ # CI4 models of the core
├── Modules/ # ← modules: Shop, Admin, NovaPoshta, Search, payments, ...
├── Propel/ # generated legacy ORM models (Base/, Map/, Query)
└── ThirdParty/ # vendored legacy libraries
themes/
├── administrator/ # admin theme (admin panel Twig/JS/CSS)
└── <storefront>/ # storefront themes (default, goodlook, fauna, ...)
public/ # front controller index.php + static assets
writable/ # caches, logs, uploads, update backups,
# store_modules.json, schedule.json
Core
The core is CodeIgniter 4 with extensions:
- Template pipeline (
App\Libraries\Template) — renders storefront pages through Twig themes, with insertion points for modules (analytics, image optimization, etc.). - Events — key business actions fire events that modules subscribe to:
shopMakeOrder— an order was created (storefront or admin);shopAdminOrderEdit— an order was saved in the admin panel;shopAdminOrderUserCreate— a new customer was created in the admin panel;storefront_render_point:<name>— a theme asked for module markup at a render point;post_controller— request-scoped hook (global HTML injection, scheduler tick).
- Filters — CSRF, page cache (
LsCacheFilter), locale, module-enabled gate, the admin-only debug toolbar (Profiler). - Module registries (
Config\ModuleExtensions) — the one place modules plug into core lists without editing core files. Each module ships aConfig/Registrar.phpwhose staticModuleExtensions()returns any of:csrfExempt,lsCacheNever,moduleEnabledAlways,adminMenu,jobHandlers,scheduledTasks,adminTranslations. CI4 registrar auto-discovery merges them at config build. - Internal scheduler (
App\Libraries\Scheduler) — apost_controllerlistener ticks at most once a minute and runs duescheduledTasksas separatesparksubprocesses; state inwritable/schedule.json, admin screen at/admin/scheduler. A site needs no hosting crontab (quiet sites may still point a real cron atspark cms9:schedule-run). - Job queue (
App\Libraries\JobQueue) — persistent jobs resolved bymodule:typekeys registered injobHandlers.
Modules
Every module lives in app/Modules/<Name>/ and is registered in the database
(the components table: enabled, active). Its PSR-4 namespace comes either
from a line in app/Config/Autoload.php (first-party modules) or from
writable/store_modules.json (modules installed from the store, merged at
boot). See Module structure.
Data
Two layers coexist:
- CI4 Query Builder /
CodeIgniter\Model(\Config\Database::connect()) — the rule for all new code: new modules, new files in existing modules. - Propel (legacy) —
App\Propel\*models with generated Query classes (SOrdersQuery::create()->filterById(...)->findOne()); booted on every request and still dense in the Shop admin and exports. Edits inside existing Propel code stay in its style; do not mix both in one method. The direction is to retire Propel step by step, so new code must not add to it.
Schema: the core ships migrations in app/Database/Migrations/ (the baseline
migration is guarded — it skips when the schema already exists and refuses to
roll back a populated shop), so php spark migrate is safe on live sites.
Modules do not ship migrations; they create their own tables idempotently in
an install()/ensureSchema() method (CREATE TABLE IF NOT EXISTS), and
store packages may add Resources/install*.sql which the installer applies
statement by statement, skipping failures.
Themes
Storefront themes are Twig. A theme must not contain hardcoded references to
a specific site; external resources — local only, or official CDNs. Themes
expose module widgets through render_point() calls (see
Render points); spark shop:theme-coverage
reports what a theme surfaces. Themes are not part of the core package, so a
theme fix is deployed per site. The admin theme is shared across
installations.