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