Türchen 22: How to: Complex product configurator in Magento 2

Dec
22
2018

On behalf of a merchant we built a complex product configurator for pallet racks. It is able to create individually assembled products from a large range of grouped products and displays them as a single entity – a bundled product – in cart. In this instructive post we demonstrate how this can be done in Magento 2.

The problem on a practical level

Our client’s expectations were defined clearly: Customers in the web shop shall be able to customize a rack consisting of a larger range of different parts to their individual needs by means of the configurator. After choosing the right edition (light, intermediate, or heavy duty) for the pallet rack he shall be able to define the number of rack bays and the number of shelves per rack bay. Mandatory supply like nuts and bolts shall be included automatically and upon request also optional extra parts like anti-ramming protection corners.

Product configurator in Magento 2

After finishing configuration the customer shall be able to add the individually assembled rack to cart and find it there as one entity instead of a chaotic hotchpotch of parts. Otherwise it would be unreasonable for the customer to select all the right parts in the accurate quantity to remove a complete rack from a cart full of parts belonging to two or more racks for example. And even if a customer would take on this busywork it could still happen that in the end there remains a selection of parts in the cart that do not fit together and would not add up to a complete and viable but to a completely insecure rack. This is what had to be avoided.

The problem on a technical level

As our task was to develop a configurator for the individual assembling of products, it was not an option to rely on pre-defined bundled products from the beginning. But with regard to the three editions of racks in the web shop there was the possibility to define a complex grouped product for each of the payloads as base for the configurator. So there would be one configurator on each of the three product pages. But in the end only the contained parts of a grouped product, i.e. simple products, are added to cart. And in the case of an individually configured pallet rack this would be quite a lot of parts. And as soon as more products are added to cart it would no longer be apparent which parts belong to the rack and which don’t.

Hotchpotch of simple products in cart

Hotchpotch of simple products in cart

This is why it was necessary to automatically create a bundled product from the parts of the grouped product and set the corresponding options out of the configurator for displaying the completely assembled rack as an entity as soon as the customer adds the rack to cart. A customer that would have configured two different racks would find in his cart only two products that are bundled products on a technical level and contain exactly the parts belonging to the respective rack.

Two pallet racks as bundled products in cart

Two pallet racks as bundled products in cart

The solution

In the Magento tests there is a php file that creates a bundled product. This file is to be found in
tests/integration/testsuite/Magento/Bundle/_files/product.php.
We used this file as a basis for the creation of a bundled product in our use case.

To transform the grouped product to a bundled product in the right moment (when adding it to cart) it is necessary to edit Magento\Checkout\Model\Cart. The method addProduct($productInfo, $requestInfo = null) has to be extended with this request:


