Skip to content
intermediate Phase 22 · Module Directories Deep Dive

Controller Directory - Module Directories Deep Dive

Understanding the Magento 2 Controller directory: Action classes, ResultFactory, forwards, redirects, and controller action patterns

45m
0 problems
Topic Progress 0%

Controller Directory Structure

Directory Layout

Controllers live under the module's Controller directory, organized by path segments:

Vendor/Module/Controller/
├── Product/
│   ├── View.php          → catalog/product/view
│   ├── Index.php         → catalog/product/index
│   └── Edit/
│       ├── Index.php     → catalog/product/edit/index
│       └── Save.php      → catalog/product/edit/save
├── Ajax/
│   └── Search.php        → catalog/ajax/search
└── Index.php             → catalog/index (root action)

Controller Naming

  • Each action is a separate class
  • Class name matches the action name: ViewAction, IndexAction, SaveAction
  • The Action suffix is required but implied in the URL
// URL: catalog/product/view
// File: Controller/Product/View.php
namespace Vendor\Catalog\Controller\Product;

class View extends \Magento\Framework\App\Action\Action
{
    public function execute()
    {
        // ...
    }
}

HTTP Method Interfaces

Actions should implement the appropriate HTTP method interface:

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\HttpPostActionInterface;

// GET-only action
class View implements HttpGetActionInterface
{
    public function execute() { /* ... */ }
}

// POST-only action
class Save implements HttpPostActionInterface
{
    public function execute() { /* ... */ }
}

// Both GET and POST
class Edit implements HttpGetActionInterface, HttpPostActionInterface
{
    public function execute() { /* ... */ }
}

This prevents unauthorized HTTP methods from reaching your action.

ResultFactory - Creating Responses

ResultFactory Types

ResultFactory creates different response types for controller actions:

Type Use Case
Page Render a full page with layout
Json Return JSON data (AJAX/API)
Redirect Redirect to another URL
Forward Forward to another controller internally
Raw Send raw content with custom headers

Page Result

namespace Vendor\Module\Controller\Index;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\Result\Page;
use Magento\Framework\Result\PageFactory;

class Index implements HttpGetActionInterface
{
    public function __construct(
        private PageFactory $resultPageFactory
    ) {}

    public function execute(): Page
    {
        $page = $this->resultPageFactory->create();
        $page->getConfig()->getTitle()->set(__('My Custom Page'));
        return $page;
    }
}

JSON Result

namespace Vendor\Module\Controller\Ajax;

use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\Result\JsonFactory;

class Search implements HttpPostActionInterface
{
    public function __construct(
        private JsonFactory $resultJsonFactory
    ) {}

    public function execute()
    {
        $searchTerm = $this->getRequest()->getParam('q');
        $results = $this->searchService->search($searchTerm);

        $result = $this->resultJsonFactory->create();
        $result->setData([
            'success' => true,
            'results' => $results,
            'count' => count($results),
        ]);
        return $result;
    }
}

Redirect Result

use Magento\Framework\Controller\Result\RedirectFactory;

class Save implements HttpPostActionInterface
{
    public function __construct(
        private RedirectFactory $resultRedirectFactory
    ) {}

    public function execute()
    {
        try {
            $this->saveData();
            $resultRedirect = $this->resultRedirectFactory->create();
            $resultRedirect->setPath('vendor/module/index');
            $resultRedirect->setData(['success' => true]);
            return $resultRedirect;
        } catch (\Exception $e) {
            $resultRedirect = $this->resultRedirectFactory->create();
            $resultRedirect->setPath('vendor/module/index');
            $resultRedirect->setData(['error' => true]);
            return $resultRedirect;
        }
    }
}

Redirect paths:

// Absolute path
$resultRedirect->setPath('catalog/product/view', ['id' => 1]);

// Referer URL
$resultRedirect->setUrl($this->_redirect->getRefererUrl());

// Back to form
$resultRedirect->setUrl($this->_redirect->getRedirectUrl());

Forward vs Redirect

Forward (Internal Transfer)

A forward passes the request to another controller without a new HTTP request. The URL doesn't change:

use Magento\Framework\Controller\Result\ForwardFactory;

class Check implements HttpGetActionInterface
{
    public function __construct(
        private ForwardFactory $resultForwardFactory
    ) {}

    public function execute()
    {
        $isValid = $this->validateRequest();

        if (!$isValid) {
            $forward = $this->resultForwardFactory->create();
            $forward->setController('index');
            $forward->setAction('index');
            $forward->forward('index');
            return $forward;
        }

        // Continue processing...
    }
}

Redirect (HTTP 302)

A redirect sends a new HTTP request. The browser URL changes:

public function execute()
{
    $resultRedirect = $this->resultRedirectFactory->create();
    $resultRedirect->setPath('catalog/product/view', ['id' => $productId]);
    return $resultRedirect;
}

Key Differences

Aspect Forward Redirect
HTTP Request Same request New request
URL Changes No Yes
Browser History No entry Entry added
Performance Faster Slower
Use Case Internal logic Post/Redirect/Get

