Support & Example
A tour of the support utilities — Collection, Root and base_path() — followed by a complete end-to-end feature and the framework’s known quirks.
Collection
A lightweight array bag with magic accessors:
use System\Support\Collection;
$bag = Collection::make(["name" => "Lunaris"]);
$bag->set("version", "0.2.0");
$bag->get("name"); // "Lunaris"
$bag->has("version"); // true
$bag->remove("version");
$bag->all(); // ["name" => "Lunaris"]
Magic property access also works: $bag->name, isset($bag->name).
Root & base_path()
The project root path is registered at boot; use base_path() to build root-anchored paths:
base_path(); // <project root>
base_path("config"); // <project root>/config
base_path("views/welcome.php"); // <project root>/views/welcome.php
End-to-end example
A small “products” feature: list products (GET) and create one via a form (POST) with CSRF protection, validation, a flash message and logging.
1. Routes — routes/web.php
<?php
use App\Actions\ListProducts;
use App\Actions\CreateProduct;
use System\Routing\Route;
Route::get("/products", ListProducts::class)->name("products.index");
Route::post("/products", CreateProduct::class)->name("products.store");
2. List action — app/Actions/ListProducts.php
<?php
namespace App\Actions;
use System\Http\Action;
class ListProducts extends Action {
public function middlewares() {
return [];
}
public function main() {
$products = [
["name" => "Espresso Maker", "price" => 100],
["name" => "Ceramic Mug", "price" => 50],
];
return response()
->view("products/index", ["products" => $products])
->render();
}
}
3. Create action — app/Actions/CreateProduct.php
<?php
namespace App\Actions;
use System\Http\Action;
use System\Http\Middlewares\CsrfMiddleware;
use System\Http\Traits\Validateable;
use System\Logs\Log;
use System\Support\Flash;
class CreateProduct extends Action {
use Validateable;
public function middlewares() {
return [CsrfMiddleware::class];
}
public function main() {
$this->vfield("name", $this->input("name"))->required()->min_length(2);
$this->vfield("price", $this->input("price"))->required();
if (!$this->validated()) {
Flash::error($this->errors());
return response()->redirect(route("products.index"));
}
Log::info("Product {name} created", ["name" => $this->input("name")]);
Flash::success(["Product created!"]);
return response()->redirect(route("products.index"));
}
}
4. View — views/products/index.php
<?php $template->extends("layouts/app") ?>
<?php $template->section("content") ?>
<h1>Products</h1>
<?php Flash::render("success", '<p class="flash">@message</p>') ?>
<?php Flash::render("error", '<p class="flash flash-error">@message</p>') ?>
<ul>
<?php foreach ($products as $product): ?>
<li><?= htmlspecialchars($product["name"]) ?> — $<?= $product["price"] ?></li>
<?php endforeach; ?>
</ul>
<h2>Add a product</h2>
<form method="POST" action="<?= route("products.store") ?>">
<?= csrf_field() ?>
<label>Name <input type="text" name="name"></label>
<label>Price <input type="number" name="price"></label>
<button>Create</button>
</form>
<?php $template->endSection(); ?>
That’s the whole request cycle: route → middleware (CsrfMiddleware) → action → validation → flash → redirect → rendered view.
Known quirks & limitations
Intentional or current behaviours that differ from what you might expect from a mature framework — verified against the source:
- Unmatched placeholders remain in URLs —
route($name, $params)doesn’t clean up{param}tokens you didn’t supply. Session::get()/Session::has()returnfalseinstead ofnullfor missing keys.Action::data($key)always wraps the value — a missing key yieldsStr(null), so->int()produces0, notnull.- No global
view()helper — render views throughresponse()->view(...)->render(). - View responses have a limited status API —
status()/header()on the response are ignored; the renderer only accepts the threeErrorenum values. - Middlewares and actions are instantiated via
new— no DI container, no constructor injection. - No database layer yet — MariaDB/MySQL is provisioned in Docker, but the framework ships no query builder/ORM. Atlas is on the way.
- No auth, caching, service providers or asset pipeline — sessions rely on native PHP session handling.
Routeisfinalwith static state — not instance-scoped; a non-issue under per-request PHP processes.- Log error-message typo — “nor found” should read “not found” (cosmetic only).
Contributing
Contributions are welcome — bug fixes, features and docs. The project uses a
three-branch model: develop (active dev) → stage
(pre-production) → main (production); topic branches
(feature/*, fix/*, docs/*) always fork from
develop.
See CONTRIBUTING.md for the full workflow.
License
MIT — see composer.json.
Part of the Lunaris documentation · Source on Codeberg · MIT License