Yaroslav Rogoza Avatar

Redirects is a widely used approach not only in Magento but in most of other web applications. Saying “redirect” we usually mean a rule or a set of rules for sending user from one URL to another. Using Magento 1 you can easily handle redirects by writing just one line of the code, but in Magento 2, because of its new architecture based on Dependency injection, the process of building a redirect is a bit more complex. Let’s review a new redirect creation by building a simple extension that redirects user from a category page to a product view page if there’s only one product in this category.

Also, take a look at our best Magento SEO extensions list.

In Magento 1 we can redirect a user by inserting the following line almost on any stage of application execution flow (before the response has been sent):

Mage::app()->getFrontController()->getResponse()->setRedirect([some_url]);

Fortunately, in Magento 2 we don’t have such factory of global methods as `Mage::app()`. Instead of this we can use either ‘ObjectManager’ or, even better, the dependency injection flow based on the classes’ constructors. Usually in a web application requests flow we have two entities, such as request and response. In Magento 2 we have a set of classes that correspond to the mentioned entities. In case of standard requests to the web pages we have two classes connected to request and response: \Magento\Framework\App\Request\Http (Request) and \Magento\Framework\App\Response\Http (Response). And for setting redirect headers we need to operate with the Response object. Of course, in some cases we can change the execution flow using also the Request object by calling the “forward” method, but it’s not a sort of redirect and it’s a different story.

Our simple extension will redirect a user to the only product in a category. First of all, we need to create the extension’s bootstrap to make it visible for Magento. For that create composer.json file with an extension name and also specified extension dependencies (we are not going to bother you with dependancies in this simple case, but feel free to experiment) and registration.php for module registration in system:

<!-- app/code/Atwix/FirstProduct/composer.json --> 
{
    "name": "atwix/firstproduct",
    "description": "N/A",
    "require": {
        "php": "~5.5.0|~5.6.0|~7.0.0"
    },
    "type": "magento2-module",
    "version": "0.0.1-beta",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "autoload": {
        "files": [ "registration.php" ],
        "psr-4": {
            "Atwix\\FirstProduct\\": ""
        }
    }
}
<?php

// app/code/Atwix/FirstProduct/registration.php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Atwix_FirstProduct',
    __DIR__
);

?>

Then, create the module definition itself:

<!-- app/code/Atwix/FirstProduct/etc/module.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Atwix_FirstProduct" setup_version="0.0.1">
    </module>
</config>

Now we need few actions for making the extension work. Go to the /bin directory of your Magento installation and run the following commands:

./magento module:enable Atwix_FirstProduct
./magento setup:upgrade

On the next step, we will find a way for checking for a products count in a category and create the redirection logic. We need something similar to this flow: on the product listing page check if there’s only one product, if yes – go to that product’s page. The block’s class, responsible for the products list rendering, is Magento\Catalog\Block\Product\ListProduct. By looking into this class you will find the getLoadedProductCollection() method which is very handy for our redirection logic. Now we need to modify the behaviour of this object somehow. In Magento 1 we could simply override the entire class or the single method. And in Magento 2 we would go the same way, however, the new Magento version provides a lot of possibilities to avoid potential conflicts in the code. One of those possibilities is Magento 2 plugin injection. Using plugins we can change the behaviour of some system method without overriding it. Therefore, create the plugin definition and the class with our modified getLoadedProductCollection() method.

<!-- app/code/Atwix/FirstProduct/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Block\Product\ListProduct">
        <plugin name="firstproduct-category-mod" type="Atwix\FirstProduct\Block\Catalog\Product\ListProduct" sortOrder="1"/>
    </type>
</config>
<?php
// app/code/Atwix/FirstProduct/Block/Catalog/Product/ListProduct.php
namespace Atwix\FirstProduct\Block\Catalog\Product;

class ListProduct
{
    /**
     * @var \Magento\Framework\App\Response\Http
     */
    protected $response;

    public function __construct(
       \Magento\Framework\App\Response\Http $response
    )
    {
        $this->response = $response;
    }

    /**
     * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $resultCollection
     * @param \Magento\Catalog\Block\Product\ListProduct $subject
     * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection $resultCollection
     */
    public function afterGetLoadedProductCollection(\Magento\Catalog\Block\Product\ListProduct $subject, $resultCollection)
    {
        if ($resultCollection->count() == 1) {
            /** @var \Magento\Catalog\Model\Product $product */
            $product = $resultCollection->getFirstItem();
            $this->response->setRedirect($product->getProductUrl());
        }

        return $resultCollection;
    }
}
?>

As you can see, our new method’s name is not the same as in the original class, but has the prefix “after” and also it has few parameters. In this case, we “tell” the system that our method should be called after the original one. The $subject parameter is a ListProduct object instance and the $resultCollection is a result returned by the original method. In that way we are able to execute our logic before or after (or both) the original method without unnecessary classes/objects overrides.
If you take a deeper look into our custom ListProduct class, you will notice that we have added the new constructor object with only one parameter – $response. That’s how we tell the built in dependancy injection system that we need to operate with the $response object, responsible for operations with the server response. By having a reference of this object (it’s not formally reference, it’s an instance but with all necessary data initiated), we can call different methods of this object. Absolutely in the same way you can handle the response object almost in any place of your extension: by injection of \Magento\Framework\App\Response\Http into a constructor method.
After this, we get the collection of Magento products listing page products checking if there’s only one product and if this condition is met – we are fetching the product’s URL and calling setRedirect() method. That’s it, a bit more complex in comparison to Magento 1 but much more flexible and “clean”. If you are interested in the described extension, you can download it here. Feel free to share your opinion and questions in the comments below and good luck with your Magento 2 findings ;)