public function addProduct($productInfo, $requestInfo = null)
{
if ($productInfo->getTypeId() === 'grouped') {
   		 $entityId = $this->_createBundleCopy($productInfo, $requestInfo);
   		 $requestInfoNew = $this->_changeRequestInfo
($entityId,$requestInfo);  
		 // ………

First a bundled product with all needed options is being created using the method _createBundleCopy($productInfo, $requestInfo):


protected function _createBundleCopy($productInfo, $requestInfo) {
	// Last assigned increment-id
   	 $entityId = intval($this->_helper->getEntityId()) + 1;
	// Create options for bundled product
   	 $this->_createOptionsArray($requestInfo);
   	 
   	 $productRepository = $this->_objectManager
->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);

   	 $product = $this->_objectManager->create
(\Magento\Catalog\Model\Product::class);
   	 $product->setTypeId('bundle')
   		 ->setId($entityId)
   		 ->setAttributeSetId(4)
   		 ->setWebsiteIds([1])
   		 ->setName($productInfo->getName().'-'.$entityId)
   		 ->setSku($productInfo->getName().'-'.$entityId)->setVisibility
    (\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
   		 ->setStockData(['use_config_manage_stock' => 1, 'qty' => 1,
 				     'is_qty_decimal' => 0,'is_in_stock' => 1])
   		 ->setPriceView(1)
   		 ->setPriceType(0)
   		 ->setShipmentType(1)
   		 ->setPrice(0);
	//………

The options for the bundled product are created in _createOptionsArray($requestInfo). For the right mapping to be created it is necessary to get a closer look at the request. A request for a grouped product looks like this:


[uenc] => aHR0cDovL2Fja3J1dGF0LW1pY2hhLnNwbGVuZGlkLWRldi5kZS9yZWdhbC1sZWljaHRlLWF1c2Z1aHJ1bmcuaHRtbA,,
[product] => 29
[selected_configurable_option] =>
[related_product] =>
[form_key] => 0YYmuQDRUOKvb2v4
[super_group] => Array
(
	[1] => 10
      [2] => 5
      [3] => 4
	[4] => 0
)

Here the array super_group is in focus because the options for the bundled product need to be created from it. The respective key in the array is the entity ID of the single product. The value is the quantity of the selected product. With these informations the options are created with the method _createOptionsArray($requestInfo). As it is known precisely which simple products were chosen in which quantity by the customer from the configuration of the grouped product the exactly matching options for the bundled product can be created from it.


protected function _createOptionsArray($requestInfo) {
foreach($requestInfo['super_group'] as $key => $value) {
   		if(intval($value) !== 0) {
   		$this->_bundleOptionsData[]=  
array('title' => 'Name',
   			      'default_title' => 'Name',
   			      'type' => 'select',
   			      'required' => 0,
   			      'delete' => '');
   		$this->_bundleSelectionsData[] =  
array(array('product_id' => $key,
   			            'selection_qty' => $value,
   	                      	'selection_can_change_qty' => 1,
   			            'delete' => ''));
   		 }   		 
   	 }
}

Now the options can be added to the bundled product. This is done in the second part of the method _createBundleCopy.


protected function _createBundleCopy($productInfo, $requestInfo) {
	// ………..


	// Add options to bundled product
   	 if (count($this->_bundleOptionsData) > 0) {
   		 $options = [];
   		 foreach ($this->_bundleOptionsData as $key => $optionData) {
   			 if (!(bool)$optionData['delete']) {
   				 $option = $this->_objectManager->create
(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class)
   					 ->create(['data' => $optionData]);
   				 $option->setSku($product->getSku());
   				 $option->setOptionId(null);

   				 $links = [];
   				 $bundleLinks = $this->_bundleSelectionsData;
   				 if (!empty($bundleLinks[$key])) {
   					 foreach ($bundleLinks[$key] as $linkData) {
   						 if (!(bool)$linkData['delete']) {
   							 $link = $this->_objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class)
   								 ->create(['data' => $linkData]);
   							 $linkProduct = $productRepository->getById($linkData['product_id']);
   							 $link->setSku($linkProduct->getSku());
   							 $link->setQty($linkData['selection_qty']);
   							 if (isset($linkData['selection_can_change_qty'])) {
   								 $link->setCanChangeQuantity($linkData['selection_can_change_qty']);
   							 }
   							 $links[] = $link;
   						 }
   					 }
   					 $option->setProductLinks($links);
   					 $options[] = $option;
   				 }
   			 }
   		 }
   		 $extension = $product->getExtensionAttributes();
   		 $extension->setBundleProductOptions($options);
   		 $product->setExtensionAttributes($extension);
   	 }
   	 $product->save();
   	 return $entityId;
}

After the options are added, the bundled product is saved and will now be available in the web shop.

As a next step the request has to be edited. It has to be transformed from the original request for a grouped product to a request for a bundled product. This requires a closer look at the request for a bundled product:


[uenc] => aHR0cDovL2Fja3J1dGF0LW1pY2hhLnNwbGVuZGlkLWRldi5kZS9idW5kbGV0ZXN0Lmh0bWw,
[product] => 34
[selected_configurable_option] =>
[related_product] =>
[form_key] => 0YYmuQDRUOKvb2v4
[bundle_option] => Array
(
   [3] => 15
   [4] => 18
)
[qty] => 1

The options for the bundled product can be found in the array [bundle_option]. These have already been created from the array [super_group] by means of the method _createOptionsArray($requestInfo). Now the request is converted so it can be used for a bundled product with the method _changeRequestInfo($entityId, $requestInfo). To do so the options have to be fetched from the database. The options for this bundled product can be found in the table catalog_product_bundle_selection. From the column parent_product_id in this table the entries containing the entity ID of the bundled product have to be selected. As the options have been created just as they are needed for this configuration before, they can now be added to the request:


protected function _changeRequestInfo($entityId, $requestInfo) 
{
	 // Get options for the bundled product from the database
   	 $result = $this->_helper->getBundleOptions($entityId);
   	 foreach($result as $res) {
   		 $option[$res['option_id']] = $res['selection_id'];
   	 }
   	 $requestNew = array();
   	 $requestNew['uenc'] = $requestInfo['uenc'];
	 // Enter the bundled products entity ID here
   	 $requestNew['product'] = $entityId;
   	 $requestNew['selected_configurable_option'] = 
$requestInfo['selected_configurable_option'];
   	 $requestNew['related_product'] = $requestInfo['related_product'];
   	 $requestNew['form_key'] = $requestInfo['form_key'];
   	 $requestNew['bundle_option'] = $option;
   	 $requestNew['qty'] = 1;
   	 return $requestNew;
    }

After the request for a grouped product has been transformed into a request for a bundled product the product can now be added to cart as usual. This is done with the method addProduct($productInfo, $requestInfo = null) again:


public function addProduct($productInfo, $requestInfo = null)
	{
   	 if ($productInfo->getTypeId() === 'grouped') {
		 $entityId = $this->_createBundleCopy($productInfo, $requestInfo);
   		 $requestInfoNew = $this->_changeRequestInfo
($entityId, $requestInfo);
   		 $bundleProduct = $this->_helper->getProductById($entityId);

   		 $product = $this->_getProduct($bundleProduct);
   		 $request = $this->_getProductRequest($requestInfoNew);
   		 $productId = $product->getId();
   	 } else

	// ……

The further processing is standard Magento.

But there is one thing to note: With this solution a new bundled product will be created from the grouped product in each purchase. We solved that by automatically deleting the respective bundled product whenever a rack has been bought in the web shop.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *