📝 Community Note The content on this page was generated with the assistance of AI and is pending a human review. While we've done our best to ensure accuracy, there may be discrepancies or areas that could be improved.
How to add your own products-links providers (related, upsell, crossell)
- Introduction
- Prerequisites
- Step 1: build a module skeleton
- Step 2: creating data patch file to insert our custom link type in Mage-OS database tables
Introduction
This tutorial defines how to add your custom product link provider. We'll go through all the needed steps and code in order to build a module which will provide with such functionality.
Prerequisites
Before proceeding with the below steps, please ensure you have a local installation of Mage-OS ready and running.
If you don't have it yet, please follow installation instructions in order to have Mage-OS ready for this tutorial.
Step 1: build a module skeleton
We'll build the module under: path-to-your-projects-root/app/code
.
First step is to create our module's vendor folder and module's folder under: path-to-your-projects-root/app/code/MageOS/CustomProductLink
Where MageOS
is the vendor name and CustomProductLink
the name of our module.
Then we'll create the 2 mandatory files so Mage-OS recognize our module:
path-to-your-projects-root/app/code/MageOS/CustomProductLink/etc/module.xml
Where module.xml
will contain basic information about our module and its modules dependencies:
<?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="MageOS_CustomProductLink" > <sequence> <module name="Magento_Catalog"/> </sequence> </module></config>
path-to-your-projects-root/app/code/MageOS/CustomProductLink/registration.php
Where registration.php
will contain the needed code to register our module under our Mage-OS project:
<?php use Magento\Framework\Component\ComponentRegistrar; ComponentRegistrar::register( ComponentRegistrar::MODULE, 'MageOS_CustomProductLink', __DIR__);
After having this completed, we should be able to activate our module, to do so, we need to go to a console and at the Mage-OS's root directory, run:
bin/magento module:enable MageOS_CustomProductLink
and we should see an output like below:
The following modules have been enabled:- MageOS_CustomProductLink
This means that our new module has been correctly registered and enabled in our project.
Step 2: creating data patch file to insert our custom link type in Mage-OS database tables
Before proceeding with this step, it's recommended checking your current database (especially if it's not a clean Mage-OS installation) to see what was the latest product link type inserted in order to reserve a new ID for our new custom link type.
To do so, connect to your database, navigate to table catalog_product_link_type
and check for the highest link_type_id
value.
In a fresh Mage-OS installation, there should be something like that:
> select * from catalog_product_link_type;+--------------+------------+| link_type_id | code |+--------------+------------+| 1 | relation || 3 | super || 4 | up_sell || 5 | cross_sell |+--------------+------------+
As we can see, the highest link_type_id
is 5
, so we'll use 6
for our custom type.
Now, we're ready to create our data patch file. To do so, we need to create a data patch file under: path-to-your-projects-root/app/code/MageOS/CustomProductLink/Setup/Patch/Data/AddCustomProductLink.php
This file will insert the needed data to link tables so our custom product link type will be recognized by Mage-OS.
The file looks like below:
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Setup\Patch\Data; use Magento\Framework\Setup\ModuleDataSetupInterface;use Magento\Framework\Setup\Patch\DataPatchInterface;use MageOS\CustomProductLink\Model\Product;use MageOS\CustomProductLink\Model\Product\Link; class AddCustomProductLink implements DataPatchInterface{ private ModuleDataSetupInterface $moduleDataSetup; public function __construct(ModuleDataSetupInterface $moduleDataSetup) { $this->moduleDataSetup = $moduleDataSetup; } public static function getDependencies() { return []; } public function getAliases() { return []; } public function apply() { $this->moduleDataSetup->getConnection()->insertForce( $this->moduleDataSetup->getTable('catalog_product_link_type'), [ 'link_type_id' => Link::LINK_TYPE_ACCESSORY, 'code' => Product::CUSTOM_LINK_CODE ] ); $this->moduleDataSetup->getConnection()->insertMultiple( $this->moduleDataSetup->getTable('catalog_product_link_attribute'), [ [ 'link_type_id' => Link::LINK_TYPE_ACCESSORY, 'product_link_attribute_code' => 'position', 'data_type' => 'int' ] ] ); }}
we need to declare:
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Model; class Product{ public const CUSTOM_LINK_CODE = 'accessory';}
and the last file is:
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Model\Product; class Link extends \Magento\Catalog\Model\Product\Link{ public const LINK_TYPE_ACCESSORY = 6; /** * @return $this */ public function useAccessoryLinks() { $this->setLinkTypeId(self::LINK_TYPE_ACCESSORY); return $this; }}
After having this done, we should run:
bin/magento setup:upgrade
And if everything went well, we should see the accessory link an output like below:
> select * from catalog_product_link_type;+--------------+------------+| link_type_id | code |+--------------+------------+| 1 | relation || 3 | super || 4 | up_sell || 5 | cross_sell || 6 | accessory |+--------------+------------+
Like explained above, we're using 6
for our link_type_id
.
Also, you can add some attributes to the accessory link type, we'll use position
and short_description
so you can see we can use different attribute types.
For now, 3 data types are allowed for these attributes, as there are 3 tables reserved for attribute types in the database: catalog_product_link_attribute_decimal
, catalog_product_link_attribute_varchar
, catalog_product_link_attribute_int
.
To set up a custom product link, we’ll need to inject certain dependencies into the core link provider classes. For this, create the following di.xml file.
path-to-your-projects-root/app/code/MageOS/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\CopyConstructor\Composite"> <arguments> <argument name="constructors" xsi:type="array"> <item name="accessory" xsi:type="string">MageOS\CustomProductLink\Model\Product\CopyConstructor\Accessory</item> </argument> </arguments> </type> <type name="Magento\Catalog\Model\Product\LinkTypeProvider"> <arguments> <argument name="linkTypes" xsi:type="array"> <item name="accessory" xsi:type="const">MageOS\CustomProductLink\Model\Product\Link::LINK_TYPE_ACCESSORY</item> </argument> </arguments> </type> <type name="Magento\Catalog\Model\ProductLink\CollectionProvider"> <arguments> <argument name="providers" xsi:type="array"> <item name="accessory" xsi:type="object">MageOS\CustomProductLink\Model\ProductLink\CollectionProvider\Accessory</item> </argument> </arguments> </type></config>
create the Accessory class:
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Model\Product\CopyConstructor; use Magento\Catalog\Model\Product;use Magento\Catalog\Model\Product\CopyConstructorInterface;use Magento\Catalog\Model\Product\Link; class Accessory implements CopyConstructorInterface{ public function build(Product $product, Product $duplicate) { $data = []; $attributes = []; $link = $product->getLinkInstance(); $link->useAccessoryLinks(); foreach ($link->getAttributes() as $attribute) { if (isset($attribute['code'])) { $attributes[] = $attribute['code']; } } /** @var Link $link */ foreach ($product->getAccessoryLinkCollection() as $link) { $data[$link->getLinkedProductId()] = $link->toArray($attributes); } $duplicate->setAccessoryLinkData($data); }}
The class MageOS\CustomProductLink\Model\Product\Link
was created earlier, so no additional work is need it.
Now we need to create the class responsible for the collection provider MageOS\CustomProductLink\Model\ProductLink\CollectionProvider\Accessory
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Model\ProductLink\CollectionProvider; use Magento\Catalog\Model\Product;use Magento\Catalog\Model\ProductLink\CollectionProviderInterface; class Accessory implements CollectionProviderInterface{ protected $accessoryModel; public function __construct( \MageOS\CustomProductLink\Model\Accessory $accessoryModel ) { $this->accessoryModel = $accessoryModel; } /** * {@inheritdoc} */ public function getLinkedProducts(Product $product) { return $this->accessoryModel->getAccessoryProducts($product); }}
The two classes MageOS\CustomProductLink\Model\Product\CopyConstructor\Accessory
and MageOS\CustomProductLink\Model\ProductLink\CollectionProvider\Accessory
are using the class \MageOS\CustomProductLink\Model\Accessory
. The class is providing data from the database, so let's implement it:
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Model; use Magento\Catalog\Model\Product;use Magento\Catalog\Model\ResourceModel\Product\Link\Collection;use Magento\Framework\DataObject;use MageOS\CustomProductLink\Model\Product\Link; class Accessory extends DataObject{ /** * Product link instance * * @var Product\Link */ protected $linkInstance; /** * Accessory 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 Accessory products * * @param Product $currentProduct * @return array */ public function getAccessoryProducts(Product $currentProduct) { if (!$this->hasAccessoryProducts()) { $products = []; $collection = $this->getAccessoryProductCollection($currentProduct); foreach ($collection as $product) { $products[] = $product; } $this->setAccessoryProducts($products); } return $this->getData('accessory_products'); } /** * Retrieve accessory products identifiers * * @param Product $currentProduct * @return array */ public function getAccessoryProductIds(Product $currentProduct) { if (!$this->hasAccessoryProductIds()) { $ids = []; foreach ($this->getAccessoryProducts($currentProduct) as $product) { $ids[] = $product->getId(); } $this->setAccessoryProductIds($ids); } return $this->getData('accessory_product_ids'); } /** * Retrieve collection accessory product * * @param Product $currentProduct * @return \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection */ public function getAccessoryProductCollection(Product $currentProduct) { $collection = $this->getLinkInstance()->useAccessoryLinks()->getProductCollection()->setIsStrongMode(); $collection->setProduct($currentProduct); return $collection; } /** * Retrieve collection accessory link * * @param Product $currentProduct * @return Collection */ public function getAccessoryLinkCollection(Product $currentProduct) { $collection = $this->getLinkInstance()->useAccessoryLinks()->getLinkCollection(); $collection->setProduct($currentProduct); $collection->addLinkTypeIdFilter(); $collection->addProductIdFilter(); $collection->joinAttributes(); return $collection; }}
We are almost there, now we need to display the accessory items associated with the product, create 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="accessory" xsi:type="array"> <item name="class" xsi:type="string">MageOS\CustomProductLink\Ui\DataProvider\Product\Form\Modifier\Accessory</item> <item name="sortOrder" xsi:type="number">120</item> </item> </argument> </arguments> </virtualType></config>
Then we need to create the class that will just defined in the admin di.xml file: \MageOS\CustomProductLink\Ui\DataProvider\Product\Form\Modifier\Accessory
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Related;use Magento\Ui\Component\Form\Fieldset; class Accessory extends Related{ public const DATA_SCOPE_ACCESSORY = 'accessory'; /** * @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_ACCESSORY => $this->getAccessoryFieldset() ], 'arguments' => [ 'data' => [ 'config' => [ 'label' => __('Related Products, Up-Sells, Cross-Sells and Accessory'), '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 getAccessoryFieldset() { $content = __( 'Custom type products are shown to customers in addition to the item the customer is looking at.' ); return [ 'children' => [ 'button_set' => $this->getButtonSet( $content, __('Add Accessory Products'), $this->scopePrefix . static::DATA_SCOPE_ACCESSORY ), 'modal' => $this->getGenericModal( __('Add Accessory Products'), $this->scopePrefix . static::DATA_SCOPE_ACCESSORY ), static::DATA_SCOPE_ACCESSORY => $this->getGrid($this->scopePrefix . static::DATA_SCOPE_ACCESSORY), ], 'arguments' => [ 'data' => [ 'config' => [ 'additionalClasses' => 'admin__fieldset-section', 'label' => __('Accessory Products'), 'collapsible' => false, 'componentType' => Fieldset::NAME, 'dataScope' => '', 'sortOrder' => 90, ], ], ] ]; } /** * Retrieve all data scopes * * @return array */ protected function getDataScopes() { return [ static::DATA_SCOPE_ACCESSORY ]; }}
Since the UI target in the modifier class is set to accessory_product_listing, we need to create the corresponding accessory_product_listing.xml component file.
MageOS/CustomProductLink/view/adminhtml/ui_component/accessory_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">accessory_product_listing.accessory_product_listing_data_source</item> <item name="deps" xsi:type="string">accessory_product_listing.accessory_product_listing_data_source</item> </item> <item name="spinner" xsi:type="string">product_columns</item> </argument> <dataSource name="accessory_product_listing_data_source"> <argument name="dataProvider" xsi:type="configurableObject"> <argument name="class" xsi:type="string">MageOS\CustomProductLink\Ui\DataProvider\Product\Related\AccessoryDataProvider</argument> <argument name="name" xsi:type="string">accessory_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">accessoryProductGrid</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>
then we need to create the Accessory Data Provider class:
<?php declare(strict_types=1); namespace MageOS\CustomProductLink\Ui\DataProvider\Product\Related; use Magento\Catalog\Ui\DataProvider\Product\Related\AbstractDataProvider;use MageOS\CustomProductLink\Ui\DataProvider\Product\Form\Modifier\Accessory; class AccessoryDataProvider extends AbstractDataProvider{ protected function getLinkType() { return Accessory::DATA_SCOPE_ACCESSORY; }}