| Next revision | Previous revision |
| scriptlog:architecture:overview [2026/09/26 09:34] – admin admin | scriptlog:architecture:overview [2026/09/28 02:01] (current) – admin admin |
|---|
| The code is organised into the following layers. | The code is organised into the following layers. |
| |
| | Layer | Namespace / location | Role | | ^ Layer ^ Namespace / location ^ Role ^ |
| |---|---|---| | |
| | Controller | `Scriptlog\Controller\` in `lib/controller/` | HTTP endpoints. Read the request, call one service, render a view. | | | Controller | `Scriptlog\Controller\` in `lib/controller/` | HTTP endpoints. Read the request, call one service, render a view. | |
| | Service | `Scriptlog\Service\` in `lib/service/` | Business logic. Owns the rules that decide what happens. | | | Service | `Scriptlog\Service\` in `lib/service/` | Business logic. Owns the rules that decide what happens. | |
| |
| The order matters, because each step depends on the previous one having | The order matters, because each step depends on the previous one having |
| finished. It is worth reading as a strict pipeline. | finished. Read it as a strict pipeline, top to bottom — the diagram first, |
| | then the nine numbered steps underneath it (this wiki has no numbered-list |
| | syntax, so the steps carry explicit numbers): |
| |
| # **Entry.** A web server is pointed at the project root `index.php`. It starts output buffering, then `require`s `lib/main.php`. | <code> |
| # **Options and constants.** `lib/main.php` includes `lib/options.php` and `lib/common.php` first. These define the path constants and the `define()`d guards that every other file's `Direct access not permitted` check depends on. | index.php ---require---> lib/main.php ---require---> lib/core/Bootstrap.php |
| # **Early route sniff.** Before loading anything heavy, `lib/main.php` matches the request against a small list of known path patterns and query parameters. An unrecognised URL is answered immediately, without booting the framework. | | ob_start() | options.php + common.php | initialize(APP_ROOT) |
| # **Autoloaders.** It requires `lib/Autoloader.php`, then `lib/vendor/autoload.php` if readable, and loads `.env` through Dotenv. | | page cache | route sniff --> 404, die | AppContext + services |
| # **Backward-compatibility aliases.** `lib/autoload-aliases-map.php` is read and registered with `class_alias()` for every global class name that predates the namespace. This is a lazy map: an alias is only created when the old global name is actually touched. | | $app->dispatcher | autoload + .env + aliases | dispatcher + security |
| # **Bootstrap class.** `lib/core/Bootstrap.php` is required. | | ->dispatch() | config? no --> install/ | |
| # **Initialise.** `Bootstrap::initialize(APP_ROOT)` returns an `AppContext`. Loading continues only if a `config.php` exists; otherwise the request is redirected to the installer in `install/`. | | | scheduled --> publish | |
| # **Scheduled posts.** Due `scheduled` posts are promoted to `publish`. The code comments this as "never fails a request", and it runs *before* output is served, so a scheduled post cannot stay hidden behind the page cache. | | | switch-lang --> redirect | |
| # **Language switch and output.** The `switch-lang` query parameter is handled, then `lib/main.php` returns to `index.php`, which hands control to `$app->dispatcher->dispatch()`. | </code> |
| | |
| | * **Step 1 - Entry.** A web server is pointed at the project root `index.php`. It starts output buffering, then `require`s `lib/main.php`. |
| | * **Step 2 - Options and constants.** `lib/main.php` includes `lib/options.php` and `lib/common.php` first. These define the path constants and the `define()`d guards that every other file's `Direct access not permitted` check depends on. |
| | * **Step 3 - Early route sniff.** Before loading anything heavy, `lib/main.php` matches the request against a small list of known path patterns and query parameters. An unrecognised URL is answered with a bare 404 on the spot, without booting the framework. |
| | * **Step 4 - Autoloaders.** It requires `lib/Autoloader.php`, then `lib/vendor/autoload.php` if readable, and loads `.env` through Dotenv. |
| | * **Step 5 - Backward-compatibility aliases.** `lib/autoload-aliases-map.php` is read and registered with `class_alias()` for every global class name that predates the namespace. This is a lazy map: an alias is only created when the old global name is actually touched. |
| | * **Step 6 - Bootstrap class.** `lib/core/Bootstrap.php` is required. |
| | * **Step 7 - Initialise.** `Bootstrap::initialize(APP_ROOT)` returns an `AppContext`. Loading continues only if a `config.php` exists; otherwise the request is redirected to the installer in `install/`. |
| | * **Step 8 - Scheduled posts.** Due `scheduled` posts are promoted to `publish`. The code comments this as "never fails a request", and it runs *before* output is served, so a scheduled post cannot stay hidden behind the page cache. |
| | * **Step 9 - Language switch and output.** The `switch-lang` query parameter is handled, then `lib/main.php` returns to `index.php`, which hands control to `$app->dispatcher->dispatch()`. |
| |
| ===== 3. Wiring ===== | ===== 3. Wiring ===== |
| |
| `Bootstrap` is a static factory made of many small private methods. The order of | `Bootstrap` is a static factory made of many small private methods. `initialize()` runs four stages: `loadConfiguration()`, the `utility-loader.php` require, `initializeServices()`, then `applySecurity()` (which ends in `initializePostSecurity()`). The service graph inside `initializeServices()` runs top to bottom: |
| its own methods is the service graph: | |
| |
| * `loadConfiguration()`, `initializeServices()`, `createDatabaseConnection()` | ^ Order ^ Methods ^ |
| * `initializeRegistry()`, `createBaseDaos()`, `createSession()` | | 1 | `createDatabaseConnection()`, `defineRoutingRules()`, `initializeRegistry()` | |
| * `createAuthenticator()`, `createThemeRenderer()`, `createFrontService()` | | 2 | `createBaseDaos()`, `createSession()`, `createAuthenticator()` | |
| * `createDownloadChain()`, `createContentDaos()`, `storeInRegistry()` | | 3 | `createThemeRenderer()`, `createDownloadChain()`, `createContentDaos()` | |
| * `createDispatcher()`, `buildServiceMap()`, `buildHandlerRegistry()` | | 4 | `storeInRegistry()`, `createFrontService()`, `createDispatcher()` | |
| * `initializeI18n()`, `applySecurity()`, `initializePostSecurity()` | | 5 | `buildServiceMap()`, `buildHandlerRegistry()`, `buildAdminActionRegistry()` | |
| | | 6 | `initializeI18n()` | |
| |
| Everything is then published into the global `Registry` via `Registry::setAll()`, | **Ordering constraint:** `createFrontService()` resolves its DAOs from the `Registry` at construction time, so it must run *after* `storeInRegistry()`. |
| and the service map is bound into a service locator. Controllers reach their | |
| collaborators through that locator rather than through constructors, which is | The graph is composed with plain `new` calls, so each object's collaborators are visible in its constructor signature. Shared instances are additionally published into the global `Registry` (`Registry::setAll()` in `initializeRegistry()`, plus `Registry::set('frontService', ...)` afterwards) for late binding from theme helpers, `HandleRequest` and `front_service()`. |
| why the dependency graph is hard to read from the controller class alone. | |
| |
| ===== 4. Request lifecycle ===== | ===== 4. Request lifecycle ===== |
| |
| Once `Dispatcher::dispatch()` runs, the shape of the response depends on one | Once `Dispatcher::dispatch()` runs, one configuration value decides the shape of the response: `rewrite_status`. |
| configuration value, `rewrite_status`: | |
| |
| * `'yes'` — SEO-friendly URLs. `handleSeoFriendlyUrl()` resolves an optional locale prefix first, strips it, and matches the remainder against the route table. | ^ `rewrite_status` ^ Handler ^ Behavior ^ |
| * anything else — `handleQueryStringUrl()` serves the classic `?pg=`, `?p=`, `?a=`, `?cat=`, `?tag=`, `?search=`, `?q=` form. | | `'yes'` | `handleSeoFriendlyUrl()` | Resolves an optional locale prefix first, strips it, then matches the remainder against the route table. | |
| | | anything else | `handleQueryStringUrl()` | Serves the classic `?pg=`, `?p=`, `?a=`, `?cat=`, `?tag=`, `?search=`, `?q=` form through `HandleRequest::deliverQueryString()`. | |
| |
| The `?download=` case is handled **before** the SEO branch as well, so a | The `?download=` case is handled first at the top of the SEO branch as well, so a |
| download keeps working regardless of the permalink setting. | download keeps working regardless of the permalink setting. |
| |
| A matched route is then validated by a `validate*()` method | Every matched route then flows through one pipeline: |
| (`validateContentExists`, `validateSinglePost`, `validatePage`, | |
| `validateCategory`, `validateArchive`, `validateTag`), the matching theme | <code> |
| handler is looked up, and the response is produced by `invokeTheme()`. | route match --> validate*() --> handler lookup |
| `renderTheme()` serves a full page; `renderHtmxFragment()` serves a partial for | --> invokeTheme() --> renderTheme() | renderHtmxFragment() |
| HTMX requests. Frontend routes are compiled once into `$frontendCompiled`. | </code> |
| | |
| | Validation is one `validate*()` method per page type (`validateContentExists`, `validateSinglePost`, `validatePage`, `validateCategory`, `validateArchive`, `validateTag`). `renderTheme()` serves a full page; `renderHtmxFragment()` serves a partial for HTMX requests. Frontend routes are compiled once into `$frontendCompiled`. |
| |
| ===== 5. The route table ===== | ===== 5. The route table ===== |
| It is a single source of truth; adding a page type means adding a pattern here. | It is a single source of truth; adding a page type means adding a pattern here. |
| |
| | Key | Pattern | Named captures | | ^ Key ^ Pattern ^ Named captures ^ |
| |---|---|---| | |
| | `home` | `/` | | | | `home` | `/` | | |
| | `category` | `/category/(?'category'[\w\-]+)` | `category` | | | `category` | `/category/(?'category'[\w\-]+)` | `category` | |
| | `archive` | `/archive/[0-9]{2}/[0-9]{4}` | | | | `archive` | `/archive/[0-9]{2}/[0-9]{4}` | | |
| | `archives` | `/archives` | | | | `archives` | `/archives` | | |
| | `blog` | `/blog([^/]*)` | | | | `blog` | `<nowiki>/blog([^/]*)</nowiki>` | | |
| | `page` | `/page/(?'page'[^/]+)` | `page` | | | `page` | `<nowiki>/page/(?'page'[^/]+)</nowiki>` | `page` | |
| | `single` | `/post/(?'id'\d+)/(?'post'[\w\-]+)` | `id`, `post` | | | `single` | `/post/(?'id'\d+)/(?'post'[\w\-]+)` | `id`, `post` | |
| | `tag` | `/tag/(?'tag'[\w\- ]+)` | `tag` | | | `tag` | `/tag/(?'tag'[\w\- ]+)` | `tag` | |