Pop PHP
The Basics

Applications & Bootstrap

A Pop application is one object. Pop\Application owns the router, the service locator, the event manager, the middleware manager and the module manager, and the front controller does nothing but construct it, load it and run it. The split between those three steps is the thing worth understanding: the constructor wires up whatever the config array declares, load() is the hook where your own bootstrap goes, and run() matches a route and dispatches it.

The Front Controller#

public/index.php is the only PHP file a web server ever executes. Everything else is reached from here.

PHPpublic/index.php
<?php

$autoloader = include __DIR__ . '/../vendor/autoload.php';

$dotEnv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
$dotEnv->safeLoad();

try {
    $app = new App\Application($autoloader, include __DIR__ . '/../app/config/app.http.php');
    $app->load();
    $app->run();
} catch (\Exception $exception) {
    $app = new App\Application();
    $app->httpError($exception);
}

include on Composer's autoloader returns the ClassLoader object, which is why it is captured rather than discarded — handing it to the application lets a config array carrying prefix and src register a namespace with it, which is how Modules get autoloaded. The dotenv call populates $_ENV before any config file reads it.

Then the three application calls, each doing something distinct:

Call What it does
new App\Application(...) bootstraps from the config array — routes, services, events, middleware
$app->load() your bootstrap: connections opened, listeners attached, runtime branch taken
$app->run() matches the request against the route table and dispatches it

Nothing in the framework calls httpError(). It's a method on your own application class, invoked by the catch block above, and the same shape as initDb() — a convention the scaffold writes rather than something Pop\Application declares.

What the Constructor Does#

The constructor takes its arguments by type rather than by position, so any of them can be omitted and the rest given in any order. An autoloader, a config array or ArrayAccess object, and any of the five managers are all recognized:

PHP
$app = new Pop\Application(
    new Pop\Router\Router(null, new Pop\Router\Match\Http()),
    new Pop\Service\Locator(),
    ['name' => 'my-app', 'version' => '1.0.0']
);

Bootstrap then runs immediately, in this order:

  1. Registers the autoloader, if one was passed.
  2. Instantiates every manager you did not supply — router, service locator, event manager, middleware manager, module manager. All five always exist afterward.
  3. Registers the config's prefix and src with the autoloader, as PSR-4 unless psr-0 is set.
  4. Applies the config's name and version.
  5. Loads the pop-utils helper functions, unless helper_functions is false.
  6. Hands routes to the router, services to the locator, events to the event manager and middleware to the middleware manager.
  7. Registers itself with Pop\App, the static accessor the rest of the application reaches it through.

Every one of those config keys is covered in Configuration. What matters here is that all of it happens inside new, before a single line of your code runs — so an application constructed with a complete config array is already routable, with no imperative setup at all.

Pop\App::get() returns the most recently constructed application, so in a process that builds more than one — a test suite, a long-running worker — reach for the application object you hold rather than the static accessor.

Writing load()#

Pop\Application::load() is a hook that does nothing and returns $this. Overriding it is how an application turns config data into live wiring, and it's the one place that work belongs — never inline in a config file, which holds data only.

PHPapp/src/Application.php
<?php

namespace App;

use Pop\Console\Console;

class Application extends \Pop\Application
{

    public function load(): Application
    {
        if (isset($this->config['database'])) {
            $this->initDb($this->config['database']);
        }

        $this->setService('logger', [
            'call'   => 'Pop\Log\Logger',
            'params' => ['writer' => new \Pop\Log\Writer\File(__DIR__ . '/../../data/logs/app.log')]
        ]);

        if ($this->router() !== null) {
            if ($this->router()->isHttp()) {
                $this->on('app.dispatch.pre', 'App\Http\Event\Options::send', 1);
            } else if ($this->router()->isCli()) {
                $console = new Console(120);
                $console->write(PHP_EOL . $console->header('My App', '=', null, 'left', false, true));
                $this->on('app.dispatch.post', function() { echo PHP_EOL; });
            }
        }

        return $this;
    }

}

Three jobs, and they are the three most applications need. load() returns the application so the front controller can chain, and the return type is the subclass — Pop\Application::load() declares AbstractApplication, which any subclass may narrow.

Call load() before run(), as the generated front controller does. That is the call that opens the connection, defines the services and attaches the listeners.

Initializing the Database#

