Layer Boundaries and Dependencies
The intended architecture is described on
Architecture Overview. This page measures
what the imports actually do, and lists every place the two disagree.
1. The measured import graph
Every `use` statement that resolves to a first-party class, counted by the
layer that wrote it and the layer that declares the target. 503 imports across
28 distinct layer pairs.
| from \ to | admin | controller | core | dao | dto | handler | model | service | validator |
| — | — | — | — | — | — | — | — | — | — |
| admin | | | 4 | | | | | | |
| controller | | | 122 | 11 | 8 | | | 31 | 3 |
| core | | 5 | | 15 | | 46 | 5 | 7 | |
| dao | | | 25 | | | | | | |
| handler | | 1 | 48 | | | | | | |
| model | | | 16 | | | | | | |
| service | | | 47 | 38 | | | 2 | | 8 |
| utility | | | 2 | | | | | | |
| validator | | | | | 4 | | | | |
Read it as a graph and five cycles are immediately visible:
core ↔ controller — `controller → core` 122, `core → controller` 5
core ↔ handler — `handler → core` 48, `core → handler` 46
core ↔ dao — `dao → core` 25, `core → dao` 15
core ↔ model — `model → core` 16, `core → model` 5
core ↔ service — `service → core` 47, `core → service` 7
2. Which cycles are real, and which are the composition root
A cycle is only a design problem if the dependency carries *behaviour*. Most of
`core`'s upward reach is concentrated in the two files whose entire job is to
know about every other layer.
| Edge | Imports | Dominant file | Share |
| — | — | — | — |
| `core → handler` | 46 | `lib/core/Bootstrap.php` | 98% |
| `core → dao` | 15 | `lib/core/Bootstrap.php` | 47% |
| `core → service` | 7 | `lib/core/Bootstrap.php` | 43% |
| `core → controller` | 5 | `lib/core/Dispatcher.php` | 60% |
| `core → model` | 5 | `lib/core/AtomWriter.php` | 40% |
| `admin → core` | 4 | `admin/authenticator.php` | 100% |
`core → handler` is not a layering defect. 45 of its 46 imports sit in
`Bootstrap.php`, and `Bootstrap` is the composition root: it exists to
instantiate the handler registry. Reading a cycle through the composition root
is a category error.
Two upward dependencies are *not* explained that way, and are the real findings:
`lib/core/AtomWriter.php → model` (2 imports, `FrontContentModel` and `PostModel`). Atom generation is a `core` class reaching into the domain model. This is a genuine upward edge in a leaf-ish class.
`lib/core/Authentication.php → dao` (2 imports, `UserDao` and `UserTokenDao`). Authentication is a `core` class that queries the data layer directly rather than going through a service, so it carries SQL concerns that belong one layer up.
3. The rules the code states, and whether it keeps them
The DAO layer's own description claims: *“No HTML, no session, no
superglobals.”* Measured against comment-stripped source:
| Layer | `$_GET` | `$_POST` | `$_SERVER` | `$_SESSION` | `$_COOKIE` | `$_FILES` | `$GLOBALS` | Total |
| — | — | — | — | — | — | — | — | — |
| controller | 56 | 398 | 77 | 261 | 3 | 48 | 11 | 854 |
| admin | 149 | 71 | 27 | 26 | 15 | 4 | 0 | 292 |
| utility | 7 | 14 | 107 | 46 | 18 | 2 | 20 | 215 |
| core | 16 | 0 | 94 | 32 | 24 | 0 | 12 | 178 |
| install | 9 | 49 | 41 | 40 | 0 | 0 | 0 | 139 |
| tests | 37 | 57 | 247 | 93 | 48 | 9 | 33 | 524 |
| service | 0 | 48 | 4 | 7 | 10 | 0 | 0 | 69 |
| public | 4 | 0 | 6 | 6 | 3 | 0 | 9 | 28 |
| handler | 0 | 0 | 23 | 0 | 0 | 0 | 1 | 24 |
| dto | 0 | 2 | 0 | 0 | 0 | 3 | 0 | 5 |
| api | 0 | 0 | 4 | 0 | 0 | 0 | 1 | 5 |
| libroot | 6 | 0 | 13 | 1 | 0 | 0 | 0 | 20 |
| root | 4 | 0 | 2 | 0 | 0 | 0 | 0 | 6 |
| dao | 0 | 2 | 0 | 0 | 0 | 0 | 0 | 2 |
| model | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| validator | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
The DAO rule is almost kept. Two `$_POST` mentions across 19 DAO files is the whole violation, and neither is a query input.
`model` and `validator` are completely clean — zero superglobal mentions in either.
The “only controllers read superglobals” claim is badly wrong. The controller layer is the worst offender at 854, but `utility` (215), `core` (178) and `admin` (292) all reach into global state too, and `admin` is not a controller at all.
`service` is the surprise: it reads `$_POST` 48 times while claiming no superglobal access. Business logic that reads request data directly cannot be called from a non-HTTP context — a cron job, a CLI import or a test cannot supply `$_POST`.
Counts are of comment-stripped source, but string literals are still counted, so
treat them as *mentions* rather than proven reads.
4. Other boundaries that are declared but not enforced
The deprecated facade is a soft boundary. `Scriptlog\Core\FrontHelper` delegates to `FrontService` when `Bootstrap` has registered one, and returns `null` or an empty array when it has not. Nothing raises; a missing registration degrades into empty output rather than an error.
The `handler` layer reaches back up once. `lib/handler/DownloadHandler.php` imports `Scriptlog\Controller\DownloadController`, the only `handler → controller` edge in the tree.
`service` depends on `utility` (8 imports) for the export feature: `ExportService` imports `BlogspotExporter`, `GhostExporter` and `ScriptlogExporter`. A service layer importing flat procedural helpers is the boundary most likely to be crossed by a new contributor, because `lib/utility/` is unnamespaced and looks like a neutral dumping ground.
`admin/` is procedural, not namespaced. Its 103 files include `admin/ui/**` templates that declare no classes at all, so the admin UI has no layer boundary to lean on.