Architecture deep-dive
This page traces a request end to end and names the real classes, events and services involved. It complements Concepts: read that first for the vocabulary, read this to understand the mechanics. It's also the page to read if you (or an AI assistant) need a complete mental model of how Melis works.
Bootstrap
public/index.php loads Composer's autoloader, merges config/application.config.php with config/development.config.php (when present), then runs the Laminas MVC application.
config/application.config.php builds the module list dynamically:
'modules' => array_merge(
MelisCore\MelisModuleManager::getModuleComponents(), // framework components first
MelisCore\MelisModuleManager::getModules() // then Melis modules
),
'module_listener_options' => [
'module_paths' => ['./module', './module/MelisSites'],
'config_glob_paths' => [
realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php',
realpath(__DIR__) . '/autoload/platforms/' . getenv('MELIS_PLATFORM') . '.php',
],
],MelisCore\MelisModuleManager (vendor/melisplatform/melis-core/src/MelisModuleManager.php) assembles three kinds of modules depending on the request:
- Components — framework dependencies, declared per module in
config/module.load.php. - Modules — the backoffice modules from
config/melis.module.load.php. - Site modules — for a front-office URL, the site selected by
MELIS_MODULE(frommodule/MelisSites/<name>or a vendor site likeMelisDemoCms).
Finally the platform file config/autoload/platforms/<MELIS_PLATFORM>.php injects the database connection and platform settings into the merged config.
Backoffice request lifecycle
For a /melis… URL, MelisCore drives the flow. Key hooks are attached in MelisCore\Module::onBootstrap():
- Routing — the
melis-backofficeroute (and its children:login,authenticate,logout,zoneview, …) matches. MvcEvent::EVENT_ROUTE→ identity check —Module::checkIdentity()runs. If the matched route isn't in the excluded list (login,authenticate,change-language…) and the user isn't authenticated, it redirects to/melis/login(or returns 404 for non-GET).- Session & language — the session container
meliscoreis initialised; the locale (melis-lang-locale) drivesModule::createTranslations()which loadslanguage/<locale>.{interface,forms,…}.php. EVENT_DISPATCH— the layout is set tolayout/layoutCore, and core listeners run:MelisCoreCheckUserRightsListener(re-reads rights periodically),MelisCoreFlashMessengerListener,MelisCorePhpWarningListener, and others.- Zone rendering — the backoffice UI is a tree of zones;
PluginViewControllerresolves each zone'sforward(module/controller/action) and renders it, assembling the final HTML (see Concepts → zones & forwards).
GET /melis
→ route: melis-backoffice
→ EVENT_ROUTE: checkIdentity() → redirect to /melis/login if not logged in
→ EVENT_DISPATCH: layout = layout/layoutCore; rights/flash/warning listeners
→ PluginViewController renders zones (header, left menu, center, footer) via forwards
→ responseAuthentication & rights
Login is handled by MelisCoreAuth (MelisCoreAuthService), a Laminas authentication service over the melis_core_user table (usr_login / usr_password, bcrypt via password_hash). The authenticated identity — including the user's usr_rights — is stored in the session.
Rights gate the backoffice. MelisCoreRights (MelisCoreRightsService) reads the user's usr_rights — an XML allow-list — to decide what's visible and dispatchable:
- The left-menu sections a user sees are the
*_toolstree_sectionnodes listed in their rights (isAccessible()); an empty rights XML means full access. - A tool the user lacks rights for yields "You don't have access to this tool".
- Rights live on
melis_core_user.usr_rights; for role-based users they can come from the role (melis_core_user_role).MelisCoreCheckUserRightsListenerrefreshes them periodically and logs the user out ifusr_statusbecomes inactive.
Grant a new tool
After adding a tool, grant access via the backoffice Access management, or inject the section into the rights XML with a migration — see flyway/sql/V3__add_melisai_rights.sql.
Front-office request lifecycle
For a public URL, MelisFront + MelisEngine render a CMS page:
- Routing —
melis-frontmatches…/id/{idpage}. SEO URLs (/about-us) are resolved to a page id byMelisFrontSEORouteListener(it queries the page-SEO table and registers a dynamic route at module-load time). - Dispatch — front listeners select the front layout and consult the page cache.
- Page load —
MelisEngine\Service\MelisPageService::getDatasPage($idPage, $type)returns aMelisPage(page-tree data + template), cached undergetDatasPage_{id}_{type}. - Template render — the template's
ZF2controller/action renders the site module's.phtml;MelisTagzones andMelisDragDropZoneplugins are filled from the published content.
GET /about-us
→ MelisFrontSEORouteListener maps /about-us → idpage=5
→ MelisFront\Controller\Index::index(idpage=5)
→ MelisPageService::getDatasPage(5, 'published') (cached)
→ template ZF2 controller/action → site .phtml → MelisTag / MelisDragDropZone
→ responseCaching
Melis caches aggressively through filesystem caches under cache/:
| Cache | Holds |
|---|---|
meliscore_platform_cache-* | rendered backoffice zones / platform config |
meliscms_page-*, melisfront_pages_file_cache-* | rendered CMS pages |
cache/config/ | merged Laminas config (only if config_cache_enabled) |
datasource-*, melistoolcreator-* | module-specific caches |
MelisCoreCacheSystemService is the cache API (getCacheByKey/setCacheByKey/ deleteCacheByPrefix). Caches are invalidated on key events (module changes, page publish, rights updates) and can be cleared manually by deleting the relevant cache/* folders — see Troubleshooting.
Events
Melis is heavily event-driven. Modules attach listeners in Module::onBootstrap() (and via the shared event manager) to the MVC lifecycle (EVENT_ROUTE, EVENT_DISPATCH, EVENT_RENDER, EVENT_FINISH) and to Melis domain events (e.g. melis_core_auth_login_ok, page save events). Domain services extend MelisGeneralService, which adds sendEvent() so any service can publish events other modules subscribe to. This is the primary, decoupled extension mechanism — prefer a listener over patching another module.
The request, in one picture
public/index.php
→ application.config.php (MelisModuleManager assembles modules + platform DB config)
→ Laminas MVC run
├── /melis… → auth (checkIdentity) → rights → PluginViewController zones (forwards)
└── public URL → MelisFront route (id/SEO) → MelisEngine page load → site template
→ caching at every expensive step (MelisCoreCacheSystemService)
→ responseKey files
| Concern | Path |
|---|---|
| App config / bootstrap | config/application.config.php |
| Module assembly | vendor/melisplatform/melis-core/src/MelisModuleManager.php |
| Core bootstrap / listeners | vendor/melisplatform/melis-core/src/Module.php |
| Auth | vendor/melisplatform/melis-core/src/Service/MelisCoreAuthService.php |
| Rights | vendor/melisplatform/melis-core/src/Service/MelisCoreRightsService.php |
| Config tree | vendor/melisplatform/melis-core/src/Service/MelisCoreConfigService.php |
| Zone rendering | vendor/melisplatform/melis-core/src/Controller/PluginViewController.php |
| Cache API | vendor/melisplatform/melis-core/src/Service/MelisCoreCacheSystemService.php |
| Front routing/SEO | vendor/melisplatform/melis-front/src/Listener/MelisFrontSEORouteListener.php |
| Page service | vendor/melisplatform/melis-engine/src/Service/MelisPageService.php |