Session Messages with Redirects

use Magento\Framework\Message\ManagerInterface;

class Save implements HttpPostActionInterface
{
    public function __construct(
        private RedirectFactory $resultRedirectFactory,
        private ManagerInterface $messageManager
    ) {}

    public function execute()
    {
        try {
            $this->saveData();
            $this->messageManager->addSuccessMessage(__('Item saved successfully.'));
        } catch (\Exception $e) {
            $this->messageManager->errorMessage(__('Error saving item: %1', $e->getMessage()));
        }

        return $this->resultRedirectFactory->create()->setPath('vendor/module/index');
    }
}

Messages persist across the redirect via the session.

Controller Lifecycle and Request Handling

Request Object

Access request data in controllers:

public function execute()
{
    $request = $this->getRequest();

    // GET/POST params
    $id = $request->getParam('id');
    $name = $request->getParam('name', 'default');

    // HTTP method
    $method = $request->getMethod(); // GET, POST, PUT, DELETE

    // Headers
    $contentType = $request->getHeader('Content-Type');

    // Full body (for API)
    $body = $request->getContent();

    // Check if AJAX
    $isAjax = $request->isAjax();
}

CSRF Protection

Magento automatically validates form keys on POST requests. To skip validation:

use Magento\Framework\App\Action\Context;

class Save implements HttpPostActionInterface
{
    public function __construct(
        Context $context,
        // ...
    ) {
        parent::__construct($context);
    }

    public function execute()
    {
        // If you need to bypass CSRF for API-style requests:
        $this->_validateFormKey();
        // Or skip it entirely by not calling parent
    }
}

Controller ACL

Admin controllers should implement ACL checking:

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;

class Save extends Action
{
    const ADMIN_RESOURCE = 'Vendor_Module::manage';

    public function __construct(
        Context $context,
        // ...
    ) {
        parent::__construct($context);
    }

    public function execute()
    {
        // ACL check is automatic based on ADMIN_RESOURCE
    }
}

Dependency Injection in Controllers

Controllers support full DI:

namespace Vendor\Module\Controller\Product;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Result\PageFactory;
use Vendor\Module\Service\ProductService;

class View implements HttpGetActionInterface
{
    public function __construct(
        private Context $context,
        private PageFactory $resultPageFactory,
        private ProductService $productService
    ) {}

    public function execute()
    {
        $productId = $this->getRequest()->getParam('id');
        $product = $this->productService->getById($productId);

        $page = $this->resultPageFactory->create();
        $page->getLayout()->getBlock('product.view')
            ->setData('product', $product);

        return $page;
    }
}

Complete CRUD Controller Example

// IndexAction - List items
class Index implements HttpGetActionInterface {
    public function execute(): Page { /* list page */ }
}

// ViewAction - Show single item
class View implements HttpGetActionInterface {
    public function execute(): Page { /* detail page */ }
}

// NewAction - Show create form
class NewAction implements HttpGetActionInterface {
    public function execute(): Page { /* form page */ }
}

// SaveAction - Process create/edit form
class Save implements HttpPostActionInterface {
    public function execute() { /* save & redirect */ }
}

// DeleteAction - Remove item
class Delete implements HttpPostActionInterface {
    public function execute() { /* delete & redirect */ }
}

Quiz

1. What is the difference between forward and redirect in a controller?

Question 1 options

2. Which interface should a POST-only controller action implement?

Question 2 options

3. How do you return JSON from a controller action?

Question 3 options

Flashcards

Question

What does ResultFactory create?

Answer

Different response types: Page, Json, Redirect, Forward, Raw

Question

What is the Action suffix convention?

Answer

Class name includes Action (e.g., ViewAction) but URL omits it (e.g., /view)

Question

When should you use forward vs redirect?

Answer

Forward for internal logic transfer (same request), redirect for post/redirect/get pattern

Question

How do controller actions restrict HTTP methods?

Answer

By implementing HttpGetActionInterface or HttpPostActionInterface

Question

What is ResultFactory in Magento controllers?

Answer

A factory that creates response objects (Page, Json, Redirect, Forward) for controller actions

Revision Notes

Key Takeaways

  • 1. Controllers live under Controller/ directory with action classes per URL segment
  • 2. ResultFactory creates different response types: Page, Json, Redirect, Forward
  • 3. Use HTTP method interfaces to restrict allowed request methods
  • 4. Forward is internal (same request), redirect sends new HTTP request
  • 5. Controllers support full dependency injection and session messages

Interview Tips

  • Explain the forward vs redirect trade-offs
  • Know how to return different response types (page, JSON, redirect)
  • Discuss CSRF protection and form key validation
  • Be ready to design CRUD controller structure for a module

Cheat Sheet

Controller structure:
  Vendor/Module/Controller/Path/Action.php
  implements HttpGetActionInterface or HttpPostActionInterface

Result types:
  PageFactory → full page
  JsonFactory → JSON response
  RedirectFactory → HTTP redirect
  ForwardFactory → internal forward

Access params:
  $this->getRequest()->getParam('key')