Understanding the request lifecycle is the single most effective way to stop guessing at how your PHP application works and start knowing. Every bug you debug, every feature you add, and every performance problem you investigate happens somewhere in this cycle. Knowing exactly where gives you a huge advantage. This article traces an HTTP request from the moment it arrives at the web server through PHP process initialisation, front controller entry, routing, dispatching, controller execution, model interaction, view rendering, response assembly, and the final HTTP response back to the client. The content here aligns with PHP application architecture and the MVC patterns covered throughout the Zend Framework Book, and it generalises to every PHP framework that follows the front controller pattern. Below you will find sections on each phase of the lifecycle, how Zend Framework 1 implements each phase, how modern frameworks handle the same concerns, and where performance costs concentrate.
For more focused articles on specific PHP development topics, see the Articles hub.
The Big Picture
Before diving into each phase, here is the complete lifecycle in one view:

Every PHP web application that uses a front controller follows this sequence. The specifics of each phase vary between frameworks, but the structure is the same. Zend Framework 1, Laminas, Laravel, Symfony, and Slim all implement this pattern with different class names and conventions but identical conceptual flow.
Phase 1: HTTP Request Arrives
Everything starts with a client, usually a web browser, sending an HTTP request to your server. That request includes a method (GET, POST, PUT, DELETE), a URI path, headers (including cookies, accept types, and authentication tokens), and optionally a body (for POST and PUT requests).
The request travels over the network and arrives at your web server. At this point, PHP has not been involved. The web server is the first component to handle the request.
Phase 2: Web Server Handling
The web server, typically Apache or Nginx, receives the request and decides what to do with it.
For static files (images, CSS, JavaScript, fonts), the web server serves them directly from the filesystem. PHP never sees these requests. This is why your public directory contains static assets alongside the front controller script: the web server checks for a matching file first and only forwards to PHP when no file matches.
For dynamic requests, the web server uses its rewrite rules to forward the request to PHP. In Apache, mod_rewrite rules in an .htaccess file or virtual host configuration redirect all non-file requests to index.php. The Creating a Local Domain Using Apache Virtual Hosts chapter covers this configuration in detail. In Nginx, a try_files directive accomplishes the same thing.
The rewrite step is critical. It is the mechanism that enables clean URLs (/blog/2024/my-article instead of /index.php?controller=blog&action=view&id=42). Every request that does not match a physical file gets routed to the single front controller entry point.
Phase 3: PHP Process Starts
When the web server forwards a request to PHP, one of two things happens depending on your server configuration.
With PHP-FPM (FastCGI Process Manager), a pool of persistent PHP worker processes is already running. The web server sends the request to an available worker through a socket or TCP connection. The worker handles the request and returns to the pool for the next one. This is the standard production configuration and the one that the PHP Performance Playbook assumes.
With mod_php (Apache module), PHP runs as part of the Apache process itself. Each Apache worker thread or process has PHP embedded in it. This was the dominant configuration during the PHP 5.x era when much of the Zend Framework Book content was written.
Regardless of the mechanism, the PHP process initialises the request environment: it populates the superglobals ($_GET, $_POST, $_SERVER, $_COOKIE, $_FILES), sets up error handling according to php.ini, loads the OPcache compiled opcodes (or compiles the source files if OPcache is disabled or cold), and begins executing the front controller script.
Phase 4: Front Controller Entry
The front controller is the single PHP file that handles every dynamic request. In a Zend Framework 1 application, this is public/index.php. Its job is to bootstrap the application and hand control to the framework.
A typical ZF1 front controller looks like this:
1 | defined('APPLICATION_PATH') |
The bootstrap() call initialises all application resources: database adapters, session handlers, view renderers, plugins, and routes. The run() call starts the dispatch cycle. The Standardise the Bootstrap Class with Zend Application chapter covers this process in depth.
In modern frameworks, the front controller follows the same principle with different mechanics. Laravel’s public/index.php boots the service container and sends the request through the HTTP kernel. Symfony’s front controller loads the kernel and handles the request. The pattern is universal: one entry point, one bootstrap, one dispatch.
The glossary entry for Front Controller defines this pattern formally.
Phase 5: Routing
Once the application is bootstrapped, the router examines the request URI and determines which controller and action should handle it.
In ZF1, Zend_Controller_Router_Rewrite matches the URI against defined routes. The default route follows the pattern /:module/:controller/:action/*, so a request to /blog/entry/view/id/42 maps to the entry controller, view action, with a parameter id of 42. Custom routes can define any pattern.
Routing is a pure translation step. It does not execute any application logic. It takes the request URI and produces a set of parameters that tell the dispatcher what to invoke. If no route matches, the router typically falls through to a default route or triggers a 404 response.
The routing phase is where URL design meets application structure. Clean, predictable URLs are not just a cosmetic concern; they determine how easily you can reason about which code handles which request. This is relevant when debugging, when writing tests, and when configuring cache rules.
Phase 6: Dispatching
The dispatcher takes the routing result and invokes the appropriate controller and action. In ZF1, Zend_Controller_Dispatcher_Standard translates the controller name to a class name, the action name to a method name, instantiates the controller, and calls the action method.
The dispatch loop is one of ZF1’s more distinctive features. After an action executes, the dispatcher checks whether the action has forwarded to another controller/action pair. If it has, the loop runs again with the new target. This continues until no more forwards are pending. The glossary entry for Dispatch Loop explains this mechanism, and the Architecture chapter walks through the implementation.
Plugins can hook into the dispatch process at defined points: before routing, after routing, before dispatching each action, and after dispatching each action. This plugin architecture enables cross-cutting concerns like authentication checks, logging, and layout selection without modifying controller code.
Phase 7: Controller Execution
The controller action is where your application-specific code runs. In a well-structured application, the action method is short. It reads parameters from the request, delegates to a service or model class, and assigns results to the view.
1 | public function viewAction() |
In a poorly structured application, the controller action contains database queries, validation logic, email sending, cache management, and dozens of lines of conditional logic. The Refactoring Fat Controllers in PHP guide deals specifically with extracting this accumulated logic into proper service classes.
The controller’s job in the lifecycle is coordination. It is the bridge between the HTTP world (the request object, the response object, the view) and the domain world (services, entities, repositories). Keeping it thin keeps that boundary clear.
Phase 8: Model Interaction
When the controller delegates to the model layer, the application’s actual work happens. Database queries execute, business rules are applied, calculations run, and external services are called.
In the ZF1 model described in the Model chapter and the Domain Model implementation chapter, this involves domain entities, data mappers, and table gateways. The model layer is deliberately separated from the controller and view layers so that it can be tested independently and reused across different contexts (web requests, CLI scripts, API endpoints).
The model layer is typically where the most time is spent during a request. Database queries, in particular, dominate the execution profile of most PHP applications. The Performance topic hub covers profiling techniques for identifying slow queries and optimising data access patterns.
Phase 9: View Rendering
After the controller action completes, the view layer renders the output. In ZF1, Zend_View processes a view script (a PHP file with HTML and embedded PHP tags) and produces an HTML string. The view script has access to any variables the controller assigned via $this->view.
Zend_Layout wraps the view output in a layout template, providing the shared chrome (header, navigation, footer) around the page-specific content. This two-step rendering process, where the view script produces the content and the layout wraps it, is covered in the Design chapter.
View helpers handle reusable rendering tasks: generating URLs, escaping output, rendering navigation elements, formatting dates. The glossary entry for View Helper defines the pattern, and the Layout System entry covers the two-step rendering architecture.
In modern frameworks, templating engines like Blade (Laravel) and Twig (Symfony) replace raw PHP view scripts, adding syntax conveniences and automatic output escaping. The underlying lifecycle position is identical: the view layer runs after the controller, receives data from the controller, and produces output.
Phase 10: Response Assembly
The rendered view output becomes the body of the HTTP response. The framework assembles the complete response object with:
- An HTTP status code (200 for success, 301/302 for redirects, 404 for not found, 500 for server errors)
- Response headers (Content-Type, Cache-Control, Set-Cookie, and any custom headers)
- The response body (the rendered HTML, JSON, XML, or other content)
In ZF1, the Zend_Controller_Response_Http object accumulates headers and body content throughout the dispatch cycle. Plugins and controller actions can add headers, set the status code, and append to or replace the body. The response is not sent to the client until the dispatch cycle is complete and the front controller calls sendResponse().
This buffered approach means that a controller action can set headers, a plugin can modify them, and an error handler can replace the entire body if an exception occurs, all before anything is sent to the client.
Phase 11: HTTP Response Sent
The web server sends the assembled response back to the client. Headers go first, followed by the body. Once the response is sent, the client (browser) receives it, parses the HTML, requests any referenced assets (CSS, JavaScript, images), and renders the page.
From the client’s perspective, the entire lifecycle described above is a single HTTP round trip. The time from request to response is your application’s response time, and every phase contributes to it.
Phase 12: Cleanup
After the response is sent, PHP performs cleanup. Object destructors run, database connections close (or return to a connection pool), temporary files are removed, and session data is written. In PHP-FPM, the worker process resets its state and becomes available for the next request.
PHP’s share-nothing architecture means that every request starts with a clean slate. There is no persistent application state between requests unless you explicitly store it in a session, cache, or database. This simplifies reasoning about your code because you do not need to worry about state from a previous request affecting the current one, but it also means that expensive setup work (bootstrapping, loading configuration, establishing database connections) happens on every request. OPcache, connection pooling, and application-level caching all exist to mitigate this cost.
Where Performance Costs Concentrate
In a typical PHP MVC application, the request time breaks down roughly like this:
- Database queries: 40 to 60 percent of total time in most applications
- Framework overhead (bootstrapping, routing, dispatching): 10 to 20 percent, heavily reduced by OPcache
- View rendering: 5 to 15 percent, depending on template complexity
- Autoloading: 5 to 10 percent, reduced by optimised classmaps
- External service calls: highly variable, can dominate if present
These numbers vary widely between applications, which is why profiling matters more than rules of thumb. The PHP Performance Playbook covers profiling methodology, and the Performance Optimisation chapter applies those principles specifically to Zend Framework applications.
How This Maps to Modern Frameworks
The lifecycle described here uses ZF1 terminology and classes, but the structure is universal. In Laravel, the request goes through middleware, the router, the controller, Eloquent models, Blade templates, and back. In Symfony, it goes through the kernel, event listeners, the router, the controller, Doctrine entities, Twig templates, and back. The phase names change. The class names change. The lifecycle does not.
Understanding the lifecycle at this level means you can pick up any MVC framework and immediately know where to look for routing configuration, where controller logic executes, how the view layer is invoked, and where to add cross-cutting behaviour. That transferable understanding is more valuable than memorising any single framework’s API.
For the architectural principles behind MVC in PHP, see the Application Architecture topic hub. For the framework-specific details of how ZF1 implements this lifecycle, the Architecture chapter is the definitive reference. And for the terminology used throughout this article, the Glossary defines each concept with its ZF1-specific context.