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
Actionsuffix 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?
2. Which interface should a POST-only controller action implement?
3. How do you return JSON from a controller action?
Flashcards
Question
What does ResultFactory create?
Click to reveal answer
Answer
Different response types: Page, Json, Redirect, Forward, Raw
Question
What is the Action suffix convention?
Click to reveal answer
Answer
Class name includes Action (e.g., ViewAction) but URL omits it (e.g., /view)
Question
When should you use forward vs redirect?
Click to reveal answer
Answer
Forward for internal logic transfer (same request), redirect for post/redirect/get pattern
Question
How do controller actions restrict HTTP methods?
Click to reveal answer
Answer
By implementing HttpGetActionInterface or HttpPostActionInterface
Question
What is ResultFactory in Magento controllers?
Click to reveal answer
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')