Create Controllers
Understanding controllers as entry points for custom functionality via URLs.
Controller Architecture
1. URL Structure and Routing
URL Breakdown
A standard Magento URL has three key segments:
| Segment | Configuration Source | Example | 
|---|---|---|
| 1. Front Name | Defined in routes.xml | blog | 
| 2. Directory | Maps to directory in Controller/ | postâController/Post/ | 
| 3. Action Class | Maps to PHP class file | viewâView.php | 
URL: https://example.com/blog/post/view/id/123
File: Bonlineco/Blog/Controller/Post/View.php
bonlineco_blog_post_view= route_id + directory + action_class
Route Configuration
<!-- etc/frontend/routes.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <router id="standard">
        <route id="bonlineco_blog" frontName="blog">
            <module name="Bonlineco_Blog" />
        </route>
    </router>
</config>2. Controller Anatomy
- Implement ActionInterface(orHttpGetActionInterface)
- Have execute()method
- Inject Contextin constructor
- Return ResultInterfaceorHttpInterface
Controller Example
<?php
namespace Bonlineco\Blog\Controller\Post;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class View implements HttpGetActionInterface
{
    private $pageFactory;
    private $context;
    public function __construct(Context $context, PageFactory $pageFactory)
    {
        $this->context = $context;
        $this->pageFactory = $pageFactory;
    }
    public function execute()
    {
        $postId = $this->context->getRequest()->getParam('id');
        return $this->pageFactory->create();
    }
}3. Response Types
đ Page
PageFactory - Render HTML page
return $this->pageFactory->create();âŠī¸ Redirect
RedirectFactory - 301/302 redirect
return $this->redirectFactory
    ->create()
    ->setPath('blog/post/list');⊠Forward
ForwardFactory - Internal transfer
return $this->forwardFactory
    ->create()
    ->forward('noroute');đ§ Raw
RawFactory - Arbitrary data
return $this->rawFactory
    ->create()
    ->setContents('Custom');Don't return JSON from controllers - use REST API instead for authentication, structure, and Extension Attributes support.
4. Separation of Concerns
Best Practice Architecture
A key principle in Magento development is separating the handling of requests from data fetching and rendering.
- Minimal logic
- Call factories
- Return result
- Load from database
- Validate data
- Throw exceptions
- Format data
- Business logic
- Template helpers
Example: Clean Controller
Controller/Post/View.php - Minimal Logic
<?php
namespace Bonlineco\Blog\Controller\Post;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\PageFactory;
use Bonlineco\Blog\Model\PostDisplayRequest;
class View implements HttpGetActionInterface
{
    private $pageFactory;
    private $postDisplayRequest;
    public function __construct(
        PageFactory $pageFactory,
        PostDisplayRequest $postDisplayRequest
    ) {
        $this->pageFactory = $pageFactory;
        $this->postDisplayRequest = $postDisplayRequest;
    }
    public function execute()
    {
        // Validate request (throws NotFoundException if invalid)
        $this->postDisplayRequest->validate();
        
        // Just return the page - View Model handles data loading
        return $this->pageFactory->create();
    }
}Data Class Example
Model/PostDisplayRequest.php - Data Loading & Validation
<?php
namespace Bonlineco\Blog\Model;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Exception\NotFoundException;
use Bonlineco\Blog\Api\PostRepositoryInterface;
class PostDisplayRequest
{
    private $request;
    private $postRepository;
    public function __construct(
        RequestInterface $request,
        PostRepositoryInterface $postRepository
    ) {
        $this->request = $request;
        $this->postRepository = $postRepository;
    }
    /**
     * Validate that the post exists and is active
     * @throws NotFoundException
     */
    public function validate()
    {
        $postId = $this->request->getParam('id');
        
        if (!$postId) {
            throw new NotFoundException(__('Post ID is required'));
        }
        
        $post = $this->postRepository->getById($postId);
        
        if (!$post->getId()) {
            throw new NotFoundException(__('Post not found'));
        }
        
        if (!$post->isActive()) {
            throw new NotFoundException(__('Post is not available'));
        }
    }
    
    /**
     * Get the requested post
     */
    public function getPost()
    {
        $postId = $this->request->getParam('id');
        return $this->postRepository->getById($postId);
    }
}NotFoundException is a valid way to return a different response. Magento's exception handler catches it and displays a 404 page automatically.
    View Model Example
ViewModel/PostView.php - Format Data for Template
<?php
namespace Bonlineco\Blog\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Bonlineco\Blog\Model\PostDisplayRequest;
class PostView implements ArgumentInterface
{
    private $postDisplayRequest;
    public function __construct(
        PostDisplayRequest $postDisplayRequest
    ) {
        $this->postDisplayRequest = $postDisplayRequest;
    }
    /**
     * Get the post
     */
    public function getPost()
    {
        return $this->postDisplayRequest->getPost();
    }
    
    /**
     * Get formatted creation date
     */
    public function getFormattedDate()
    {
        $post = $this->getPost();
        return $post->getCreatedAt()->format('F j, Y');
    }
    
    /**
     * Get post excerpt
     */
    public function getExcerpt($length = 150)
    {
        $content = $this->getPost()->getContent();
        return substr(strip_tags($content), 0, $length) . '...';
    }
    
