I finally did it. In case somebody else needs it, I'll explain here what I did.
What puzzles me is that magento could already have this feature but for some reason that I cannot understand they decided not to include it. Basically:
if you check the file Magento\Checkout\Controller\Cart\Add::execute()
, you can see that magento gets the parameters from the request (around line 105), checks and normalizes the quantity that has to be added to the cart (around line 108), then does 3 things:
- loads the product using the id passed from the request (
$product = $this->_initProduct();
around line 119)
- adds this product to cart (
$this->cart->addProduct($product, $params);
around line 126)
- then (and this is the important part) checks the value of the
related_product
parameter in the request and, if that parameter is not empty, then adds these products to the cart at the same time as the main product ($this->cart->addProductsByIds(explode(',', $related));
around line 131)
This third step, unfortunately passes only the id of the products, not the options, but it's easily fixable because I, just for testing purposes, changed the Magento\Checkout\Model\Cart::addProductsByIds()
to receive indeed the options:
$request = $this->getQtyRequest($product); // this method gets the parameters from the request and return the qty only
try {
$request['options'] = $this->getOptionsFromRequest(); // I added this line to add again options to the request. This is similar to the getQtyRequest() but returns options instead of qty
$this->getQuote()->addProduct($product, $request);
} catch (\Exception $e) {
$allAdded = false;
}
And it works: the options are added to the related_item.
You don't even have to temper with the $options
($this->getRequest()->getParam('options')
) because they come as an array of option_title_id => option_value_id, meaning that the option will be passed to the cart only ti the product that has that option id assigned. So let's say I have two products:
- product A with an option with id 1
- product B with an option with id 2
I add product A as a normal product by passing to the cart the product id and the quantity, then I pass product B as a related_item and use my changed addProductsByIds()
to pass the options ({1:4, 2:7}
where 1 and 2 are the id of the option while 4 and 7 are id of the value of that option):
$params = $this->getRequest()->getParams();
$newParams = ['uenc' => $params['uenc'], 'form_key' => $params['form_key'], 'qty' => $params['qty'], 'options' => $params['options'], 'product' => $productAid, 'item' => $productAid, 'related_product' => $productBid];
$product = $this->_initProduct(); // this gets product id from $params['product'] and load the product
$this->cart->addProduct($product, $params); // this $params contains all the options but because productA has only one option with id 1, only the option with id 1 will be passed to the cart for this product
$this->cart->addProductsByIds([$productBid], $params['options']); // I added the $options. It is not really necessary as addProductsByIds() calls parameters directly from the request but this is just a test so I wanted to pass options quickly. Despite passing all the options here, because product B contains only one option with id 2, then only the option with id 2 will be saved on this product when it's added to cart
This code works as expected because in the cart, I can see my two products:
- product A with option 1 assigned
- product B with option 2 assigned
Honestly, I don't know why magento is making such a fuss to add multiple products with options when they already wrote the code, they just needed to implement it.
Anyway, at this point, if you simply need to add the options to the related_items, then you'd better intercept the addProductsByIds()
public method and call the options there. As I said, this method already calls the request to fetch the quantity, so you can just simply call the options as well.
Unfortunately, I had to do a lot more on the request before actually add products to cart: because I had to modify the controller file anyway, it seemed redundant to me to change both files, so I left the addProductsByIds()
as it is and only changed the controller using an aroundExecute plugin:
- Vendor\Module\etc\frontend\di.xml:
<type name="Magento\Checkout\Controller\Cart\Add">
<plugin name="vendor_module_checkout_cart_add_plugin" type="Vendor\Module\Plugin\ControllerCartAddPlugin" sortOrder="10" disabled="false" />
</type>
- Vendor\Module\Plugin\ControllerCartAddPlugin.php:
... // here is a bunch of code that is irrelevant to this example
$newParams = [
'uenc' => $request->getParam('uenc'),
'form_key' => $request->getParam('form_key'),
'selected_configurable_option' => $request->getParam('selected_configurable_option'),
'related_product' => $request->getParam('related_product'),
'product' => $mainProductId,
'item' => $mainProductId,
'options' => $options,
'qty' => $mainQty,
];
// adding the first product
$product = $this->getProductById($mainProductId);
$this->cart->addProduct($product, $newParams);
// adding second product
$relatedParams = $newParams;
$relatedParams['product'] = $secondPorductId;
$relatedParams['qty'] = $secondQty;
$subject->getRequest()->setParams($relatedParams);
... // here is a bunch of code that is irrelevant to this example
return $proceed();
So basically what I did is:
- load the first product and add it to cart
- set parameters for second product so that the original method (
$proceed()
) will add second product to cart when called.
I try to avoid around methods as much as I can but this time I couldn't find any other way as I needed to intercept the method before and after its execution.
I don't know it there are better ways to do it, suggestions are always welcome.
$newParams
(for each item) to question, maybe this will help to answer.