stave.dev

HelloWorld Module Walkthrough

The complete HelloWorld module — routing, views, security attributes, and input handling in one self-contained example.

Beginner

The HelloWorld module ships with Zero Framework as a reference implementation. It demonstrates the most common patterns you will use in every module you build: public endpoints, authenticated endpoints, permission-protected endpoints, variadic arguments, and input sanitization.

Real Source Code

Every code block below is the actual HelloWorld source. Nothing has been simplified or trimmed for the tutorial.

1 The Module Structure

Like every Zero module, HelloWorld follows a predictable directory layout. The controller class name matches the directory name, and assets are organized by type:

modules/ HelloWorld/ HelloWorld.php ← Controller (the file we are reading) view/ index.php ← Default landing page sayHello.php ← Public greeting page secureHello.php ← Auth-required greeting permittedHello.php ← Permission-required greeting assets/ css/ hello-world.css ← Loaded for all endpoints

The framework discovers this module automatically. No routing table, no registration — just a class in the right directory.

2 The Controller

Here is the complete HelloWorld.php controller. Read through it first, then we will break down each section:

<?php
namespace Zero\Module;
use \Zero\Core\Attribute\{AllowedMethods, RequireAuthLevel, RequirePermission};

class HelloWorld extends \Zero\Core\Module
{
    #[AllowedMethods('GET')]
    public function index()
    {
        $this->respond($this->viewPath . "index.php");
    }

    public function sayHello(...$names)
    {
        $this->data = [
            'greeting' => $this->buildGreeting($names)
        ];
        $this->respond($this->viewPath . "sayHello.php");
    }

    #[RequireAuthLevel(1)]
    public function secureHello(...$names)
    {
        $this->data = [
            'greeting' => $this->buildGreeting($names)
        ];
        $this->respond($this->viewPath . "secureHello.php");
    }

    #[RequireAuthLevel(1)]
    #[RequirePermission('hello.world')]
    public function permittedHello(...$names)
    {
        $this->data = [
            'greeting' => $this->buildGreeting($names)
        ];
        $this->respond($this->viewPath . "permittedHello.php");
    }

    private function buildGreeting(array $names): string
    {
        if (empty($names)) { return "hello world!"; }
        $names = array_map(function($name) {
            return htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8');
        }, $names);
        $names = array_filter($names, fn($name) => $name !== '');
        if (empty($names)) { return "hello world!"; }
        $count = count($names);
        if ($count === 1) { return "Hello, {$names[0]}!"; }
        if ($count === 2) { return "Hello, {$names[0]} and {$names[1]}!"; }
        $lastItem = array_pop($names);
        $list = implode(', ', $names);
        return "Hello, {$list}, and {$lastItem}!";
    }
}

Let us walk through each part.

Namespace and Imports

namespace Zero\Module;
use \Zero\Core\Attribute\{AllowedMethods, RequireAuthLevel, RequirePermission};

All top-level modules use the Zero\Module namespace. The three attribute imports give us HTTP method restriction, authentication level checks, and named permission checks. These are applied declaratively on each method.

The index() Method

#[AllowedMethods('GET')]
public function index()
{
    $this->respond($this->viewPath . "index.php");
}

The simplest possible endpoint. #[AllowedMethods('GET')] rejects POST, PUT, and other methods with a 405 Method Not Allowed response. The respond() call renders the view inside the site frame (header, footer, navigation).

Note the use of $this->viewPath — this is set automatically by the framework to point to the module's view/ directory. Always use this property instead of hardcoding paths.

The sayHello() Method

public function sayHello(...$names)
{
    $this->data = [
        'greeting' => $this->buildGreeting($names)
    ];
    $this->respond($this->viewPath . "sayHello.php");
}

This method is completely public — no attributes, so anyone can call it with any HTTP method. The ...$names variadic parameter captures all remaining URL segments as an array. Visiting /hello-world/say-hello/alice/bob passes ['alice', 'bob'] into $names.

Data is passed to the view by setting $this->data. Inside the view, this array is extracted so $greeting becomes available as a local variable.

3 Three Security Levels

The HelloWorld module demonstrates three access tiers, each building on the last:

Method Access Level Attributes
sayHello() Public — anyone can call it None
secureHello() Authenticated — must be logged in #[RequireAuthLevel(1)]
permittedHello() Permitted — must have specific permission #[RequireAuthLevel(1)] + #[RequirePermission('hello.world')]

Authenticated Endpoint

#[RequireAuthLevel(1)]
public function secureHello(...$names)
{
    $this->data = [
        'greeting' => $this->buildGreeting($names)
    ];
    $this->respond($this->viewPath . "secureHello.php");
}

RequireAuthLevel(1) means the user must be logged in with at least auth level 1 (a standard user). Anonymous visitors get a 401 Unauthorized response. The business logic is identical to sayHello() — the only difference is the attribute.

