Magento2 | PWA | GraphQL

Create Custom Product Relation for Magento with Custom Linked Product Types with GraphQL Data


In this post, we will check how to add custom link products like related products, Up-sell products and Cross-sell products. We will add new product type `customlink` (Buy together product) from admin Just below the Related, Up-sell and Cross-sell product tab.

Now, to create a custom product link type, we need to add its entry in catalog_product_link_type and catalog_product_link_attribute tables, then create modifier (to add it on product edit page), model (to get linked product collection from catalog product model), and ui_component.

It Also supports to import `custom link products` via Magento's Import CSV.
I have also added GrapQL Query to fetch the details of custom linked products.

You can find complete module in Github at Magelearn_LinkProduct




Let's start it by creating custom module.

Create a folder inside app/code/Magelearn/LinkProduct

Add registration.php file in it:

<?php

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

Add composer.json file in it:

{
    "name": "magelearn/link-product",
    "description": "Custom Link product relation for magento 2",
    "license": "proprietary",
    "authors": [
        {
            "name": "vijay rami",
            "email": "vijaymrami@gmail.com"
        }
    ],
    "require": {},
    "type": "magento2-module",
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Magelearn\\LinkProduct\\": ""
        }
    }
}

Add etc/module.xml file in it:

<?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="Magelearn_LinkProduct" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>
First We will check how to add new product type in product edit form at admin inside Related Products tab.
For that first add etc/adminhtml/di.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="customlink" xsi:type="array">
                    <item name="class" xsi:type="string">Magelearn\LinkProduct\Ui\DataProvider\Product\Form\Modifier\CustomLinkTab</item>
                    <item name="sortOrder" xsi:type="number">120</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

Now as per highlighted above add our custom product form modifier class at Ui/DataProvider/Product/Form/Modifier/CustomLinkTab.php

<?php

namespace Magelearn\LinkProduct\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Related;
use Magento\Ui\Component\Form\Fieldset;

class CustomLinkTab extends Related
{
    const DATA_SCOPE_CUSTOM = 'customlink';
    /**
     * @var string
     */
    private static $previousGroup = 'search-engine-optimization';
    /**
     * @var int
     */
    private static $sortOrder = 90;
    /**
     * {@inheritdoc}
     */
    public function modifyMeta(array $meta)
    {
        $meta = array_replace_recursive(
            $meta,
            [
                static::GROUP_RELATED => [
                    'children' => [
                        $this->scopePrefix . static::DATA_SCOPE_CUSTOM => $this->getCustomFieldset()
                    ],
                    'arguments' => [
                        'data' => [
                            'config' => [
                                'label' => __('Related Products, Up-Sells, Cross-Sells and Buy together Products'),
                                'collapsible' => true,
                                'componentType' => Fieldset::NAME,
                                'dataScope' => static::DATA_SCOPE,
                                'sortOrder' => $this->getNextGroupSortOrder(
                                    $meta,
                                    self::$previousGroup,
                                    self::$sortOrder
                                ),
                            ],
                        ],
                    ],
                ],
            ]
        );
        return $meta;
    }
    /**
     * Prepares config for the Custom type products fieldset
     *
     * @return array
     */
    protected function getCustomFieldset()
    {
        $content = __(
            'Buy Together products are shown to customers in addition to the item the customer is looking at.'
        );
        return [
            'children' => [
                'button_set' => $this->getButtonSet(
                    $content,
                    __('Add Buy Together Products'),
                    $this->scopePrefix . static::DATA_SCOPE_CUSTOM
                ),
                'modal' => $this->getGenericModal(
                    __('Add Buy Together Products'),
                    $this->scopePrefix . static::DATA_SCOPE_CUSTOM
                ),
                static::DATA_SCOPE_CUSTOM => $this->getGrid($this->scopePrefix . static::DATA_SCOPE_CUSTOM),
            ],
            'arguments' => [
                'data' => [
                    'config' => [
                        'additionalClasses' => 'admin__fieldset-section',
                        'label' => __('Add Buy Together Products'),
                        'collapsible' => false,
                        'componentType' => Fieldset::NAME,
                        'dataScope' => '',
                        'sortOrder' => 90,
                    ],
                ],
            ]
        ];
    }
    /**
     * Retrieve all data scopes
     *
     * @return array
     */
    protected function getDataScopes()
    {
        return [
            static::DATA_SCOPE_CUSTOM
        ];
    }
}

