{"id":19319020,"url":"https://github.com/zaxwebs/clara","last_synced_at":"2026-05-16T00:05:01.938Z","repository":{"id":101629615,"uuid":"224827626","full_name":"zaxwebs/clara","owner":"zaxwebs","description":"A custom built MVC PHP 7 framework.","archived":false,"fork":false,"pushed_at":"2020-01-25T03:47:14.000Z","size":422,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-24T05:12:06.232Z","etag":null,"topics":["composer","framework","model-view-controller","mvc","oop","pdo","php","php-di","php7"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zaxwebs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-29T10:03:38.000Z","updated_at":"2021-04-08T03:09:16.000Z","dependencies_parsed_at":"2023-06-06T23:15:42.045Z","dependency_job_id":null,"html_url":"https://github.com/zaxwebs/clara","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zaxwebs/clara","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaxwebs%2Fclara","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaxwebs%2Fclara/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaxwebs%2Fclara/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaxwebs%2Fclara/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zaxwebs","download_url":"https://codeload.github.com/zaxwebs/clara/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaxwebs%2Fclara/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284711515,"owners_count":27050939,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-16T02:00:05.974Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composer","framework","model-view-controller","mvc","oop","pdo","php","php-di","php7"],"created_at":"2024-11-10T01:20:41.165Z","updated_at":"2026-05-16T00:05:01.931Z","avatar_url":"https://github.com/zaxwebs.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"💠 **Clara**\n*A modern MVC framework built with PHP 8*\n\n---\n\n## Philosophy\n\nClara was born from curiosity.\n\nIt started as a personal exploration. Not to compete with established frameworks, but to understand them. To look beneath the abstractions of modern PHP development and study how an MVC framework truly works under the hood.\n\nEvery route, controller call, and model operation is written in plain, readable code you can follow step by step. The aim was to keep the framework simple and understandable.\n\nClara values:\n\n* **Transparency over magic**\n  Behavior should be traceable, readable, and debuggable.\n\n* **Simplicity over cleverness**\n  Straightforward architecture teaches more than hidden automation.\n\n* **Control over convenience**\n  Developers should understand the full request lifecycle.\n\n* **Learning through building**\n  The best way to understand frameworks is to create one.\n\nClara is both a framework and a study artifact. A place to experiment, break things, and refine understanding of modern PHP design.\n\n---\n\n## Why Clara Exists\n\nModern frameworks accelerate development, but they also abstract fundamentals. Clara was built to:\n\n* Deconstruct MVC architecture piece by piece\n* Study routing, dependency flow, and application bootstrapping\n* Explore PHP 8 features in a controlled environment\n* Serve as a lightweight foundation for custom experimentation\n* Provide a reference implementation without enterprise complexity\n\nIt is intentionally minimal.\n\n---\n\n## Requirements\n\n* PHP 8.3+\n* Composer 2+\n\n---\n\n## Installation\n\nInstall a local copy with the instructions below.\n\n### 1. Install LAMP Stack\n\nIt is assumed you already know how to install a LAMP stack.\nLaragon is recommended because it simplifies environment setup. It is portable, isolated, fast, and tailored for PHP development with MySQL.\n\nDownload: https://laragon.org/download/\n\n---\n\n### 2. Install Composer\n\nInstallation guide: https://getcomposer.org/download/\n\n---\n\n### 3. Setup Server\n\n1. Create a dedicated directory for hosting Clara files\n2. Clone or copy Clara into the directory\n3. Run:\n\n```bash\ncomposer install\n```\n\n4. **Point your web server's document root to the `public/` directory** — not the project root.\n\n   In Laragon: Menu → Apache → `sites-enabled/auto.clara.test.conf` → set `DocumentRoot` to `C:/laragon/www/clara/public` and update the `\u003cDirectory\u003e` path to match.\n\n   This prevents direct HTTP access to source code, config files, and the database.\n\n### 4. Run Locally (PHP Built-in Server)\n\nFor quick local testing (without Apache/Nginx), run:\n\n```bash\nphp -S 127.0.0.1:8000 -t public\n```\n\nThen open `http://127.0.0.1:8000` in your browser.\n\n---\n\n## Project Structure\n\n```\n/clara\n  composer.json            ← Dependency declarations and PSR‑4 autoload map\n  /bootstrap\n    app.php                ← Boots the Application (container, routes, facade)\n  /public                  ← Web server document root\n    .htaccess              ← Rewrites all URLs to index.php\n    index.php              ← Entry point: boots the framework\n    favicon.ico\n  /ephermal                ← Runtime data (e.g. SQLite database, not committed)\n  /config\n    app.php                ← Application + database configuration\n    routes.php             ← All route definitions\n  /src\n    /core\n      Application.php      ← Centralizes bootstrapping (container, DB, router)\n      Bootstrap.php        ← Kicks off routing\n      Router.php           ← Matches URLs to controller actions\n      Request.php          ← Reads incoming HTTP data\n      Response.php         ← Sends HTTP responses and renders views\n      Controller.php       ← Base class all controllers extend\n      DB.php               ← PDO database wrapper\n      Route.php            ← Static facade for route registration\n    /app\n      /controllers         ← Your controllers (Home, Todos, _404)\n      /models              ← Your models (Todo)\n      /views               ← PHP view templates\n  /tests                   ← Test suite (not committed)\n  /vendor                  ← Composer‑managed packages (not committed)\n```\n\nOnly the `public/` directory is exposed to the web. Everything above it — `src/`, `ephermal/`, `composer.json` — is inaccessible via HTTP.\n\n---\n\n## Dependencies\n\nClara uses two Composer packages. Understanding what they do is essential to understanding how Clara works.\n\n### PHP‑DI (`php-di/php-di`)\n\n**What it is:** A dependency injection (DI) container for PHP.\n\n**Why Clara needs it:** Clara's core classes depend on each other. For example, `Router` needs `Request`, `Response`, and the `Container` itself. Instead of manually creating and passing these objects everywhere, Clara asks PHP‑DI's `Container` to build them automatically.\n\nWhen you write:\n\n```php\n$container = new Container();\n$router = $container-\u003eget(Router::class);\n```\n\nPHP‑DI reads `Router`'s constructor, sees it requires `Request`, `Response`, and `Container`, creates those first, then injects them into the `Router`. This is called **autowiring** — the container resolves the entire dependency tree for you.\n\nThe same pattern is used by `Application`: it asks the container for `Router` and other dependencies, then coordinates the request lifecycle from one clear place. This means every class gets exactly the collaborators it needs without a single manual `new` call for core services.\n\nAutowiring extends to the application layer too. The `Todos` controller declares `Todo` as a constructor dependency. PHP‑DI sees `Todo` requires `DB`, resolves `DB` first, then injects it into `Todo`, then injects `Todo` into `Todos`. The entire dependency chain is resolved automatically.\n\n**Where to see it:**\n\n* `bootstrap/app.php` + `Application` — container creation and route registration\n* `Router::dispatch()` — `$this-\u003econtainer-\u003eget($controller)` to instantiate controllers\n* `Todos` controller — `Todo` model injected via constructor, which itself receives `DB`\n\n---\n\n### Kint (`kint-php/kint`)\n\n**What it is:** A debugging tool for PHP. It provides `d()` and `dd()` helper functions.\n\n**Why Clara needs it:** During development, calling `d($variable)` displays a rich, interactive dump of any variable directly in the browser. `dd()` does the same but halts execution immediately after. It replaces messy `var_dump()` / `print_r()` calls with something far more readable.\n\n**How to use it:** Call `d()` or `dd()` anywhere in your code:\n\n```php\npublic function index(): void\n{\n    d($this-\u003erequest);   // Dump and continue\n    dd($someData);        // Dump and die\n}\n```\n\n---\n\n### PSR‑4 Autoloading\n\nIn `composer.json`, the autoload section maps the `Clara\\` namespace to the `src/` directory:\n\n```json\n\"autoload\": {\n    \"psr-4\": {\n        \"Clara\\\\\": \"src/\"\n    }\n}\n```\n\nThis means a class like `Clara\\core\\Router` is expected to live at `src/core/Router.php`. Composer generates the autoloader in `vendor/autoload.php`, so any class following this convention is loaded automatically — no manual `require` statements needed for your own classes.\n\n---\n\n## How It Works — The Full Request Lifecycle\n\nThis is the core of Clara. Every HTTP request follows this exact path from browser to screen. Read the files alongside this guide to see each step in the actual code.\n\n### Step 1 · URL Rewriting (`public/.htaccess`)\n\n```apache\nRewriteEngine On\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule ^ index.php [QSA,L]\n```\n\nThe `.htaccess` file lives inside `public/`, which is the web server's document root. Apache's `mod_rewrite` intercepts every incoming request. If the URL does not point to an existing file or directory on disk within `public/`, it silently forwards the request to `public/index.php`. This is called the **front controller pattern** — one file handles all requests regardless of URL.\n\nBecause only `public/` is exposed, requests like `/config/app.php` never reach the filesystem — Apache looks inside `public/` for that path, finds nothing, and routes to `index.php` instead.\n\n---\n\n### Step 2 · Entry Point (`public/index.php`)\n\n```php\ndefine('BASE_PATH', dirname(__DIR__));\n\nrequire_once BASE_PATH . '/vendor/autoload.php';\n\n$app = require BASE_PATH . '/bootstrap/app.php';\n$app-\u003erun();\n```\n\nThis file lives in `public/` and executes top to bottom:\n\n1. **BASE_PATH** — `dirname(__DIR__)` resolves to the project root (one level above `public/`). Every other file uses this constant, so paths are always relative to the project root.\n2. **Autoloader** — Loads Composer's autoloader so all `Clara\\*` classes and vendor packages resolve automatically.\n3. **Application bootstrap** — Loads `bootstrap/app.php`, which boots the container, registers the router in the `Route` facade, and loads route definitions.\n4. **Run** — `$app-\u003erun()` dispatches the current request through the router.\n\n---\n\n### Step 3 · Configuration (`config/app.php`)\n\n```php\nreturn [\n    'app' =\u003e [\n        'name' =\u003e 'Clara',\n    ],\n    'database' =\u003e [\n        'dsn' =\u003e 'sqlite:' . BASE_PATH . '/ephermal/db.sqlite',\n        'username' =\u003e null,\n        'password' =\u003e null,\n        'options' =\u003e [\n            PDO::ATTR_ERRMODE =\u003e PDO::ERRMODE_EXCEPTION,\n            PDO::ATTR_DEFAULT_FETCH_MODE =\u003e PDO::FETCH_ASSOC,\n            PDO::ATTR_EMULATE_PREPARES =\u003e false,\n        ],\n    ],\n];\n```\n\nConfiguration is defined as a returned PHP array, similar to Laravel-style config files. The `dsn` string is what PDO expects, so switching databases only means changing values in this file. For MySQL, it would look like:\n\n```php\nreturn [\n    'database' =\u003e [\n        'dsn' =\u003e 'mysql:host=localhost;dbname=clara;charset=utf8mb4',\n        'username' =\u003e 'root',\n        'password' =\u003e '',\n        'options' =\u003e [],\n    ],\n];\n```\n\nSQLite setup can be handled at bootstrap time by checking whether the DSN starts with `sqlite:` and creating the directory if needed. The path uses `BASE_PATH` instead of a hardcoded `__DIR__` chain.\n\n---\n\n### Step 4 · Registering Routes (`config/routes.php`)\n\n```php\nuse Clara\\core\\Route;\n\nuse Clara\\app\\controllers\\Home;\nuse Clara\\app\\controllers\\Todos;\n\nRoute::get('/', [Home::class, 'index']);\n\nRoute::get('/todos', [Todos::class, 'index']);\nRoute::post('/todos', [Todos::class, 'store']);\nRoute::post('/todos/toggle', [Todos::class, 'toggle']);\nRoute::post('/todos/delete', [Todos::class, 'delete']);\n```\n\nRoutes are registered using the static `Route` facade. The format is:\n\n```\nRoute::{method}(path, [ControllerClass::class, 'actionMethod']);\n```\n\n* **method** — `get` or `post` (matches the HTTP method).\n* **path** — The URL path to match (e.g. `/todos`).\n* **handler** — A class-method pair, e.g. `[Home::class, 'index']`, which points to a concrete controller action.\n\n`Route` is a thin static facade over the `Router` instance. Each call like `Route::get(...)` delegates to `$router-\u003eget(...)` internally. The `Route` class is initialized with the `Router` instance inside `Application` during bootstrap.\n\nRoutes are stored in a simple array inside the `Router`. They are not executed here — just registered for later matching.\n\n---\n\n### Step 5 · Application Bootstrap (`bootstrap/app.php` + `src/core/Application.php`)\n\n`bootstrap/app.php` is the glue between the entry point and the framework:\n\n```php\nreturn Application::boot(BASE_PATH)\n    -\u003ewithRoutes(BASE_PATH . '/config/routes.php');\n```\n\nIt returns a fully configured `Application` instance back to `index.php`, which then calls `$app-\u003erun()`.\n\n`Application` itself centralizes all bootstrapping in a Laravel-like way while staying lean:\n\n```php\nfinal class Application\n{\n    private function __construct(\n        private readonly string $basePath,\n        private readonly array $config,\n    ) {\n        // 1. Auto-create the SQLite directory if the DSN uses sqlite:\n        // 2. Build the DI container with DB bindings from config\n        // 3. Resolve Router from the container\n        // 4. Hand the Router to the Route facade\n    }\n\n    public static function boot(string $basePath): self\n    {\n        return new self($basePath, require $basePath . '/config/app.php');\n    }\n\n    public function withRoutes(string $routesPath): self\n    {\n        require $routesPath;   // Route::get(...) calls register into the Router\n        return $this;\n    }\n\n    public function run(): void\n    {\n        $this-\u003erouter-\u003edispatch();\n    }\n}\n```\n\nThe lifecycle is:\n\n1. **`boot()`** — Loads `config/app.php`, creates the DI container with explicit `DB` bindings (DSN, username, password, options from config), resolves `Router`, and wires the `Route` facade.\n2. **`withRoutes()`** — Requires the routes file, which calls `Route::get(...)` / `Route::post(...)` to register routes into the `Router`.\n3. **`run()`** — Delegates to `$this-\u003erouter-\u003edispatch()` to handle the current request.\n\nThis keeps `public/index.php` minimal and easy to reason about.\n\n---\n\n### Step 6 · Routing (`src/core/Router.php`)\n\nThis is the heart of Clara. When `dispatch()` is called, the following happens:\n\n#### 6a. Read the Request\n\n```php\n$method = $this-\u003erequest-\u003emethod() === 'HEAD' ? 'GET' : $this-\u003erequest-\u003emethod();\n$path = $this-\u003enormalizePath($this-\u003erequest-\u003epath());\n```\n\nThe `Request` object reads `$_SERVER['REQUEST_METHOD']` and `$_SERVER['REQUEST_URI']`, giving the router the HTTP method (`GET`, `POST`) and the clean path (`/todos`). `HEAD` requests are treated as `GET`.\n\n#### 6b. Find a Matching Route\n\n```php\n$match = $this-\u003efindRoute($method, $path);\n```\n\n`findRoute()` loops through the registered `$this-\u003eroutes` array and looks for an entry where both the method and path match. If found, it returns the route array. If not, it returns `null` and a 404 status is set.\n\n#### 6c. Resolve the Handler\n\n```php\n[$controller, $action] = $this-\u003eresolveHandler($match['handler'] ?? self::NOT_FOUND_HANDLER);\n```\n\n`resolveHandler()` normalizes handlers into `[ControllerClass, 'method']` format.\n\n* It accepts explicit class-method arrays (Laravel-style route handlers)\n* It still supports legacy `'Controller@method'` strings for compatibility\n* If no route matched, the fallback `_404@index` is used instead.\n\n#### 6d. Instantiate the Controller\n\n```php\n$invoked = $this-\u003econtainer-\u003eget($controller);\n```\n\nPHP‑DI creates (or retrieves) the controller instance. Since every controller extends the base `Controller` class, the container autowires `Request`, `Response`, and optionally `DB` into the controller's constructor.\n\n#### 6e. Call the Action\n\n```php\n$invoked-\u003e{$action}();\n```\n\nThe target method is called on the controller instance. This is where your application logic runs. If the method doesn't exist on the resolved controller, the router falls back to `_404@index`.\n\n---\n\n### Step 7 · The Request Object (`src/core/Request.php`)\n\n`Request` is a clean wrapper around PHP's superglobals:\n\n| Method | Reads from | Purpose |\n|---|---|---|\n| `get($key)` | `$_GET` | Query string parameters |\n| `post($key)` | `$_POST` | Form body fields |\n| `files($key)` | `$_FILES` | Uploaded files |\n| `session($key)` | `$_SESSION` | Session data |\n| `cookie($key)` | `$_COOKIE` | Cookie values |\n| `server($key)` | `$_SERVER` | Server/environment info |\n| `body()` | `php://input` | Raw request body |\n| `method()` | `$_SERVER['REQUEST_METHOD']` | HTTP method (GET, POST) |\n| `uri()` | `$_SERVER['REQUEST_URI']` | Full URI including query string |\n| `path()` | Parsed from URI | Clean path without query string |\n\nEvery method accepts a `$default` parameter returned when the key is missing. The private `search()` method handles the lookup with `array_key_exists`.\n\n---\n\n### Step 8 · The Base Controller (`src/core/Controller.php`)\n\n```php\nabstract class Controller\n{\n    public function __construct(\n        protected readonly Request $request,\n        protected readonly Response $response,\n        protected readonly ?DB $db = null,\n    ) {}\n}\n```\n\nEvery controller you write extends this class. PHP‑DI injects `Request` and `Response` automatically. `DB` is optional (nullable) — it's injected only if the database is configured and available.\n\nThe base controller provides shorthand methods so your controllers stay clean:\n\n| Method | Delegates to | What it does |\n|---|---|---|\n| `$this-\u003eview(name, data)` | `Response::view()` | Render a view template with data |\n| `$this-\u003esetStatus(code)` | `Response::setStatus()` | Set the HTTP status code |\n| `$this-\u003esetHeader(k, v)` | `Response::setHeader()` | Set a response header |\n| `$this-\u003eget(key)` | `Request::get()` | Read a `$_GET` parameter |\n| `$this-\u003epost(key)` | `Request::post()` | Read a `$_POST` parameter |\n| `$this-\u003esession(key)` | `Request::session()` | Read a session value |\n| `$this-\u003ecookie(key)` | `Request::cookie()` | Read a cookie value |\n\n---\n\n### Step 9 · The Response Object (`src/core/Response.php`)\n\n`Response` handles everything sent back to the browser.\n\n**Setting status and headers:**\n\n```php\n$this-\u003eresponse-\u003esetStatus(404);\n$this-\u003eresponse-\u003esetHeader('Content-Type', 'application/json');\n```\n\nThese are stored internally and sent when `send()` is called. `send()` emits the HTTP status line and all queued headers via PHP's `header()` function.\n\n**Rendering views:**\n\n```php\n$this-\u003eresponse-\u003eview('home.index', ['message' =\u003e 'Hello World']);\n```\n\n1. Calls `send()` to flush status and headers.\n2. Calls `extract($data, EXTR_SKIP)` to turn the `$data` array into local variables. The key `'message'` becomes a `$message` variable.\n3. Uses `require` to load the view file at `BASE_PATH . '/src/app/views/home.index.php'`. Because `extract` ran first, `$message` is available inside that template.\n\nThe dot in the view name (`home.index`) maps directly to a filename: `home.index.php`. The path is resolved via `BASE_PATH` instead of relative `__DIR__` chains.\n\n**Redirecting:**\n\n```php\n$this-\u003eresponse-\u003eredirect('/todos');  // 302 redirect to /todos\n$this-\u003eresponse-\u003eback();              // Redirect to the previous page\n```\n\nBoth methods set a `Location` header, send it, and immediately `exit` to prevent further execution.\n\n---\n\n### Step 10 · Writing a Controller\n\nHere is the `Home` controller as an example:\n\n```php\nclass Home extends Controller\n{\n    public function index(): void\n    {\n        $this-\u003eview('home.index', [\n            'message' =\u003e 'Hello World',\n        ]);\n    }\n}\n```\n\n1. Extends `Controller`, so `$this-\u003erequest`, `$this-\u003eresponse`, and `$this-\u003edb` are available.\n2. Defines an `index()` method, matching the `Home@index` handler registered in `routes.php`.\n3. Calls `$this-\u003eview()` to render `src/app/views/home.index.php`, passing `$message = 'Hello World'` into the template.\n\n---\n\n### Step 11 · Writing a Model (`src/app/models/Todo.php`)\n\nModels handle data access. They receive the `DB` instance via constructor injection — no manual connection setup:\n\n```php\nclass Todo\n{\n    public function __construct(private readonly DB $db)\n    {\n        $this-\u003emigrate();\n    }\n\n    private function migrate(): void\n    {\n        $this-\u003edb-\u003eexec('CREATE TABLE IF NOT EXISTS todos (...)');\n    }\n\n    public function all(): array\n    {\n        return $this-\u003edb-\u003erun('SELECT * FROM todos ORDER BY created_at DESC')-\u003efetchAll();\n    }\n\n    public function create(string $title): void\n    {\n        $this-\u003edb-\u003erun('INSERT INTO todos (title) VALUES (:title)', ['title' =\u003e $title]);\n    }\n\n    // toggleComplete() and delete() follow the same pattern\n}\n```\n\nModels are plain classes. There is no base `Model` class. The `DB` dependency is injected by PHP‑DI using values from `config/app.php`. Models use `$this-\u003edb-\u003erun()` for all queries — the same `run()` helper that handles both simple queries and parameterized statements.\n\nThe `ephermal/` directory (used for SQLite storage) sits outside `src/` to separate runtime data from source code. It is listed in `.gitignore` so the database file is never committed. `public/index.php` can auto-create this directory when the DSN uses SQLite.\n\n---\n\n### Step 12 · Writing a View\n\nViews are plain PHP files that output HTML. Data passed via `$this-\u003eview(name, data)` is available as local variables:\n\n```php\n\u003c!-- src/app/views/home.index.php --\u003e\n\u003ch1\u003e\u003c?= htmlspecialchars($message) ?\u003e\u003c/h1\u003e\n\u003cp\u003eWelcome to Clara.\u003c/p\u003e\n```\n\n* Use `\u003c?= ?\u003e` for echoing and `\u003c?php ?\u003e` for logic.\n* Always escape output with `htmlspecialchars()` to prevent XSS.\n* The view naming convention is `controller.action.php` (e.g. `home.index.php`, `todos.index.php`).\n\n---\n\n### Step 13 · 404 Handling\n\nIf no route matches the request, Clara falls back to the `_404` controller:\n\n```php\nclass _404 extends Controller\n{\n    public function index(): void\n    {\n        $this-\u003esetStatus(404);\n        $this-\u003eview('_404.index');\n    }\n}\n```\n\nThe `Router` triggers this automatically in two cases:\n\n1. No route matched the requested method + path combination.\n2. A route matched, but the specified action method does not exist on the controller.\n\n---\n\n### Step 14 · The DB Wrapper (`src/core/DB.php`)\n\n```php\nclass DB extends PDO\n{\n    public function __construct(string $dsn, ?string $username = null, ?string $password = null, array $options = [])\n    {\n        parent::__construct($dsn, $username, $password, $options);\n    }\n\n    public function run(string $sql, array $args = []): PDOStatement|false { ... }\n}\n```\n\n`DB` now receives PDO connection parameters directly in its constructor (`dsn`, `username`, `password`, `options`) and passes them to `PDO`. It works with any PDO-supported driver — SQLite, MySQL, PostgreSQL — controlled entirely by config values.\n\n`DB` extends PHP's native `PDO` class and accepts whichever PDO options you provide in config. A common default set is:\n\n* **Exception mode** — Errors throw exceptions instead of silent failures.\n* **Associative fetch** — Query results return associative arrays by default.\n* **Real prepared statements** — Emulated prepares are disabled for security.\n\nThe `run()` method simplifies queries:\n\n```php\n// Simple query (no parameters)\n$this-\u003edb-\u003erun('SELECT * FROM users');\n\n// Parameterized query (safe from SQL injection)\n$this-\u003edb-\u003erun('SELECT * FROM users WHERE id = :id', ['id' =\u003e 1]);\n```\n\nIf you pass parameters, it uses `prepare()` + `execute()`. If not, it uses `query()` directly.\n\n---\n\n## Putting It All Together\n\nHere is the complete lifecycle for a `GET /todos` request:\n\n```\nBrowser → GET /todos\n  ↓\n.htaccess → No file called \"todos\" exists → forward to index.php\n  ↓\nindex.php → Define BASE_PATH → load autoloader\n         → require bootstrap/app.php\n  ↓\nApplication::boot() → Load config/app.php\n                    → Build DI container with DB bindings\n                    → Resolve Router, wire Route facade\n  ↓\nApplication::withRoutes() → Require config/routes.php → routes registered\n  ↓\nApplication::run() → $router-\u003edispatch()\n  ↓\nRouter → Request says method=GET, path=/todos\n       → findRoute() matches: {method: GET, path: /todos, handler: [Todos::class, 'index']}\n       → resolveHandler(...) keeps the class/method pair\n       → $container-\u003eget(Todos::class) → autowires Request, Response, DB, and Todo\n       → $invoked-\u003eindex()\n  ↓\nTodos::index() → $this-\u003etodo-\u003eall() fetches rows via injected DB\n              → $this-\u003eview('todos.index', ['todos' =\u003e $rows])\n  ↓\nResponse::view() → send() emits HTTP 200 + headers\n               → extract() turns ['todos' =\u003e $rows] into $todos variable\n               → require('src/app/views/todos.index.php')\n  ↓\nView → Renders HTML using $todos → sent to browser\n```\n\n---\n\n## Usage\n\n* Configuration files: `config/`\n* Core framework files: `src/core/`\n* Controllers: `src/app/controllers/`\n* Models: `src/app/models/`\n* Views: `src/app/views/`\n\nClara follows a traditional MVC separation while keeping the internal flow explicit and easy to trace.\n\n---\n\nClara is not about scale.\nIt is about understanding.\n\nNot about abstraction layers.\nAbout seeing the layers that already exist.\n\nBuild with it. Break it. Learn from it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzaxwebs%2Fclara","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzaxwebs%2Fclara","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzaxwebs%2Fclara/lists"}