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.
Related
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)
);
}
I have a manytomany relation between two entities (product and quote) so that one or more products could be in one or more quotes. For example:
assuming that a customer chooses two products from a specific enterprise and he would like to receive a quote that summarize all the chosen products. Then he decides to choose another product of another enterprise to get at the end another quote. So here we have two quotes of different enterprises and of same user that each quote has its own products.
you will say that it is a onetomany relation between the quote and the product because as I mentioned above that one quote can have many products so in the database you will have a quote_id column in the product table.
However, if another customer chooses the same products, a new quote will be created but when the query will insert these products in the quote by filling the quote_id column of the product table, it finds out that these articles are already have a quote_id of another quote.
That's why it is a manytomany relation so that many articles could be in many quotes.
This part is realized and I can match many products to many quotes without any problems.
Here is the entity of quote:
/**
* #ORM\ManyToMany(targetEntity="ArticleBundle\Entity\Article", mappedBy="quotes")
*/
private $articles;
public function __construct() {
$this->articles = new \Doctrine\Common\Collections\ArrayCollection();
$this->createdAt = new \DateTime();
}
/**
* Set articles
*
* #param \ArticleBundle\Entity\Article $articles
* #return Quote
*/
public function setArticles(\ArticleBundle\Entity\Article $articles = null) {
$this->articles = $articles;
return $this;
}
/**
* Get articles
*
* #return \ArticleBundle\Entity\Article
*/
public function getArticles() {
return $this->articles;
}
/**
* Add articles
*
* #param \QuoteBundle\Entity\Quote$articles
* #return Devis
*/
public function addArticle(\ArticleBundle\Entity\Article $article) {
$article->addQuote($this); // synchronously updating inverse side
$this->articles[] = $article;
return $this;
}
/**
* Remove articles
*
* #param \QuoteBundle\Entity\Quote $articles
*/
public function removeArticle(\ArticleBundle\Entity\Article $article) {
$this->articles->removeElement($article);
}
}
the entity article:
/**
* #ORM\ManyToMany(targetEntity="QuoteBundle\Entity\Quote", inversedBy="articles")
* #ORM\JoinTable(name="quotes_articles")
*/
private $quotes;
/**
* Add devises
*
* #param \DevisBundle\Entity\Quote $quote
* #return Article
*/
public function addQuote(\QuoteBundle\Entity\Quote $quote) {
$this->quotes[] = $quote;
return $this;
}
/**
* Remove quotes
*
* #param \QuoteBundle\Entity\Quote $quote
*/
public function removeQuote(\QuoteBundle\Entity\Quote $quote) {
$this->quotes->removeElement($quote);
}
/**
* Get quotes
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuotes() {
return $this->quotes;
}
}
The part that I find it difficult to reach is that when a customer chooses only one product , I would like to let him specify the quantity of the chosen product.
so when I updated the doctrine schema I got these tables :
quote table
product table
quotes_products table
can anyone tell me in which table I must add the quantity column and how can I write the annotation to let it added automatically in the database . I think it will be added to the quotes_products table.
Thank you for helping me.
when adding a quantity column to the join table, you are making of your relationship a non pure manytomany. So, there will be no problem in separating the manyToMany relationships to two oneToMany relationships, and create an Entity Class for the join with additional field quantity.
The idea in pseudo code:
Class Quote
{
private $products;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="quote")
*/
}
Product:
Class Product
{
private $quotes;
/**
* #ORM\OneToMany(targetEntity="Quote", mappedBy="product")
*/
}
ProdcutQuote:
Class ProductQuote
{
/**
* #ORM\ManyToOne(targetEntity="Quote")
* #ORM\JoinColumn(name="quote_id", referencedColumnName="id")
*/
private $quote;
/**
* #ORM\ManyToOne(targetEntity="Product")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
private $quantity;
}
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 7 years ago.
Improve this question
I'm developing an API for an app and the results of offers need to be filterable, examples of this would be:
Filters
Price Min - Price Max
Category Ids
Min Review (1-5 stars) if 3 is sent then only 3 and above
Min Distance
Max Distance
Location (for the above 2)
Sort By
Distance,
Price,
Review
I'm confused on the most efficient way to do this, I will be posting the users filter options but executing a query based on their options is the part that has me lost.
Any help would be massively appreciated! Thanks all :)
When you start applying conditions on your queries, you'll need to take note of the following function: newQuery(). This function will allow you to start a new query builder and chain your filters/order bys onto it.
Example Schema:
Columns in Product table:
id | name | price | date_received
Columns in product_tag table:
id | tag_id | product_id
Column in tag table:
id | name
Relationship:
Many products have many tags
I won't be setting up the eloquent models, though note that Product model has a tag() function with a hasManyThrough() relationship
A filter class has been setup with the main purpose of applying your filters & order bys. Note: The class can be abstracted even further.
Your Filter Class:
class ProductFilter {
/**
* Fluent query builder
* #var mixed $queryBuilder
*/
private $queryBuilder;
/**
* Http Request
* #var \Illuminate\Http\Request $request
*/
protected $request;
/**
* Filters collection
* #var array $filters
*/
private $filters = [];
/**
* Order Bys Collection
* #var array $orderBys
*/
private $orderBys = [];
/**
* Class constructor
*
* #param array $input
*/
public function __construct(\Illuminate\Http\Request $request, &$queryBuilder)
{
//Initialize Query Builder
$this->queryBuilder = $queryBuilder;
//Get input
$this->request = $request;
//Register Filters
$this->registerFilters();
//Register Order Bys
$this->registerOrderBys();
}
/**
* Register Filters in the function below
* Each filter is in the form of an array
*/
private function registerFilters()
{
$this->filters['product_name'] = ['name'=>'Name',
'value' => $this->request->get('filter_product_name'),
'enabled' => $this->request->has('filter_product_name'),
'function' => 'filterProductName'
];
$this->filters['tag'] = ['name'=>'End Date',
'value' => $this->request->get('filter_tag'),
'enabled' => $this->request->has('filter_tag'),
'function' => 'filterTag'
];
}
/**
* Check if any filters are active
* Useful to show/hide filter bar
*
* #return bool
*/
public function isFiltersActive()
{
return (boolean)count(
array_filter($this->filters,function($v){
return $v['enabled'] === true;
})
);
}
/**
* Register Order Bys in the function below
* Each order by is in the form of an array
*
*/
private function registerOrderBys()
{
$this->orderBys['name'] = [
'name' => 'Order By Name',
'value' => $this->request->get('order_by_product_name','ASC'),
'enabled' => $this->request->has('order_by_product_name'),
'function' => 'orderByProductName'
];
}
/**
* Check if any order bys are active
* Useful to show/hide order by bar
*
* #return bool
*/
public function isOrderBysActive()
{
return (boolean)count(
array_filter($this->orderBys,function($v){
return $v['enabled'] === true;
})
);
}
/**
* Apply Filters
* Loop through each filter, check if they are enabled. If they are, apply filter to query builder
*/
public function applyFilters()
{
foreach($this->filters as $filter_name => $filter_array)
{
if($filter_array['enabled'] &&
array_key_exists('function',$filter_array) &&
method_exists($this,$filter_array['function']))
{
$this->{$filter_array['function']}($filter_array);
}
}
return $this;
}
/**
* Apply Order Bys
* Loop through each order by, check if they are enabled. If they are, apply order by to query builder
*/
public function applyFilters()
{
foreach($this->orderBys as $order_by_name => $order_by_array)
{
if($order_by_array['enabled'] &&
array_key_exists('function',$order_by_array) &&
method_exists($this,$order_by_array['function']))
{
$this->{$order_by_array['function']}($order_by_array);
}
}
return $this;
}
/*
* Filter Functions: START
*/
/**
* Filter by Product Name
*
* #param array $filterArray
*/
private function filterProductName($filterArray)
{
$this->queryBuilder
->where('name','=',$filterArray['value']);
}
/**
* Filter by Product Tag
*
* #param array $filterArray
*/
private function filterTag($filterArray)
{
$this->queryBuilder
->whereHas('tag',function($query) use ($filterArray){
return $query->where('name','=',$filterArray['value']);
});
}
/*
* Filter Functions: END
*/
/*
* Order By Functions: START
*/
/**
* Order By Name
* #param array $orderByArray
*/
private function orderByName($orderByArray)
{
$this->queryBuilder
->orderBy('name', $orderByArray['value']);
}
/*
* Order By Functions: END
*/
}
How to Use:
//In my controller function
public function getListOfProducts(\Illuminate\Http\Request $request)
{
//Init Product Query
$productQuery = \App\Models\Product()::newQuery();
//Apply all filters and order bys
$productFilter = app('ProductFilter',[$request,$productQuery])->applyFilters()->applyOrderBys();
//Fetch Product Result
$productResult = $productQuery->get();
}
I've recently included Prattski's Hide Empty Categories module to Magento, however; The extension removes categories that have no products available, which is great. What it doesn't do is hide categories for products disabled. Therefore, the categories are still visible when all items in a particular category are disabled.
Below is the Observer.php code from the extension:
<?php
/**
* Hide Empty Categories
*
* #category Prattski
* #package Prattski_HideEmptyCategories
* #copyright Copyright (c) 2011 Prattski (http://prattski.com/)
* #author Josh Pratt (Prattski)
*/
/**
* Event Observer
*
* #category Prattski
* #package Prattski_HideEmptyCategories
*/
class Prattski_HideEmptyCategories_Model_Observer extends Mage_Core_Model_Abstract
{
/**
* Remove hidden caegories from the collection
*
* #param Varien_Event_Observer $observer
*/
public function catalogCategoryCollectionLoadAfter($observer)
{
if ($this->_isApiRequest()) return;
$collection = $observer->getEvent()->getCategoryCollection();
$this->_removeHiddenCollectionItems($collection);
}
/**
* Remove hidden items from a product or category collection
*
* #param Mage_Eav_Model_Entity_Collection_Abstract|Mage_Core_Model_Mysql4_Collection_Abstract $collection
*/
public function _removeHiddenCollectionItems($collection)
{
// Loop through each category or product
foreach ($collection as $key => $item)
{
// If it is a category
if ($item->getEntityTypeId() == 3) {
$products = Mage::getModel('catalog/category')->load($item->getId())
->getProductCollection()
->addAttributeToSelect('entity_id')
->addAttributeToFilter('status',Mage_Catalog_Model_Product_Status::STATUS_ENABLED)
->addAttributeToFilter('visibility',Mage_Catalog_Model_Product_Visibility::VISIBILITY_BOTH);
$prodcount = $products->count();
if ($item->prodcount) {
$collection->removeItemByKey($key);
}
}
}
}
/**
* Return true if the reqest is made via the api
*
* #return boolean
*/
protected function _isApiRequest()
{
return Mage::app()->getRequest()->getModuleName() === 'api';
}
}
Any ideas on how to disable the categories with disabled products?
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.