As because of our Data Scope is 'customlink' we will add view/adminhtml/ui_component/customlink_product_listing.xml
<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">customlink_product_listing.customlink_product_listing_data_source</item>
            <item name="deps" xsi:type="string">customlink_product_listing.customlink_product_listing_data_source</item>
        </item>
        <item name="spinner" xsi:type="string">product_columns</item>
    </argument>
    <dataSource name="customlink_product_listing_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">Magelearn\LinkProduct\Ui\DataProvider\Product\Related\CustomLinkDataProvider</argument>
            <argument name="name" xsi:type="string">customlink_product_listing_data_source</argument>
            <argument name="primaryFieldName" xsi:type="string">entity_id</argument>
            <argument name="requestFieldName" xsi:type="string">id</argument>
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
                    <item name="update_url" xsi:type="url" path="mui/index/render"/>
                    <item name="storageConfig" xsi:type="array">
                        <item name="cacheRequests" xsi:type="boolean">false</item>
                    </item>
                </item>
            </argument>
        </argument>
    </dataSource>
    <listingToolbar name="listing_top">
        <filters name="listing_filters">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="statefull" xsi:type="array">
                        <item name="applied" xsi:type="boolean">false</item>
                    </item>
                    <item name="params" xsi:type="array">
                        <item name="filters_modifier" xsi:type="array" />
                    </item>
                    <item name="observers" xsi:type="array">
                        <item name="filters" xsi:type="object">Magento\Catalog\Ui\Component\Listing\Filters</item>
                    </item>
                </item>
            </argument>
        </filters>
        <paging name="listing_paging"/>
    </listingToolbar>
    <columns name="product_columns" class="Magento\Catalog\Ui\Component\Listing\Columns">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="childDefaults" xsi:type="array">
                    <item name="fieldAction" xsi:type="array">
                        <item name="provider" xsi:type="string">customLinkProductGrid</item>
                        <item name="target" xsi:type="string">selectProduct</item>
                        <item name="params" xsi:type="array">
                            <item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
                        </item>
                    </item>
                </item>
            </item>
        </argument>
        <selectionsColumn name="ids">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="indexField" xsi:type="string">entity_id</item>
                    <item name="sortOrder" xsi:type="number">0</item>
                    <item name="preserveSelectionsOnFilter" xsi:type="boolean">true</item>
                </item>
            </argument>
        </selectionsColumn>
        <column name="entity_id">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">textRange</item>
                    <item name="sorting" xsi:type="string">asc</item>
                    <item name="label" xsi:type="string" translate="true">ID</item>
                    <item name="sortOrder" xsi:type="number">10</item>
                </item>
            </argument>
        </column>
        <column name="thumbnail" class="Magento\Catalog\Ui\Component\Listing\Columns\Thumbnail">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/thumbnail</item>
                    <item name="add_field" xsi:type="boolean">true</item>
                    <item name="sortable" xsi:type="boolean">false</item>
                    <item name="altField" xsi:type="string">name</item>
                    <item name="has_preview" xsi:type="string">1</item>
                    <item name="align" xsi:type="string">left</item>
                    <item name="label" xsi:type="string" translate="true">Thumbnail</item>
                    <item name="sortOrder" xsi:type="number">20</item>
                </item>
            </argument>
        </column>
        <column name="name">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="add_field" xsi:type="boolean">true</item>
                    <item name="label" xsi:type="string" translate="true">Name</item>
                    <item name="sortOrder" xsi:type="number">30</item>
                </item>
            </argument>
        </column>
        <column name="attribute_set_id">
            <argument name="data" xsi:type="array">
                <item name="options" xsi:type="object">Magento\Catalog\Model\Product\AttributeSet\Options</item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">select</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item>
                    <item name="dataType" xsi:type="string">select</item>
                    <item name="label" xsi:type="string" translate="true">Attribute Set</item>
                    <item name="sortOrder" xsi:type="number">40</item>
                </item>
            </argument>
        </column>
        <column name="attribute_set_text" class="Magento\Catalog\Ui\Component\Listing\Columns\AttributeSetText">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="sortOrder" xsi:type="number">41</item>
                    <item name="label" xsi:type="string" translate="true">AttributeSetText</item>
                    <item name="visible" xsi:type="boolean">false</item>
                </item>
            </argument>
        </column>
        <column name="status">
            <argument name="data" xsi:type="array">
                <item name="options" xsi:type="object">Magento\Catalog\Model\Product\Attribute\Source\Status</item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">select</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item>
                    <item name="dataType" xsi:type="string">select</item>
                    <item name="label" xsi:type="string" translate="true">Status</item>
                    <item name="sortOrder" xsi:type="number">50</item>
                </item>
            </argument>
        </column>
        <column name="status_text" class="Magento\Catalog\Ui\Component\Listing\Columns\StatusText">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="sortOrder" xsi:type="number">51</item>
                    <item name="label" xsi:type="string" translate="true">StatusText</item>
                    <item name="visible" xsi:type="boolean">false</item>
                </item>
            </argument>
        </column>
        <column name="type_id">
            <argument name="data" xsi:type="array">
                <item name="options" xsi:type="object">Magento\Catalog\Model\Product\Type</item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">select</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item>
                    <item name="dataType" xsi:type="string">select</item>
                    <item name="label" xsi:type="string" translate="true">Type</item>
                    <item name="sortOrder" xsi:type="number">60</item>
                </item>
            </argument>
        </column>
        <column name="sku">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">SKU</item>
                    <item name="sortOrder" xsi:type="number">70</item>
                </item>
            </argument>
        </column>
        <column name="price" class="Magento\Catalog\Ui\Component\Listing\Columns\Price">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">textRange</item>
                    <item name="add_field" xsi:type="boolean">true</item>
                    <item name="label" xsi:type="string" translate="true">Price</item>
                    <item name="sortOrder" xsi:type="number">80</item>
                </item>
            </argument>
        </column>
    </columns>
