Doctrine 2 ArrayCollection - how to find and modify a specific element - php

I have a Cart entity that contains an ArrayCollection of CartItems. The relationship in code -
Cart.php:
class Cart
{
/**
* #ORM\OneToMany(targetEntity="CartItem", mappedBy="cart")
*/
private $cartItems;
public function __construct(User $user)
{
$this->cartItems = new ArrayCollection();
}
}
CartItems are essentially a DB pointer (for lack of a better term) to a Product entity, with an additional quantity field.
CartItem.php:
class CartItem
{
/**
* #ORM\ManyToOne(targetEntity="Cart", inversedBy="cartItems")
*/
private $cart;
/**
* #ORM\OneToOne(targetEntity="Product")
*/
private $product;
/**
* #ORM\Column(type="float")
*
* #Assert\NotBlank(message="You must specify how much of the item you wish to buy")
* #Assert\Regex(pattern="/^\d+(\.\d{1,2})?$/", message="The quantity must be a number")
*/
private $quantity;
}
In my app, when a user wants to add a Product to their Cart, I want to check to see if that particular Product is already one of the CartItems. If so, I want to increase the quantity, otherwise add it to the collection. I'm not exactly sure how to do this.
ArrayCollection methods exists and contains simply return true/false. Given my setup, I'm not sure what I'd use as a key for the collection.
Any suggestions?

You can filter CartItems for new Product. If filtered CartItems is not empty increase the quantity, otherwise add new CartItem.
$newProduct // Product Object
$cartItems = $cart->getCartItems()->filter(function(CartItem $cartItem) (use $newProduct) {
return $cartItem->getProduct() === $newProduct;
});
if ($cartItems->count()) {
foreach ($cartItems as $cartItem) {
$cartItem->setQuantity($cartItem->getQuantity() + 1);
}
} else {
$cart->addCartItem(
(new CartItem())
->setProduct($newProduct)
->setQuantity(1)
);
}

Related

Doctrine only select last entry of collection

I have the following Entity :
class Product
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\StockEntry", mappedBy="product")
*
* #Groups({"product:read", "product:read:all"})
*/
private Collection $stockEntries;
public function getStockEntries(): Collection
{
return $this->stockEntries;
}
public function getLastBuyPrice(): float
{
/** #var StockEntry $lastEntry */
$lastEntry = $this->getStockEntries()->last();
if ($lastEntry) {
return $lastEntry->getBuyPrice() ?: 0.;
}
return 0.;
}
}
My problem is that when I call my getLastBuyPrice() method, All StockEntries are retrieved, which can be very long (a product can have hundreds of stock entries). I'm looking for a way to rewrite getLastBuyPrice() so that only the most recent StockEntry is retrieved to compute the lastBuyPrice.
I recommend that you create a method in "StockEntryRepository".
This method will retrieve the last item you need without iterating through all of the items.

Symfony 3 Exception: Type error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection

