Controllers in Magento 2, like on other PHP MVC frameworks, are important part of Mvc flow. In Magento 2 there are lots of changes in controllers, for example; how they are structured and how they work compared with Magento 1. If you are familiar with Magento 1 controllers, then you know they can have multiple actions (class methods). In Magento 2 controllers have only one method (execute) that will be called by front controller. This article covers controller basics, matching flow, controller types (admin and frontend), changes on existing controllers, instructions to create custom controllers and an example how to create a few controllers.
Controller
Controller is a class located in module Controller folder, responsible for specific Url or group of Url’s. It’s different than controller in Magento 1 because it has only one called method (in Magento 1 was as many as we wanted). All needed data is populated trough DI with object manager. For every action we have one controller class with execute method. Execute method is called when router matches controller action class, and it’s responsible for returning response to front controller. All controllers are extending MagentoFrameworkAppActionAction class which has dispatch method which will call execute method in controller, but we’ll cover flow later. There are two controller types: frontend and admin. They have similar behavior but admin has additional methods for permission checks. Controllers are structured in a specific way so they can be matched. Url structure for matching is:
www.inchoomagento2.net/frontName/action path/action class/
- frontName – it’s set in routes.xml configuration, and has unique value which will be matched by router
- action path – folder name inside Controller folder, default is index
- action class – our action class which we call Controller, default is index
Now, we’ll go trough action controller methods and explain their purposes.
Execute method
This method is “first” called controller action class and it is inherited from MagentoFrameworkAppActionAction which every controller class extends. It’s called by MagentoFrameworkAppActionAction::dispatch() method. In this method we should have all of our controllers logic (we can, of course, have logic in additional methods, but execute method will call them) and it will return response (mostly rendered page).
MagentoFrameworkAppActionAction
This is main Magento framework action class and every controller must extend this class (admin controllers are extending MagentoBackendAppAction which extends MagentoFrameworkAppActionAction). It’s important that every controller extends this class to inherit needed methods and to allow front controller to call dispatch method (which will call execute method).
Dispatch
This method will be called first by Front Controller (MagentoFrameworkAppFrontController)
MagentoFrameworkAppFrontController::dispatch() – calling dispatch in our Action class:
$result = $actionInstance->dispatch($request);
It’s important to understand that dispatch method in our Action class is used for front controllers. For admin controllers dispatch method is rewritten (in MagentoBackendAppAction), so it can check is user allowed to access. Let’s analyze our dispatch method:
/**
* Dispatch request
*
* @param RequestInterface $request
* @return ResponseInterface
* @throws NotFoundException
*/
public function dispatch(RequestInterface $request)
{
$this->_request = $request;
$profilerKey = 'CONTROLLER_ACTION:' . $request->getFullActionName();
$eventParameters = ['controller_action' => $this, 'request' => $request];
$this->_eventManager->dispatch('controller_action_predispatch', $eventParameters);
$this->_eventManager->dispatch('controller_action_predispatch_' . $request->getRouteName(), $eventParameters);
$this->_eventManager->dispatch(
'controller_action_predispatch_' . $request->getFullActionName(),
$eventParameters
);
MagentoFrameworkProfiler::start($profilerKey);
$result = null;
if ($request->isDispatched() && !$this->_actionFlag->get('', self::FLAG_NO_DISPATCH)) {
MagentoFrameworkProfiler::start('action_body');
$result = $this->execute();
MagentoFrameworkProfiler::start('postdispatch');
if (!$this->_actionFlag->get('', self::FLAG_NO_POST_DISPATCH)) {
$this->_eventManager->dispatch(
'controller_action_postdispatch_' . $request->getFullActionName(),
$eventParameters
);
$this->_eventManager->dispatch(
'controller_action_postdispatch_' . $request->getRouteName(),
$eventParameters
);
$this->_eventManager->dispatch('controller_action_postdispatch', $eventParameters);
}
MagentoFrameworkProfiler::stop('postdispatch');
MagentoFrameworkProfiler::stop('action_body');
}
MagentoFrameworkProfiler::stop($profilerKey);
return $result ?: $this->_response;
}
As we can see, our dispatch method will return results or response from context. It will check if request is dispatched and than call execute method in controller action class which extends MagentoFrameworkAppActionAction:
$result = $this→execute();
There are two more important methods that we will need in our action classes: _forward and _redirect. Let’s explain that methods:
Forward method
This protected method will transfer control to another action controller, controller path and module. This is not redirect and it will run one more router loop to pass control to another controller action. For more detailed flow you should check our “Routing in Magento 2” article.
Redirect method
It will redirect user on new Url by setting response headers and redirect url.
Controller action match flow
FrontController::dispatch() → Router::match() → Controller::dispatch() -> Controller::execute()
Above you can see high level flow, but since flow is a little more complicated than this, let’s go fast trough more detailed level:
FrontController::dispatch()
while (!$request->isDispatched() && $routingCycleCounter++ < 100) {
/** @var MagentoFrameworkAppRouterInterface $router */
foreach ($this->_routerList as $router) {
try {
$actionInstance = $router->match($request);
It will first match router, as you can see in the code above, and router match will return action class (MagentoFrameworkAppActionFactory) instance. After that, front controller will call dispatch method on action class instance:
$result = $actionInstance->dispatch($request);
As we’ve already covered dispatch method, it will call action class execute method:
$result = $this->execute();
This is shortly how application flow gets in our action class execute method.
Difference between admin and front controller
Main difference between these two controllers is in additional check and additional methods in admin controller. Both controllers eventually extend MagentoFrameworkAppActionAction class, but admin controller extend MagentoBackendAppAction class, which extends MagentoFrameworkAppActionAction. In admin controller dispatch, redirect and rewrite methods are rewritten to provide logic for checking ACL (Access control list).
Admin controller
It extends MagentoBackendAppAction class and has _isAllowed method which checks access control. In dispatch method it will check if user is allowed to access current Url and it will redirect to login (if user is not allowed) or it will set response with status 403 (forbidden):
public function dispatch(MagentoFrameworkAppRequestInterface $request)
{
if (!$this->_processUrlKeys()) {
return parent::dispatch($request);
}
if ($request->isDispatched() && $request->getActionName() !== 'denied' && !$this->_isAllowed()) {
$this->_response->setStatusHeader(403, '1.1', 'Forbidden');
if (!$this->_auth->isLoggedIn()) {
return $this->_redirect('*/auth/login');
}
$this->_view->loadLayout(['default', 'adminhtml_denied'], true, true, false);
$this->_view->renderLayout();
$this->_request->setDispatched(true);
return $this->_response;
}
if ($this->_isUrlChecked()) {
$this->_actionFlag->set('', self::FLAG_IS_URLS_CHECKED, true);
}
$this->_processLocaleSettings();
return parent::dispatch($request);
}
If you are creating admin controller and want to add some custom permission, you need to add access check in _isAllowed method, for example:
protected function _isAllowed()
{
return $this->_authorization->isAllowed('Magento_EncryptionKey::crypt_key');
}
Changes on existing controllers
For changing existing controller, there are couple of methods how they can be changed. You can do that by preference, plugin or use “old” style after/before like Magento 1. Preference will change complete controller with your controller code (we can call that like complete rewrite). Plugins will change only desired controlled method. Lastly, after and before will change location of controller for custom front name. Example for this is how you add new controller on admin area:
<router id="admin">
<route id="catalog" frontName="catalog">
<module name="Magento_Catalog" before="Magento_Backend" />
</route>
</router>
Action wrapper class
Action wrapper class is a class in Controller folder which extends MagentoFrameworkAppActionAction, and then our action class extends that action wrapper. If you have common logic for multiple action classes, than we will write logic in action wrapper class and use it on every action class we need.
Let’s see, for example, MagentoCatalogControllerProduct which is action wrapper class, and it’s used in many action classes located in CatalogController* folder (action classes: MagentoCatalogControllerProductCompare, MagentoCatalogControllerProductGallery etc), all of them use _initProduct which is responsible for loading the product through helper.
/**
* Product controller.
*
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
namespace MagentoCatalogController;
use MagentoCatalogControllerProductViewViewInterface;
use MagentoCatalogModelProduct as ModelProduct;
abstract class Product extends MagentoFrameworkAppActionAction implements ViewInterface
{
/**
* Initialize requested product object
*
* @return ModelProduct
*/
protected function _initProduct()
{
$categoryId = (int)$this->getRequest()->getParam('category', false);
$productId = (int)$this->getRequest()->getParam('id');
$params = new MagentoFrameworkDataObject();
$params->setCategoryId($categoryId);
/** @var MagentoCatalogHelperProduct $product */
$product = $this->_objectManager->get('MagentoCatalogHelperProduct');
return $product->initProduct($productId, $this, $params);
}
}
How to create custom controller
- Create routes.xml in etc/frontend or etc/adminhtml folder (first one is for frontend and second one is for admin controller).
- Add your custom configuration for controller in routes.xml, for example:
– router: id – standard(frontend)/admin
– route: id – your unique route id
– route: frontName – unique name in url, this is first part of url in base router (www.inchootest.net/frontName/actionpath/actionclass/)
– module name – your module name - Create your action class following the url structure above:
Controller/Actionpath/Actionclass.php
Examples – Admin and Front controllers
We will create example module for custom controllers demonstration. So first, let’s create a module:
etc/module.xml:
<?xml version="1.0"?>
<!--
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
* Module is created for Custom Controllers demonstration
*/
-->
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
<module name="Inchoo_CustomControllers" setup_version="2.0.0"></module>
</config>
etc/frontend/routes.xml – routes configuration for frontend; for demonstration we will match “inchoofronttest” as frontname (part of url after domain name – for example: www.inchootest.net/inchoofronttest/)
<?xml version="1.0"?>
<!--
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
* Module is created for Custom Controllers demonstration
*/
-->
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
<router id="standard">
<route id="inchootestfrontend" frontName="inchoofronttest">
<module name="Inchoo_CustomControllers" />
</route>
</router>
</config>
etc/adminhtml/routes.xml – routes configuration for admin, for demonstration we will match “inchooadmintest” as frontname (part of url after /admin/)
<?xml version="1.0"?>
<!--
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
* Module is created for Custom Controllers demonstration
*/
-->
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
<router id="admin">
<route id="inchooadmintest" frontName="inchooadmintest">
<module name="Inchoo_CustomControllers" before="Magento_Backend" />
</route>
</router>
</config>
Now we will create action classes for both of the controllers configuration. On frontend let’s create action path and action class for url siteurl/inchoofronttest/demonstration/sayhello/. You can see that we need Demonstration folder (this is called action path) inside of our Controller folder, and inside that folder we need Sayhello.php controller action class:
Controller/Demonstration/Sayhello.php:
<?php
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
*/
namespace InchooCustomControllersControllerDemonstration;
class Sayhello extends MagentoFrameworkAppActionAction
{
/**
* say hello text
*/
public function execute()
{
die("Hello ;) - Inchoo\CustomControllers\Controller\Demonstration\Sayhello - execute() method");
}
}
For admin controller let’s match url siteurl/admin/inchooadmintest/demonstration/sayadmin/. For that we need Demonstration folder in Controller/Adminhtml, and inside that action class we need to create Sayadmin.php
Controller/Adminhtml/Demonstration/Sayadmin.php
<?php
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
*/
namespace InchooCustomControllersControllerAdminhtmlDemonstration;
class Sayadmin extends MagentoBackendAppAction
{
/**
* say admin text
*/
public function execute()
{
die("Admin ;) - Inchoo\CustomControllers\Controller\Adminhtml\Demonstration\Sayadmin - execute() method");
}
}
Note that by default _isAllowed will return true, and for creating custom permission you must add check under that method. For demonstration we will keep default value so user can access that controller.
Note: after you add new controller, flush everything from console in Magento root:
php bin/magento setup:upgrade
You can pull this example and install from github.
Here is the installation instruction:
Add repository to composer configuration:
composer config repositories.inchoocustomcontroller vcs git@github.com:zoransalamun/magento2-custom-controllers.git
Require new package with composer:
composer require inchoo/custom-controllers:dev-master
Enable Inchoo CustomControllers module:
php bin/magento module:enable Inchoo_CustomControllers
Flush everything:
php bin/magento setup:upgrade
I hope this article helped you understand Controllers in Magento 2.
It looks like a complicated topic, but once you create few test controllers it becomes straight forward job to do. Please leave a comment, suggestion or ask questions. 🙂
If you’re having questions or need help regarding Magento development, we would be happy to help you out by creating a detailed custom report based on our technical audit. Feel free to get in touch!