5

I want to create a single product in Magento 2 which can be shipped or downloaded by user choice on frontend.

Download or ship product example

Till now, I've used this "trick" in Magento 1 but it's really inconvenient because I need to create 3 different products for each single product every time to make it works:

1 Grouped Product visible in catalog and search which contains 1 Simple Product not visible individually and 1 Downloadable Product not visible individually.

Is there a way to achieve something like that without this hassle? Should I create a new product type based on Configurable Product type which allows the download?

  • 1
    I would say a configurable product is your best bet. – Philipp Sander Feb 15 at 14:51
  • You can try Magento Configurable Product with two child products. Create one simple product ( For CD/DVD) and one Downloadable Product (For Download). – Pritam Info 24 Feb 18 at 13:29
  • 3
    So, you are requesting a custom extension in exchange for 200 points? Nice move :) – Bunyamin Inan Feb 18 at 16:03
2
+100

TL;DR: It's possible in Magento 2, Configurable Product w/ Downloadable Product is a variation.

Let's call this StackOverflow_ConfigurableDownloadable

As you know, configurable products only allow you to add simple, virtual as a child product, this module extends that capabilities which allow you to add downloadable product as a child too.

The approach looks like this:

You need a new product attribute, e.g: is_configurable_downloadable (note: this attribute applied to configurable product only)

$isConfigurableDownloadable = $catalogSetup->getAttribute($entityTypeId, 'is_configurable_downloadable');

if (null === $isConfigurableDownloadable) {
    $catalogSetup->addAttribute(
        \Magento\Catalog\Model\Product::ENTITY,
        'is_configurable_downloadable',
        [
            'group' => 'General',
            'label' => 'Is Configurable Downloadable',
            'input' => 'boolean',
            'type' => 'int',
            'class' => '',
            'source' => \Magento\Eav\Model\Entity\Attribute\Source\Boolean::class,
            'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL,
            'visible' => true,
            'required' => false,
            'user_defined' => false,
            'default' => '0',
            'apply_to' => 'configurable',
            'visible_on_front' => true,
            'used_in_product_listing' => true,
            'is_used_in_grid' => false,
            'is_visible_in_grid' => false,
            'is_filterable_in_grid' => false,
        ]
    );
}

// and another attribute, let call it's "Type" (like swatches attribute when you create Configurations). E.g: CD/DVD - simple, Download - downloadable.

Create a plugin for this class (allows you able to save the Downloadable Product)

Magento\ConfigurableProduct\Model\Product\VariationHandler

<?php
/**
 * StackOverflow Configurable Downloadable.
 *
 * @category  Mage
 *
 * @author    Toan Nguyen <https://magento.stackexchange.com/users/18821/toan-nguyen>
 * @copyright 2019 Toan Nguyen (https://about.me/nntoan)
 */

namespace StackOverflow\ConfigurableDownloadable\Plugin\Model\Product;

use Magento\Catalog\Model\Product;
use Magento\ConfigurableProduct\Model\Product\VariationHandler as OriginaleVariationHandler;
use Magento\Downloadable\Model\Product\Type as Downloadable;

/**
 * Class VariationHandler.
 */
class VariationHandler
{
    /**
     * Udpate product type in the case parent is a configuration of downloadable products.
     *
     * @param \Magento\ConfigurableProduct\Model\Product\VariationHandler $configurable
     * @param \Magento\Catalog\Model\Product                              $product
     * @param array                                                       $productsData
     *
     * @return array|null
     * @codingStandardsIgnoreStart
     */
    public function beforeGenerateSimpleProducts(
        OriginaleVariationHandler $configurable,
        Product $product,
        $productsData
    ) {
        //@codingStandardsIgnoreEnd
        if (false === (bool) $product->getData('is_configurable_downloadable')) {
            return null;
        }

        foreach ($productsData as $key => $simpleProductData) {
            /*
             * Set default value for some attributes of downloadable product
             */
            $simpleProductData['type_id'] = Downloadable::TYPE_DOWNLOADABLE;

            $productsData[$key] = $simpleProductData;
        }

        return [$product, $productsData];
    }
}

Another plugin

Magento\ConfigurableProduct\Model\Product\Type\Configurable

<?php
/**
 * StackOverflow Configurable Downloadable.
 *
 * @category  Mage
 *
 * @author    Toan Nguyen <https://magento.stackexchange.com/users/18821/toan-nguyen>
 * @copyright 2019 Toan Nguyen (https://about.me/nntoan)
 */

namespace StackOverflow\ConfigurableDownloadable\Plugin\Model\Product\Type;

use Magento\Catalog\Model\Config as CatalogConfig;
use Magento\Catalog\Model\Product;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable as OriginalConfigurable;

/**
 * Class Configurable.
 *
 * @codingStandardsIgnoreFile
 */
