moving away from gross globals
This commit is contained in:
@@ -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`.
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user