Module 2. Request Flow. Routers, Actions, Rewrites

  • Request flow overview
  • Request routing
    • Frontend Controller
    • Admin Controller
    • Forwarding and Redirection
  • Working with controller actions
  • URL Rewrites

Request flow overview

The Request Flow principle in Magento 2 is similar to one in Magento 1.x, but the code was significantly refactored. As there is no more god-object Mage class, the responsibilities for handling the request were distributed between Bootstrap, App, and FrontController. While dispatching the request, FrontController class tries to match the request against every Router configured in the system, and the first router that is able to match would resolve the Controller class name, and perform Controller::execute() method.

Previously, the functionality that loaded and rendered layout was part of the controller action class. Not anymore, now the View class is responsible for building the layout tree and rendering it.

Once the controller action is handled and supposedly rendered, which the FrontController sends the content as a Response body, to the end user as a result of the whole Request Flow.

Request routing

Step

Description

Everything starts with instantiating the Bootstrap instance, fetching the Application instance using it, then calling Bootstrap::run() method and passing the Application to it. The run() method initializes the Error Handler and the Object Manager, asserts if Magento is in maintenance more or not installed yet, and then…

…launches the Application by calling Application::launch(). There can be different application started at this stage. In case of the application that processes HTTP requests (\Magento\Framework\App\Http), the config that corresponds to the specific area is loaded, then FrontController (\Magento\Framework\App\FrontControllerInterface) is instantiated, and then…

FrontController::dispatch() is called. Inside of that method, a loop is running that will end only if request obtains isDispatched flag, or if amount of iterations exceeds 100.

Inside of the FrontController::dispatch() loop, each of the routers from the router list, is trying to match the request. If it succeeds, the match() method returns an instance of action controller. In case of any uncaught exception, a preconfigured “noroute” action will be dispatched.

Interestingly, the router loop processing is flexible: the action controller instance may either be extended from the \Magento\Framework\App\Action\AbstractAction, or simply contain a public execute() method. In first case, the Controller::dispatch() method is called, that may have different implementation but in general calls the execute() method, or the Controller::execute() method is directly called.

Inside of the Controller::execute() method, models can be instantiated to provide some business logic, request can be validated, then forwarding or redirecting may occur or the action can render a resulting HTML. For that, the instance of View class can be used to load the layout tree and instantiate all blocks inside it using View::loadLayout(), and then, when it’s appropriate, render the resulting HTML by calling View::renderLayout();

Finally, the rendered content needs to be sent to the browser. For that purpose, the Response::sendResponse() is used. It sends the rendered content as a body, and prepends it with all default and additional headers generated during the request flow processing.


Controller architecture

Controller is a class specific to a URL or group of URLs. 

In Magento 2, a controller can only process a single action. Each Controller, or Action class includes:

  • a normal __construct() constructor used for injecting all necessary dependencies (as controller class is injectable)
  • a public execute() method that represents the action and contains necessary logic
  • extra private or protected methods and properties that help implementing controller logic.

Frontend Controller

Usually, frontend-related controllers extend \Magento\Framework\App\Action\Action, which, in its turn, is extended from two other abstract classes that implement \Magento\Framework\App\ActionInterface. It’s very important to extend your frontend controllers from \Magento\Framework\App\Action\Action as it implements dispatch() method and extra important methods used by developers. The Router calls dispatch(), not execute() method when routing happens.

Sometimes, when it’s impossible to extend \Magento\Framework\App\Action\Action, another class implementing \Magento\Framework\App\ActionInterface can be used. In that case the developer must implement the dispatch() method on his/her own.

Admin Controller

The difference of admin controller is that it requires to check a user permissions to decide whether the user is eligible to visit this controller. It also implements some admin panel-specific functionality. For that purpose, they are extended from \Magento\Backend\App\Action which has a modified dispatch() method which checks if the user logged in, and calls $this->_isAllowed() that checks if the ACL role of the current user is linked with the action to allow using it.

The admin action controller also has its own implementation for redirect() and forward() methods, as well as knows how to handle form keys.

There are also some auxiliary methods that can be used in the admin action controllers:

  • _getSession()
  • _addBreadcrumb()
  • _addJs()
  • _addContent()
  • _addLeft()
  • _getUrl()

