Designing a Bootstrap Script
bluf:
require dirname(__DIR__) . '/vendor/autoload.php';
use Project\Bootstrap\ContainerFactory;
use Project\Presentation\Http\HttpFrontController;
new ContainerFactory()
->newContainer()
->getService(HttpFrontController::class)
->run();
I.
Most PHP systems these days have a bootstrap script (index.php for the web, or console.php for the command line). I've had to review a lot of these as part of my legacy modernization work.
The following survey of public PHP frameworks should give you a good idea of the wide range of design choices made by different bootstrap authors:
- Aura :
web/index.php - Bear :
public/index.php - CakePHP :
webroot/index.php - CodeIgniter :
index.php - FatFree :
index.php - FlightPHP :
app/config/bootstrap.php - FuelPHP :
public/index.php - Joomla :
includes/app.php - Klein : (documentation)
- Kohana :
index.php - Laminas-MVC :
public/index.php - Laravel :
public/index.php - LeafPHP :
public/index.php - LightMVC :
public/index.php - Lithium :
webroot/index.php - Mezzio :
public/index.php - Nette :
www/index.php - Phalcon :
public/index.php - PHPixie :
web/index.php - Silex : (documentation)
- Slim :
public/index.php - Symfony : (template)
- Tempest :
public/index.php - Yii :
public/index.php
Each of the bootstrap scripts performs at least a few of the following tasks, though not all of them perform all of the tasks, and none of them necessarily in this order:
- Require an autoloader
- Call
ini_set(),error_reporting(), etc. - Start/stop a timer to measure execution time
- Load or set environment variables
- Check the SAPI or the environment to change how the bootstrap behaves
- Explicitly
requireframework-specific library files - Determine directory paths (e.g. the project path, config path, app path, document root, etc.)
- Load or set framework-specific configuration files and values
- Load or set middleware stacks
- Load or set routes
- Instantiate a request object
- Create a service container
- Add or register services on a container
- Instantiate or obtain a front controller
- Dispatch the request through a router
- Invoke the front controller
- Send a response object
- Call
exit()
Often enough, one or more of the above tasks is accomplished by including another script from the bootstrap. The CodeIgniter and Joomla bootstrap scripts are typical examples of that. They are a lot like what I see from many in-house PHP bootstrap scripts.
II.
Looking again at that list of tasks above, is there a way to consolidate some of them into more appropriate locations and maybe even describe a common approach to bootstrap scripts?
First, they could use autoloading exclusively, instead of using require to bring
in other scripts. That gets rid of:
- Explicitly require framework-specific library files
Next, given the existence of a service container, all configuration tasks could be encapsulated by configuration objects in (or service providers to) that container. Doing so would consolidate these tasks, moving the relevant logic out of scripts and into more-easily-tested units:
- Call
ini_set(),error_reporting(), etc. - Start/stop a timer to measure execution time
- Add or register services on a container
- Determine directory paths (e.g. the project path, config path, app path, document root, etc.)
- Load or set framework-specific configuration files and values
- Load or set middleware stacks
- Load or set routes
After that, request and response work can be moved into the front controller itself, removing these tasks from the bootstrap:
- Instantiate a request object
- Dispatch the request through a router
- Send a response object
That leaves only these remaining tasks:
- Require an autoloader
- Load or set environment variables
- Check the SAPI or the environment to change how the bootstrap behaves
- Create a service container
- Instantiate or obtain a front controller
- Invoke the front controller
- Call
exit()
These look like reasonable concerns for a bootstrap script.
Are there any bootstrap scripts that already work that way, or close to it? Yes:
These almost work that way ...
... but otherwise limit themselves to the reduced set of bootstrap tasks.
III.
At this point, it looks like the major source of variation is how the bootstrap scripts obtain and run the front controller (usually called an Application or a Kernel) :
// Aura
$kernel = (new \Aura\Project_Kernel\Factory)->newKernel(...);
$kernel();
// Laminas-MVC
$container = require __DIR__ . '/../config/container.php';
$app = $container->get('Application');
// Laravel
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
// Mezzio
$app = $container->get(Application::class);
$app->run();
// Nette
$application = $container->getByType(Application::class);
$application->run();
// PHPixie
$pixie = new \App\Pixie()->bootstrap($root);
$pixie->bootstrap($root)->http_request()->execute();
// Tempest
HttpApplication::boot(...)->run();
// Yii
$runner = new HttpApplicationRunner(...);
$runner->run();
On examination, we find three kinds of relationships between the front controller (Application) and the service Container:
-
Combined: the Application extends the Container.
-
Service-located: the Container is created, then passed into the Application so the Application can locate services; alternatively, the Application itself creates and retains the Container.
-
Dependency-injected: the Container is created, then the Application is obtained from it.
All of the above variations look like they could be easily reconciled to use a factory to create a service container, then obtain a front controller from the service container and run it.
IV.
With that one final adjustment toward a factory and dependency injection, we find a prototypical set of bootstrap tasks:
- Require an autoloader.
- Instantiate a service container factory.
- Obtain the service container from the factory.
- Obtain a front controller from the service container.
- Run the front controller.
This set of tasks should be achievable with little effort in any PHP system. Here is an example with hypothetical implementations of the Star-Interop standard interfaces for IocContainerFactory, IocContainer, and FrontController ...
require dirname(__DIR__) . '/vendor/autoload.php';
use Project\Bootstrap\ContainerFactory;
use Project\Presentation\Http\HttpFrontController;
new ContainerFactory()
->newContainer()
->getService(HttpFrontController::class)
->run();
... but of course using the Star-Interop standards is not a requirement. Any PHP system with an autoloader, container factory, service container, and front controller can follow this idiom.
There are still three activities from the researched bootstrap scripts that are not covered by the prototype ...
- Check the SAPI or the environment to change how the bootstrap behaves
- Load or set environment variables
- Call
exit()
... and those could reasonably be added to the bootstrap code. However, all other bootstrap script activity has been consolidated into the container factory, container, or front controller.
V.
This approach also lends itself to command-line systems; we need only to swap
the HTTP front controller for a CLI front controller and add an exit():
exit(
new ContainerFactory()
->newContainer()
->getService(CliFrontController::class)
->run()
);
In conclusion, the prototypical design ...
- moves the bootstrap logic out of scripts and into classes as early as possible;
- eliminates the introduction of global variables and constants from the bootstrap; and,
- decouples the bootstrap script from any specific project directory structure (i.e., there is no need to require a script file from a particular location to get the service container).
In short, it standardizes a common approach to bootstrap scripts that is useful to, and recognizable in, any PHP system.