public/index.php — the entry point dissected
Concept
public/index.php is the single entry point for every HTTP request in a Laravel application. It is 40 lines of PHP that orchestrate the entire framework boot sequence. Understanding each line explains why Laravel is structured the way it is and why certain things (like calling config() before the app bootstraps) simply cannot work.
The file lives in public/ deliberately. Your web server's document root points to this directory, which means PHP files, configuration, and the vendor/ directory are outside the webroot and inaccessible via HTTP. This is a security architecture decision: an attacker cannot request ../app/Http/Controllers/UserController.php or ../.env directly — the web server will 404 because those paths are not under public/.
The first meaningful operation is loading Composer's autoloader from vendor/autoload.php. This file was generated by composer dump-autoload and contains a PHP array (or a series of arrays) mapping every fully-qualified class name to its file path. After this line executes, any use App\Models\User anywhere in your codebase will just work — PHP's spl_autoload_register callbacks handle the lazy require.
Next, bootstrap/app.php is included. In Laravel 11 this file uses the new Application::configure() fluent builder. In older versions (10 and below) it bound the HTTP Kernel, Console Kernel, and App\Exceptions\Handler to their contracts manually. Either way, the result is the same: a fully configured Application instance is returned.
Request::capture() is the Symfony-inherited bridge: it reads $_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, and php://input and wraps them in an immutable Illuminate\Http\Request object. This is the last time PHP superglobals appear; from here on everything flows through the object.
$kernel->handle($request) is where the framework actually runs. This method calls $this->sendRequestThroughRouter($request), which bootstraps the application (if not already bootstrapped), then sends the request through the middleware pipeline via Illuminate\Pipeline\Pipeline.
The $response->send() call is a Symfony Response method. It calls sendHeaders() (which issues header() for each header) and then sendContent() (which echoes the response body). After this line, the HTTP response is on the wire — the browser is already reading it.
$kernel->terminate() fires after send(). Terminable middleware (classes implementing TerminableMiddleware with a terminate() method) run here. Common examples: writing the session, pushing queued log records, or recording request metrics without slowing the user-perceived response time.
Code Example
<?php
// public/index.php — full annotated version (Laravel 11)
// Record start time for telescope/debugbar timing
define('LARAVEL_START', microtime(true));
// --- Maintenance mode fast-path ---
// artisan down writes storage/framework/maintenance.php
// This bypasses the entire bootstrap for speed and reliability
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance; // may exit() or return a response
}
// --- Composer PSR-4 autoloader ---
// After this line: all namespaced classes are lazily loadable
require __DIR__.'/../vendor/autoload.php';
// --- Application bootstrap ---
// Returns Illuminate\Foundation\Application
// In Laravel 11: uses Application::configure() fluent builder
// In Laravel 10: manually binds Http\Kernel, Console\Kernel, ExceptionHandler
$app = require_once __DIR__.'/../bootstrap/app.php';
// --- HTTP Kernel resolution ---
// App\Http\Kernel in L10, or the auto-configured kernel in L11
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// --- Request creation from PHP superglobals ---
// Reads $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES, php://input
// This is the LAST time superglobals are touched
$request = Illuminate\Http\Request::capture();
// --- Run the application ---
// 1. Bootstraps (env, config, exceptions, facades, providers)
// 2. Runs request through Pipeline (middleware)
// 3. Router matches route, dispatches to controller
// 4. Response travels back through middleware
$response = $kernel->handle($request);
// --- Flush response to client ---
// sendHeaders() → header() calls
// sendContent() → echo $this->content
$response->send();
// --- Post-response hooks ---
// Session writes, deferred log flushes, terminable middleware
// Client connection may already be closed by this point
$kernel->terminate($request, $response);Interview Q&A
Q: Why is Laravel's web server document root set to public/ instead of the project root?
Setting the document root to public/ ensures only index.php and static assets (CSS, JS, images) are directly accessible via HTTP. Everything else — app/, config/, .env, vendor/, storage/ — is outside the webroot and cannot be fetched with a browser request. If the document root were the project root, an attacker could potentially request /.env and read database credentials, API keys, and other secrets directly, since Nginx/Apache would serve it as a plain text file.
Q: What does Request::capture() actually do, and why does it matter that it's called before $kernel->handle()?
Request::capture() constructs an Illuminate\Http\Request by calling createFromGlobals() on the Symfony base class, which reads $_GET, $_POST, $_COOKIE, $_SERVER, and $_FILES and wraps them in an object. By capturing the request before calling the kernel, we create a single immutable snapshot of the incoming HTTP state. Everything that runs inside the kernel — middleware, route resolution, controllers — works against this captured object, not against mutable superglobals. This makes the request testable (you can construct Request objects in tests without actual HTTP), and prevents any part of the application from accidentally modifying $_GET/$_POST and affecting other parts.
Q: What is the role of $kernel->terminate() and why does it run after $response->send()?
terminate() runs terminable middleware — those with a terminate(Request $request, Response $response) method — after the response has already been sent to the client. This is a performance pattern: work like writing the session to Redis, flushing buffered log records, or recording analytics can happen without blocking the user-perceived response time. In PHP-FPM, fastcgi_finish_request() is called (or the equivalent) to close the connection before this cleanup work starts. In CLI/testing environments terminate() still runs but there's no connection to close first.