Paul M. Jones

Don't listen to the crowd, they say "jump."

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:

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 require framework-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 ...

  • Laravel creates a request object;
  • Mezzio makes two configuration calls;
  • PHPixie sends the response;

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

  1. Require an autoloader.
  2. Instantiate a service container factory.
  3. Obtain the service container from the factory.
  4. Obtain a front controller from the service container.
  5. 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.


Ioc-Interop Now Open For Public Review

I am pleased to announce that the Ioc-Interop standard interface package is now open for public review.

Ioc-Interop provides an interoperable package of standard interfaces for inversion-of-control (IOC) service container functionality. It reflects, refines, and reconciles the common practices identified within several pre-existing projects.

Ioc-Interop is functionally almost identical to PSR-11. However, Ioc-Interop is intended to contain only services (object). PSR-11 is intended to contain anything (mixed).

Ioc-Interop also offers an IocContainerFactory interface, whereas PSR-11 offers none.

Ioc-Interop is focused on the concerns around obtaining and consuming services. The affordances for managing and producing services are a set of separate concerns. Earlier drafts of Ioc-Interop were much more expansive, including a resolver subsystem and a service management subsystem. These have been extracted to separate standards, each of which is dependent on Ioc-Interop:

This separation helps to maintain a boundary between the needs of service consumers (afforded by Ioc-Interop) and service producers (afforded by Service-Interop and Resolver-Interop).

Note that Ioc-Interop is independent of Service-Interop and Resolver-Interop. Ioc-Interop implementations can use them, or avoid them, as implementors see fit.

The reference implementations demonstrate how the interfaces can be used.

Please offer your comments and criticism as issues or PRs out at Github.


Upload-Interop Standard Now Stable

I am pleased to announce that the Upload-Interop standard interface package has been released at 1.0.0 stable, along with reference implementations for the interfaces.

Upload-Interop provides an interoperable package of standard interfaces for working with upload structures in PHP 8.4+. It reflects, refines, and reconciles the common practices identified within several pre-existing projects.

If you discover omissions or oversights, please raise an issue or PR at Github.


Env-Interop Now Open For Public Review

After a brief round of private review, the Env-Interop standard interface package is now open for public review.

Env-Interop provides interoperable interfaces to load and parse environment files, and encapsulate environment variables, in PHP 8.4 or later. It reflects, refines, and reconciles the common practices identified within several pre-existing projects to define these interfaces:

  • EnvLoaderService affords loading environment variables parsed from an environment file into $_ENV (and possibly elsewhere).

  • EnvParserService affords parsing a string for environment variables.

  • EnvSetterService affords adding or replacing an environment variable in $_ENV (and possibly elsewhere).

  • EnvGetter affords getting environment variable values.

  • EnvThrowable interface extends Throwable to mark an Exception as environment-related.

  • EnvTypeAliases provides PHPStan type aliases to aid static analysis.

The reference implementations demonstrate how the interfaces can be used.

If you have to deal with environment variables and are interested an interoperable set of interfaces, please offer your comments and criticism as issues or PRs out at Github.


Response-Interop Now Open For Public Review

After several rounds of private review, the Response-Interop standard interface package is now open for public review.

Response-Interop provides interoperable interfaces to encapsulate, buffer, and send server-side response values in PHP 8.4 or later, in order to reduce the global mutable state and inspection problems that exist with the PHP response-sending functions. It reflects, refines, and reconciles the common practices identified within several pre-existing projects to define these interfaces:

The reference implementations demonstrate how the interfaces can be used.

If you have to deal with server-side responses and are interested an interoperable set of interfaces, please offer your comments and criticism as issues or PRs out at Github.



More-Than-One Class Per File

In this PHP internals message we read:

I have participated in some internals discussions that point out a fact from the other direction: PHP makes no imposition about file structure. 1-class-per-file is strictly a PSR-4 implementation of a custom autoloader which we have gotten used to for so long. I would really love to be able to declare small related classes/interfaces in a single file ...

There is something to be said for more-than-one class per file, and after a few evenings of tinkering, I can now present the moto/autoload package.

The Moto name-to-file resolution algorithm is an ideological descendant from Horde/PEAR through PSR-0 and PSR-4, with the latest evolution being that everything after an underscore in the terminating class name portion is ignored:

/**
 * Foo_Bar\Baz\ maps to /classes/foo-bar/baz/src/
 */
Foo_Bar\Baz\Dib     => /classes/foo-bar/baz/src/Dib.php
Foo_Bar\Baz\Dib_Zim => /classes/foo-bar/baz/src/Dib.php
Foo_Bar\Baz\Dib_Gir => /classes/foo-bar/baz/src/Dib.php
Foo_Bar\Baz\Dib_Irk => /classes/foo-bar/baz/src/Dib.php

This means the Dib.php file can contain any number of underscore-suffixed classes so long as they are prefixed with Dib ...

// /classes/foo-bar/src/Baz/Dib.php
namespace Foo_Bar/Baz;

class Dib { }

class Dib_Zim { }

class Dib_Gir { }

class Dib_Irk { }

... and a Moto-compliant resolver will find the correct file for that class.

And if function autoloading becomes supported by PHP, the Moto name-to-file resolution algorithm can support multiple functions per file:

// Foo_Bar\Baz => /vendor/foo-bar/baz/src/dib.php
namespace Foo_Bar\Baz;

function dib() {}

function dib_zim() {}

function dib_gir() {}

function dib_irk() {}

I'm soliciting feedback on this approach, so please post your comments and concerns at Github as issues and pull requests.


Request-Interop Now Open For Public Review

After an extensive period of untagged private review, the Request-Interop standard interface package is now open for public review.

Request-Interop provides an interoperable package of standard interfaces for encapsulating readable server-side request values in PHP 8.4 or later, in order to reduce the global mutable state problems that exist with PHP superglobals. It reflects, refines, and reconciles the common practices identified within several pre-existing projects to define these interfaces:

Request-Interop also defines a marker interface, RequestThrowable, for marking an Exception as request-related.

Finally, Request-Interop defines a RequestTypeAliases interface with PHPStan types to aid static analysis.

The reference implementations demonstrate how the interfaces can be used.

If you have to deal with server-side requests and are interested an interoperable set of interfaces, please offer your comments and criticism as issues or PRs out at Github.


Upload-Interop Now Open For Public Review

After a brief period of private review, the Upload-Interop standard interface package is now open for public review.

Upload-Interop provides an interoperable package of standard interfaces for working with upload structures in PHP 8.4+. It reflects, refines, and reconciles the common practices identified within several pre-existing projects to define these interfaces:

Upload-Interop also defines an UploadTypeAliases interface with PHPStan types to aid static analysis.

The reference implementations demonstrate how the interfaces can be used.

If you have to deal with uploads and are interested an interoperable set of interfaces, please offer your comments and criticism as issues or PRs out at Github.


Uri-Interop Standard Now Stable

I am pleased to announce that the Uri-Interop standard interface package has been released at 1.0.0 stable, along with reference implementations for the interfaces.

The standard includes interfaces for readable, mutable, and immutable URI objects, as well as URI-specific PHPStan type aliases.

The standard also includes interfaces for a factory. normalizer, resolver, and parser.

If you discover omissions or oversights, please raise an issue or PR at Github.