-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started Tutorial
Build a working Razy application from scratch in under 10 minutes. By the end, you'll have a multi-page site with routing, templates, and a JSON API — all running locally.
-
PHP 8.2+ with
ext-zip,ext-curl,ext-json -
Composer installed globally
-
A terminal (PowerShell, Bash, etc.)
# Create a project directory
mkdir my-razy-app
cd my-razy-app
# Install via Composer
composer require rayfunghk/razy
# Build the Razy working environment
php Razy.phar build
This creates the directory structure:
my-razy-app/
→?→ Razy.phar # Framework binary
→?→ config.inc.php # Global configuration
→?→ sites.inc.php # Domain →distributor mapping
→?→ index.php # Web entry point
→?→ shared/
→ →?→ module/ # Shared modules (all distributors)
→?→ sites/ # Per-distributor sites
→?→ data/ # Global data storage
A distributor is a self-contained site/app within Razy. Each distributor has its own modules, routes, and configuration.
# Create a distributor called "mysite"
php Razy.phar init dist mysite
This creates sites/mysite/ with a dist.php configuration file.
Modules are the building blocks of a Razy application. Each module has a controller, routes, templates, and optionally an API.
Create the following directory structure inside sites/mysite/:
sites/mysite/
→?→ app/
→?→ hello/
→?→ module.php
→?→ default/
→?→ package.php
→?→ controller/
→ →?→ hello.php
→ →?→ hello.home.php
→ →?→ hello.about.php
→ →?→ hello.greet.php
→?→ view/
→?→ home.tpl
→?→ about.tpl
Now create each file:
This file declares who the module is. The module_code must match the directory path (app/hello).
<?php
return [
'module_code' => 'app/hello',
'name' => 'Hello Module',
'author' => 'Your Name',
'description' => 'My first Razy module',
'version' => '1.0.0',
];This file defines what the module does at runtime. It sits inside the version folder (default/).
<?php
return [
'module_code' => 'app/hello',
'author' => 'Your Name',
'description' => 'My first Razy module',
'version' => '1.0.0',
];The main controller is an anonymous class extending Controller. The __onInit method is where you register routes, APIs, and event listeners.
<?php
namespace Razy\Module\hello;
use Razy\Agent;
use Razy\Controller;
return new class extends Controller {
/**
* Called when the module initializes.
* Register all routes here.
*/
public function __onInit(Agent $agent): bool
{
// Lazy routes: map URL paths to handler files
// '/' → hello.home.php, 'about' → hello.about.php
$agent->addLazyRoute([
'/' => 'home',
'about' => 'about',
]);
// Regex route: capture a name from the URL
// /hello/greet/Alice → hello.greet.php with $name = 'Alice'
$agent->addRoute('/hello/greet/(:a)', 'greet');
return true;
}
};What's happening here:
-
addLazyRoute()maps simple path segments to handler files. The paths are relative to the module's alias (hello), so'/'becomes/hello/and'about'becomes/hello/about. -
addRoute()uses a regex pattern with the full path from the site root.(:a)captures any non-slash characters and passes them as a function argument.
Route handler files return a Closure. Inside the closure, $this refers to the controller, giving you access to templates, APIs, and other module features.
<?php
use Razy\Controller;
return function (): void {
/** @var Controller $this */
header('Content-Type: text/html; charset=UTF-8');
// Load the 'home' template from view/home.tpl
$source = $this->loadTemplate('home');
// Assign variables to the template
$source->assign([
'title' => 'Welcome to Razy',
'message' => 'Your first module is working!',
]);
echo $source->output();
};<?php
use Razy\Controller;
return function (): void {
/** @var Controller $this */
header('Content-Type: text/html; charset=UTF-8');
$source = $this->loadTemplate('about');
$source->assign([
'title' => 'About',
]);
echo $source->output();
};This handler demonstrates URL parameter capture. The (:a) token from addRoute() is passed as the $name argument.
<?php
return function (string $name): void {
header('Content-Type: application/json');
echo json_encode([
'greeting' => "Hello, {$name}!",
'time' => date('Y-m-d H:i:s'),
], JSON_PRETTY_PRINT);
};Razy templates use {$variable} syntax for variable substitution and <!-- START BLOCK: name --> / <!-- END BLOCK: name --> for repeatable blocks.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{$title}</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 20px; color: #1e293b; }
h1 { color: #2563eb; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
.nav { display: flex; gap: 16px; margin-bottom: 32px; padding: 12px 0; border-bottom: 1px solid #e2e8f0; }
.card { background: #f8fafc; border-radius: 8px; padding: 20px; margin: 16px 0; }
</style>
</head>
<body>
<nav class="nav">
<a href="{$site_url}hello/">Home</a>
<a href="{$site_url}hello/about">About</a>
<a href="{$site_url}hello/greet/World">Greet API</a>
</nav>
<h1>{$title}</h1>
<div class="card">
<p>{$message}</p>
</div>
<h2>Try It</h2>
<ul>
<li><a href="{$site_url}hello/greet/Alice">/hello/greet/Alice</a> →JSON response</li>
<li><a href="{$site_url}hello/greet/Razy">/hello/greet/Razy</a> →Another name</li>
</ul>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{$title}</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 20px; color: #1e293b; }
h1 { color: #2563eb; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
.nav { display: flex; gap: 16px; margin-bottom: 32px; padding: 12px 0; border-bottom: 1px solid #e2e8f0; }
</style>
</head>
<body>
<nav class="nav">
<a href="{$site_url}hello/">Home</a>
<a href="{$site_url}hello/about">About</a>
</nav>
<h1>{$title}</h1>
<p>This is a simple Razy module demonstrating routing and templates.</p>
<p>Built with the <strong>Razy Framework</strong> →a modular PHP framework for multi-site development.</p>
</body>
</html>Edit sites/mysite/dist.php to load your module:
<?php
return [
'dist' => 'mysite',
'global_module' => false,
'autoload_shared' => false,
'greedy' => false,
'modules' => [
'*' => [
'app/hello' => 'default', // Load 'default' version of app/hello
],
],
];Key settings:
| Key | Value | Meaning |
| --- | --- | --- |
| dist | 'mysite' | Distributor identifier |
| global_module | false | Don't auto-load shared modules |
| autoload_shared | false | Don't auto-load from shared/module/ |
| greedy | false | Return 404 for unmatched routes (don't pass to next distributor) |
| modules | [...] | Module list under the '*' (default) tag |
| 'app/hello' => 'default' | — | Load app/hello using the default/ version folder |
Edit sites.inc.php in the project root to map localhost to your distributor:
<?php
return [
'domains' => [
'localhost' => [
'/mysite' => 'mysite',
],
],
'alias' => [],
];This means: when someone visits http://localhost/mysite/..., Razy routes it to the mysite distributor.
You have two options to test:
The runapp command lets you test routes without setting up a web server:
# Start the interactive shell
php Razy.phar runapp mysite
# Inside the shell, type a path to test:
> hello/
# →Outputs the home page HTML
> hello/about
# →Outputs the about page HTML
> hello/greet/Alice
# →{"greeting": "Hello, Alice!", "time": "2026-02-25 12:00:00"}
# Type 'exit' to quit
> exit
# Generate rewrite rules
php Razy.phar rewrite mysite
# Start the PHP development server
php -S localhost:8080 router.php
Then open your browser:
-
http://localhost:8080/mysite/hello/→ Home page -
http://localhost:8080/mysite/hello/about→ About page -
http://localhost:8080/mysite/hello/greet/World→ JSON API
Request: GET /mysite/hello/greet/Alice
│
│
┌────────┴────────┐
│ Application │ →sites.inc.php: /mysite →mysite distributor
└────────┬────────┘
│
┌────────┴────────┐
│ Distributor │ →dist.php: loads app/hello module
│ (mysite) │
└────────┬────────┘
│
┌────────┴────────┐
│ app/hello │ →module.php: identity
│ Controller │ →hello.php: __onInit registers routes
└────────┬────────┘
│
┌────────┴────────┐
│ Route Match │ →addRoute('/hello/greet/(:a)', 'greet')
│ (:a) = "Alice" │
└────────┬────────┘
│
┌────────┴────────┐
│hello.greet.php │ →function(string $name) →JSON
└─────────────────┘
| Problem | Cause | Fix |
| --- | --- | --- |
| "Module not found" | module_code doesn't match directory path | Ensure app/hello matches sites/mysite/app/hello/ |
| Route returns 404 | Missing leading / in addRoute() | Use /hello/greet/(:a) not hello/greet/(:a) |
| Empty page | Handler doesn't echo output | Use echo $source->output() or echo json_encode(...) |
| Template variables blank | Forgot to call assign() | Call $source->assign([...]) before output() |
| Wrong handler file loaded | File naming mismatch | Must be {controller_name}.{handler_name}.php (e.g., hello.home.php) |
— See the Learning Roadmap for a structured path: 30-minute intro — 1-day hands-on — 1-week mastery.
Now that you have a working module, explore these topics to build more complex applications:
| Topic | What You'll Learn |
| --- | --- |
| Module-System | Module lifecycle hooks, dependencies, versioning |
| Routing | Regex routes, HTTP method filtering, shadow routes |
| Template-Engine | Blocks, modifiers, function tags, iteration |
| Database | Query builder, Simple Syntax, migrations |
| Cross-Module-API | Module-to-module communication |
| Event-System | Inter-module events and listeners |
| CLI-Commands | All available CLI commands |
| Collection | Data manipulation with selectors and processors |