URLs in Magento have the format of <AreaFrontName>/<VendorName>/<ModuleName>/<ControllerName>/<ActionName>, but this does not mean that we actually use the area, vendor, or module name in the URL any time we wish to access a certain controller. For example, the area for a request is defined by the first request path segment, such as admin for adminhtml area, and none for frontend area.
We use the router class to assign a URL to a corresponding controller and its action. The router's match method finds a matching controller, which is determined by an incoming request.
Conceptually, creating a new router is as simple as doing the following:
- Inject the new item under the routerList argument of the Magento\Framework\App\RouterList type via the di.xml file.
- Create a router file (by using the match method, which implements \Magento\Framework\App\RouterInterface).
- Return an instance of \Magento\Framework\App\ActionInterface.
By doing a lookup for the name="routerList" string across all of the <MAGENTO_DIR> di.xml files, we can see the following router definitions:
- Magento\Robots\Controller\Router (robots)
- Magento\Cms\Controller\Router (cms)
- Magento\UrlRewrite\Controller\Router (urlrewrite)
- Magento\Framework\App\Router\Base (standard)
- Magento\Framework\App\Router\DefaultRouter (default)
- Magento\Backend\App\Router (admin)
Let's take a closer look at the robots router under <MAGENTO_DIR>/module-robots. etc/frontend/di.xml injects the new item under the routerList argument as follows:
<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="robots" xsi:type="array">
<item name="class" xsi:type="string">Magento\Robots\Controller\Router</item>
<item name="disable" xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">10</item>
</item>
</argument>
</arguments>
</type>
The Magento\Robots\Controller\Router class has been further defined as per the following partial extract:
class Router implements \Magento\Framework\App\RouterInterface {
// Magento\Framework\App\ActionFactory
private $actionFactory;
// Magento\Framework\App\Router\ActionList
private $actionList;
// Magento\Framework\App\Route\ConfigInterface
private $routeConfig;
public function match(\Magento\Framework\App\RequestInterface $request) {
$identifier = trim($request->getPathInfo(), '/');
if ($identifier !== 'robots.txt') {
return null;
}
$modules = $this->routeConfig->getModulesByFrontName('robots');
if (empty($modules)) {
return null;
}
$actionClassName = $this->actionList->get($modules[0], null, 'index', 'index');
$actionInstance = $this->actionFactory->create($actionClassName);
return $actionInstance;
}
}
The match method checks if the robots.txt file was requested and returns the instance of the matched \Magento\Framework\App\ActionInterface type. By following this simple implementation, we can easily create the route of our own.
Conceptually, creating a new controller is as simple as doing the following:
- Register a route via router.xml.
- Create an abstract controller file (as an abstract class, which extends \Magento\Framework\App\Action\Action).
- Create an action controller file (which extends the main controller file with the execute method, and implements \Magento\Framework\App\ActionInterface).
- Return an instance of \Magento\Framework\Controller\ResultInterface.
The separation of the controller into the main and action controller files is not a technical requirement, but rather a recommended organizational one. Magento does this across the majority of its modules.
By doing a lookup for the <route string across the <MAGENTO_DIR> routes.xml files, we can see that Magento uses hundreds of route definitions, which are spread across its modules. Each route represents one controller.
Let's take a closer look at one of Magento's controllers, <MAGENTO_DIR>/module-customer, which maps to the http://magelicious.loc/customer/address/form URL. The route itself is registered via frontend/di.xml under the standard router with a customer ID and a customer frontName, as follows:
<router id="standard">
<route id="customer" frontName="customer">
<module name="Magento_Customer" />
</route>
</router>
The abstract controller file Controller/Address.php is defined partially as follows:
abstract class Address extends \Magento\Framework\App\Action\Action {
// The rest of the code...
}
The abstract controller is where we want to add functionality and dependencies that are shared across all of the child action controllers.
We can further see three different action controllers defined within the subdirectory which has the same name as the abstract class. The Controller/Address directory contains six action controllers: Delete.php, Edit.php, Form.php, FormPost.php, Index.php, and NewAction.php. Let's take a closer look at the following partial Form.php file's content:
class Form extends \Magento\Customer\Controller\Address {
public function execute() {
/** @var \Magento\Framework\View\Result\Page $resultPage */
$resultPage = $this->resultPageFactory->create();
$navigationBlock = $resultPage->getLayout()->getBlock('customer_account_navigation');
if ($navigationBlock) {
$navigationBlock->setActive('customer/address');
}
return $resultPage;
}
}
The example here uses the create method of the injected Magento\Framework\View\Result\PageFactory type to create a new page result. The various types of controller results can be found within the <MAGENTO_DIR>/framework directory:
- Magento\Framework\Controller\Result\Json
- Magento\Framework\Controller\Result\Raw
- Magento\Framework\Controller\Result\Redirect
- Magento\Framework\Controller\Result\Forward
- Magento\Framework\View\Result\Layout
- Magento\Framework\View\Result\Page
We can take the unified way of creating result instances by using the create method of \Magento\Framework\Controller\ResultFactory. The ResultFactory defines the TYPE_* constant for each of the mentioned controller result types:
const TYPE_JSON = 'json';
const TYPE_RAW = 'raw';
const TYPE_REDIRECT = 'redirect';
const TYPE_FORWARD = 'forward';
const TYPE_LAYOUT = 'layout';
const TYPE_PAGE = 'page';
Keeping these constants in mind, we can easily write our action controller code as follows:
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$resultRedirect->setPath('adminhtml/*/index');
return $resultRedirect;
A quick lookup for the $this->resultFactory-> create string, across the entire <MAGENTO_DIR> directory, can give us lots of examples of how to use the ResultFactory for our own code.