</listing>
As per highlighted code above we will add our DataProvider class to display data properly on listing of this xml file.

Add Dataprovider class at Ui/DataProvider/Product/Related/CustomLinkDataProvider.php
<?php

namespace Magelearn\LinkProduct\Ui\DataProvider\Product\Related;

use Magento\Catalog\Ui\DataProvider\Product\Related\AbstractDataProvider;

class CustomLinkDataProvider extends AbstractDataProvider
{
    /**
     * {@inheritdoc
     */
    protected function getLinkType()
    {
        return 'customlink';
    }
}
After adding the above code new `customlink` product type will be displayed in the Product edit form below cross-sell products.

Now to create a custom product relation and save custom linked product types in database, we will add our install schema script.

This script will run only a single time during module installation command. Once the module will be installed, you need to remove module name (Magelearn_LinkProduct) entry from `seup_module` table to run it again.

Create Setup/InstallSchema.php file.
<?php

namespace Magelearn\LinkProduct\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magelearn\LinkProduct\Model\Product\Link;
use Magelearn\LinkProduct\Ui\DataProvider\Product\Form\Modifier\CustomLinkTab;

class InstallSchema implements InstallSchemaInterface
{

    /**
     * @param SchemaSetupInterface $setup
     * @param ModuleContextInterface $context
     */
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $setup->startSetup();

        /**
         * Install product link types in table (catalog_product_link_type)
         */
        $catalogProductLinkTypeData = [
            'link_type_id' => Link::LINK_TYPE_CUSTOMLINK,
            'code' => CustomLinkTab::DATA_SCOPE_CUSTOM
        ];

