====== Architecture Overview ====== This page describes how Scriptlog is put together: the order in which the application boots, how a request travels through it, and where the boundaries between layers are supposed to be. Every statement here is traceable to the generated reference pages linked throughout. **Related:** [[scriptlog:nonfunctional:index|Non-functionality]] | [[scriptlog:nonfunctional:boundaries|Layer Boundaries]] | [[scriptlog:nonfunctional:limits|Limits]] | [[scriptlog:nonfunctional:requirements|Requirements]] | [[scriptlog:nonfunctional:negative-space|Negative Space]] | [[scriptlog:index|Contents]] ===== 1. Shape of the code ===== Scriptlog is a PHP 7.4–8.5 blog application with **no framework**. It has its own PSR-4 autoloader, its own service registry, its own router and its own template renderer. Dependencies are wired by hand in `lib/core/Bootstrap.php` and stored in a global `Scriptlog\Core\Registry`. The code is organised into the following layers. ^ Layer ^ Namespace / location ^ Role ^ | 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. | | DAO | `Scriptlog\Dao\` in `lib/dao/` | SQL. Prepared statements in, associative arrays out. | | Model | `Scriptlog\Model\` in `lib/model/` | Domain value objects that express a rule the rest of the app must respect. | | DTO | `Scriptlog\Dto\` in `lib/dto/` | Small data carriers passed between layers. | | Handler | `Scriptlog\Handler\` in `lib/handler/` | Framework handlers: bootstrap, front controller, routing, dispatch, error handling. | | Validator | `Scriptlog\Validator\` in `lib/validator/` | Input validation. Returns errors a form can display. | | Core | `Scriptlog\Core\` in `lib/core/` | Shared infrastructure: base `Dao`, `BaseModel`, `Database`, `Sanitizer`, `Logger`, `AppConfig`, `ApiRouter`, `Bootstrap`, `Registry`, exceptions. | | Utility | `lib/utility/*.php`, global namespace | Flat procedural helper files. No classes, no namespace. | | Admin | `admin/` | The admin UI, mostly as plugin directories. | `lib/utility/` is the one area with **no namespace at all**. Its 224 files define global functions guarded by `defined('SCRIPTLOG') || die(...)`, which is why they cannot be autoloaded and must be required by `lib/utility-loader.php`. Two areas sit outside that stack: * **Theme functions** (`lib/themes//*.php`) are global, procedural and called directly by templates, not through a handler interface. * **Bootstrap files** (`lib/*.php`, project root `*.php`) are the runtime itself: the autoloader, the alias map, options and the front controller. ===== 2. Boot order ===== The order matters, because each step depends on the previous one having 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): index.php ---require---> lib/main.php ---require---> lib/core/Bootstrap.php | ob_start() | options.php + common.php | initialize(APP_ROOT) | page cache | route sniff --> 404, die | AppContext + services | $app->dispatcher | autoload + .env + aliases | dispatcher + security | ->dispatch() | config? no --> install/ | | | scheduled --> publish | | | switch-lang --> redirect | * **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 ===== `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: ^ Order ^ Methods ^ | 1 | `createDatabaseConnection()`, `defineRoutingRules()`, `initializeRegistry()` | | 2 | `createBaseDaos()`, `createSession()`, `createAuthenticator()` | | 3 | `createThemeRenderer()`, `createDownloadChain()`, `createContentDaos()` | | 4 | `storeInRegistry()`, `createFrontService()`, `createDispatcher()` | | 5 | `buildServiceMap()`, `buildHandlerRegistry()`, `buildAdminActionRegistry()` | | 6 | `initializeI18n()` | **Ordering constraint:** `createFrontService()` resolves its DAOs from the `Registry` at construction time, so it must run *after* `storeInRegistry()`. 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()`. ===== 4. Request lifecycle ===== Once `Dispatcher::dispatch()` runs, one configuration value decides the shape of the response: `rewrite_status`. ^ `rewrite_status` ^ Handler ^ Behavior ^ | `'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 first at the top of the SEO branch as well, so a download keeps working regardless of the permalink setting. Every matched route then flows through one pipeline: route match --> validate*() --> handler lookup --> invokeTheme() --> renderTheme() | renderHtmxFragment() 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 ===== `Bootstrap::defineRoutingRules()` returns the complete front-end route table. It is a single source of truth; adding a page type means adding a pattern here. ^ Key ^ Pattern ^ Named captures ^ | `home` | `/` | | | `category` | `/category/(?'category'[\w\-]+)` | `category` | | `archive` | `/archive/[0-9]{2}/[0-9]{4}` | | | `archives` | `/archives` | | | `blog` | `/blog([^/]*)` | | | `page` | `/page/(?'page'[^/]+)` | `page` | | `single` | `/post/(?'id'\d+)/(?'post'[\w\-]+)` | `id`, `post` | | `tag` | `/tag/(?'tag'[\w\- ]+)` | `tag` | | `search` | `/search` | | | `privacy` | `/privacy` | | | `locale` | `/locale` | | | `download` | `/download/(?'identifier'[a-f0-9\-]+)` | `identifier` | | `download_file` | `/download/(?'identifier'[a-f0-9\-]+)/file` | `identifier` | ===== 6. Where the layers do not hold ===== The layers above are the intended shape, not the actual one. The generated reference shows controllers importing DAOs directly, `core` importing `controller` and `handler`, and `handler` importing `core` back. Those cycles are real and are documented with evidence in [[scriptlog:nonfunctional:boundaries|Layer Boundaries]]. The [[scriptlog:nonfunctional:limits|limits]] page lists the boundaries that the code does not enforce, and [[scriptlog:nonfunctional:negative-space|Negative Space]] lists what is present but unreachable.