class Configurable
{
    /**
     * Cache key for Used Product Attributes.
     *
     * @var string
     *
     * @since 100.1.0
     */
    private $usedProductAttributes = '_cache_instance_used_product_attributes';

    /**
     * Cache key for Used Attributes.
     *
     * @var string
     */
    private $usedAttributesCode = '_cache_instance_used_attributes';

    /**
     * @var \Magento\Catalog\Model\Config
     */
    private $catalogConfig;

    /**
     * Product collection factory.
     *
     * @var \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory
     */
    private $productCollectionFactory;

    /**
     * Configurable constructor.
     *
     * @param \Magento\Catalog\Model\Config                                                                        $catalogConfig
     * @param \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory $productCollectionFactory
     */
    public function __construct(
        CatalogConfig $catalogConfig,
        \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory $productCollectionFactory
    ) {
        $this->catalogConfig = $catalogConfig;
        $this->productCollectionFactory = $productCollectionFactory;
    }

    /**
     * Update product collection in the case configurable product contains downloadable products.
     *
     * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable $configurable
     * @param \Closure                                                     $proceed
     * @param \Magento\Catalog\Model\Product                               $product
     * @param null|array                                                   $requiredAttributeIds
     *
     * @return ProductInterface[]
     *
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function aroundGetUsedProducts(
        OriginalConfigurable $configurable,
        \Closure $proceed,
        Product $product,
        $requiredAttributeIds = null
    ) {
        if ((bool) $product->getIsConfigurableDownloadable() === true) {
            return $this->getUsedProductCollection($product, $configurable);
        }

        return $proceed($product, $requiredAttributeIds);
    }

    /**
     * @param $product
     * @param $configurable
     *
     * @return $this
     *
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    private function getUsedProductCollection($product, $configurable)
    {
        $collection = $this->productCollectionFactory->create()->setFlag(
            'product_children',
            true
        )->setProductFilter(
            $product
        );
        if (null !== $this->getStoreFilter($product)) {
            $collection->addStoreFilter($this->getStoreFilter($product));
        }
        $collection->setFlag('has_stock_status_filter', false);
        $collection
            ->addAttributeToSelect($this->getAttributesForCollection($product, $configurable));
        $collection->setStoreId($product->getStoreId());
        $collection->addMediaGalleryData();
        $collection->addTierPriceData();

        return $collection;
    }

    /**
     * Retrieve store filter for associated products.
     *
     * @param \Magento\Catalog\Model\Product $product
     *
     * @return int|\Magento\Store\Model\Store
     */
    private function getStoreFilter($product)
    {
        $cacheKey = '_cache_instance_store_filter';

        return $product->getData($cacheKey);
    }

    /**
     * @param \Magento\Catalog\Model\Product $product
     * @param                                $configurable
     *
     * @return array
     */
    private function getAttributesForCollection(\Magento\Catalog\Model\Product $product, $configurable)
    {
        $productAttributes = $this->catalogConfig->getProductAttributes();

        $requiredAttributes = [
            'name',
            'price',
            'weight',
            'image',
            'thumbnail',
            'status',
            'visibility',
            'media_gallery',
            'is_configurable_downloadable',
        ];
        $usedAttributes = array_map(
            function ($attr) {
                if (!is_array($attr) && !is_string($attr)) {
                    return $attr->getAttributeCode();
                }
            },
            $this->getUsedProductAttributes($product, $configurable)
        );

        return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes));
    }

    /**
     * @param $product
     * @param $configurable
     *
     * @return mixed
     */
    private function getUsedProductAttributes($product, $configurable)
    {
        if (!$product->hasData($this->usedProductAttributes)) {
            $usedProductAttributes = [];
            $usedAttributes = [];
            foreach ($configurable->getConfigurableAttributes($product) as $attribute) {
                if (null !== $attribute->getProductAttribute()) {
                    $id = $attribute->getProductAttribute()->getId();
                    $usedProductAttributes[$id] = $attribute->getProductAttribute();
                    $usedAttributes[$id] = $attribute;
                }
            }
            $product->setData($this->usedAttributesCode, $usedAttributes);
            $product->setData($this->usedProductAttributes, $usedProductAttributes);
        }

        return $product->getData($this->usedProductAttributes);
    }
}

another one to update layout handles...

Magento\Catalog\Helper\Product\View

<?php
/**
 * StackOverflow Configurable Downloadable.
 *
 * @category  Mage
 *
 * @author    Toan Nguyen <https://magento.stackexchange.com/users/18821/toan-nguyen>
 * @copyright 2019 Toan Nguyen (https://about.me/nntoan)
 */

namespace StackOverflow\ConfigurableDownloadable\Plugin\Helper\Product;

use Magento\Catalog\Helper\Product\View as OriginalView;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\View\Result\Page as ResultPage;

/**
 * Class View.
 */
