moving away from gross globals

This commit is contained in:
2026-07-03 21:05:39 -07:00
parent 230476e8e5
commit 7ee47a0b1e
7 changed files with 138 additions and 10 deletions
+66
View File
@@ -0,0 +1,66 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
Novaconium is a PHP 8.1+ micro-framework distributed as a Composer package (`4lt/novaconium`, PSR-4 autoloaded under the `Novaconium\` namespace from `src/`). This repo *is* the framework itself — there is no "app" here to run directly. Consuming projects `composer require` it and copy `skeleton/novaconium/` into their own project to get an `App/` directory (config, controllers, views, routes) plus a `public/` entrypoint.
Because of this, most of the interesting behavior lives in two places:
- `src/` — the framework runtime (bootstrap, router, session, db, etc.)
- `controllers/` + `views/` + `twig/` — the framework's own built-in admin panel ("NOVACONIUM" control panel routes), shipped so consuming apps get login/dashboard/page-editing/messages for free.
`skeleton/` is what gets copied into a new consuming project — treat it as a template, not live framework code.
## Development
There is no local PHP/Composer install expected — Composer, PHP/Apache, and Sass all run in Docker containers (see `docs/docker.md`, `docs/Composer.md`, `docs/Sass.md`). There is no test suite or linter configured in this repo.
Compiling Sass (from repo root):
```bash
docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app sass-container --style=compressed sass/novaconium.sass skeleton/novaconium/public/css/novaconium.css
```
For iterating against a real consuming project without re-running `composer require` on every change, use the fake autoloader dev pattern in `docs/Dev-Fake_autoload.md` (registers a `spl_autoload_register` shim pointing at a manually cloned copy of this repo under the app's `vendor/4lt/novaconium`).
## Request lifecycle
Entry point in a consuming app is `App/public/index.php` (see `skeleton/novaconium/public/index.php`), which:
1. Defines `BASEPATH` (app root) and `FRAMEWORKPATH` (`BASEPATH/vendor/4lt/novaconium`) — these two constants are used everywhere in `src/` to resolve app vs. framework paths, and are assumed to already be defined by the time any framework code runs.
2. Requires Composer's autoloader, then `FRAMEWORKPATH/src/novaconium.php` (the bootstrap).
3. Calls the global `makeitso()` at the very end.
`src/novaconium.php` (the bootstrap) runs in this order and wires up several **globals** that framework code and app controllers rely on implicitly (`$config`, `$log`, `$data`, `$session`, `$messages`, `$db`, `$post`, `$redirect`, `$router`):
1. Loads `App/config.php` (falls back to the skeleton's default config if the app hasn't created one).
2. Loads `src/functions.php` (global helpers: `dd()`, `makeitso()`) and `src/twig.php` (global `view()` helper).
3. Creates `$log` (`Novaconium\Logger`), `$session` (`Novaconium\Session`), `$messages` (`Novaconium\MessageHandler`, seeded from flashed session messages), `$db` (`Novaconium\Database`, only if `config['database']['host']` is set), `$post` (`Novaconium\Post`, only if `$_POST` is non-empty), `$redirect` (`Novaconium\Redirect`).
4. Builds `$router` (`Novaconium\Router`), whose constructor resolves the matched controller file, then immediately `require`s it — so the matched controller file executes inline, still inside bootstrap, with all the globals above already in scope.
Controllers are plain PHP scripts (not classes) that use the ambient globals directly (e.g. `$session->get(...)`, `$messages->error(...)`, `$post->get(...)`, `$db->getRow(...)`) and typically end by calling the global `view($templateName, $data)` or setting `$redirect->url(...)` followed by `makeitso()` to end the request (writes session, closes DB, performs any pending redirect, flashes messages, then `exit`).
## Router (`src/Router.php`)
Full behavioral spec is in `docs/Router.md` — read it before touching routing logic, since several quirks are **intentionally preserved for backward compatibility**, not bugs:
- Only the *first* `{param}` in a route pattern is ever extracted; subsequent params in the same pattern are ignored.
- For parameterized routes, the first route whose static prefix + segment count matches wins, even if that route has no handler for the current HTTP method — the router does not keep searching for a better match.
- No wildcard/optional-segment support; segment counts must match exactly.
Routes are plain associative arrays keyed by path, mapping `get`/`post` to a controller string (see `config/routes.php` for the framework's own admin routes, `skeleton/novaconium/App/routes.php` for the app-level shape). `App/routes.php` routes are merged with (and can override) framework routes. Controller strings prefixed with `NOVACONIUM` resolve against `FRAMEWORKPATH/controllers/`; everything else resolves against `BASEPATH/App/controllers/`. Unresolved routes/files fall back to the app's `404.php`, then the framework's `controllers/404.php`.
## Views (Twig)
`view($name, $data)` (`src/twig.php`) loads templates from `App/views/` by default, with two additional Twig namespaces registered: `@novaconium``FRAMEWORKPATH/twig` (layout partials: `master.html.twig`, `nav.html.twig`, `head.html.twig`, etc.) and `@novacore``FRAMEWORKPATH/views` (the framework's built-in admin pages: dashboard, pages, messages, settings). Apps can override any framework template by placing a same-named file under `App/templates/` (`override` namespace). The framework's own controllers render via the `@novacore/...` namespace (e.g. `view('@novacore/dashboard', $data)`).
## Other framework primitives (all under `src/`, namespace `Novaconium`)
- `Database` — thin `mysqli` wrapper (`query`, `getRow`, `getRows`); always uses prepared statements but binds every parameter as a string type.
- `Session` — thin `$_SESSION` wrapper; auto-initializes `token`/`messages`/`formData`/`errors` keys; `flash()` reads-then-deletes.
- `MessageHandler` — categorized flash messages (`error`/`warning`/`notice`/`success`), persisted across requests via `$session`.
- `Post` — sanitizes `$_POST` via `FILTER_SANITIZE_FULL_SPECIAL_CHARS` on construction; access via `$post->get($key)`.
- `Redirect` — buffers a target URL/status; only the *last* `url()` call before `makeitso()` takes effect; actual `header()` redirect happens inside `makeitso()`.
- `Logger` — level-gated file logger (`DEBUG`/`INFO`/`WARNING`/`ERROR`/`NONE`), configured via `config['logfile']`/`config['loglevel']`.
- `src/Services/` — newer, thinner service classes (`Auth`, `TagManager`) alongside the older top-level `src/` classes; check here first before adding new cross-cutting logic.
## Configuration
App-level config lives in `App/config.php` (see `docs/ConfigurationFile.md` and `skeleton/novaconium/App/config.php` for the shape): `database` (mysqli connection), `base_url`, `secure_key` (used to pepper password hashes via `hash_hmac('sha3-512', ...)`, see `controllers/authenticate.php`), `logfile`, `loglevel`, plus optional `matomo_url`/`matomo_id`/`fonts` passed straight into Twig's global `$data`.
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace Novaconium;
final class Context
{
public function __construct(
public array $config,
public Logger $log,
public Session $session,
public MessageHandler $messages,
public ?Database $db,
public ?Post $post,
public Redirect $redirect,
public Router $router,
public array $data = [],
) {}
/**
* Merge additional values into the Twig data array.
*/
public function withData(array $more): static
{
$this->data = array_merge($this->data, $more);
return $this;
}
}
+5 -5
View File
@@ -25,7 +25,7 @@ class Database {
// Prepare the SQL statement
$stmt = $this->conn->prepare($query);
if (!$stmt) {
throw new Exception("Query preparation failed: " . $this->conn->error);
throw new \Exception("Query preparation failed: " . $this->conn->error);
}
// Bind parameters if needed
@@ -37,7 +37,7 @@ class Database {
// Execute the statement
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Query execution failed: " . $stmt->error);
throw new \Exception("Query execution failed: " . $stmt->error);
}
// Save last insert id if it's an INSERT query
@@ -70,7 +70,7 @@ class Database {
// Prepare the SQL statement
$stmt = $this->conn->prepare($query);
if (!$stmt) {
throw new Exception("Query preparation failed: " . $this->conn->error);
throw new \Exception("Query preparation failed: " . $this->conn->error);
}
// Bind parameters
@@ -82,7 +82,7 @@ class Database {
// Execute the statement
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Query execution failed: " . $stmt->error);
throw new \Exception("Query execution failed: " . $stmt->error);
}
// Get result
@@ -92,7 +92,7 @@ class Database {
$stmt->close();
return $row;
} catch (Exception $e) {
} catch (\Exception $e) {
echo "An error occurred: " . $e->getMessage();
return null;
}
+1 -1
View File
@@ -21,7 +21,7 @@ class MessageHandler {
// Add a message of a specific type
public function addMessage($type, $message) {
if (!isset($this->messages[$type])) {
throw new Exception("Invalid message type: $type");
throw new \Exception("Invalid message type: $type");
}
$this->messages[$type][] = $message;
}
+13 -2
View File
@@ -24,7 +24,18 @@ function dd(...$vars): void {
* connection if configured, and performs a redirect.
*/
function makeitso(): void {
global $session, $db, $redirect, $config, $messages, $log;
global $session, $db, $redirect, $config, $messages, $log, $ctx;
// Prefer $ctx when available; its service properties are the same
// object instances as the bare globals, so this is behavior-neutral.
if (isset($ctx)) {
$session = $ctx->session;
$db = $ctx->db;
$redirect = $ctx->redirect;
$config = $ctx->config;
$messages = $ctx->messages;
$log = $ctx->log;
}
// ------------------------------
// Close database if configured
@@ -44,7 +55,7 @@ function makeitso(): void {
// Set all messages in the session
$session->set('messages', $messages->getAllMessages());
// Write any buffered session data to persistent storage
$session->write();
+18
View File
@@ -7,6 +7,7 @@ use Novaconium\Database;
use Novaconium\Post;
use Novaconium\Redirect;
use Novaconium\Router;
use Novaconium\Context;
$db = null;
$post = null;
@@ -56,4 +57,21 @@ $redirect = new Redirect();
// --- Router ---
$router = new Router();
// --- Typed Context ---
// Bundles the globals above into one typed object. The bare globals are
// left in place (same object instances) so existing plain-script
// controllers keep working unmodified; new controllers can use $ctx.
$ctx = new Context(
config: $config,
log: $log,
session: $session,
messages: $messages,
db: $db,
post: $post,
redirect: $redirect,
router: $router,
data: $data,
);
require_once $router->controllerPath;
+9 -2
View File
@@ -15,13 +15,20 @@ use Twig\Loader\FilesystemLoader;
*/
function view(string $name = '', array $moreData = []): bool
{
global $config, $data;
global $config, $data, $ctx;
if (!is_array($data)) {
$data = [];
}
if (!empty($moreData)) {
// Reconcile the bare $data global with $ctx->data: either style of
// controller ($data = array_merge($data, [...]) or $ctx->withData([...]))
// ends up folded in here, regardless of which one was used.
if (isset($ctx)) {
$data = array_merge($data, $ctx->data, $moreData);
$ctx->data = $data;
$config = $ctx->config;
} elseif (!empty($moreData)) {
$data = array_merge($data, $moreData);
}