Pop PHP
Getting Started

Application Structure

The Skeleton#

./kettle pop:init writes a plain directory tree. This is a web application with a stand-alone console application alongside it — answer yes to both prompts and this is what you get:

BASH
my-app/
├── app/
│   ├── assets/
│   │   ├── css/app.css           # application CSS, if front-end is installed
│   │   └── js/app.js             # application JS, if front-end is installed
│   ├── config/
│   │   ├── app.http.php          # routes + config for the web front controller dispatch
│   │   ├── app.console.php       # routes + config for console dispatch
│   │   └── database.php          # connection credentials, read from $_ENV, if database installed
│   ├── src/
│   │   ├── Application.php       # your Pop\Application subclass — the bootstrap
│   │   ├── Exception.php         # app-scoped exception type
│   │   ├── Console/
│   │   │   ├── Controller/       # AbstractController.php, ConsoleController.php (stand-alone CLI application)
│   │   │   └── Command/          # one-off commands, auto-discovered by ./script/app (stand-alone CLI application)
│   │   │       └── Kettle/       # one-off commands, auto-discovered by ./kettle
│   │   └── Http/
│   │       ├── Controller/       # AbstractController.php, IndexController.php
│   │       └── Event/            # Options.php — the CORS preflight listener
│   └── view/                     # error.phtml, exception.phtml, index.phtml, maintenance.phtml
├── data/                         # writable scratch space (file queues, exports)
├── database/                     # database folder, if database installed 
│   ├── migrations/default/       # one subdirectory per connection, 'default' being the initial database folder
│   ├── seeds/default/
│   └── snapshots/default/
├── public/
│   ├── .htaccess                 # Apache rewrite to index.php
│   └── index.php                 # the HTTP front controller
├── script/
│   └── app                       # the console application script (stand-alone CLI application)
├── vendor/
├── .env                          # real environment values, gitignored
├── .env.example                  # the checked-in template
├── composer.json                 # your namespace lives in autoload.psr-4
└── kettle                        # the CLI helper script

The pop:init prompts shape it. Declining the stand-alone console application drops script/ and app/src/Console/Controller/, leaving Console/Command/Kettle/ behind. A CLI-only application drops public/, app/view/, app/src/Http/ and app/config/app.http.php. Adding a database writes app/config/database.php and a top-level database/ holding migrations/, seeds/ and snapshots/, one subdirectory per connection. Choosing a front-end framework adds app/assets/, package.json and vite.config.js.

Everything above vendor/ is yours. pop:init writes it once in a familiar shape — app/src for classes, app/config for configuration, app/view for templates, public/ as the document root. script/ as the console entry point — and from there you can rename or restructure any of it. The framework locates your classes through Composer's autoloader and reads routes from an array it is handed, so it never learns where either one lives.

app/config#

Config files return arrays of data — routes, credentials, service definitions, response headers. One file per runtime: app.http.php carries the URL route table, app.console.php carries the command route table, and a front controller includes exactly one of them. app/config/database.php reads its values from $_ENV, so credentials stay in .env. See Configuration for what each file holds.

app/src#

Your namespace is rooted here. pop:init writes "App\\": "app/src/" into composer.json and re-runs composer dump-autoload, so a class at app/src/Http/Controller/Users.php is App\Http\Controller\Users with no further registration.

Application.php is the file to read first. It extends Pop\Application and overrides load(), where the database is registered, the HTTP-versus-CLI branch is taken and event listeners are attached. It also carries httpError() and cliError(), which the front controllers call if bootstrap throws — see Applications & Bootstrap.

Below that the tree splits by runtime. Http/ holds the web side: AbstractController with the shared send(), sendJson(), error() and prepareView() helpers, and IndexController extending it. Console/ holds the command side, in two directories that feed two different scripts — see below.

Script and the Console Directories#

