1.05

Create Controllers

Understanding controllers as entry points for custom functionality via URLs.

Why This Matters: Controllers are the entry points for all custom functionality accessed via URLs in Magento. They handle requests and prepare responses for both storefront and Admin Panel.

Controller Architecture

mindmap root((Controllers)) URL Structure Front Name routes.xml Directory Controller folder Action Class PHP file Requirements ActionInterface execute method Context injection Return ResultInterface Response Types Page HTML render Redirect 301/302 Forward Internal transfer JSON Use REST API Raw Arbitrary data Admin Controllers adminhtml/routes.xml ADMIN_RESOURCE ACL enforcement Secret key URLs

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

Layout Handle: 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

✅ Required Elements:
  1. Implement ActionInterface (or HttpGetActionInterface)
  2. Have execute() method
  3. Inject Context in constructor
  4. Return ResultInterface or HttpInterface

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');
âš ī¸ JSON Response = Code Smell!

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.

Controller
  • Minimal logic
  • Call factories
  • Return result
Data Class
  • Load from database
  • Validate data
  • Throw exceptions
View Model
  • 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);
    }
}
Key Point: Throwing 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());
    }
}
Template Usage:
<!-- 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>
Admin URL: https://example.com/admin/blog/post/edit/key/abc123
Note 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;
    }
}
Important: The 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>
How It Works:
  1. Define resources in acl.xml
  2. Assign resources to admin roles in System → User Roles
  3. Controller checks ADMIN_RESOURCE against user's permissions
  4. 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>
Menu URLs are auto-generated with secret keys - no need to manually construct them!

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/abc123def456
Never manually construct admin URLs! Always use UrlInterface 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