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
- A “Json” result
- A “Raw” result
- A “Forward” result
- A “Redirect” result
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).