Please help me with my project for a shopping cart. I am trying to add products to the basket and get this error while adding a new product: Type error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in C:\Users\Angel's\Desktop\untitled2\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php on line 605.
Here is my CartController:
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\Product;
use AppBundle\Entity\Cart;
use AppBundle\Entity\Shipping;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class CartController extends Controller
{
/**
* #Route("/", name="homepage")
*/
/*public function indexAction(Request $request)
{
// replace this example code with whatever you need
return $this->render('default/index.html.twig', array(
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
));
}*/
/**
* #Route("/cart", name="view_cart")
*/
public function showAction()
{
# Get object from doctrine manager
$em = $this->getDoctrine()->getManager();
# Get logged user then get his ['id']
$user = $this->container->get('security.token_storage')->getToken()->getUser();
/** Check IF user have exist cart **/
# select cart from database where user id equal to cureent logged user using [ findByUser() ]
$user_cart = $this->getDoctrine()
->getRepository('AppBundle:Cart')
->findBy(['user' => $user]);
if ( $user_cart )
{
# Then select all user cart products to display it to user
$user_products = $this->getDoctrine()
->getRepository('AppBundle:Shipping')
->findBy( array('cart' => $user_cart[0]->getId()) );
# pass selected products to the twig page to show them
return $this->render('cart/show.html.twig', array(
'products' => $user_products,
'cart_data' => $user_cart[0],
));
}
//return new Response(''. $user_products[0]->getProduct()->getPrice() );
# pass selected products to the twig page to show them
return $this->render('cart/show.html.twig');
}
/**
* #Route("/cart/addTo/{productId}", name="add_to_cart")
* #param $productId
* #return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function addAction($productId)
{
# First of all check if user logged in or not by using FOSUSERBUNDLE
# authorization_checker
# if user logged in so add the selected product to his cart and redirect user to products page
# else redirect user to login page to login first or create a new account
$securityContext = $this->container->get('security.authorization_checker');
# If user logged in
if ( $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED') )
{
# Get object from doctrine manager
$em = $this->getDoctrine()->getManager();
# Get logged user then get his ['id']
$user = $this->container->get('security.token_storage')->getToken()->getUser();
# for any case wewill need to select product so select it first
# select specific product which have passed id using ['find(passedID)']
$product = $this->getDoctrine()
->getRepository('AppBundle:Product')
->find($productId);
/** Check IF user have exist cart **/
# select cart from database where user id equal to cureent logged user using [ findByUser() ]
$exsit_cart = $this->getDoctrine()
->getRepository('AppBundle:Cart')
->findBy(['user' => $user]);
# if there's no cart to this user create a new one
if ( !$exsit_cart )
{
# defince cart object
$cart = new Cart();
# set user whose own this cart
$cart->setUser($user);
# set initail total price for cart which equal to product price
$cart->setTotalPrice($product->getPrice());
$cart->setQuantity(1);
# persist all cart data to can use it in create shipping object
$em->persist($cart);
# flush it
$em->flush();
# create shipping object
$ship = new Shipping();
# set all its data quantity initail equal to 1 and passed product and cart created
$ship->setQuantity(1);
$ship->setProduct($product);
$ship->setCart($cart);
# persist it and flush doctrine to save it
$em->persist($ship);
$em->flush();
}
# if user have one so just add new item price to cart price and add it to shipping
else
{
# Get cart from retrived object
$cart = $exsit_cart[0];
# set initail total price for cart which equal to product price
$cart->setTotalPrice($cart->getTotalPrice() + $product->getPrice());
# persist all cart data to can use it in create shipping object
$em->persist($cart);
# flush it
$em->flush();
# create shipping object
$ship = new Shipping();
# set all its data quantity initail equal to 1 and passed product and cart created
$ship->setQuantity(1);
$ship->setProduct($product);
$ship->setCart($cart);
# persist it and flush doctrine to save it
$em->persist($ship);
$em->flush();
}
//return new Response('user id '.$product->getId());
return $this->redirect($this->generateUrl('products_index'));
}
# if user not logged in yet
else
{
# go to adding product form
return $this->redirect($this->generateUrl('login'));
}
}
/**
* #Route("/cart/remove/{itemProduct}/{itemCart}", name="remove_item")
*/
public function removeActione($itemProduct, $itemCart)
{
# get an object from doctrine db and get Shipping Entity to work on it
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('AppBundle:Shipping');
# select wanted item from shipping table to delete it
$ship = $repository->findOneBy(array('product' => $itemProduct, 'cart' => $itemCart));
# Calculate the new total price for cart by subtract deleted item price from total one
$final_price = $ship->getCart()->getTotalPrice() - ($ship->getProduct()->getPrice() * $ship->getQuantity());
# update the total price of cart
$ship->getCart()->setTotalPrice($final_price);
# Remove item from db
$em->remove($ship);
$em->flush();
return $this->redirect($this->generateUrl('view_cart'));
}
/**
* #Route("/cart/edit/{itemProduct}/{itemCart}", name="edit item")
*/
public function editActione(Request $request, $itemProduct, $itemCart)
{
# in the start check if user edit field and click on button
if ( $request->getMethod() === 'POST' )
{
# read data from quantity field
$new_quantity =$request->request->get('quantity');
# get oject from doctrine manager to mange operation
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('AppBundle:Shipping');
# select wanted item from shipping table to edit it
$ship = $repository->findOneBy(array('product' => $itemProduct, 'cart' => $itemCart));
# check if new quantity less than old one so subtract total price
# otherwise, add to it
if( $ship->getQuantity() < $new_quantity )
{
# edit selected item quantity
$ship->setQuantity($new_quantity);
# Calculate the new total price for cart by sum added item price to total one
$final_price = $ship->getCart()->getTotalPrice() + $ship->getProduct()->getPrice();
# update the total price of cart
$ship->getCart()->setTotalPrice($final_price);
}
elseif( $ship->getQuantity() > $new_quantity )
{
# edit selected item quantity
$ship->setQuantity($new_quantity);
# Calculate the new total price for cart by sum added item price to total one
$final_price = $ship->getCart()->getTotalPrice() - $ship->getProduct()->getPrice();
# update the total price of cart
$ship->getCart()->setTotalPrice($final_price);
}
# flush operations to update database
$em->flush();
}
//return new Response(''. $new_quantity );
return $this->redirect($this->generateUrl('view_cart'));
}
/**
* #Route("/cart/clear/{cart}", name="clear_cart")
*/
public function clearActione($cart)
{
# get an object from doctrine db and get Shipping Entity to work on it
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('AppBundle:Shipping');
# select wanted item from shipping table to delete it
$ship = $repository->findBy(array('cart' => $cart));
# Fetch all them using foeach loop and delete them
foreach ($ship as $one_prod)
{
# Remove item from db
$em->remove($one_prod);
$em->flush();
}
$cart_repository = $em->getRepository('AppBundle:Cart');
$one_cart = $cart_repository->findOne(['id' => $cart]);
$em->remove($one_cart);
$em->flush();
return $this->redirect($this->generateUrl('view_cart'));
}
}
This is my Cart Entity code:
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Shipping;
#use AppBundle\Entity\User;
#use AppBundle\Entity\Product;
/**
* #ORM\Entity
* #ORM\Table(name="cart")
*/
class Cart
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="decimal", scale=2)
*/
private $total_price;
/**
* #ORM\Column(type="integer")
*/
private $quantity;
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="cart")
*/
private $user;
/** #ORM\OneToMany(targetEntity="Shipping", mappedBy="cart") */
protected $cartProducts;
/**
* Constructor
*/
public function __construct()
{
$this->cartProducts = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set total_price
*
* #param string $totalPrice
* #return Cart
*/
public function setTotalPrice($totalPrice)
{
$this->total_price = $totalPrice;
return $this;
}
/**
* Get total_price
*
* #return string
*/
public function getTotalPrice()
{
return $this->total_price;
}
/**
* Set quantity
*
* #param integer $quantity
* #return Cart
*/
public function setQuantity($quantity)
{
$this->quantity = $quantity;
return $this;
}
/**
* Get quantity
*
* #return integer
*/
public function getQuantity()
{
return $this->quantity;
}
/**
* Set user
*
* #param \AppBundle\Entity\User $user
* #return Cart
*/
public function setUser(\AppBundle\Entity\User $user = null)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* #return \AppBundle\Entity\User
*/
public function getUser()
{
return $this->user;
}
/**
* Add cartProducts
*
* #param Shipping $cartProducts
* #return Collection
*/
public function addCartProduct(array $cartProducts)
{
$this->cartProducts[] = $cartProducts;
return $this;
}
/**
* Remove cartProducts
*
* #param Shipping $cartProducts
*/
public function removeCartProduct(array $cartProducts)
{
$this->cartProducts->removeElement($cartProducts);
}
/**
* Get cartProducts
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCartProducts()
{
return $this->cartProducts;
}
}
And my Shipping Entity:
<?php
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
#use AppBundle\Entity\User;
#use AppBundle\Entity\Product;
/**
* #ORM\Entity
* #ORM\Table(name="shipping")
*/
class Shipping
{
/**
* #ORM\Column(type="integer")
*/
private $quantity;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Product", inversedBy="cartProducts")
*/
protected $product;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Cart", inversedBy="cartProducts")
*/
protected $cart;
/**
* Set product
*
* #param \AppBundle\Entity\Product $product
* #return Shipping
*/
public function setProduct(\AppBundle\Entity\Product $product)
{
$this->product = $product;
return $this;
}
/**
* Get product
*
* #return \AppBundle\Entity\Product
*/
public function getProduct()
{
return $this->product;
}
/**
* Set cart
*
* #param \AppBundle\Entity\Cart $cart
* #return Shipping
*/
public function setCart(\AppBundle\Entity\Cart $cart)
{
$this->cart = $cart;
return $this;
}
/**
* Get cart
*
* #return \AppBundle\Entity\Cart
*/
public function getCart()
{
return $this->cart;
}
/**
* Set quantity
*
* #param integer $quantity
* #return Shipping
*/
public function setQuantity($quantity)
{
$this->quantity = $quantity;
return $this;
}
/**
* Get quantity
*
* #return integer
*/
public function getQuantity()
{
return $this->quantity;
}
}
Try using doctrine query builder with ->select('your join entity', 'alias') by using ->select() method you retrieve array not object from query

Magento2 Enterprise update smart Categories

My task is related to the smart categories. Smart categories are those which assign the products based on the rules.
Rules of categories can be
Category A: Those products which quantity is more than 1000 should be assign to this category.
Category B: Assign the products based on the brand of the product.
Magento 2 Enterprise provide us rules functionality after adding which we can achieve the above functionality. But we want to update category whenever any product full fill the rule then it should assign to rule related category.
Magento 2 Enterprise does not assign newly products which follow the rules automatically until we re-save the category. So i try to re-save the smart categories using cron. It's code is
class Category
{
/**
* #var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory
*/
protected $_categoryCollectionFactory;
/**
* #var \Magento\Catalog\Api\CategoryRepositoryInterface
*/
protected $_repository;
/**
* #var \Psr\Log\LoggerInterface
*/
protected $_logger;
/**
* #var \Magento\VisualMerchandiser\Model\Rules
*/
protected $_modelRulesFactory;
/**
* Index constructor. Here we are gettingthe logger for log. categorycollection to get all the categories and category repository to save the smart categories
* #param \Magento\Framework\App\Action\Context $context
* #param LoggerInterface $logger
* #param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory
* #param \Magento\Catalog\Api\CategoryRepositoryInterface $repository
* #param RulesFactory $modelRulesFactory
*/
public function __construct(\Psr\Log\LoggerInterface $logger,
\Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory,
\Magento\Catalog\Api\CategoryRepositoryInterface $repository,
RulesFactory $modelRulesFactory) {
$this->_logger = $logger;
$this->_categoryCollectionFactory = $categoryCollectionFactory;
$this->_repository = $repository;
$this->_modelRulesFactory = $modelRulesFactory;
}
/**
* In this function we load all the categories and only save the smart categories so rules for products can apply and assign the new best seller products according to rules
*
* #throws \Magento\Framework\Exception\LocalizedException
*/
public function execute() {
try{
$this->_logger->addDebug('Updating categories');
$categories = $this->_categoryCollectionFactory->create();
$categories->addAttributeToSelect('*');
$rulesModel = $this->_modelRulesFactory->create();
foreach ($categories as $category) {
$rule = $rulesModel->loadByCategory($category);
if ($rule->getId() && $rule->getIsActive()) {
$category->setStoreId(0);
$rule->setIsActive(1);
$this->_repository->save($category);
}
}
}catch (Exception $e){
$this->_logger->addDebug($e->getMessage());
}
}
}
The above code works very good with those rules which are using the rules based on the default attributes. For example if rule is quantity > 1000 here quantity is default attributes so those categories update very well. But if it is like Brand = 'Nike', then after execute the above code it's assigned products become 0.
Can you please help me why it is happening? Thank you very much.

Get OneToMany 'Child' to return object of 'Parent' if relationship is present

Using the doctrine docs example below, I would like to be able to return the all features when querying for a product.
So effectively selecting all products where feature.product_id = product.id
But would like to do this the doctrine object oriented way if possible. Is there any way in doctrine to match these relationships the opposite way?
<?php
use Doctrine\Common\Collections\ArrayCollection;
/** #Entity **/
class Product
{
// ...
/**
* #OneToMany(targetEntity="Feature", mappedBy="product")
**/
private $features;
// ...
public function __construct() {
$this->features = new ArrayCollection();
}
}
/** #Entity **/
class Feature
{
// ...
/**
* #ManyToOne(targetEntity="Product", inversedBy="features")
* #JoinColumn(name="product_id", referencedColumnName="id")
**/
private $product;
// ...
}
Source: http://doctrine-orm.readthedocs.org/en/latest/reference/association-mapping.html#one-to-many-bidirectional
Thanks,
Selecting a product in this case, where product.id = feature.product_id will only return 1 result, assuming your product table's id is its primary key, auto incremented. Are you sure you're not trying to return all features where feature.product_id == (specific product.id)?
If the latter is the case,
$features = $em->getRepository('MyBundle:Feature')
->findBy(
array('product_id' => $productId)
);
Where you have the $productId, a unique integer, in hand.

How to display configurable product in each color in Magento product listing?

I have a configurable product which is available in many different colors and sizes. I want the configurable product to appear once for every color. My idea is to assign one simple product of the configurable product in every color to the category of the configurable product. Then I want to change the listing, so that the (colored) simple product links to it's master product (the configurable one).
The other way would be, to just assign the configurable product to a category and then list it multiple times with different colors. But I think this would be to complicated.
Solution
Sincerely I have lost my code. But here is how I've managed it:
Set visibility for all slave products to catalog so that they
appear in the product listing
Override the Product Model and it's getProductUrl function:
public function getProductUrl($useSid = null)
{
$product = $this;
$product->loadParentProductIds();
$parentIds = $product->getParentProductIds();
if(count($parentIds) > 0 && $product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_SIMPLE)
{
$parent = Mage::getModel("catalog/product")->setId($parentIds[0])->load();
return $this->getUrlModel()->getProductUrl($parent, $useSid);
}
return $this->getUrlModel()->getProductUrl($product, $useSid);
}
This way each slave product links to it's master product. The tricky part is to attach the attributes to the url. You can add #attributecode1=value1&attributecode2=value2 to the url to preselect the attribute select boxes. I only had this part quick & dirty and am pretty sure someone can do this much better.
Example for preselection:
http://demo.magentocommerce.com/anashria-womens-premier-leather-sandal-7.html
http://demo.magentocommerce.com/anashria-womens-premier-leather-sandal-7.html#502=43
I don't understand why you just don't make a configurable product based on size for every color? That way you don't need to hack the way Magento works.
If you make a simple product that is part of a configurable product visible on the frontend, it will not link to a configurable product, if it is part of one (as you have found out). It wouldn't really make sense for you either because if your configurable products are based on size AND color, the simple products are going to have a set size and set color.
You would be done, fully functional, and hack-free if you just made a configurable product for each shirt color. Then, you can also use related products to show other shirt colors.
The less hacking, the better. That's my opinion.
Necroing this thread in case others need to do this in Magento 2.
Below is my solution. Please keep in mind that it is hacky and will break many things, so only use if you're a Magento developer who knows what he/she is doing and can either fix or live with the negative side-effects of this code.
registration.php
<?php
use \Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Antti_ConfigurableProductSplitter', __DIR__);
etc/module.xml
<?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="Antti_ConfigurableProductSplitter" >
<sequence>
<module name="Magento_Catalog" />
</sequence>
</module>
</config>
etc/frontend/events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="catalog_block_product_list_collection">
<observer name="cps_catalog_block_product_list_collection" instance="Antti\ConfigurableProductSplitter\Observer\CatalogBlockProductCollectionBeforeToHtmlObserver" shared="false" />
</event>
<event name="cps_product_data_merge_after">
<observer name="cps_product_data_merge_after" instance="Antti\ConfigurableProductSplitter\Observer\SetColorPreselectedAfterProductDataMerge" shared="false" />
</event>
</config>
Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php
<?php
namespace Antti\ConfigurableProductSplitter\Observer;
use Magento\Framework\Event\ObserverInterface;
use Antti\ConfigurableProductSplitter\Model\ProductCollectionSplitter;
class CatalogBlockProductCollectionBeforeToHtmlObserver implements ObserverInterface
{
/**
* #var ProductCollectionSplitter
*/
private $productSplitter;
/**
* CatalogBlockProductCollectionBeforeToHtmlObserver constructor.
*
* #param ProductCollectionSplitter $productSplitter
*/
public function __construct(
ProductCollectionSplitter $productSplitter
) {
$this->productSplitter = $productSplitter;
}
/**
* #param \Magento\Framework\Event\Observer $observer
*
* #return $this
* #throws \Magento\Framework\Exception\LocalizedException
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
$productCollection = $observer->getEvent()->getCollection();
if ($productCollection instanceof \Magento\Framework\Data\Collection) {
if (!$productCollection->isLoaded()) {
$productCollection->load();
}
$this->productSplitter->splitConfigurables($productCollection);
}
return $this;
}
}
Observer/SetColorPreselectedAfterProductDataMerge.php
<?php
namespace Antti\ConfigurableProductSplitter\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Eav\Model\Config as EavConfig;
class SetColorPreselectedAfterProductDataMerge implements ObserverInterface
{
/**
* #var EavConfig
*/
private $eavConfig;
/**
* ProductDataMerger constructor.
*
* #param EavConfig $eavConfig
*/
public function __construct(
EavConfig $eavConfig
) {
$this->eavConfig = $eavConfig;
}
/**
* #param \Magento\Framework\Event\Observer $observer
*
* #return $this
* #throws \Magento\Framework\Exception\LocalizedException
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
$product = $observer->getEvent()->getSimple();
$merged = $observer->getEvent()->getMerged();
$this->setColorPreselected($merged, $product->getColor());
return $this;
}
/**
* #param ProductInterface $product
* #param int $color
*
* #throws \Magento\Framework\Exception\LocalizedException
*/
private function setColorPreselected(ProductInterface &$product, int $color)
{
$attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'color');
$preconfiguredValues = new \Magento\Framework\DataObject();
$preconfiguredValues->setData('super_attribute', [$attribute->getId() => $color]);
$product->setPreconfiguredValues($preconfiguredValues);
// TODO: should test whether this works if there is no url rewrite
$product->setRequestPath(sprintf('%s#%d=%d', $product->getRequestPath(), $attribute->getId(), $color));
}
}
Model/ProductDataMerger.php
<?php
namespace Antti\ConfigurableProductSplitter\Model;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\EntityManager\EventManager;
class ProductDataMerger
{
/**
* #var EventManager
*/
private $eventManager;
/**
* #param EventManager $eventManager
*/
public function __construct(
EventManager $eventManager
) {
$this->eventManager = $eventManager;
}
/**
* #param ProductInterface $product
* #param ProductInterface $parentProduct
*
* #return ProductInterface
*/
public function merge(ProductInterface $product, ProductInterface $parentProduct)
{
$merged = clone $parentProduct;
$merged->setParentId($merged->getId());
$merged->setId($product->getId());
$this->setImageFromChildProduct($merged, $product);
$this->eventManager->dispatch(
'cps_product_data_merge_after',
['merged' => $merged, 'simple' => $product, 'configurable' => $parentProduct]
);
return $merged;
}
/**
* #param ProductInterface $product
* #param ProductInterface $childProduct
*/
public function setImageFromChildProduct(ProductInterface &$product, ProductInterface $childProduct)
{
foreach (['image', 'small_image', 'thumbnail'] as $imageType) {
if ($childProduct->getData($imageType) && $childProduct->getData($imageType) !== 'no_selection') {
$product->setData($imageType, $childProduct->getData($imageType));
} else {
$product->setData($imageType, $childProduct->getData('image'));
}
}
}
}
Model/ProductCollectionSplitter.php
<?php
namespace Antti\ConfigurableProductSplitter\Model;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Antti\ConfigurableProductSplitter\Model\ProductDataMerger;
use Magento\Catalog\Model\Layer\Resolver;
class ProductCollectionSplitter
{
/**
* #var \Magento\Catalog\Model\Layer
*/
private $catalogLayer;
/**
* #var ProductDataMerger
*/
private $productDataMerger;
/**
* ProductCollectionSplitter constructor.
*
* #param Resolver $layerResolver
* #param ProductDataMerger $productDataMerger
*/
public function __construct(
Resolver $layerResolver,
ProductDataMerger $productDataMerger
) {
$this->catalogLayer = $layerResolver->get();
$this->productDataMerger = $productDataMerger;
}
/**
* #param \Magento\Framework\Data\Collection $collection
*
* #return $this
* #throws \Magento\Framework\Exception\LocalizedException
*/
public function splitConfigurables(\Magento\Framework\Data\Collection $collection)
{
$items = $collection->getItems();
if (sizeof($items) == 0) {
return $this;
}
$configurables = $otherProducts = [];
$colorFilterValue = $this->getCurrentColorFilterValue();
foreach ($items as $index => $product) {
if ($product->getTypeId() === Configurable::TYPE_CODE) {
/** #var \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection $childProducts */
$childProducts = $product->getTypeInstance()->getUsedProductCollection($product);
if ($colorFilterValue !== null) {
$childProducts->addAttributeToFilter('color', ['eq' => $colorFilterValue]);
}
$childProducts->groupByAttribute('color');
foreach ($childProducts as $childProduct) {
$childProduct->setParentId($product->getId());
$otherProducts[] = $childProduct;
}
$configurables[$product->getId()] = $product;
} else {
$otherProducts[] = $product;
}
$collection->removeItemByKey($index);
}
foreach ($otherProducts as $product) {
if ($product->getParentId() && isset($configurables[$product->getParentId()])) {
$product = $this->productDataMerger->merge($product, $configurables[$product->getParentId()]);
}
$collection->addItem($product);
}
return $this;
}
/**
* #return string|null
* #throws \Magento\Framework\Exception\LocalizedException
*/
private function getCurrentColorFilterValue()
{
/** #var \Magento\Catalog\Model\Layer\Filter\Item $filter */
foreach ($this->catalogLayer->getState()->getFilters() as $filter) {
if($filter->getFilter()->getAttributeModel()->getName() == 'color') {
return $filter->getValueString();
}
}
return null;
}
}
Known issues:
Because of modifying collection items after it has loaded, the collection count will be
invalid, and this may cause issues elsewhere.
Also product ids in the collection will be invalid since configurable items' ids gets replaced by simple products' ids.
If the collection will be loaded again elsewhere, configurable products do not get splitted.
Products per page limiter does not work anymore.
Products count on product list is invalid.
3rd party search modules such as Klevu might not work
Other 3rd party modules may have issues with the implementation.
Product stock data, reviews etc. are broken on product list (although is probably easy to fix in frontend).
Not sure if implementation will work without url rewrites (should be easy to fix though).
Other issues I might not be aware of.
One way would be to make the size and color part of the catalog number (or whatever unique identifying number you are using for the product)
So lets say you have a widget that comes in 2 colors and 3 sizes, and it's catalog number is "qwe123". You would enter the following 2 items into the system, along with appropriate images. I'm assuming you already have a way to deal with the sizes.
qwe123-red
qwe123-blue
There is no extra programing involved to do it this way, but if you want to link to the other colors that are available from the product page then you will have to parse out the first part of the catalog number and search for the ones that match.
In order to redirect simple products to configurable parent product, you can create a Plugin (Interceptor) for Magento\Catalog\Model\Product::getProductUrl(), where to change URL for simple products:
if ($product->getTypeId() === 'simple') {
/*Get the configurable parent product URL and assign it to a simple product.*/
}
To preselect a simple product in a configurable product, the address of a simple product should look like this for example:
/mona-pullover-hoodlie.html#143=167&93=53
where
/mona-pullover-hoodlie.html - configurable product URL,
143, 93 - attributes IDs,
167, 53 - option IDs.
Attributes IDs and option IDs can be obtained using Magento\ConfigurableProduct\Model\Product\Type\Configurable::getConfigurableAttributesAsArray($product) function.
I made a VCT Simple Product URL module on Magento Marketplace that solves this problem.

Categories