        $setup->getConnection()->insertOnDuplicate(
            $setup->getTable('catalog_product_link_type'),
            $catalogProductLinkTypeData
        );

        /**
         * install product link attributes position in table catalog_product_link_attribute
         */
        $catalogProductLinkAttributeData = [
            'link_type_id' => Link::LINK_TYPE_CUSTOMLINK,
            'product_link_attribute_code' => 'position',
            'data_type' => 'int',
        ];

        $setup->getConnection()->insert(
            $setup->getTable('catalog_product_link_attribute'),
            $catalogProductLinkAttributeData
        );

        $setup->endSetup();
    }
}
Now, To create a custom product link, we need to inject some dependencies to core link provider classes, so create the following di.xml for that inside etc/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\Model\Product\LinkTypeProvider">
        <arguments>
            <argument name="linkTypes" xsi:type="array">
                <item name="customlink" xsi:type="const">Magelearn\LinkProduct\Model\Product\Link::LINK_TYPE_CUSTOMLINK</item>
            </argument>
        </arguments>
    </type>
    <type name="Magento\Catalog\Model\ProductLink\CollectionProvider">
        <arguments>
            <argument name="providers" xsi:type="array">
                <item name="customlink" xsi:type="object">Magelearn\LinkProduct\Model\ProductLink\CollectionProvider\CustomLinkProducts</item>
            </argument>
        </arguments>
    </type>
    <!-- used for import custom link products via import CSV feature -->
    <type name="Magento\CatalogImportExport\Model\Import\Product\LinkProcessor">
        <arguments>
            <argument name="linkNameToId" xsi:type="array">
                <item name="_customlink_" xsi:type="const">Magelearn\LinkProduct\Model\Product\Link::LINK_TYPE_CUSTOMLINK</item>
            </argument>
        </arguments>
    </type>
    <type name="Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType">
        <plugin sortOrder="1" name="magelearnLinkProductAbstractType" type="Magelearn\LinkProduct\Plugin\Model\Import\Product\Type\AbstractTypePlugin"/>
    </type>
</config>
As per highlighted above code add our model class at Model/Product/Link.php
<?php

namespace Magelearn\LinkProduct\Model\Product;

class Link extends \Magento\Catalog\Model\Product\Link
{
    const LINK_TYPE_CUSTOMLINK = 7;
    
    /**
     * @return $this
     */
    public function useCustomLinks()
    {
        $this->setLinkTypeId(self::LINK_TYPE_CUSTOMLINK);
        return $this;
    }
}
Add Model/ProductLink/CollectionProvider/CustomLinkProducts.php
<?php

namespace Magelearn\LinkProduct\Model\ProductLink\CollectionProvider;

use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ProductLink\CollectionProviderInterface;

class CustomLinkProducts implements CollectionProviderInterface
{
    /** @var \Magelearn\LinkProduct\Model\CustomLinkProduct */
    protected $customLinkModel;

    /**
     * Custom Link constructor.
     * @param \Magelearn\LinkProduct\Model\CustomLinkProduct $customLinkModel
     */
    public function __construct(
        \Magelearn\LinkProduct\Model\CustomLinkProduct $customLinkModel
    ) {
        $this->customLinkModel = $customLinkModel;
    }

    /**
     * {@inheritdoc}
     */
    public function getLinkedProducts(Product $product)
    {
        return (array) $this->customLinkModel->getCustomLinkProducts($product);
    }
}
Now as per highlighted code above add our custom link product model class at Magelearn/LinkProduct/Model/CustomLinkProduct.php
<?php

namespace Magelearn\LinkProduct\Model;

use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Link\Collection;
use Magento\Framework\DataObject;
use Magelearn\LinkProduct\Model\Product\Link;

class CustomLinkProduct extends DataObject
{
    /**
     * Product link instance
     *
     * @var Product\Link
     */
    protected $linkInstance;
    