Pop\Application declares no initDb(). It's an app-level convention: a protected method on your own class that reads the database config array, verifies each connection, registers it as a service named database (or database_<name> for additional connections) and binds the default one to Pop\Db\Record. The method is shown in full, with the config shape it consumes, in Records & the ORM.

The reason it lives in load() rather than in app/config/database.php is that opening a connection is behavior, not data. The credentials are values, and belong in a config file; the Db::connect() call that turns them into a live adapter belongs here, where it can be skipped, branched on, or deferred.

Registering Services and Listeners#

setService() takes a name and a callable definition, and getService() resolves it. The definition is lazy — nothing is constructed until something asks for the service by name, and the result is then held, so every later getService('logger') returns the same object until services()->reload('logger') rebuilds it.

PHP
$app = new Pop\Application();

$app->setService('logger', [
    'call'   => 'Pop\Log\Logger',
    'params' => ['writer' => new Pop\Log\Writer\File(__DIR__ . '/app.log')]
]);

$app->getService('logger')->info('Application started');

An unknown name throws Pop\Service\NotFoundException with the message Error: The service 'nope' has not been added to the service locator, so probe with $app->services()->isAvailable('nope') when the service is optional. Services covers defining one in full.

Listeners attach the same way, through on(). Both a config events array and an on() call end up in the same event manager, and the reason to prefer on() inside load() is that a listener registered in code can be conditional — attached only for HTTP, only in production, only when a feature is enabled — while a config array attaches unconditionally.

Dispatchable Parameters#

The router constructs a dispatchable object that is typically a controller or a command object extending AbstractDispatcher. Left as is, its constructor is injected with a predetermined set of objects, which minimally includes the $application object for reference. On top of that, a dispatchable using HttpTrait gets a default $request/$response object pair, and one using ConsoleTrait gets a $console object.

If those default constructor objects need to be overridden with something more specific to your application's need, it can be done with the router's addDispatchableParams method.

Target a specific dispatchable object#

Defining a specific dispatchable class will inject the parameters into only an object of that class:

PHPapp/src/Application.php
<?php

namespace App;

use Pop\Http\Server\Request;
use Pop\Http\Server\Response;
use Pop\Http\Uri;
use Pop\Mail\Mailer;
use Pop\Mail\Transport\Sendmail;

class Application extends \Pop\Application
{

    public function load(): Application
    {
        $this->setService('mailer', new Mailer(new Sendmail()));

        $this->router()->addDispatchableParams(
            'App\Http\Controller\InvoicesController',
            [$this, new Request(new Uri(), ['strip_tags']), new Response(), $this->getService('mailer')]
        );

        return $this;
    }

}

Target dispatchable objects with a fallback#

Using the * wildcard will inject the parameters into all other dispatchable objects that don't have a specific addDispatchableParams call:

PHPapp/src/Application.php
<?php

namespace App;

use Pop\Http\Server\Request;
use Pop\Http\Server\Response;
use Pop\Http\Uri;
use Pop\Mail\Mailer;
use Pop\Mail\Transport\Sendmail;

class Application extends \Pop\Application
{

    public function load(): Application
    {
        $this->setService('mailer', new Mailer(new Sendmail()));

        $this->router()->addDispatchableParams(
            '*', [$this, new Request(new Uri(), ['strip_tags']), new Response(), $this->getService('mailer')]
        );

        return $this;
    }

}

They are keyed by the dispatchable class, not by the route, so every route naming that class is constructed the same way. Five methods manage the registry:

Method Effect
addDispatchableParams($class, $params) sets the arguments, replacing any already registered
appendDispatchableParams($class, $params) adds to the arguments already registered
hasDispatchableParams($class) whether any are registered for that class
getDispatchableParams($class) the array, or null
removeDispatchableParams($class) drops them, restoring the default construction

A route can also carry the arguments declaratively, with a params key, for the case where they are plain values rather than services:

PHP
$app->addRoute('/invoices', [
    'controller' => 'App\Http\Controller\InvoicesController',
    'action'     => 'index',
    'params'     => [$app, 'sendmail']
]);

A bare value is wrapped for you, so 'params' => 'sendmail' and 'params' => ['sendmail'] are the same.

Note If the dispatchable object does not use AbstractDispatcher, HttpTrait or ConsoleTrait, then when it's created, no constructor parameters will be passed into it. This scenario serves as a final fallback if dispatchable parameters aren't provided and the dispatchable object isn't an instance of one of the above classes or traits.

