Architecture
Lunaris has no hidden runtime. This page walks through the project layout and follows a single request through every layer of the framework.
Directory structure
lunaris/
├── .env # Environment variables (APP_KEY, APP_NAME, APP_URL)
├── .htaccess # Front-controller rewrite + blocks .env / *.log
├── bootstrap.php # Wires config, env, root path, logs, routes
├── composer.json # PSR-4: System\ → system/, App\ → app/
├── helpers.php # Global helpers placeholder (autoloaded)
├── lunar # CLI entry point (php lunar <command>)
├── app/
│ ├── Actions/ # Actions (no controllers!)
│ │ └── HelloWorld.php
│ └── Middlewares/ # Your custom middlewares (create as needed)
├── config/
│ └── vars.php # Central configuration file
├── docker/
│ ├── Dockerfile # PHP 8.3 + Apache + extensions
│ ├── Dockerfile.mysql # MariaDB 10.11
│ ├── docker-compose.yml
│ └── podman-compose.yml
├── public/ # The web root
│ ├── index.php # Front controller (entry point)
│ └── .htaccess
├── routes/
│ ├── web.php # HTTP routes
│ └── console.php # CLI command registry
├── storage/
│ ├── app/ # General application storage
│ └── logs/ # Daily log files (YYYY-MM-DD.log)
├── system/ # The framework itself (namespace System\)
└── views/ # View templates
├── layouts/ # Layout templates (views/layouts/*.php)
├── errors/ # Optional custom 403/404/500 pages
├── primitives/ # Reusable inline partials (create as needed)
└── components/ # Components with slot support (create as needed)
| Folder | Purpose |
|---|---|
app/ | Application code — one class per route in Actions/, plus your own Middlewares/. |
config/ | vars.php, the config array read by config(). |
public/ | Web root: front controller, rewrite rules, static assets. |
routes/ | web.php for HTTP routes, console.php for the command map. |
storage/ | App files and daily log files. |
system/ | The framework source (System\ namespace): Cli, Exceptions, Http, Logs, Routing, Security, Support, View. |
views/ | Plain-PHP templates, layouts, error pages, primitives, components. |
vendor/ | Composer dependencies (vlucas/phpdotenv, defuse/php-encryption, …). |
Request lifecycle
A request flows through the system like this:
Browser / CLI
│
▼
public/index.php 1. Creates storage/logs, wires PHP error log,
│ starts the session, loads vendor + bootstrap
▼
Bootstrap::configure() 2. Config::init() → Container->loadEnv()
│ → Root::registerRoot() → loadLogs()
▼
Container::loadRoutes() 3. require routes/web.php, then dispatch
│
▼
Route::dispatch() 4. Match REQUEST_METHOD + REQUEST_URI
│ against registered routes (regex, first-match-wins)
▼
Route::executeHandler() 5. new Action → boot() → middleware pipeline
│ → main($params)
▼
Middleware pipeline 6. Each middleware may short-circuit or pass on
│ ($next($request))
▼
Action->main() 7. Return a response / view render / string
│
▼
Echoed to the client 8. Result is output (or rendered by the view engine)
The front controller
public/index.php does the boring-but-important setup before the framework ever runs:
<?php
$logDir = __DIR__ . '/../storage/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$logFile = $logDir . '/' . date('Y-m-d') . '.log';
ini_set("display_errors", 0);
ini_set("log_errors", 1);
ini_set("error_log", $logFile);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
require_once __DIR__ . "/../vendor/autoload.php";
require_once __DIR__ . "/../bootstrap.php";
(new Bootstrap(__DIR__ . "/../"))->configure();
- Error display is off, errors are logged to
storage/logs/Y-m-d.log. - Session cookies are hardened —
httponly,samesite=Lax,secureon HTTPS — beforesession_start(). - Bootstrap::configure() loads config, env, the root path, log drivers, and finally the routes.
The console lifecycle
The lunar script runs configureConsole() instead — the same
boot sequence but it stops before loading routes, so commands never dispatch
HTTP. env(), config() and Log::* are all
available inside commands.
Error handling
Any Throwable thrown during load or dispatch is caught once in
Container::loadRoutes() and passed to Route::handle($e),
which maps it to an HTTP status (404 / 403 / 500) and renders a configured error
page — or prints the raw message if none is configured.
Point errors.notfound, errors.forbidden and errors.exception at views like errors/404. See Logging & Errors.
Part of the Lunaris documentation · Source on Codeberg · MIT License