Skip to content

Your first application

Build an HTML home page and a JSON endpoint in the NAF starter. By the end, / will display Hello, World! and /hello/Ada will return a JSON greeting. You will connect a route to a controller, pass data to a template and check both responses through HTTP.

Required packages

In an existing NAF application:

composer require naf/view

Before you begin

Complete the starter installation, then stop its development server with Ctrl+C. Work from the nafphp-demo/ project root. The starter already includes naf/view; you can use it without another package installation.

This exercise replaces the starter's demonstration routes. Each titled code block is the complete contents of that file. Create missing directories and keep the capital C in app/Controllers so Composer can find the class on case-sensitive systems. Add all the files below before starting the server again.

Continuing with an older starter

The tutorial requires naf/framework 0.2.2 or newer and naf/form 0.2.1 or newer. Starters 0.2.2 and 0.2.3 already include compatible dependencies. For an older application, follow the dependency update before continuing. The later form recipes update naf/form to 0.2.3 themselves.

Files and bootstrap

First, establish where the application starts and where its files belong:

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

declare(strict_types=1);

use function Naf\app;

define('BASE_PATH', __DIR__);

require __DIR__ . '/vendor/autoload.php';

// Register application services here, before run() handles the request.
app()->run();
public/index.php
<?php

declare(strict_types=1);

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

declare(strict_types=1);

return [];

public/index.php loads bootstrap.php, which loads Composer and runs NAF. The empty app/config.php replaces the demo configuration; you can add application settings later. If a .env.local exists, NAF loads it instead of .env. Put APP_ENV=dev there too, or move that file aside for this exercise so the environment matches the example.

Register two named routes

With the entry point in place, tell NAF which action should answer each URL. The controller comes in the next step; registering its class name here does not construct it yet.

app/routes.php
<?php

declare(strict_types=1);

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

Now add the class referenced by the routes. Its index() action returns the home page; hello() returns JSON using the name from the URL.

app/Controllers/HomeController.php
<?php

declare(strict_types=1);

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

The controller's render('home', ...) call needs this template. It receives $name from the controller and uses the named hello route for its link to the JSON endpoint.

app/views/home.phtml
<?php

declare(strict_types=1);

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. Import each helper in the file that uses it, including templates, and use s() where a dynamic value enters HTML. PHP templates do not escape values automatically.

Run and verify

The route, controller and template are now connected. From nafphp-demo/, refresh Composer's autoload files and start the server:

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

Open http://127.0.0.1:8000/: the page says Hello, World! and links to the JSON endpoint. It has no styles yet; the template contains only the heading and the link.

An unstyled page with the heading "Hello, World!" and the link "Try the JSON endpoint"

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.

When these checks pass, you have followed a complete request through the application: the route selects an action, the action creates a response and NAF sends it to the client.

If the result is different

What you see What to check
The original welcome page Check that you replaced app/routes.php in the project served by this terminal
A class-not-found error Check App\Controllers, the capital C in app/Controllers, and run composer dump-autoload from the project root
A missing home view Check that app/views/home.phtml exists and that its filename matches render('home', ...)
/hello/Ada returns 404 Confirm the route uses GET and /hello/{name}, then restart the server from this project's root with -t public
The server reports that port 8000 is in use Stop the earlier development server, or choose a free port and use it in the browser and curl commands

For another failure, check the terminal running PHP and logs/app.log if it exists. Troubleshooting explains how to narrow down routing, autoload and bootstrap problems.

Continue building

Choose the next feature you need; you can return to the reference chapters as questions arise.

Use Testing applications to automate verification and Deployment when preparing the application for production. The optional cleanup below can wait until you start adapting the project for your own application.

Remove the starter demo

The tutorial works with the unused demo files still present. When you are ready to remove them, use these steps after checking that your own pages no longer depend on them.

Optional cleanup steps

First replace the demo's routes and bootstrap, as this tutorial does, so they no longer reference WebsiteController or a QuoteService binding. Save any changes you want to keep, then run this from your project root:

rm app/Controllers/WebsiteController.php app/Service/QuoteService.php \
  app/views/welcome.phtml app/views/contact.phtml app/Jobs/SendMailJob.php
composer dump-autoload
Remove-Item app/Controllers/WebsiteController.php, app/Service/QuoteService.php,
  app/views/welcome.phtml, app/views/contact.phtml, app/Jobs/SendMailJob.php
composer dump-autoload

The quotes belong to QuoteService; remove that file only when your own pages no longer use it. SendMailJob is an unused example. The empty app/config.php above removes the demo's quote configuration.

If your starter includes a shared contact-form template or interactive demo script, remove those unused files too:

rm -f app/views/partials/contact-form.phtml public/js/demo.js
Remove-Item app/views/partials/contact-form.phtml, public/js/demo.js -ErrorAction SilentlyContinue

Remove the script's asset()->add() registration from the shared layout if you keep that layout for your own pages. The -f (or -ErrorAction SilentlyContinue) allows this step to work with older starters that do not include these files.

This tutorial's home.phtml is a complete HTML document. If your application also no longer uses the shared demo layout, you can remove app/views/layout.phtml and public/css/naf.css, then delete the unused starter artwork from public/images/. Keep any layouts, styles or images that your own views still reference.

The starter's tests/smoke.py checks its demo routes. Replace it with tests for your own application, or remove it and the test entry in Composer's scripts object. Keep composer.json, composer.lock, vendor/, bootstrap.php, public/index.php and the App\ autoload mapping. The framework and its installed plugins continue to work after the demonstration is gone.

Start with a fresh directory

Create another starter project

To get a new copy of the starter, stop the development server with Ctrl+C and run this from the parent directory, choosing a name that does not already exist:

composer create-project naf/app my-next-app
cd my-next-app
APP_ENV=dev php -S 127.0.0.1:8000 -t public

In PowerShell, set the variable with $env:APP_ENV = "dev" before starting the server.

Your old project stays available while you copy over anything you need. A fresh starter includes its demo again; use the steps above to replace it with your own pages. For an HTTP service that needs no templates or forms, start with the core-only project.