A image of a person coding.

index.php and bootstrap.php – The First Breath of a Raw PHP Framework

There’s a moment in every project where the code stops being lines on a screen and starts acting like a system. Not quite alive, but breathing. Reacting. Responding.

That moment begins here, in public/index.php.

<?php
declare(strict_types=1);

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

require_once __DIR__ . '/../bootstrap.php';

$container = require __DIR__ . '/../bootstrap.php';

$router = $container->get(App\Core\Router::class);

$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

$router->dispatch($method, $uri);

Strict typing. Full error visibility. Load the container. Extract the route. Dispatch the request. We’re showing all errors because this setup is strictly for development, not production. I want every warning, notice, or fatal exception exposed early, while it’s still safe to break things.

Everything begins here. This is the first file hit by every web request. It’s the front controller, a clean, predictable, no-nonsense handoff.

But the real work? That happens in bootstrap.php.

And that’s where we’re going today.


This is part of a new weekly series

If you missed where this all started, check out Part 1 of the series, originally written while I was still working in PHP 8.3.

Since then, I’ve moved everything to PHP 8.4, and as the code has matured, so has the approach, but the philosophy hasn’t changed.

This is still a raw PHP framework. No third-party Composer packages, aside from phpunit for testing and phpstan for static analysis. Everything else is handcrafted, from the router to the services to the container.

And look, everyone has their own coding style. That’s a good thing. I’m not claiming this is the way to do it. Just a way. My way. Yours might look totally different. That’s what makes building things from scratch worth doing.


So what does bootstrap.php actually do?

This file sets up the whole framework, wiring together every dependency before the app handles a single request.

<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';

use App\Config\LoadEnv;
use App\Core\Container;
use App\Definitions\DatabaseDefinitions;
use App\Definitions\RoutingDefinitions;
use App\Services\EncryptionService;
use App\Response\ValidationResponse;
use App\Definitions\ModelsDefinitions as MD;

$env = __DIR__ . '/.env';
if (!file_exists($env)) {
    die("Environment file not found.");
}

LoadEnv::load($env);


This section enforces strict types, loads Composer’s autoloader (just for namespacing), and checks for the existence of a .env file. No .env, no app, fail fast and loud.

$container = new Container();

This container is our lightweight DI system. It stores and resolves objects and services on demand.

Then we start registering services:

foreach (DatabaseDefinitions::getDefinitions() as $name => $definition) {
    $container->register($name, $definition, true);
}

Each service is manually registered. We don’t use “magic” discovery, we declare exactly what we want loaded, when, and how.

The rest of the bootstrap file does the same for encryption, models, routing, and validation helpers:

$container->register(EncryptionService::class, fn(Container $c) => new EncryptionService(), true);

foreach (MD::getDefinitions() as $model => $factory) {
    $container->register($model, $factory, true);
}

foreach (RoutingDefinitions::getDefinitions() as $name => $definition) {
    $container->register($name, $definition, true);
}

$container->register(ValidationResponse::class, fn(Container $c) => new ValidationResponse(), true);

return $container;


When index.php loads this file, it receives a fully built container with everything it needs to handle the request lifecycle.


Why build it like this?

Because nothing is hidden. Every component is registered deliberately. You can trace every service from definition to instantiation. If something breaks, you don’t need to read 20 pages of framework documentation, you just open the file.

This is also a security-first project. OWASP principles are baked in from the start, not tacked on as an afterthought. The goal is to make each component small, testable, and hardened against common attack vectors from day one.


Next Wednesday: We’ll dig into LoadEnv, the container, and the database definitions.

In the meantime, if you want to poke around the code, here’s the repo:

🧭 Sample PHP Code – Routing Branch

Thanks to those who asked for this series to continue, I’ve missed writing it.

More soon.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.