Categories


Archives


Recent Posts


Categories


Magento 2 Controller Result Objects

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

If you’ve read previous articles on the subject, you know that every URL in Magento 2 corresponds to a single controller file, and each controller file has a single execute method. This execute method is responsible for returning a “result” object (i.e. an object that implements the MagentoFrameworkControllerResultInterface` interface).

Most standard tutorials outline how to return a page result object. However, there’s a number of different objects you can return from execute for different sorts of result types.

Unfortunately, Magento 2’s java/spring like approach to PHP development has created a number of redundancies in the core code, and there’s no clear path forward for third party developers looking to take advantage of these different response type. This article attempts to sort out your options so you can make an intelligent choice on your own.

This article assumes a familiarity with Magento’s object manager and automatic constructor dependency injection features. Also, this article is not intended as a comprehensive guide to the result object hierarchy, so proceed with all due caution.

Page Factories

The best place to start is the aforementioned page result object. You can create a page result by injecting a page factory in your controller’s constructor.

public function __construct(
    //...
    $pageFactory MagentoFrameworkViewResultPageFactory
    //...        
)
{
    //...    
    $this->pageResultFactory = $pageFactory
    //...        
}

and then using the page factory to create your response object in execute

public function execute()
{
    //...
    return $this->pageResultFactory->create();
    //...
}

When you return a page result object from your execute method, you’re telling Magento to kick off the standard layout handle XML file page rendering.

Different Results on the Frontend and Backend

If you look at a page factory’s source

#File: vendor/magento/framework/View/Result/PageFactory.php
//...
public function __construct(
    ObjectManagerInterface $objectManager,
    $instanceName = 'MagentoFrameworkViewResultPage'
) {
    $this->objectManager = $objectManager;
    $this->instanceName = $instanceName;
}

//...

public function create($isView = false, array $arguments = [])
{
    /** @var MagentoFrameworkViewResultPage $page */
    $page = $this->objectManager->create($this->instanceName, $arguments);
    // TODO Temporary solution for compatibility with View object. Will be deleted in MAGETWO-28359
    if (!$isView) {
        $page->addDefaultHandle();
    }
    return $page;
}

You see the create method uses the object manager directly to create an object.

$page = $this->objectManager->create($this->instanceName, $arguments);

This object’s class is set in the constructor, and by default is MagentoFrameworkViewResultPage. However, thanks to this bit of dependency injection

#File: vendor/magento/module-backend/etc/adminhtml/di.xml
<type name="MagentoFrameworkViewResultPageFactory"><arguments><argument name="instanceName" xsi:type="string">MagentoBackendModelViewResultPage</argument></arguments></type>

the page factory object will return a MagentoBackendModelViewResultPage instead when you’re working in the adminhtml area. In other words, the above configuration, replaces the $instanceName parameter’s value in MagentoFrameworkViewResultPageFactory objects. It replaces it with the string “MagentoBackendModelViewResultPage”. It only replaces it in the adminhtml area since the file path is etc/adminhtml/di.xml, which means the configuration is only loaded in the adminhtml area.

Other Return Types

So far so good. Next up, let’s cover the result types that aren’t page objects. Specifically

Remember, this is not a comprehensive guide to these objects – so checkout their source files and Magento core controllers if you’re curious what these objects can do.

JSON Results

You’ll use a JSON result when you want to return a JSON object. You’ll do this if you’re implementing a custom API endpoint, or a simple AJAX endpoint. If its a JSON result you want, inject the json result factory

public function __construct(
    //...
    MagentoFrameworkControllerResultJsonFactory $jsonResultFactory,
    //...        
)
{
    $this->jsonResultFactory = $jsonResultFactory;
}

and then in execute use this factory to create an object, set that new object’s data, and return the object

public function execute()
{
    $result = $this->jsonResultFactory();

    $o = new stdClass;              
    $o->foo = 'bar';
    $result->setData($o);
    return $result;              
}

Raw Result

You’ll use a raw result when you want to return a plain string without Magento layout and view rendering. By default a raw result will be returned with a text/html header, if you want something else (text/xml, text/plain) then you’ll want to use the setHeader method of the result object.

To get a raw result, inject a raw result factory object via automatic constructor dependency injection

public function __construct(
    //...
    MagentoFrameworkControllerResult $rawResultFactory ,
    //...        
)
{
    $this->rawResultFactory = $rawResultFactory;
}

and use that factory to create a raw result

public function execute(
)
{
    //...
    $result = $this->rawResultFactory->create();
    $result->setHeader('Content-Type', 'text/xml');
    $result->setContents('<root><science></science></root>);
    return $result;
}

Forwarding Result

Forwarding in Magento 2 is similar to forwarding in Magento 1, and a little tricky if you’ve never encountered it before. When you forward a request you’re telling Magento’s internal system that it should process a different route without making another HTTP request. I generally stay away from forwarding, because there’s a lot of unanswered/poorly documented behavior on which controller’s request (the original or the forwarded) will effect other parts of the system (like layout handles, etc.)

However, forwarding survives in Magento 2 so you’ll want to be aware of it.

To use forwarding, inject a forward factory via automatic constructor dependency injection

public function __construct(
    //...
    MagentoFrameworkControllerResultForwardFactory $resultForwardFactory
    //...        
)
{
    $this->resultForwardFactory = $resultForwardFactory;
}

and use that factory to create a forward result

public function execute()
{
    $result = $this->resultForwardFactory->create();
    $result->forward('noroute');    
    return $result;
}

Redirect Result

When you want to send the user/request to a new URL via an HTTP location header redirect, you’ll want a redirect result.

To create a redirect result, inject a redirect factory via automatic constructor dependency injection

public function __construct(
    //...
    MagentoFrameworkControllerResultRedirectFactory $resultRedirectFactory
    //...        
)
{
    $this->resultRedirectFactory = $resultRedirectFactory;
}

and then use that factory to create a redirect response

public function execute()
{
    $result = $this->resultRedirectFactory->create();
    $result->setPath('*/*/index');
    return $result;
}

The Role of Factories and Magento Areas

OK, so that’s a lot of information, but so far things are relatively straight forward. A Magento controller should return a “result” object. Each result object does a different thing. There’s a result object for pages, internal forwards, redirects, JSON results, and arbitrary/text results. To get different result objects, use their respective factories.

At the start of this article, we inspected the page factory, and made note of how it used automatic constructor dependency injection and a di.xml configuration to return the correct type of page object depending on the request’s location in the frontend (cart) or adminhtml (backend admin) area of the application.

In a perfect world, each of the other four factory types would also implement this pattern. Unfortunately, they do not, and this is where things start to fall off the rails.

Factory: Real or Generated

The first wrinkle – of the four remaining result types, only redirects have an actual factory class

./vendor/magento/framework/Controller/Result/RedirectFactory.php

the factories for the others are all generated factory class files

./var/generation/Magento/Framework/Controller/Result/RawFactory.php
./var/generation/Magento/Framework/Controller/Result/JsonFactory.php
./var/generation/Magento/Framework/Controller/Result/ForwardFactory.php

If your’e not familiar with Magento 2 code generation, we first covered it in our article on Proxy objects. Similar to proxy objects, Magento will auto-generate a factory if it finds no real factory class.

The generated factories are Magento’s standard generated factory classes, which means there’s no place for a special $instanceName parameter to swap out with di.xml configuration. For the RawFactory and JsonFactory classes, this doesn’t matter, as those results act the same regardless of area. However, redirects and forwards (similar to page objects) do need different behavior dependent on area.

Extra Backend Factories

As mentioned earlier, the page factory uses di.xml configuration to swap out a different instanceName class. The ForwardFactory class can’t do this since its generated. Magento 2 does have two different forwarding objects. A default for the frontend area

vendor/magento/framework/Controller/Result/Forward.php

and another for the adminhtml area

vendor/magento/module-backend/Model/View/Result/Forward.php

For reasons that aren’t 100% clear, rather than implement a di.xml based solution for this, Magento 2’s core code actually uses two different generated factories. One for the frontend, and one for the backend

MagentoBackendModelViewResultForwardFactory
MagentoFrameworkControllerResultForwardFactory

It’s up to you, the extension developer, to remember to use the backend forwarding factory when you’re on backend pages.

Forward, raw, and json explained, that leaves only the redirect result type. If you look at the RedirectFactory source code, you’ll see a pattern similar to the PageFactory

#File: vendor/magento/framework/Controller/Result/RedirectFactory.php
public function __construct(
    ObjectManagerInterface $objectManager,
    $instanceName = 'MagentoFrameworkControllerResultRedirect'
) {
    $this->objectManager = $objectManager;
    $this->instanceName = $instanceName;
}

//...
public function create(array $data = [])
{
    return $this->objectManager->create($this->instanceName, $data);
}

Again, the object manager uses an injected $instanceName to instantiate an object. However, again, for reasons that aren’t entirely clear, despite the RedirectFactory design enabling it, there is no di.xml configuration for the RedirectFactory objects. Instead, Magento 2 has a second non-generated redirect factory for the backend.

vendor/magento/module-backend/Model/View/Result/RedirectFactory.php

and its up to the individual module developer to know they need to inject a MagentoBackendModelViewResultRedirectFactory class when working in the backend.

Speaking frankly, the above is a bit of a mess, and an almost textbook example of how “Uncle Bob” style design patterns are often misapplied by teams without strong leadership and mentorship cultures, or by teams working under time pressures.

The Generic Result Factory

In addition to the individual page, json, raw, redirect, and forward factories mentioned above, Magento 2 also has a generic result factory factory object (MagentoFrameworkControllerResultFactory). Like all factory objects, you inject it via automatic constructor dependency injection

public function __construct(
    MagentoFrameworkControllerResultFactory $resultFactory 
)
{
    $this->resultFactory = $resultFactory;
}

Then, in execute, you use this factory, along with a predefined set of constants to instantiate different result types

public function execute()
{
    $resultJson = $this->resultFactory->create(
        MagentoFrameworkControllerResultFactory::TYPE_JSON);                   

    $resultRaw  = $this->resultFactory->create(
        MagentoFrameworkControllerResultFactory::TYPE_RAW);       

    $resultRedirect = $this->resultFactory->create(
        MagentoFrameworkControllerResultFactory::TYPE_REDIRECT;       

    $resultForward = $this->resultFactory->create(
        MagentoFrameworkControllerResultFactory::TYPE_FORWARD);       

    $resultPage = $this->resultFactory->create(
        MagentoFrameworkControllerResultFactory::TYPE_PAGE);                    
}

The generic result factory allows you to create any of the five result types we previously mentioned – albeit with a more verbose syntax. The ResultFactory’s functionality is implemented by a hard coded list of default object types to instantiate, which is merged with an injected $typeMap parameter

#File: vendor/magento/framework/Controller/ResultFactory.php
protected $typeMap = [
    self::TYPE_JSON     => 'MagentoFrameworkControllerResultJson',
    self::TYPE_RAW      => 'MagentoFrameworkControllerResultRaw',
    self::TYPE_REDIRECT => 'MagentoFrameworkControllerResultRedirect',
    self::TYPE_FORWARD  => 'MagentoFrameworkControllerResultForward',
    self::TYPE_LAYOUT   => 'MagentoFrameworkViewResultLayout',
    self::TYPE_PAGE     => 'MagentoFrameworkViewResultPage',
];
//...
public function __construct(
    ObjectManagerInterface $objectManager,
    array $typeMap = []
) {
    $this->objectManager = $objectManager;
    $this->mergeTypes($typeMap);
}

The merging allows programmers to change which objects the results factory returns via di.xml configuration. Similar to the PageFactory, Magento’s core developers add the following di.xml configuration for the adminhtml area.

<!-- File: vendor/magento/module-backend/etc/adminhtml/di.xml -->
<type name="MagentoFrameworkControllerResultFactory"><arguments><argument name="typeMap" xsi:type="array"><item name="redirect" xsi:type="array"><item name="type" xsi:type="const">MagentoFrameworkControllerResultFactory::TYPE_REDIRECT</item><item name="class" xsi:type="string">MagentoBackendModelViewResultRedirect</item></item><item name="page" xsi:type="array"><item name="type" xsi:type="const">MagentoFrameworkControllerResultFactory::TYPE_PAGE</item><item name="class" xsi:type="string">MagentoBackendModelViewResultPage</item></item><item name="forward" xsi:type="array"><item name="type" xsi:type="const">MagentoFrameworkControllerResultFactory::TYPE_FORWARD</item><item name="class" xsi:type="string">MagentoBackendModelViewResultForward</item></item></argument></arguments></type>

The di.xml configuration swaps in the backend versions of the Page object, forward object, and type objects. Unlike the individual factory approach, the above means that a module developer using the ResultsFactory does not need to keep track of which area they’re working in. Just inject a ResultsFactory, and use its more verbose syntax to grab the result you want, and you’re good to go.

Which to Use

Given all the evidence above, is seems like a developer’s best bet is to always use the MagentoFrameworkControllerResultFactory. This object is also baked into the base controller class via a context object

#File: vendor/magento/framework/App/Action/AbstractAction.php
 */
public function __construct(
    MagentoFrameworkAppActionContext $context
) {
    $this->_request = $context->getRequest();
    $this->_response = $context->getResponse();
    $this->resultRedirectFactory = $context->getResultRedirectFactory();
    $this->resultFactory = $context->getResultFactory();
}    

#File: vendor/magento/framework/App/Action/Context.php    
use MagentoFrameworkControllerResultFactory;
//...
public function __construct(
    //...
    MagentoFrameworkControllerResultRedirectFactory $resultRedirectFactory,
    ResultFactory $resultFactory
) {
    //...
    $this->resultRedirectFactory = $resultRedirectFactory;
    $this->resultFactory = $resultFactory;
}

Which means you don’t even need to inject it – $this->resultFactory is always there in a controller.

However, even this isn’t clear, as the base controller class also bakes in a stand alone redirect result factory. Also – there’s compelling reasons to use the stand alone objects as well.

Suffice it to say, things are a bit of an unclear mess. Hopefully a direction emerges from the core team as time goes on.

The Stu Sutcliffe of Result Types

A few last things before we finish up. We deliberately left out a sixth result type above to avoid extra confusion. In addition to the page result, Magento also offers a layout result type. The layout result has an individual factory

public function __construct(
    //...
    MagentoFrameworkViewResultLayoutFactory $resultLayoutFactory,
    //...
) {
    //...
    $this->resultLayoutFactory = $resultLayoutFactory;
    //...        
} 

//...

public function execute()
{
    $result = $this->resultLayoutFactory->create();
    return $result;              
}    

as well as a constant in the generic result factory

$resultPage = $this->resultFactory->create(
    MagentoFrameworkControllerResultFactory::TYPE_LAYOUT);

Without getting too deeply into it, the layout result type is very similar to the page result type. When you return a layout result, Magento still builds a response from the layout handle XML files. However, unlike the page result, this result type does not use Magento 2’s new root.phtml template file

vendor/magento/module-theme/view/base/templates/root.phtml        

Instead, per Magento 1 layout rendering, a layout result starts rendering the root layout node and any other layout nodes marked for output.

Returning the Response Object

Magento 2, like Magento 1, instantiates a request and response object for each HTTP request and response. While not recommended, it’s still possible to directly manipulate the response object from a controller, and return that object.

public function execute()
{
    return $this->getResponse()->setBody('Hello World');
}

You may also see something like this in Magento’s core code

public function __construct(
        MagentoFrameworkAppResponseHttpFileFactory $fileFactory,
)
{
    $this->fileFactory = $fileFactory;
}

public function execute()
{
    //...
    return $this->fileFactory->create(
        'filename.txt',
        'file contents',
        MagentoFrameworkAppFilesystemDirectoryList::VAR_DIR,
        'application/text'
    );   
}

A file factory allows you to send a response that automatically downloads a file (after saving it to the specified directory locally). The file factory does not return a proper result type. Instead, if you look at the file factory’s source code

#File: vendor/magento/framework/App/Response/Http/FileFactory.php
public function __construct(
    MagentoFrameworkAppResponseInterface $response,
    MagentoFrameworkFilesystem $filesystem
) {
    $this->_response = $response;
    $this->_filesystem = $filesystem;
}
//...    
public function create(
    $fileName,
    $content,
    $baseDir = DirectoryList::ROOT,
    $contentType = 'application/octet-stream',
    $contentLength = null
) {
    //...
    return $this->_response;
}

You’ll see the file factory returns a response object directly (after creating the file and manipulating it with the proper contents and header).

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 18th March 2016