Getting Started

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)
FolderPurpose
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, secure on HTTPS — before session_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.

Custom error pages

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