Forwarding and Redirection

  • _forward() — is delegating execution to another action that correspond to the specified path. This happens inside of the FrontController and routing loop, it simply modifies the request object, tells the FrontController that the action was not dispatched, and route matching process starts anew.
  • _redirect() — is returning a Response object with redirect header set, after returning that object in the action, a real redirection is initiated instead of rendering a page.

Working with controller actions

In order to create a new action controller, three steps must be done:

  1. Create a route.xml config file
  2. Create a correct action class and implement an execute() method
  3. Test the controller

Here are examples for router configurations and action controller classes for Magento_Catalog module:

Frontend

Admin

URL: /catalog/product/view

URL: /<adminPath>/catalog/product/upsell

1

2

3

4

5

6

7

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

    <router id=“standard”>

        <route id=“catalog” frontName=“catalog”>

            <module name=“Magento_Catalog” />

        </route>

    </router>

</config

1

2

3

4

5

6

7

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

    <router id=“admin”>

        <route id=“catalog” frontName=“catalog”>

            <module name=“Magento_Catalog” before=“Magento_Backend” />

        </route>

    </router>

</config>

\Magento\Catalog\Controller\Product\View

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

namespace Magento\Catalog\Controller\Product;

 

use Magento\Framework\App\Action\Context;

use Magento\Framework\View\Result\PageFactory;

 

class View extends \Magento\Catalog\Controller\Product

{

    /**

     * @var \Magento\Catalog\Helper\Product\View

     */

    protected $viewHelper;

 

    /**

     * @var \Magento\Framework\Controller\Result\ForwardFactory

     */

    protected $resultForwardFactory;

 

    /**

     * @var \Magento\Framework\View\Result\PageFactory

     */

    protected $resultPageFactory;

 

    /**

     * Constructor

     *

     * @param Context $context

     * @param \Magento\Catalog\Helper\Product\View $viewHelper

     * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory

     * @param PageFactory $resultPageFactory

     */

    public function __construct(

        Context $context,

        \Magento\Catalog\Helper\Product\View $viewHelper,

        \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory,

        PageFactory $resultPageFactory

    ) {

        $this->viewHelper = $viewHelper;

        $this->resultForwardFactory = $resultForwardFactory;

        $this->resultPageFactory = $resultPageFactory;

        parent::__construct($context);

    }

 

    /**

     * Redirect if product failed to load

     *

     * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\Controller\Result\Forward

     */

    protected function noProductRedirect()

    {

        $store = $this->getRequest()->getQuery(‘store’);

        if (isset($store) && !$this->getResponse()->isRedirect()) {

            $resultRedirect = $this->resultRedirectFactory->create();

            return $resultRedirect->setPath();

        } elseif (!$this->getResponse()->isRedirect()) {

            $resultForward = $this->resultForwardFactory->create();

            $resultForward->forward(‘noroute’);

            return $resultForward;

        }

    }

 

    /**

     * Product view action

     *

     * @return \Magento\Framework\Controller\Result\Forward|\Magento\Framework\Controller\Result\Redirect

     */

    public function execute()

    {

        // Get initial data from request

        $categoryId = (int) $this->getRequest()->getParam(‘category’, false);

        $productId = (int) $this->getRequest()->getParam(‘id’);

        $specifyOptions = $this->getRequest()->getParam(‘options’);

 

        if ($this->getRequest()->isPost() && $this->getRequest()->getParam(self::PARAM_NAME_URL_ENCODED)) {

            $product = $this->_initProduct();

            if (!$product) {

                return $this->noProductRedirect();

            }

            if ($specifyOptions) {

                $notice = $product->getTypeInstance()->getSpecifyOptionMessage();

                $this->messageManager->addNotice($notice);

            }

            if ($this->getRequest()->isAjax()) {

                $this->getResponse()->representJson(

                    $this->_objectManager->get(‘Magento\Framework\Json\Helper\Data’)->jsonEncode([

                        ‘backUrl’ => $this->_redirect->getRedirectUrl()

                    ])

                );

                return;

            }

            $resultRedirect = $this->resultRedirectFactory->create();

            $resultRedirect->setRefererOrBaseUrl();

            return $resultRedirect;

        }

 

        // Prepare helper and params

        $params = new \Magento\Framework\DataObject();

        $params->setCategoryId($categoryId);

        $params->setSpecifyOptions($specifyOptions);

 

        // Render page

        try {

            $page = $this->resultPageFactory->create(false, [‘isIsolated’ => true]);

            $this->viewHelper->prepareAndRender($page, $productId, $this, $params);

            return $page;

        } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {

            return $this->noProductRedirect();

        } catch (\Exception $e) {

            $this->_objectManager->get(‘Psr\Log\LoggerInterface’)->critical($e);

            $resultForward = $this->resultForwardFactory->create();

            $resultForward->forward(‘noroute’);

            return $resultForward;

        }

    }

\Magento\Catalog\Controller\Adminhtml\Product\Upsell

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

namespace Magento\Catalog\Controller\Adminhtml\Product;

 

class Upsell extends \Magento\Catalog\Controller\Adminhtml\Product

{

