Magento 2 Controllers

controllers

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 \Magento\Framework\App\Action\Action 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 \Magento\Framework\App\Action\Action which every controller class extends. It’s called by \Magento\Framework\App\Action\Action::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).

\Magento\Framework\App\Action\Action

This is main Magento framework action class and every controller must extend this class (admin controllers are extending \Magento\Backend\App\Action which extends \Magento\Framework\App\Action\Action). 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 (Magento\Framework\App\FrontController)

Magento\Framework\App\FrontController::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 \Magento\Backend\App\Action), 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
        );
        \Magento\Framework\Profiler::start($profilerKey);
 
        $result = null;
        if ($request->isDispatched() && !$this->_actionFlag->get('', self::FLAG_NO_DISPATCH)) {
            \Magento\Framework\Profiler::start('action_body');
            $result = $this->execute();
            \Magento\Framework\Profiler::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);
            }
            \Magento\Framework\Profiler::stop('postdispatch');
            \Magento\Framework\Profiler::stop('action_body');
        }
        \Magento\Framework\Profiler::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 \Magento\Framework\App\Action\Action:

$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 \Magento\Framework\App\RouterInterface $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 (\Magento\Framework\App\ActionFactory) 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 \Magento\Framework\App\Action\Action class, but admin controller extend \Magento\Backend\App\Action class, which extends \Magento\Framework\App\Action\Action. In admin controller dispatch, redirect and rewrite methods are rewritten to provide logic for checking ACL (Access control list).

Admin controller

It extends \Magento\Backend\App\Action 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(\Magento\Framework\App\RequestInterface $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 Magento\Framework\App\Action\Action, 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, Magento\Catalog\Controller\Product which is action wrapper class, and it’s used in many action classes located in Catalog\Controller\* folder (action classes: Magento\Catalog\Controller\Product\Compare, Magento\Catalog\Controller\Product\Gallery 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 Magento\Catalog\Controller;
 
use Magento\Catalog\Controller\Product\View\ViewInterface;
use Magento\Catalog\Model\Product as ModelProduct;
 
abstract class Product extends \Magento\Framework\App\Action\Action 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 \Magento\Framework\DataObject();
        $params->setCategoryId($categoryId);
 
        /** @var \Magento\Catalog\Helper\Product $product */
        $product = $this->_objectManager->get('Magento\Catalog\Helper\Product');
        return $product->initProduct($productId, $this, $params);
    }
}

How to create custom controller

  1. Create routes.xml in etc/frontend or etc/adminhtml folder (first one is for frontend and second one is for admin controller).
  2. 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
  3. 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 Inchoo\CustomControllers\Controller\Demonstration;
 
class Sayhello extends \Magento\Framework\App\Action\Action
{
    /**
     * 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 Inchoo\CustomControllers\Controller\Adminhtml\Demonstration;
 
class Sayadmin extends \Magento\Backend\App\Action
{
    /**
     * 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. 🙂


About Zoran Salamun

Backend Developer

Zoran strives to do his best in everything he attempts. He likes to play PC games, but since he doesn’t know when to stop, it always ends up with deleting them.

Read more posts by Zoran / Visit Zoran's profile

11 comments

  1. Love the way the title image combines the Magento Logo with a 60ies style Chernoble interface.

    …and thanks for the article, it really helps me get more control over this over-engineered software

  2. Thanks for the great article! The quote below supposed to be the first two lines of this article 🙂

    > Note: after you add new controller, flush everything from console in Magento root:
    >>> php bin/magento setup:upgrade

  3. Hello,
    i am new in magento2 backhand development.
    I want to create a simple module to save config data in backhand database. But am not connecting view with controller.
    Plz can you provide me best suggestion to creating it.

  4. Please disregard my first reply.
    (## 1)
    <config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd”>

    (## 2)
    <config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:App/etc/routes.xsd”>

  5. Hi Zoran,
    Great post. Simple and straight forward.
    I am just starting with “Magento2”.

    One question though, which of the following is a better declaration “(## 1)” OR “(## 2)”?
    The main difference between the two being the “xsi:noNamespaceSchemaLocation” bit.

    I found the “(## 2)” declaration in different “Magento2 modules” that came with the installation,
    but when Googled custom modules found that everybody was using “(## 1)” kind of declaration [even in your post],
    which is confusing for me as “Magento2 beginner”.

    (## 1)

    (## 2)

    1. (## 1)
      >config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd”<

      (## 2)
      >config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:App/etc/routes.xsd”<

  6. Hey!
    Thanks for the article.
    I added a menu to the admin menu panel, and want to put our company’s page inside that page. I managed to create a new customed page, But I’m having trouble with keeping the whole admin layout intact. (Menu, breadcrumbs, profile menu, colors, so on.
    Tried to follow the other default pages but couldn’t understand what should be inside my execute method in order to make that happen.
    Thanks!

  7. Hey, great tutorial.
    I have one question:
    I’ve implemented a module with some backend controllers. How can I access an backend controller with jQuery ( POST or GET request)?

    I have read in your tutorial, that backend controllers will check the login status and redirect to login page in error case. This is my problem, when I proceed the request, magento backend will redirect me to login page.

    Could you please provide an sample with an GET/POST-Request against an Backend Controller with jQuery/Javascript?

    Thanks in advance.
    Ben

    1. Hi Ben
      I don’t understand how and from where you want to access to backend controller, if you want to access remotely to controller, than it shouldn’t be to backend controller, you have front controllers for that.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.