Saying yes to the stand-alone console application gives you a second front controller at script/<slug>, named after a kebab-cased slug of your namespace — App becomes script/app, AcmeBlog becomes script/acme-blog. It is executable and runs your commands directly:

BASH
./script/app help
TEXT
    App
    ===

    ./app app:report    This is the app:report command
    ./app help          Show the help screen

It bootstraps the way public/index.php does, from app/config/app.console.php rather than app.http.php, and constructs the same App\Application class — so both runtimes share load(), your services and your event listeners.

The two directories under app/src/Console/ are not variations on one idea — they feed different scripts. The purpose of this is that it allows you the choice to either quickly add commands to the existing kettle script, or build out a more separated, extensive CLI application with its own namespace. Commands can be added to a stand-alone CLI application via command classes (every command gets a class), or via controllers, which act as a "container" class for a group of commands, e.g., UsersController holds list(), create() and delete()

Directory Run by Written by
Console/Command/ ./script/<slug> ./kettle create:command app:sync --app
Console/Command/Kettle/ ./kettle ./kettle create:command app:sync
Console/Controller/ ./script/<slug> ./kettle create:ctrl --cli Reports

Console/Command/Kettle/ is the one every install gets, and commands there appear in ./kettle help next to db:seed and migrate:rollback — which suits a handful of one-offs. Console/Command/ and Console/Controller/ arrive with the stand-alone script and keep your commands on their own help screen, as above, rather than mixed into Kettle's.

Both scripts discover commands the same way, through Pop\Console\CommandRegistry::loadRoutes() scanning the directory, so a command class is available as soon as the file exists. Every class in one directory shares a namespace. Building Console Applications covers writing them, and The Kettle CLI covers the commands Kettle ships with.

app/view#

Plain .phtml templates rendered by Pop\View\View — a file path plus data assigned onto the view object. pop:init writes the four the scaffolded controllers reference: index.phtml for the landing page, error.phtml for the HTML 404, maintenance.phtml for the 503, and exception.phtml, rendered by Application::httpError().

AbstractController declares protected string $viewPath pointing at app/view, and prepareView('index.phtml') resolves against it. Change the property to render from somewhere else. Subdirectories work as you'd expect — ./kettle create:view users/index.phtml writes app/view/users/index.phtml. See Views & Templates.

public#

The web root. Point your web server at public/ — that's what keeps app/, vendor/ and .env outside the document root.

public/index.php is the whole HTTP entry point: include the autoloader, load .env, construct your application with the HTTP config, load(), run(). public/.htaccess sends any request that doesn't match a real file to index.php; nginx wants an equivalent try_files directive, and ./kettle web:serve needs neither.

Where Your Code Goes#

The scaffold stops at controllers and views. The rest grows to fit, and kettle generates each skeleton with the namespace, parent class and file location already right:

Directory Holds Scaffolded by
app/src/Model/ business logic; Pop\Utils\AbstractModel subclasses ./kettle create:model Users
app/src/Model/ data models bound to a table, extending Pop\Db\Model\AbstractDataModel ./kettle create:model --data Orders
app/src/Table/ one Pop\Db\Record subclass per database table create:model --data, or by hand
app/src/Http/Controller/ HTTP controllers, one subdirectory per nested resource ./kettle create:ctrl Users
app/src/Console/Controller/ grouped commands for a stand-alone console script ./kettle create:ctrl --cli Reports
app/src/Console/Command/ one-off commands for ./script/<slug> ./kettle create:command app:report --app
app/src/Console/Command/Kettle/ one-off commands registered with ./kettle ./kettle create:command app:sync
app/src/Module.php a Pop\Module\Module subclass, when the app splits into modules by hand

data/ is writable scratch space that the framework never touches on its own — ./kettle queue:config defaults a file-backed queue to data/queue/<name>. Treat it as disposable and keep regenerable contents out of version control. tests/ has no scaffold: point PHPUnit's <source> at app/src and give the suite its own tests/config/.env.

See Also#