class View
{
    /**
     * Update layout if product is configurable downloadable.
     *
     * @param \Magento\Catalog\Helper\Product\View $viewHelper
     * @param \Magento\Framework\View\Result\Page  $resultPage
     * @param                                      $product
     * @param null                                 $params
     *
     * @return array|null
     * @codingStandardsIgnoreStart
     */
    public function beforeInitProductLayout(
        OriginalView $viewHelper,
        ResultPage $resultPage,
        $product,
        $params = null
    ) {
        //@codingStandardsIgnoreEnd
        if (($product->getTypeId() === Configurable::TYPE_CODE)
            && (bool) $product->getIsConfigurableDownloadable() === true) {
            $resultPage->addHandle('catalog_product_view_type_configurable_downloadable');

            return [$resultPage, $product, $params];
        }

        return null;
    }
}

and another one...

Magento\ConfigurableProduct\Block\Product\View\Type\Configurable

<?php
/**
 * StackOverflow Configurable Downloadable.
 *
 * @category  Mage
 *
 * @author    Toan Nguyen <https://magento.stackexchange.com/users/18821/toan-nguyen>
 * @copyright 2019 Toan Nguyen (https://about.me/nntoan)
 */

namespace StackOverflow\ConfigurableDownloadable\Plugin\ConfigurableProduct\Block\Product\View;

use Magento\Catalog\Model\Product;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Locale\Format;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Store\Model\StoreManagerInterface;

class Configurable
{
    /**
     * @var SerializerInterface
     */
    private $serializer;
    /**
     * @var Format
     */
    private $localeFormat;

    /**
     * Customer session.
     *
     * @var \Magento\Customer\Model\Session
     */
    private $customerSession;

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var PriceCurrencyInterface
     */
    private $priceCurrency;

    /**
     * Store manager.
     *
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    private $storeManager;

    /**
     * Configurable constructor.
     *
     * @param \Magento\Framework\Serialize\SerializerInterface   $serializer
     * @param \Magento\Framework\Locale\Format                   $localeFormat
     * @param \Magento\Customer\Model\Session                    $customerSession
     * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
     * @param \Magento\Framework\Pricing\PriceCurrencyInterface  $priceCurrency
     * @param \Magento\Store\Model\StoreManagerInterface         $storeManager
     */
    public function __construct(
        SerializerInterface $serializer,
        Format $localeFormat,
        CustomerSession $customerSession, // @codingStandardsIgnoreLine
        ScopeConfigInterface $scopeConfig,
        PriceCurrencyInterface $priceCurrency,
        StoreManagerInterface $storeManager
    ) {
        $this->serializer = $serializer;
        $this->localeFormat = $localeFormat;
        $this->customerSession = $customerSession;
        $this->scopeConfig = $scopeConfig;
        $this->priceCurrency = $priceCurrency;
        $this->storeManager = $storeManager;
    }

    /**
     * @param \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable $subject
     * @param                                                                   $result
     *
     * @return bool|string
     *
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function afterGetJsonConfig(
        \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable $subject,
        $result
    ) {
        if ((bool) $subject->getProduct()->getIsConfigurableDownloadable() === false) {
            return $result;
        }

        $decoded = $this->serializer->unserialize($result);
        $decoded['is_configurable_downloadable'] = '1';
        foreach ($subject->getAllowProducts() as $product) {
            $downloadableProductData = $this->getDownloadableProductData($product);
            $decoded['downloadableProductData'][$product->getId()] = $downloadableProductData;
        }

        return $this->serializer->serialize($decoded);
    }

    public function getDownloadableProductData(Product $product)
    {
        // at this moment, $product is Downloadable Product, lets do something with the data and returns it as an array
        // this is the JSON you are playing with when user switch the type (CD/DVD - simple, Download - downloadable)
    }
}

This is just a draft of it, but above plugins is the core of whole extension which demonstrate how could you save Downloadable as variations of Configurable Product. You still need to do the following to finish this extension:

  1. The layout handles (configurable_downloadable), in this layout you need to add the blocks downloadable product currently using.
  2. Adding Magento JS component for Downloadable Product able to load in frontend. The data you encoded in (getDownloadableProductData())

Good luck!

  • You are sales a custom ext for zero points :| – HoangHieu Feb 20 at 12:50
  • @HoangHieu Doesn't matter :) – Toan Nguyen Feb 20 at 14:04
  • Nice decision! take + from me;) – Serhii Havrylchuk Feb 22 at 10:36
-1

Nice Idea to get rid of configurable or bundle products etc (which are really load on system for all kind of stuff). I really like this, really increase performance.

Steps in my mind are:

1) Create two custom options for above
2) Place order as normal
3) After order decide which option selected if downloadable selected In this case 
4) email customer with link to download (a lot of methods to secure this link available)
5) Else process order normally
6) you can also create Order Flag as well which let you know in admin what kind of this order is

Simple Module you can create it without any issue.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.