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:
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:
./script/app help
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#
- Configuration — what belongs in a config file and what belongs in
load() - Applications & Bootstrap — the bootstrap sequence
public/index.phptriggers - Routing — the
routesarray both config files carry - Views & Templates — how
app/viewtemplates are rendered