Operations

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:

  1. Unmatched placeholders remain in URLs — route($name, $params) doesn’t clean up {param} tokens you didn’t supply.
  2. Session::get() / Session::has() return false instead of null for missing keys.
  3. Action::data($key) always wraps the value — a missing key yields Str(null), so ->int() produces 0, not null.
  4. No global view() helper — render views through response()->view(...)->render().
  5. View responses have a limited status API — status()/header() on the response are ignored; the renderer only accepts the three Error enum values.
  6. Middlewares and actions are instantiated via new — no DI container, no constructor injection.
  7. No database layer yet — MariaDB/MySQL is provisioned in Docker, but the framework ships no query builder/ORM. Atlas is on the way.
  8. No auth, caching, service providers or asset pipeline — sessions rely on native PHP session handling.
  9. Route is final with static state — not instance-scoped; a non-issue under per-request PHP processes.
  10. 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