Result Objects

Why an action returns a Page, Json, Raw, Redirect or Forward object instead of echoing, and how each one is built.

Syntax$this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_JSON) // ResultInterface

A Magento action prints nothing. It returns a result object: an instance of something implementing Magento\Framework\Controller\ResultInterface. The front controller passes it back to Http::launch(), which calls renderResult() on it with the response. Returning rather than echoing keeps the response editable after your code has finished — headers can still be added, the full-page cache can store the body, and a unit test can inspect what the action returned.

The types

ConstantClassUse
TYPE_PAGEView\Result\Pagea full HTML page
TYPE_LAYOUTView\Result\Layoutrendered blocks without the page wrapper
TYPE_JSONController\Result\Jsonan AJAX endpoint
TYPE_RAWController\Result\Rawa body you build yourself: a file, CSV, plain text
TYPE_REDIRECTController\Result\Redirecta Location header, 302 unless you say otherwise
TYPE_FORWARDController\Result\Forwardre-dispatch to another action, same URL

Two ways to build one

Inject Magento\Framework\Controller\ResultFactory and call create(ResultFactory::TYPE_JSON), or inject Magento\Framework\Controller\Result\JsonFactory and call create(). Both ask the object manager for the same class, so the objects are identical. The specific factory names the dependency in the constructor, which is why new code prefers it.

What trips people up

Forward is not an HTTP redirect. It rewrites the module, controller and action on the current request and lets the router loop run again, so the browser's URL does not change. Redirect sends 302 unless you call setHttpResponseCode(301).

Example

Example · php
<?php
// app/code/Vendor/Stock/Controller/Ajax/Check.php
declare(strict_types=1);

namespace Vendor\Stock\Controller\Ajax;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;

class Check implements HttpGetActionInterface
{
    public function __construct(
        private readonly RequestInterface $request,
        private readonly JsonFactory $jsonFactory
    ) {
    }

    public function execute(): ResultInterface
    {
        $sku = trim((string)$this->request->getParam('sku', ''));
        $result = $this->jsonFactory->create();

        if ($sku === '') {
            $result->setHttpResponseCode(400);

            return $result->setData(['message' => 'sku is required']);
        }

        return $result->setData(['sku' => $sku, 'checked' => true]);
    }
}

When to use it

  • Return JSON from an endpoint a storefront script calls.
  • Send a CSV download from an admin controller without touching the response directly.
  • Show the 404 page for a missing record while keeping the requested URL in the address bar.
  • Redirect back to a listing page after handling a form POST.

More examples

A page, or the 404 page, from one action

This is the shape of almost every storefront page controller. forward('noroute') changes the action name and sends the request back through the router loop, which reaches the no-route action and renders the 404 page at the original URL — better than a redirect, because the broken link stays visible in the address bar. Which XML file fills the returned Page with content is Stage 4 material; the controller's job ends at handing the Page back. Nothing here needs a command in developer mode, but a brand new module needs bin/magento setup:upgrade before any of its routes answer.

Example · php
<?php
// app/code/Vendor/Blog/Controller/Post/View.php
declare(strict_types=1);

namespace Vendor\Blog\Controller\Post;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\ForwardFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\View\Result\PageFactory;

class View implements HttpGetActionInterface
{
    public function __construct(
        private readonly RequestInterface $request,
        private readonly PageFactory $pageFactory,
        private readonly ForwardFactory $forwardFactory
    ) {
    }

    public function execute(): ResultInterface
    {
        $postId = (int)$this->request->getParam('id');

        if ($postId <= 0) {
            return $this->forwardFactory->create()->forward('noroute');
        }

        $page = $this->pageFactory->create();
        $page->getConfig()->getTitle()->set(__('Post %1', $postId));

        return $page;
    }
}

A file download with Raw

Raw sets only the response body, so every header is yours to set. Headers registered on a result are applied just before its render step, which is why Raw keeps the Content-Type you gave it and Json does not. Note that Magento\Framework\Controller\Result\RawFactory has no file in the magento2 source: factories for classes that do not ship one are generated into generated/code, which is why bin/magento setup:di:compile has to run before a production deployment.

Example · php
<?php
// app/code/Vendor/Export/Controller/Adminhtml/Report/Csv.php
declare(strict_types=1);

namespace Vendor\Export\Controller\Adminhtml\Report;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\Controller\Result\RawFactory;
use Magento\Framework\Controller\ResultInterface;

class Csv extends Action implements HttpGetActionInterface
{
    public const ADMIN_RESOURCE = 'Vendor_Export::report';

    public function __construct(
        Context $context,
        private readonly RawFactory $rawFactory
    ) {
        parent::__construct($context);
    }

    public function execute(): ResultInterface
    {
        $raw = $this->rawFactory->create();
        $raw->setHeader('Content-Type', 'text/csv', true);
        $raw->setHeader('Content-Disposition', 'attachment; filename="report.csv"', true);
        $raw->setContents("sku,qty\n24-MB01,100\n");

        return $raw;
    }
}

The generic factory, two results, one action

Reach for ResultFactory when one action can end in more than one kind of result; the type constants and the per-type factories produce the same objects, so $this->resultFactory->create(ResultFactory::TYPE_JSON) and $this->jsonFactory->create() are interchangeable. Because create() is declared as returning ResultInterface, annotate the variable before calling setData() or setPath() so static analysis can follow it. isXmlHttpRequest() is defined on Magento\Framework\App\Request\Http rather than on RequestInterface, which is why the constructor asks for the HTTP request implementation by name.

Example · php
<?php
// app/code/Vendor/Favourites/Controller/Item/Add.php
declare(strict_types=1);

namespace Vendor\Favourites\Controller\Item;

use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Controller\ResultInterface;

class Add implements HttpPostActionInterface
{
    public function __construct(
        private readonly HttpRequest $request,
        private readonly ResultFactory $resultFactory
    ) {
    }

    public function execute(): ResultInterface
    {
        $sku = trim((string)$this->request->getParam('sku', ''));

        if ($this->request->isXmlHttpRequest()) {
            /** @var \Magento\Framework\Controller\Result\Json $json */
            $json = $this->resultFactory->create(ResultFactory::TYPE_JSON);

            return $json->setData(['added' => $sku !== '']);
        }

        /** @var \Magento\Framework\Controller\Result\Redirect $redirect */
        $redirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);

        return $redirect->setPath('favourites/index/index');
    }
}

Discussion

  • Be the first to comment on this lesson.