Skip to content

This chapter needs an extra package

composer require naf/view

Your first application

Start with Installation. The skeleton already includes naf/view, so you do not need to run the package command above again. The form recipes use naf/form 0.2.1 or newer; this command also updates older starter installations that still contain 0.2.0:

composer require 'naf/form:^0.2.1'

This tutorial replaces the starter's demonstration routes with an HTML home page and a JSON endpoint. Each titled code block is the complete contents of that file. Create missing directories; keep the capital C in app/Controllers on every operating system.

Files and bootstrap

Your application will use:

nafphp-demo/
├── app/
│   ├── Controllers/HomeController.php
│   ├── views/home.phtml
│   ├── config.php
│   └── routes.php
├── public/index.php
├── vendor/
├── .env
├── bootstrap.php
└── composer.json

The starter's composer.json maps App\ to app/. Composer can therefore load App\Controllers\HomeController from app/Controllers/HomeController.php.

bootstrap.php
<?php

define('BASE_PATH', __DIR__);
require __DIR__ . '/vendor/autoload.php';

use Naf\Core\Event;
use Psr\Http\Message\ResponseInterface;
use function Naf\{app, event, request};

// Register application services here, before run() handles the request.
// naf/framework 0.2.1 redirects carry HTTP/2; PHP's local server needs HTTP/1.x.
event()->listen(Event::RESPONSE_HEADER, static fn(ResponseInterface $response) =>
    $response->withProtocolVersion(request()->getProtocolVersion()));

app()->run();

The response listener keeps the outgoing protocol consistent with the request. In naf/framework 0.2.1, redirect() creates an HTTP/2 response; emitted by php -S this produces an invalid HTTP/1.x status line. The listener makes redirects usable with the documented development server.

public/index.php
<?php

require dirname(__DIR__) . '/bootstrap.php';
.env
APP_ENV=dev
app/config.php
<?php

return [];

If a .env.local exists, NAF loads it instead of .env. Remove that ambiguity for this exercise, or put APP_ENV=dev there too.

Register two named routes

app/routes.php
<?php

use App\Controllers\HomeController;
use function Naf\route;

route()->add('GET', '/', [HomeController::class, 'index'], 'home');
route()->add('GET', '/hello/{name}', [HomeController::class, 'hello'], 'hello');

Every route has a unique name. The {name} placeholder matches the controller argument $name.

Write the controller

app/Controllers/HomeController.php
<?php

namespace App\Controllers;

use Psr\Http\Message\ResponseInterface;
use function Naf\json;
use function Naf\View\render;

final class HomeController
{
    public function index(): ResponseInterface
    {
        return render('home', ['name' => 'World']);
    }

    public function hello(string $name): ResponseInterface
    {
        return json(['hello' => $name]);
    }
}

Render the page

app/views/home.phtml
<?php
use function Naf\route;
use function Naf\View\s;
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>My first NAF application</title>
</head>
<body>
    <h1>Hello, <?= s($name) ?>!</h1>
    <a href="<?= s(route('hello', ['name' => 'Ada'])) ?>">Try the JSON endpoint</a>
</body>
</html>

The s() call escapes text for HTML. PHP templates do not escape values automatically. Functions must be imported in each PHP file that uses them, including templates.

Run and verify

From nafphp-demo/:

composer dump-autoload
php -S 127.0.0.1:8000 -t public

Open http://127.0.0.1:8000/: the page says Hello, World!. In another terminal:

curl -i http://127.0.0.1:8000/hello/Ada

Expect HTTP 200, Content-Type: application/json; charset=UTF-8 and a body containing "hello": "Ada". An unknown path, such as /does-not-exist, should return 404.

These commands run the application through HTTP. Executing php bootstrap.php alone does not serve a page: app()->run() returns immediately under CLI.

Continue building