This page walks you through creating a working page from scratch — a controller, a route, and a view. By the end you will have a URL that renders HTML in the browser.
This assumes you have already completed installation. If not, see Installation & Setup.
Every page in PHP-SF requires three things:
#[Route] attribute — declares the URL and HTTP methodThe framework scans your controller directories on first request, registers all routes, and caches the result in Redis.
Create a new file in App/Http/Controller/:
// App/Http/Controller/HelloController.php
namespace App\Http\Controller;
use PHP_SF\System\Attributes\Route;
use PHP_SF\System\Classes\Abstracts\AbstractController;
use PHP_SF\System\Core\Response;
final class HelloController extends AbstractController
{
#[Route( url: 'hello', httpMethod: 'GET' )]
public function hello_page(): Response
{
return $this->render( hello_page::class );
}
}
The method name (hello_page) becomes the route name by default — you use this name in routeLink() and redirectTo().
Create a new file in templates/:
// templates/hello_page.php
namespace App\View;
use PHP_SF\System\Classes\Abstracts\AbstractView;
// @formatter:off
final class hello_page extends AbstractView { public function show(): void { ?>
<!--@formatter:on-->
<h1>Hello, world!</h1>
<p>This is my first PHP-SF page.</p>
<!--@formatter:off-->
<?php } }
The class name must match the filename (without .php) and must be lowercase with underscores. The // @formatter:off / <!--@formatter:on--> comments are PhpStorm formatter directives — they prevent the IDE from reformatting mixed PHP/HTML content.
Navigate to https://127.0.0.1:7000/hello. You should see "Hello, world!" rendered in the browser.
If you see a 404, clear the route cache:
php bin/console app:cache:clear
Routes are cached in Redis on first request. In DEV_MODE = true (the default for development) the cache is cleared automatically on every request, so a 404 after creating a new controller is usually a sign that DEV_MODE is set to false.
Controllers pass data to views as an associative array. Each key becomes a property on the view instance:
#[Route( url: 'hello/{name}', httpMethod: 'GET' )]
public function hello_page( string $name ): Response
{
return $this->render( hello_page::class, [
'name' => $name,
] );
}
Access it in the view via $this->propertyName:
// templates/hello_page.php
namespace App\View;
use PHP_SF\System\Classes\Abstracts\AbstractView;
/**
* @property string $name
*/
// @formatter:off
final class hello_page extends AbstractView { public function show(): void { ?>
<!--@formatter:on-->
<h1>Hello, <?= htmlspecialchars( $this->name ) ?>!</h1>
<!--@formatter:off-->
<?php } }
The @property docblock is optional but recommended — it gives your IDE accurate type information and autocompletion for $this->name.
Navigate to /hello/Alice and the page renders "Hello, Alice!".
Pass a custom page title as the third argument to render():
return $this->render( hello_page::class, [ 'name' => $name ], 'Welcome' );
If no title is provided, APPLICATION_NAME from config/constants.php is used as the default. The title is available inside the view via $this->pageTitle and is set in the default header template automatically.
You do not call header and footer from inside a view. And you don't have to write it in every view. Instead, The framework renders them automatically around every view's show() output. The default templates are configured once in public/index.php:
$kernel = ( new PHP_SF\Kernel )
->setHeaderTemplateClassName( header::class )
->setFooterTemplateClassName( footer::class );
Every page rendered through the framework will use these defaults. Your view only needs to output its own body content.
For pages that need a different layout, change the header or footer from within middleware using changeHeaderTemplateClassName() / changeFooterTemplateClassName(). Two common approaches:
Use the built-in blank middleware — swaps both header and footer for an empty view, giving your show() full control over the entire HTML output. Use this for standalone pages that manage their own <html> structure:
use App\Http\Middleware\blank;
#[Route( url: 'hello', httpMethod: 'GET', middleware: blank::class )]
public function hello_page(): Response
{
return $this->render( hello_page::class );
}
Create a custom middleware — when you need a specific header or footer (e.g. a different navigation for an admin area), create your own middleware and assign whatever view classes you need:
final class admin extends Middleware
{
protected function result(): bool
{
$this->changeHeaderTemplateClassName( admin_header::class );
$this->changeFooterTemplateClassName( admin_footer::class );
return true;
}
}
Then assign it to the route the same way as any other middleware. See Middleware for the full middleware API.
Class name doesn't match filename — the view class name must exactly match the filename without .php. hello_page.php must contain class hello_page.
Forgetting htmlspecialchars() — always escape user-supplied data before outputting it in HTML. $this->name coming from a URL parameter is user input.
Route not found after adding a controller — the route list is cached in Redis. In DEV_MODE = true this cache is rebuilt on every request. In DEV_MODE = false you must clear the cache manually after adding or changing routes.
Accessing an undefined view property — AbstractView::__get() throws an E_USER_ERROR for any property not set by the controller. If you access $this->name but the controller didn't pass name in the data array, you get a fatal error with the exact property name. Add a @property docblock and check the controller call.