Your First Application
One pass through the whole loop: install, scaffold, add a route, write a controller, render a template and
load the result in a browser. The application is a greeting page at /greet/:name.
Create the Project#
composer create-project popphp/framework hello-pop
cd hello-pop
./kettle pop:init
pop:init asks seven questions. The first two name the application; the rest decide how much scaffolding you
get. For this walkthrough:
| Prompt | Answer |
|---|---|
What is the namespace of your app? [App] |
Hello |
What is the name of your app? [Hello] |
accept the default |
Is this a CLI-only application? [Y/N] |
n |
What is the URL of your app? [http://localhost] |
accept the default |
Initialize a stand-alone CLI application? [Y/N] |
n |
Would you like to configure a database? [Y/N] |
n |
Would you like to install a front-end? [Y/N] |
n |
The namespace is the one answer worth thinking about, because it's written into composer.json as
"Hello\\": "app/src/" and every class you write from here on lives under it. pop:init finishes by running
composer dump-autoload, so the mapping is live before you write your first file.
What you have now is a complete, working application: app/config/app.http.php with a route table,
app/src/Application.php, an IndexController, four view templates and public/index.php. It serves a
welcome page already. The rest of this page adds one route to it.
Add a Route#
Routes are data, so they go in the config file rather than in code. Open app/config/app.http.php and add
one entry to the existing get group:
<?php
return [
'routes' => [
'get' => [
'/api[/]' => [
'controller' => 'Hello\Http\Controller\IndexController',
'action' => 'index'
],
'[/]' => [
'controller' => 'Hello\Http\Controller\IndexController',
'action' => 'index'
],
'/greet/:name' => [
'controller' => 'Hello\Http\Controller\GreetController',
'action' => 'index'
],
],
'*' => [
'*' => [
'controller' => 'Hello\Http\Controller\IndexController',
'action' => 'error'
]
]
],
'http_options_headers' => [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Headers' => 'Accept, Authorization, Content-Type',
'Access-Control-Allow-Methods' => 'HEAD, OPTIONS, GET, PUT, POST, PATCH, DELETE',
'Content-Type' => 'application/json'
]
];
:name is a required parameter — the router captures that segment and passes it into the action
positionally. Nesting the route under the get key constrains it to GET, which is what the scaffold already
does for the two routes it wrote. The * block at the bottom is the catch-all, and its position in the file
is cosmetic: the router picks the most specific match regardless of declaration order.
The controller value names a class that does not exist yet. Nothing complains until a request actually
arrives for that path, so the order of the next two steps is up to you.
Write the Controller#
Generate the class rather than typing the namespace out:
./kettle create:ctrl GreetController
That writes app/src/Http/Controller/GreetController.php as an empty subclass of the scaffolded
Hello\Http\Controller\AbstractController. Fill in the action the route names:
<?php
namespace Hello\Http\Controller;
class GreetController extends AbstractController
{
public function index(string $name): void
{
$this->prepareView('greet.phtml');
$this->view->title = 'Hello';
$this->view->name = ucfirst($name);
$this->send();
}
}
Four lines, and three of them come from the parent class rather than from the framework. pop:init wrote
that AbstractController, and it's worth knowing what it gives you, because none of it is magic:
prepareView()builds aPop\View\Viewfrom a template name resolved againstapp/view.$this->viewholds it, so any property you assign becomes a variable inside the template.send()renders the view, setsContent-Type: text/htmland a200, and sends the response.
The parent also declares Pop\Dispatch\HttpTrait, which is what makes $this->request, $this->response
and $this->application() available in any controller extending it.
Route parameters arrive positionally, in pattern order. /greet/:name has one, so index() takes one
argument, and it's always a string — /greet/world calls index('world').
Render a View#
Views are plain PHP templates in app/view, with no template language in between. Create
app/view/greet.phtml:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><?=$title; ?></title>
</head>
<body>
<h1>Hello, <?=$name; ?>!</h1>
</body>
</html>
The two properties the controller assigned — title and name — are in scope as $title and $name. That
is the entire contract between a controller and its template.
./kettle create:view greet.phtml creates the file too, seeded with a copy of the scaffolded welcome
page — reach for it when you want that layout as a starting point.
Run It#
./kettle web:serve
Open http://localhost:8000/greet/world and the page reads Hello, World! — ucfirst() in the controller
capitalizing the segment the router captured. http://localhost:8000/greet/pop renders Hello, Pop!, and
http://localhost:8000/ still serves the welcome page the scaffold wrote.
Write the route as /greet[/:name] and give the parameter a default to make the segment optional, so
/greet on its own renders too. POST /greet/world answers 405 Method Not Allowed with an Allow: GET
header, since the route lives under the get group — see Routing.
Leave the server running while you work. Every request re-executes public/index.php, so an edit to a
route, a controller or a template shows up on the next reload with no restart and no build step.
What to Read Next#
You've touched every layer a Pop application has. Each one goes deeper:
- Routing — optional and collection parameters, named routes, CLI commands, dynamic routing
- Controllers — default actions, dispatch precedence, and what a controller receives
- Views & Templates — includes, inheritance, iteration and template filters
- Configuration —
.env, environments, and what belongs inload() - Application Structure — where models, tables and console commands go next
- Records & the ORM — adding a database, which is the step this page skipped