Scriptlog Docs

Scriptlog Documentation

Code reference for the Scriptlog codebase

User Tools

Site Tools


scriptlog:architecture:overview

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: Non-functionality | Layer Boundaries | Limits | Requirements | Negative Space | 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/<theme>/*.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 Layer Boundaries.

The limits page lists the boundaries that the code does not enforce, and Negative Space lists what is present but unreachable.

scriptlog/architecture/overview.txt · Last modified: by admin