Permission-Protected Endpoint

#[RequireAuthLevel(1)]
#[RequirePermission('hello.world')]
public function permittedHello(...$names)
{
    $this->data = [
        'greeting' => $this->buildGreeting($names)
    ];
    $this->respond($this->viewPath . "permittedHello.php");
}

This endpoint stacks two attributes. The user must be logged in and have the hello.world permission in their user settings. Attributes execute in declaration order, so the auth level is checked first. If both pass, the method runs normally.

Attribute Stacking

You can stack as many attributes as you need. They run top-to-bottom and short-circuit on the first failure. Put the broadest check first (login) and the most specific last (permission).

4 Input Handling

The buildGreeting() private method handles all the input processing. It is a good example of defensive coding in a framework that passes raw URL segments to your methods:

private function buildGreeting(array $names): string
{
    if (empty($names)) { return "hello world!"; }
    $names = array_map(function($name) {
        return htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8');
    }, $names);
    $names = array_filter($names, fn($name) => $name !== '');
    if (empty($names)) { return "hello world!"; }
    $count = count($names);
    if ($count === 1) { return "Hello, {$names[0]}!"; }
    if ($count === 2) { return "Hello, {$names[0]} and {$names[1]}!"; }
    $lastItem = array_pop($names);
    $list = implode(', ', $names);
    return "Hello, {$list}, and {$lastItem}!";
}

Key patterns to note:

  • Empty check first — If no names are provided, fall back to a default greeting
  • Sanitize everything — Every name is run through htmlspecialchars() with ENT_QUOTES and UTF-8 encoding to prevent XSS
  • Filter blanks — After trimming, empty strings are removed so double slashes in URLs don't create ghost entries
  • Natural language formatting — One name, two names, and three-or-more names each get grammatically correct output with Oxford comma handling
Always Sanitize URL Arguments

URL segments arrive as raw strings. They are not automatically sanitized by the framework. Always sanitize before using them in output, database queries, or any other context.

5 The View Files

View files receive data through $this->data, which is extracted into local variables. Here is a typical HelloWorld view:

<h1><?php echo $greeting; ?></h1>

<p>This greeting was generated by the <code>sayHello()</code> endpoint.</p>
<p>Try adding names to the URL: <code>/hello-world/say-hello/your-name</code></p>

The view is intentionally simple. It receives $greeting (from the $this->data array) and renders it. The framework wraps this in the site frame automatically — you don't need to include headers, footers, or navigation in your view files.

The secured views follow the same pattern, with minor differences in their explanatory text:

<h1><?php echo $greeting; ?></h1>

<p>This greeting required authentication (auth level 1+).</p>
<p>You are seeing this because you are logged in.</p>
Dual Responses

If the request sends an Accept: application/json header, the same respond() call returns $this->data as JSON instead of rendering the view. Every endpoint is automatically a dual HTML/JSON endpoint.

6 URL Mapping

Here is how URLs map to method calls for the HelloWorld module:

URL Method Call Access
/hello-world index() GET only
/hello-world/say-hello sayHello() Public
/hello-world/say-hello/alice sayHello('alice') Public
/hello-world/say-hello/alice/bob sayHello('alice', 'bob') Public
/hello-world/secure-hello secureHello() Auth level 1+
/hello-world/secure-hello/alice secureHello('alice') Auth level 1+
/hello-world/permitted-hello permittedHello() Auth 1+ & hello.world
/hello-world/permitted-hello/alice/bob permittedHello('alice', 'bob') Auth 1+ & hello.world

Notice the pattern: kebab-case URLs map to camelCase methods, and each additional URL segment becomes a method argument. The variadic ...$names parameter captures any number of trailing segments.

7 Key Takeaways

The HelloWorld module demonstrates the core patterns you will use in every Zero Framework module:

  1. Convention over configuration — The module directory name, class name, and namespace follow a strict convention. No routing table or registration is needed.
  2. Declarative security — Attributes like #[RequireAuthLevel(1)] and #[RequirePermission('hello.world')] replace boilerplate authorization checks at the top of every method.
  3. Variadic arguments — Use ...$args to capture any number of URL segments. Use named parameters like $id when you expect a specific number.
  4. Always sanitize input — URL arguments are raw strings. Run them through htmlspecialchars() before output and use prepared statements for database queries.
  5. Views are simple — Pass data with $this->data, render with $this->respond(). The framework handles framing, asset loading, and JSON responses.
  6. Progressive security — Start with public endpoints, then add authentication and permissions as needed by stacking attributes.
Next Steps

Now that you understand module basics, try the Counter Component tutorial to learn ShadowComponent, or read the Attributes documentation for the full list of available attributes.