    /**

     * @var \Magento\Framework\View\Result\LayoutFactory

     */

    protected $resultLayoutFactory;

 

    /**

     * @param \Magento\Backend\App\Action\Context $context

     * @param \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder

     * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory

     */

    public function __construct(

        \Magento\Backend\App\Action\Context $context,

        \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder,

        \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory

    ) {

        parent::__construct($context, $productBuilder);

        $this->resultLayoutFactory = $resultLayoutFactory;

    }

 

    /**

     * Get upsell products grid and serializer block

     *

     * @return \Magento\Framework\View\Result\Layout

     */

    public function execute()

    {

        $this->productBuilder->build($this->getRequest());

        $resultLayout = $this->resultLayoutFactory->create();

        $resultLayout->getLayout()->getBlock(‘catalog.product.edit.tab.upsell’)

            ->setProductsUpsell($this->getRequest()->getPost(‘products_upsell’, null));

        return $resultLayout;

    }

The simplest way to test an action controller is to try the URL that corresponds the action. Of course testing requires more preliminaries in case of admin controller.

Controller classes are injectable. This means that to customize a controller, you can create a preference or plugin.

URL Rewrites

The process of URL rewriting is used to make complicated or human unreadable URL addresses more user-friendly by making them shorter, more descriptive and easier to remember.

Magento allows you to specify a so-called URL key on every static, content, product and category page. URL key can become a part of a rewrite URL, if it’s enabled in each case.

How the URL rewriting works? When during routing process no routers respond with a match for a controller action, before letting default router to forward to the “noroute” action, a URL rewrite router takes its chances. The \Magento\UrlRewrite\Controller\Router class tries to match the URL by request paths specified in the url_rewrite data table.

\Magento\UrlRewrite\Controller\Router

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

/**

 * Match corresponding URL Rewrite and modify request

 *

 * @param \Magento\Framework\App\RequestInterface $request

 * @return \Magento\Framework\App\ActionInterface|null

 */

public function match(\Magento\Framework\App\RequestInterface $request)

{

    if ($fromStore = $request->getParam(‘___from_store’)) {

        $oldStoreId = $this->storeManager->getStore($fromStore)->getId();

        $oldRewrite = $this->getRewrite($request->getPathInfo(), $oldStoreId);

        if ($oldRewrite) {

            $rewrite = $this->urlFinder->findOneByData(

                [

                    UrlRewrite::ENTITY_TYPE => $oldRewrite->getEntityType(),

                    UrlRewrite::ENTITY_ID => $oldRewrite->getEntityId(),

                    UrlRewrite::STORE_ID => $this->storeManager->getStore()->getId(),

                    UrlRewrite::IS_AUTOGENERATED => 1,

                ]

            );

            if ($rewrite && $rewrite->getRequestPath() !== $oldRewrite->getRequestPath()) {

                return $this->redirect($request, $rewrite->getRequestPath(), OptionProvider::TEMPORARY);

            }

        }

    }

    $rewrite = $this->getRewrite($request->getPathInfo(), $this->storeManager->getStore()->getId());

    if ($rewrite === null) {

        return null;

    }

 

    if ($rewrite->getRedirectType()) {

        return $this->processRedirect($request, $rewrite);

    }

 

    $request->setAlias(\Magento\Framework\UrlInterface::REWRITE_REQUEST_PATH_ALIAS, $rewrite->getRequestPath());

    $request->setPathInfo(‘/’ . $rewrite->getTargetPath());

    return $this->actionFactory->create(‘Magento\Framework\App\Action\Forward’);

}

When it’s possible to fetch a rewrite data that corresponds the current request path, a field redirect_type is checked. In case it’s “1”, a redirect happens, otherwise a forwarding action is created from action factory, and it gets forwarded with an updated request info.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s