Pop PHP
Getting Started

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#

BASH
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:

PHPapp/config/app.http.php
<?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:

BASH
./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:

PHPapp/src/Http/Controller/GreetController.php
<?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 a Pop\View\View from a template name resolved against app/view.
  • $this->view holds it, so any property you assign becomes a variable inside the template.
  • send() renders the view, sets Content-Type: text/html and a 200, 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:

PHPapp/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#

BASH
./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.

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 in load()
  • Application Structure — where models, tables and console commands go next
  • Records & the ORM — adding a database, which is the step this page skipped