Tag: product

  • Redirect disabled product pages instead of 404 page in Magento 2

    Redirect disabled product pages instead of 404 page in Magento 2

    In this blog post I’ll show how to implement a Magento 2 Extension which allows to redirect from disabled product url to

    • a custom url
    • assigned category of a highest level as next fallback if no custom url is entered on product level (custom attribute)
    • home page as last fallback if no categories found

    instead of showing a 404 Not Found page.

    We will call the extension N98_DisabledProductRedirect

    Extension Code

    Basic files

    registration.php

    <?php
    
    use Magento\Framework\Component\ComponentRegistrar;
    
    ComponentRegistrar::register(
        ComponentRegistrar::MODULE,
        'N98_DisabledProductRedirect',
        __DIR__
    );

    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="N98_DisabledProductRedirect" setup_version="1.0.0">
            <sequence>
                <module name="Magento_Catalog"/>
            </sequence>
        </module>
    </config>
    

    Plugin on Product View frontend Controller

    etc/frontend/di.xml

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework/ObjectManager/etc/config.xsd">
        <type name="Magento\Catalog\Controller\Product\View">
            <plugin name="disabled_product_redirect_plugin" type="N98\DisabledProductRedirect\Plugin\ProductRedirectPlugin" />
        </type>
    </config>
    

    Plugin/ProductRedirectPlugin.php

    <?php
    declare(strict_types=1);
    
    namespace N98\DisabledProductRedirect\Plugin;
    
    use Magento\Catalog\Api\ProductRepositoryInterface;
    use Magento\Catalog\Model\Product;
    use Magento\Framework\Controller\Result\RedirectFactory;
    use Magento\Framework\Exception\NoSuchEntityException;
    use N98\DisabledProductRedirect\Service\GetRedirectUrlForDisabledProductService;
    
    class ProductRedirectPlugin
    {
        /**
         * @var ProductRepositoryInterface
         */
        private ProductRepositoryInterface $productRepository;
    
        /**
         * @var RedirectFactory
         */
        private RedirectFactory $redirectFactory;
    
        /**
         * @var GetRedirectUrlForDisabledProductService
         */
        private GetRedirectUrlForDisabledProductService $getRedirectUrlForDisabledProductService;
    
        /**
         * ProductRedirectPlugin constructor.
         *
         * @param ProductRepositoryInterface $productRepository
         * @param RedirectFactory $redirectFactory
         * @param GetRedirectUrlForDisabledProductService $getRedirectUrlForDisabledProductService
         */
        public function __construct(
            ProductRepositoryInterface $productRepository,
            RedirectFactory $redirectFactory,
            GetRedirectUrlForDisabledProductService $getRedirectUrlForDisabledProductService
        ) {
            $this->productRepository = $productRepository;
            $this->redirectFactory = $redirectFactory;
            $this->getRedirectUrlForDisabledProductService = $getRedirectUrlForDisabledProductService;
        }
    
        /**
         * Main plugin method.
         *
         * @param \Magento\Catalog\Controller\Product\View $subject
         * @param callable $proceed
         * @return \Magento\Framework\Controller\Result\Redirect
         * @throws \Magento\Framework\Exception\LocalizedException
         */
        public function aroundExecute($subject, callable $proceed)
        {
            $productId = $subject->getRequest()->getParam('id');
    
            try {
                $product = $this->productRepository->getById($productId);
                if ((int)$product->getStatus() === Product\Attribute\Source\Status::STATUS_DISABLED) {
                    $redirectUrl = $this->getRedirectUrlForDisabledProductService->execute($product);
    
                    $resultRedirect = $this->redirectFactory->create();
                    $resultRedirect->setUrl($redirectUrl);
                    $resultRedirect->setHttpResponseCode(302);
                    return $resultRedirect;
                }
            } catch (NoSuchEntityException $e) {
                return $proceed();
            }
    
            return $proceed();
        }
    }
    

    Service “Get Redirect URL for disabled product”

    Service/GetRedirectUrlForDisabledProductService.php

    <?php
    declare(strict_types=1);
    
    namespace N98\DisabledProductRedirect\Service;
    
    use Magento\Catalog\Api\Data\ProductInterface;
    use Magento\Store\Model\StoreManagerInterface;
    
    class GetRedirectUrlForDisabledProductService
    {
        /**
         * GetRedirectUrlForDisabledProductService constructor.
         *
         * @param StoreManagerInterface $storeManager
         */
        public function __construct(private readonly StoreManagerInterface $storeManager)
        {
        }
    
        /**
         * Get redirect URL for a disabled product
         *
         * @param ProductInterface $product
         * @return string
         * @throws \Magento\Framework\Exception\LocalizedException
         * @throws \Magento\Framework\Exception\NoSuchEntityException
         */
        public function execute(ProductInterface $product): string
        {
            // Check for a defined-per-product URL attribute
            $customRedirectUrl = $product->getData('redirect_url_if_disabled');
            if ($customRedirectUrl) {
                return $customRedirectUrl;
            }
    
            // Fallback to the highest-level category URL
            /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection */
            $categoryCollection = $product->getCategoryCollection();
            $categoryCollection->addAttributeToSelect('url_key')->load();
    
            $highestLevelCategory = null;
            foreach ($categoryCollection as $category) {
                if (!$highestLevelCategory || $category->getLevel() < $highestLevelCategory->getLevel()) {
                    $highestLevelCategory = $category;
                }
            }
    
            if ($highestLevelCategory) {
                /** @var \Magento\Catalog\Model\Category $highestLevelCategory */
                return $highestLevelCategory->getUrl();
            }
    
            // Default fallback URL (home page)
            return $this->storeManager->getStore()->getBaseUrl();
        }
    }
    

    Data Patch to create attribute “redirect_url_if_disabled”

    Setup/Patch/Data/AddRedirectUrlProductAttribute.php

    <?php
    declare(strict_types=1);
    
    namespace N98\DisabledProductRedirect\Setup\Patch\Data;
    
    use Magento\Catalog\Model\Product;
    use Magento\Eav\Setup\EavSetup;
    use Magento\Eav\Setup\EavSetupFactory;
    use Magento\Framework\Setup\ModuleDataSetupInterface;
    use Magento\Framework\Setup\Patch\DataPatchInterface;
    
    class AddRedirectUrlProductAttribute implements DataPatchInterface
    {
        /**
         * AddRedirectUrlProductAttribute constructor.
         *
         * @param EavSetupFactory $eavSetupFactory
         * @param ModuleDataSetupInterface $moduleDataSetup
         */
        public function __construct(
            private readonly EavSetupFactory          $eavSetupFactory,
            private readonly ModuleDataSetupInterface $moduleDataSetup
        ) {
        }
    
        /**
         * Add attribute "redirect_url_if_disabled" to products and assign to default attribute set
         *
         * @return void
         * @throws \Magento\Framework\Exception\LocalizedException
         * @throws \Magento\Framework\Validator\ValidateException
         */
        public function apply()
        {
            /** @var EavSetup $eavSetup */
            $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
    
            $eavSetup->addAttribute(Product::ENTITY, 'redirect_url_if_disabled', [
                'type'       => 'text',
                'label'      => 'Redirect URL (if product is disabled)',
                'input'      => 'textarea',
                'required'   => false,
                'sort_order' => 100,
                'global'     => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
                'group'      => 'General',
            ]);
    
            $attributeSetId = $eavSetup->getDefaultAttributeSetId(Product::ENTITY);
            $attributeGroupId = $eavSetup->getAttributeGroupId(Product::ENTITY, $attributeSetId, 'General');
            $eavSetup->addAttributeToGroup(
                Product::ENTITY,
                $attributeSetId,
                $attributeGroupId,
                'redirect_url_if_disabled',
                100
            );
        }
    
        /**
         * @inheritDoc
         */
        public static function getDependencies()
        {
            return [];
        }
    
        /**
         * @inheritDoc
         */
        public function getAliases()
        {
            return [];
        }
    }
    

    Conclusion

    As you could see in the code, the module is pretty simple. For redirect code we used 302 but also 301 can be used or you can make it configurable via Store Configuration.

    Feel free to leave a comment.

    P.S. In your comment you can answer a quiz question: was the blog post featured image AI generated or created manually? 😊

  • Creating Configurable Products

    Basically there are different ways for creating Configurable Products in Magento2 (like in Magento1).
    You can create it using the Admin Panel, using a Setup\InstallData or Setup\UpgradeData class or during a custom import.

    ADDING THE ATTRIBUTE

    Let’s assume we are creating our attribute using the Setup\UpgradeData class in our module.
    The code we might initially create could look something like the following:

        protected function addAttributeDiameter()
        {
            $this->eavSetup->addAttribute(
                ProductModel::ENTITY,
                'dev98_diameter',
                [
                    'type' => 'int',
                    'backend' => '',
                    'frontend' => '',
                    'label' => 'Diameter',
                    'input' => 'select',
                    'class' => '',
                    'source' => '',
                    'global' => 1,
                    'visible' => true,
                    'required' => false,
                    'user_defined' => true,
                    'default' => null,
                    'searchable' => true,
                    'filterable' => true,
                    'comparable' => true,
                    'visible_on_front' => true,
                    'used_in_product_listing' => true,
                    'unique' => false,
                    'apply_to' => 'simple,configurable',
                    'system' => 1,
                    'group' => 'General',
                ]
            );
    
            // Add Options
            $attributeId = $this->eavSetup->getAttributeId(
                ProductModel::ENTITY, 'dev98_diameter'
            );
            $options = [
                'attribute_id' => $attributeId,
                'values' => ["10", "20", "30", "40", "50", "60", "70", "80", "90",],
            ];
            $this->eavSetup->addAttributeOption($options);
        }

    The above code will create an attribute with the attribute-code ‘dev98_diameter’, which is available for simple and configurable products, within the group ‘General’ and is searchable, filterable etc… Futhermore we are adding some attribute options as well.

    CREATING A CONFIGURABLE PRODUCT

    Now if you add this attribute to an attribute-set and try creating a configurable product for this attribute it won’t work out as you might expect.
    You will not see the attribute in the overlay ‘Create Product Configurations’  where you choose the attributes you want your product to be configurable with.

    In Magento 2 the Admin Edit Form for Products changed quite a bit.

    The most important change is, that the type of a product e.g. simple, configurable, etc. is no longer selected but determined automatically when saving the product.

    So the product type you select using the DropDown in the Admin Product Grid is obsolete and not really used.

    IMPROVING OUR UPDATEDATA CLASS

    Knowing that the product is virtual at the moment of creating our configurable product, we have to add to type ‘virtual’ to the apply_to parameter in our ‘Setup\UpgradeData’ class.

           $this->eavSetup->addAttribute(
                ProductModel::ENTITY,
                'dev98_diameter',
                [
                    'type' => 'int',
                    'backend' => '',
                    'frontend' => '',
                    'label' => 'Diameter',
                    'input' => 'select',
                    'class' => '',
                    'source' => '',
                    'global' => 1,
                    'visible' => true,
                    'required' => false,
                    'user_defined' => true,
                    'default' => null,
                    'searchable' => true,
                    'filterable' => true,
                    'comparable' => true,
                    'visible_on_front' => true,
                    'used_in_product_listing' => true,
                    'unique' => false,
                    'apply_to' => 'simple,configurable,virtual', // Add virtual type here
                    'system' => 1,
                    'group' => 'General',
                ]
            );

    After running the ‘setup:upgrade’ command, flushing the cache and reloading our Admin Product Edit Form we can now see the attribute ‘dev98_diameter’ in the ‘Create Product Configurations’ overlay.

    From here-on we can create configurable products with our attribute as we please.

    SUMMARY

    To sum it up in short, you have to make your product attribute apply to type virtual to create configurable products.
    At least when you are creating you attributes programmatically like in InstallData/ UpgradeData or using a attributes import.