    /**
     * Check if post has featured image
     */
    public function hasFeaturedImage()
    {
        return !empty($this->getPost()->getFeaturedImage());
    }
}<!-- In .phtml template -->
<?php /** @var $viewModel \Bonlineco\Blog\ViewModel\PostView */ ?>
<h1><?= $viewModel->getPost()->getTitle() ?></h1>
<p>Published: <?= $viewModel->getFormattedDate() ?></p>
<p><?= $viewModel->getExcerpt() ?></p>5. Admin Controllers
Security is MANDATORY for Admin Controllers
Admin controllers require additional security measures to prevent unauthorized access.
Admin Controller Requirements
| Requirement | Details | 
|---|---|
| 1. Route Location | etc/adminhtml/routes.xml(not frontend) | 
| 2. Class Location | Controller/Adminhtml/... | 
| 3. ACL Constant | const ADMIN_RESOURCE = 'Resource_Id';(MANDATORY) | 
| 4. Extend Backend Action | Extend \Magento\Backend\App\Action | 
| 5. URL Access | Via generated links with secret keys | 
1. Admin Route Configuration
etc/adminhtml/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    
    <router id="admin">
        <route id="bonlineco_blog" frontName="blog">
            <module name="Bonlineco_Blog" before="Magento_Backend" />
        </route>
    </router>
    
</config>https://example.com/admin/blog/post/edit/key/abc123Note the
/admin/ prefix and secret /key/abc123
    2. Complete Admin Controller
Controller/Adminhtml/Post/Edit.php
<?php
namespace Bonlineco\Blog\Controller\Adminhtml\Post;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Edit extends Action
{
    /**
     * Authorization level - MANDATORY
     * Maps to ACL resource in etc/acl.xml
     */
    const ADMIN_RESOURCE = 'Bonlineco_Blog::post_save';
    /**
     * @var PageFactory
     */
    private $pageFactory;
    /**
     * Constructor
     */
    public function __construct(
        Context $context,
        PageFactory $pageFactory
    ) {
        parent::__construct($context);
        $this->pageFactory = $pageFactory;
    }
    /**
     * Execute action
     */
    public function execute()
    {
        $postId = $this->getRequest()->getParam('id');
        
        $page = $this->pageFactory->create();
        $page->getConfig()->getTitle()->prepend(__('Edit Post'));
        
        return $page;
    }
    
    /**
     * Optional: Override for custom authorization logic
     */
    protected function _isAllowed()
    {
        // Check standard ACL
        $isAllowed = parent::_isAllowed();
        
        // Add custom checks
        if ($isAllowed) {
            $postId = $this->getRequest()->getParam('id');
            // Custom logic: check if user can edit this specific post
            // e.g., check ownership, status, etc.
        }
        
        return $isAllowed;
    }
}ADMIN_RESOURCE constant is checked automatically. If the admin user's role doesn't have permission for this resource, access is denied with a 403 error.
    3. ACL Configuration
etc/acl.xml - Define Admin Resources
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Bonlineco_Blog::blog" title="Blog" sortOrder="50">
                    <resource id="Bonlineco_Blog::post" title="Posts">
                        <resource id="Bonlineco_Blog::post_view" title="View Post" />
                        <resource id="Bonlineco_Blog::post_save" title="Save Post" />
                        <resource id="Bonlineco_Blog::post_delete" title="Delete Post" />
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
    
</config>- Define resources in acl.xml
- Assign resources to admin roles in System â User Roles
- Controller checks ADMIN_RESOURCEagainst user's permissions
- Access granted or denied based on role
4. Admin Menu Configuration
etc/adminhtml/menu.xml - Add Menu Items
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    
    <menu>
        <add id="Bonlineco_Blog::blog" 
             title="Blog" 
             module="Bonlineco_Blog" 
             sortOrder="50" 
             resource="Bonlineco_Blog::blog"/>
        
        <add id="Bonlineco_Blog::post_list" 
             title="Manage Posts" 
             module="Bonlineco_Blog" 
             sortOrder="10" 
             parent="Bonlineco_Blog::blog" 
             action="blog/post/index" 
             resource="Bonlineco_Blog::post_view"/>
    </menu>
    
</config>5. Generating Admin URLs in Code
When you need to generate admin URLs programmatically:
<?php
use Magento\Backend\Model\UrlInterface;
class Example
{
    private $backendUrl;
    
    public function __construct(UrlInterface $backendUrl)
    {
        $this->backendUrl = $backendUrl;
    }
    
    public function getEditUrl($postId)
    {
        return $this->backendUrl->getUrl(
            'blog/post/edit',
            ['id' => $postId]
        );
    }
}
// Generated URL:
// https://example.com/admin/blog/post/edit/id/123/key/abc123def456UrlInterface to ensure secret keys are included.
    Exam Tips
- Know the 3 URL segments: frontName, directory, action class
- Controllers must implement ActionInterface and have execute() method
- Context must be injected in constructor
- Layout handle = route_id + directory + action
- Page, Redirect, Forward, Raw are main response types
- Don't return JSON from controllers - use REST API
- Separate concerns: Controller â Data Class â View Model
- Admin controllers need ADMIN_RESOURCE constant
- Admin URLs have secret keys, access via generated links
- Throw NotFoundException for missing resources