    /**
     * CustomLinkProduct constructor.
     * @param Link $productLink
     */
    public function __construct(
        Link $productLink
        ) {
            $this->linkInstance = $productLink;
    }
    
    /**
     * Retrieve link instance
     *
     * @return  Product\Link
     */
    public function getLinkInstance()
    {
        return $this->linkInstance;
    }

    /**
     * Retrieve array of customlink products
     *
     * @param Product $currentProduct
     * @return array
     */
    public function getCustomLinkProducts(Product $currentProduct)
    {
        if (!$this->hasCustomLinkProducts()) {
            $products = [];
            $collection = $this->getCustomLinkProductCollection($currentProduct);
            foreach ($collection as $product) {
                $products[] = $product;
            }
            $this->setCustomLinkProducts($products);
        }
        return $this->getData('custom_link_products');
    }

    /**
     * Retrieve customlink products identifiers
     *
     * @param Product $currentProduct
     * @return array
     */
    public function getCustomLinkProductIds(Product $currentProduct)
    {
        if (!$this->hasCustomLinkProductIds()) {
            $ids = [];
            foreach ($this->getCustomLinkProducts($currentProduct) as $product) {
                $ids[] = $product->getId();
            }
            $this->setCustomLinkProductIds($ids);
        }
        return $this->getData('custom_link_product_ids');
    }

    /**
     * Retrieve collection customlink product
     *
     * @param Product $currentProduct
     * @return \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection
     */
    public function getCustomLinkProductCollection(Product $currentProduct)
    {
        $collection = $this->getLinkInstance()->useCustomLinks()->getProductCollection()->setIsStrongMode();
        $collection->setProduct($currentProduct);
        return $collection;
    }
}
And at last as per highlighted in di.xml file, to support this custom product link type in import/export file, we will add our plugin class at Plugin/Model/Import/Product/Type/AbstractTypePlugin.php
<?php

namespace Magelearn\LinkProduct\Plugin\Model\Import\Product\Type;

use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType;

class AbstractTypePlugin
{
    /**
     * @param AbstractType $subject
     * @param string[]     $result
     * @return string[]
     */
    public function afterGetCustomFieldsMapping(AbstractType $subject, array $result): array
    {
        $result['_customlink_sku'] = 'customlink_skus';

        return $result;
    }
}

For GraphQl 

To get this custom link product types data in GraphQl, First add file etc/schema.graphqls
interface ProductInterface {
    custom_link_products: [ProductInterface]
    @doc(description: "An array of products to be displayed in a Buy Together Products block.")
    @resolver(class: "Magelearn\\LinkProduct\\Model\\Resolver\\Batch\\BuyTogetherProducts") 
}

As per highlighted code above add resolver class at Model/Resolver/Batch/BuyTogetherProducts.php

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magelearn\LinkProduct\Model\Resolver\Batch;

use Magelearn\LinkProduct\Model\Product\Link;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\BatchResponse;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\RelatedProductGraphQl\Model\Resolver\Batch\AbstractLikedProducts;

/**
 * Buy Together Products Resolver
 */
class BuyTogetherProducts extends AbstractLikedProducts
{
    /**
     * @inheritDoc
     */
    protected function getNode(): string
    {
        return 'custom_link_products';
    }

    /**
     * @inheritDoc
     */
    protected function getLinkType(): int
    {
        return Link::LINK_TYPE_CUSTOMLINK;
    }
}
After adding above files, you can retrieve this custom type products data same as related products, up-sell products and cross-sell products data like below:
Just add `custom_link_products` in GraphQl Request.
{
  products(filter: { sku: { eq: "24-WB06" } }) {
    items {
      uid
      name
      related_products {
        uid
        name
      }
      upsell_products {
        uid
        name
      }
      crosssell_products {
        uid
        name
      }
      custom_link_products {
        uid
        name
      }
    }
  }
}
0 Comments On "Create Custom Product Relation for Magento with Custom Linked Product Types with GraphQL Data"

Back To Top