Branching on HTTP Versus CLI#

Pop\Application is runtime-agnostic; the router knows which side it is on, and $this->router()->isHttp() and isCli() are the branch. Console-only work — a header banner, a trailing newline — goes in the CLI branch, and HTTP-only work such as the CORS preflight listener in the other.

The Run Sequence#

run() takes two optional arguments: $exit, which defaults to true and lets the framework terminate the process after a 404 or a maintenance response, and $forceRoute, which substitutes a route for the real request and is covered in Routing.

The sequence is fixed, and five events punctuate it:

Step Event fired
init() is called app.init
before the router matches app.route.pre
after the match, before dispatch app.dispatch.pre
after dispatch returns app.dispatch.post
last, from a finally app.shutdown

Between app.dispatch.pre and the dispatch itself, run() picks one of three paths. If the application is down and the request carries no maintenance secret, the dispatchable's own maintenance action runs — or a generic 503 does, for a route target that is a plain closure. If the middleware manager holds handlers, the dispatch is deferred into the middleware pipeline. Otherwise the dispatchable is invoked directly.

If no route matched at all, none of that happens: the router answers with a 404, or with a 405 and an Allow header when a path matched but its method constraint did not.

Anything thrown inside run() fires app.error and is then rethrown, which is what the front controller's catch block is for. A listener's first parameter is the exception itself and its second is the application:

PHP
$app = new Pop\Application();

$app->on('app.error', function(\Throwable $exception, Pop\Application $application) {
    error_log($exception->getMessage());
});

One exception is treated differently. Pop\Event\AbortException, thrown from any listener, makes run() return quietly without dispatching and without firing app.error — the way a listener stops a request it has already answered itself. app.shutdown fires on every one of these paths, since it runs from a finally. Events covers listener signatures, priorities and teardown.

Error Handlers#

Because run() rethrows, an uncaught exception would otherwise reach PHP's default handler. The scaffolded front controller catches it, builds a second application object and hands the exception to httpError()cliError() on the console side, which prints the message and exits 127.

Guard config reads inside an error handler — $this->config['http_options_headers'] ?? [] — since the application the handler runs on is constructed without config.

An error handler is also the one place a controller cannot help you, because the exception may well have been thrown before any controller was constructed. Keep it to plain Pop\Http\Server\Response calls and a template that needs nothing from the application. Error Handling covers the whole path.

Reaching the Application from Elsewhere#

Inside a controller, $this->application() is the answer — see Controllers. Everywhere else, Pop\App is a static accessor onto the application registered during bootstrap:

PHP
use Pop\App;

$app    = App::get();                   // the Application object, or null
$dbConf = App::config('database');      // one config key, or null
$logger = App::services('logger');      // a resolved service
$router = App::router();

Every one of those returns null rather than throwing when no application has been constructed, so a class that might be used outside a request can check App::has() first. Reach for App in helpers, jobs and listeners that have no other handle on the application; prefer an injected dependency in anything you intend to test in isolation.

The Console Entry Point#

A console application bootstraps identically, from a different config file and a different script. ./kettle is one such front controller, and pop:init writes a second one named after your namespace when you ask for a stand-alone console application:

PHPscript/app
#!/usr/bin/env php
<?php

$autoloader = include __DIR__ . '/../vendor/autoload.php';

$dotEnv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
$dotEnv->safeLoad();

try {
    $app = new App\Application($autoloader, include __DIR__ . '/../app/config/app.console.php');
    $app->load();
    $app->run();
} catch (\Exception $exception) {
    $app = new App\Application();
    $app->cliError($exception);
}

Same three calls, same application class. The router detects that it is running under the CLI SAPI and matches $_SERVER['argv'] against the command route table instead of matching a URL path, which is why one load() can serve both entry points with an isHttp()/isCli() branch rather than two application classes.

The application object exposes more than this page covers — module registration and merging, custom HTTP verbs, and the manager objects behind router(), services() and events() — see the popphp README.

See Also#

  • Configuration — the config keys bootstrap reads, .env, and environments
  • Routing — the routes table the constructor hands to the router
  • Controllers — what run() dispatches to
  • Services — defining a service, and the locator
  • Events — listener signatures, priorities and AbortException
  • Middleware — the pipeline run() defers dispatch into
  • Modules — registering a module against the managers the constructor built
  • Records & the ORMinitDb() in full
  • popphp README — the full application API surface