From index.php to a Response

How a request travels from pub/index.php through the bootstrap, the front controller and the router list to your controller.

SyntaxMagento\Framework\App\FrontController::dispatch(RequestInterface $request) // returns ResponseInterface|ResultInterface

Magento serves every storefront, admin, REST and GraphQL request from one file: pub/index.php. That file does almost nothing itself. It includes app/bootstrap.php, creates a Magento\Framework\App\Bootstrap, and runs the HTTP application object. The path described here is identical in Magento Open Source and Adobe Commerce.

The four steps

  1. Bootstrap. Bootstrap::create() reads app/etc/config.php and app/etc/env.php, sets up autoloading, and builds the object manager — the container that constructs your classes and passes their constructor dependencies.
  2. Area. Http::launch() takes the first URL segment, asks AreaList which area owns that front name, and fixes it with State::setAreaCode(). An area is a named mode of the application with its own configuration; anything unclaimed is frontend.
  3. Routing. FrontController::dispatch() walks a list of routers, calling match() on each. The first router to return an action object wins.
  4. Execute. The front controller validates the request, dispatches controller_action_predispatch, then calls execute() on your action. What that returns goes back to Http::launch(), which calls renderResult() on it to fill the response.

What trips people up

Dispatch is a loop, not a single pass. A router or an action can mark the request undispatched to send it elsewhere, and the loop then restarts at the first router. After 100 turns the front controller throws a LogicException — that is what two actions forwarding to each other looks like in the log.

Example

Example · php
<?php
// app/code/Vendor/Trace/Controller/Index/Index.php
declare(strict_types=1);

namespace Vendor\Trace\Controller\Index;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\RawFactory;
use Magento\Framework\Controller\ResultInterface;

class Index implements HttpGetActionInterface
{
    public function __construct(
        private readonly RequestInterface $request,
        private readonly RawFactory $rawFactory
    ) {
    }

    public function execute(): ResultInterface
    {
        $result = $this->rawFactory->create();
        $result->setHeader('Content-Type', 'text/plain', true);
        $result->setContents(
            sprintf(
                "module=%s action=%s id=%s\n",
                $this->request->getModuleName(),
                $this->request->getActionName(),
                (string)$this->request->getParam('id', '-')
            )
        );

        return $result;
    }
}

When to use it

  • Work out why a URL renders the 404 page instead of reaching the class you wrote.
  • Explain where request validation happens relative to your own code during a review.
  • Decide whether a bug belongs in your action or earlier, in routing.
  • Read a stack trace that starts at pub/index.php and know which step each frame belongs to.

More examples

The entry point as shipped

BP is the base-path constant defined in app/autoload.php, which app/bootstrap.php includes before using it. createApplication() takes a class name as a string, and \Magento\Framework\App\Http::class is the shorthand for the string 'Magento\Framework\App\Http' — the same value, except that ::class is resolved against the file's namespace and use statements at compile time, so the name survives a renamed class and your IDE or static analysis can flag a misspelling. PHP itself does not check that the class exists, so a typo in a ::class constant still fails at runtime when the object manager tries to instantiate it. bin/magento has its own entry point and builds Magento\Framework\Console\Cli instead of Http, which is why a console process starts with no area.

Example · php
<?php
// pub/index.php (shipped with Magento 2.4.9, error handling removed)

use Magento\Framework\App\Bootstrap;

require __DIR__ . '/../app/bootstrap.php';

$bootstrap = Bootstrap::create(BP, $_SERVER);
/** @var \Magento\Framework\App\Http $app */
$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class);
$bootstrap->run($app);

The same controller with a longhand constructor

Constructor property promotion (private readonly in the parameter list) and this longhand form are the same thing: PHP declares the property and assigns the argument for you. Magento's dependency injection reads the constructor's parameter types either way, so both classes are built identically. Most code in the magento/magento2 source predates promotion and uses the longhand, so you need to read both.

Example · php
<?php
// app/code/Vendor/Trace/Controller/Index/Index.php
declare(strict_types=1);

namespace Vendor\Trace\Controller\Index;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\RawFactory;
use Magento\Framework\Controller\ResultInterface;

class Index implements HttpGetActionInterface
{
    private RequestInterface $request;

    private RawFactory $rawFactory;

    public function __construct(RequestInterface $request, RawFactory $rawFactory)
    {
        $this->request = $request;
        $this->rawFactory = $rawFactory;
    }

    public function execute(): ResultInterface
    {
        return $this->rawFactory->create()->setContents($this->request->getActionName());
    }
}

The dispatch loop in core

Reach for this when you need to know why a request ended up somewhere unexpected. Two facts matter. First, throwing Magento\Framework\Exception\NotFoundException from your execute() is the supported way to produce the 404 page: the front controller catches it, rewrites the action to noroute and runs the loop again. Second, the loop restarts at the first router after a forward, so a router earlier in the list gets a second chance at the rewritten request.

Example · php
<?php
// vendor/magento/framework/App/FrontController.php (core, trimmed)

while (!$request->isDispatched() && $routingCycleCounter++ < 100) {
    foreach ($this->_routerList as $router) {
        try {
            $actionInstance = $router->match($request);
            if ($actionInstance) {
                $result = $this->processRequest($request, $actionInstance);
                break;
            }
        } catch (\Magento\Framework\Exception\NotFoundException $e) {
            $request->initForward();
            $request->setActionName('noroute');
            $request->setDispatched(false);
            break;
        }
    }
}

Discussion

  • Be the first to comment on this lesson.