diff --git a/vendor/magento/module-target-rule/Block/Catalog/Product/ProductList/AbstractProductList.php b/vendor/magento/module-target-rule/Block/Catalog/Product/ProductList/AbstractProductList.php index 7f967eb363f..f3f1880f157 100644 --- a/vendor/magento/module-target-rule/Block/Catalog/Product/ProductList/AbstractProductList.php +++ b/vendor/magento/module-target-rule/Block/Catalog/Product/ProductList/AbstractProductList.php @@ -9,6 +9,7 @@ namespace Magento\TargetRule\Block\Catalog\Product\ProductList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\DataObject\IdentityInterface; use Magento\TargetRule\Block\Product\AbstractProduct; +use Magento\TargetRule\Model\Rotation; /** * TargetRule Catalog Product List Abstract Block @@ -53,6 +54,10 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ protected $_productCollectionFactory; + /** + * @var Rotation + */ + private $rotation; /** * @param \Magento\Catalog\Block\Product\Context $context @@ -62,6 +67,7 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn * @param \Magento\Catalog\Model\Product\Visibility $visibility * @param \Magento\TargetRule\Model\IndexFactory $indexFactory * @param array $data + * @param Rotation|null $rotation */ public function __construct( \Magento\Catalog\Block\Product\Context $context, @@ -70,11 +76,14 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, \Magento\Catalog\Model\Product\Visibility $visibility, \Magento\TargetRule\Model\IndexFactory $indexFactory, - array $data = [] + array $data = [], + Rotation $rotation = null ) { $this->_productCollectionFactory = $productCollectionFactory; $this->_visibility = $visibility; $this->_indexFactory = $indexFactory; + $this->rotation = $rotation + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Rotation::class); parent::__construct( $context, $index, @@ -94,8 +103,7 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn } /** - * Retrieve Catalog Product List Type prefix - * without last underscore + * Retrieve Catalog Product List Type prefix without last underscore * * @return string * @throws \Magento\Framework\Exception\LocalizedException @@ -115,7 +123,6 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn throw new LocalizedException( __('The Catalog Product List Type needs to be defined. Verify the type and try again.') ); - break; } return $prefix; } @@ -221,7 +228,6 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn throw new LocalizedException( __('The Catalog Product List Type needs to be defined. Verify the type and try again.') ); - break; } if ($limit !== null) { @@ -303,11 +309,21 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn $productIds = $this->_getTargetRuleProductIds($limit); + if (!empty($this->_items)) { + $limit -= count($this->_items); + } + + $productIds = $this->rotation->reorder( + $productIds, + $this->_targetRuleData->getRotationMode($this->getProductListType()), + $limit + ); + $items = []; if ($productIds) { /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $collection = $this->_productCollectionFactory->create(); - $collection->addFieldToFilter('entity_id', ['in' => $productIds]); + $collection->addFieldToFilter('entity_id', ['in' => array_keys($productIds)]); $this->_addProductAttributesAndPrices($collection); $collection->setPageSize( @@ -321,7 +337,16 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn foreach ($collection as $item) { $items[$item->getEntityId()] = $item; + $item->setPriority($productIds[$item->getEntityId()]); } + + $orderedItems = []; + foreach (array_keys($productIds) as $productId) { + if (isset($items[$productId])) { + $orderedItems[$productId] = $items[$productId]; + } + } + $items = $orderedItems; } return $items; @@ -355,19 +380,19 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn public function getAllIds() { if ($this->_allProductIds === null) { - if (!$this->isShuffled()) { - $this->_allProductIds = array_keys($this->getItemCollection()); - return $this->_allProductIds; - } - $targetRuleProductIds = $this->_getTargetRuleProductIds(); $linkProductCollection = $this->_getPreparedTargetLinkCollection(); $linkProductIds = []; foreach ($linkProductCollection as $item) { $linkProductIds[] = $item->getEntityId(); } - $this->_allProductIds = array_unique(array_merge($targetRuleProductIds, $linkProductIds)); - shuffle($this->_allProductIds); + + $targetRuleProductIds = $this->rotation->reorder( + array_diff_key($targetRuleProductIds, array_flip($linkProductIds)), + $this->_targetRuleData->getRotationMode($this->getProductListType()) + ); + + $this->_allProductIds = array_unique(array_merge($linkProductIds, array_keys($targetRuleProductIds))); } return $this->_allProductIds; @@ -382,7 +407,9 @@ abstract class AbstractProductList extends AbstractProduct implements IdentityIn { $identities = []; foreach ($this->getItemCollection() as $item) { - $identities = array_merge($identities, $item->getIdentities()); + foreach ($item->getIdentities() as $identity) { + $identities[] = $identity; + } } return $identities; } diff --git a/vendor/magento/module-target-rule/Block/Checkout/Cart/Crosssell.php b/vendor/magento/module-target-rule/Block/Checkout/Cart/Crosssell.php index d1cc096857e..b567598618e 100644 --- a/vendor/magento/module-target-rule/Block/Checkout/Cart/Crosssell.php +++ b/vendor/magento/module-target-rule/Block/Checkout/Cart/Crosssell.php @@ -9,6 +9,8 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\ObjectManager; +use Magento\TargetRule\Model\Rotation; /** * TargetRule Checkout Cart Cross-Sell Products Block @@ -86,6 +88,10 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct * @var ProductRepositoryInterface */ protected $productRepository; + /** + * @var Rotation + */ + private $rotation; /** * @param \Magento\Catalog\Block\Product\Context $context @@ -100,6 +106,7 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct * @param \Magento\Catalog\Model\ProductTypes\ConfigInterface $productTypeConfig * @param ProductRepositoryInterface $productRepository * @param array $data + * @param Rotation|null $rotation * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -114,7 +121,8 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct \Magento\TargetRule\Model\IndexFactory $indexFactory, \Magento\Catalog\Model\ProductTypes\ConfigInterface $productTypeConfig, ProductRepositoryInterface $productRepository, - array $data = [] + array $data = [], + Rotation $rotation = null ) { $this->productTypeConfig = $productTypeConfig; $this->_productCollectionFactory = $productCollectionFactory; @@ -123,6 +131,7 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct $this->_checkoutSession = $session; $this->_productLinkFactory = $productLinkFactory; $this->_indexFactory = $indexFactory; + $this->rotation = $rotation ?? ObjectManager::getInstance()->get(Rotation::class); parent::__construct( $context, $index, @@ -273,7 +282,7 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct */ public function getPositionLimit() { - return $this->_targetRuleData->getMaximumNumberOfProduct(\Magento\TargetRule\Model\Rule::CROSS_SELLS); + return $this->_targetRuleData->getMaximumNumberOfProduct($this->getProductListType()); } /** @@ -283,7 +292,7 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct */ public function getPositionBehavior() { - return $this->_targetRuleData->getShowProducts(\Magento\TargetRule\Model\Rule::CROSS_SELLS); + return $this->_targetRuleData->getShowProducts($this->getProductListType()); } /** @@ -336,7 +345,7 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct protected function _getProductIdsFromIndexByProduct($product, $count, $excludeProductIds = []) { return $this->_getTargetRuleIndex()->setType( - \Magento\TargetRule\Model\Rule::CROSS_SELLS + $this->getProductListType() )->setLimit( $count )->setProduct( @@ -382,17 +391,18 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct $productIds = $this->_getProductIdsFromIndexByProduct( $product, - $this->getPositionLimit(), + $limit, $excludeProductIds ); - // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge - $resultIds = array_merge($resultIds, $productIds); - } - $resultIds = array_unique($resultIds); - shuffle($resultIds); + foreach ($productIds as $productId => $priority) { + if (!isset($resultIds[$productId]) || $resultIds[$productId] < $priority) { + $resultIds[$productId] = $priority; + } + } + } - return array_slice($resultIds, 0, $limit); + return $resultIds; } /** @@ -426,13 +436,30 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct $limit, $excludeProductIds ); + if (!empty($this->_items)) { + $limit -= count($this->_items); + } + $productIds = $this->rotation->reorder( + $productIds, + $this->_targetRuleData->getRotationMode($this->getProductListType()), + $limit + ); $items = []; if ($productIds) { - $collection = $this->_getProductCollectionByIds($productIds); + $collection = $this->_getProductCollectionByIds(array_keys($productIds)); foreach ($collection as $product) { + $product->setPriority($productIds[$product->getEntityId()]); $items[$product->getEntityId()] = $product; } + + $orderedItems = []; + foreach (array_keys($productIds) as $productId) { + if (isset($items[$productId])) { + $orderedItems[$productId] = $items[$productId]; + } + } + $items = $orderedItems; } return $items; @@ -484,7 +511,6 @@ class Crosssell extends \Magento\TargetRule\Block\Product\AbstractProduct } else { $this->_items = $productsByLastAdded; } - $this->_items = $this->_orderProductItems($this->_items); $this->_sliceItems(); } return $this->_items; diff --git a/vendor/magento/module-target-rule/Block/DataProviders/Rotation.php b/vendor/magento/module-target-rule/Block/DataProviders/Rotation.php new file mode 100644 index 00000000000..a57dce1e851 --- /dev/null +++ b/vendor/magento/module-target-rule/Block/DataProviders/Rotation.php @@ -0,0 +1,45 @@ +targetRuleData = $targetRuleData; + } + + /** + * Check if rotation mode is set to "weighted random". + * + * @param string $type + * @return bool + */ + public function isWeightedRandom(string $type): bool + { + $rotation = $this->targetRuleData->getRotationMode($type); + return $rotation === SourceRotation::ROTATION_WEIGHTED_RANDOM; + } +} diff --git a/vendor/magento/module-target-rule/Block/Product/AbstractProduct.php b/vendor/magento/module-target-rule/Block/Product/AbstractProduct.php index 8d8163531fb..594b90a2033 100644 --- a/vendor/magento/module-target-rule/Block/Product/AbstractProduct.php +++ b/vendor/magento/module-target-rule/Block/Product/AbstractProduct.php @@ -5,9 +5,11 @@ */ namespace Magento\TargetRule\Block\Product; +use Magento\TargetRule\Model\Rule; +use Magento\TargetRule\Model\Source\Rotation; + /** * TargetRule abstract Products Block - * */ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractProduct { @@ -102,8 +104,8 @@ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractPr public function getRuleBasedBehaviorPositions() { return [ - \Magento\TargetRule\Model\Rule::BOTH_SELECTED_AND_RULE_BASED, - \Magento\TargetRule\Model\Rule::RULE_BASED_ONLY + Rule::BOTH_SELECTED_AND_RULE_BASED, + Rule::RULE_BASED_ONLY ]; } @@ -115,8 +117,8 @@ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractPr public function getSelectedBehaviorPositions() { return [ - \Magento\TargetRule\Model\Rule::BOTH_SELECTED_AND_RULE_BASED, - \Magento\TargetRule\Model\Rule::SELECTED_ONLY + Rule::BOTH_SELECTED_AND_RULE_BASED, + Rule::SELECTED_ONLY ]; } @@ -133,9 +135,8 @@ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractPr if ($this->_linkCollection) { // Perform rotation mode $select = $this->_linkCollection->getSelect(); - $rotationMode = $this->_targetRuleData->getRotationMode($this->getProductListType()); - if ($rotationMode == \Magento\TargetRule\Model\Rule::ROTATION_SHUFFLE) { - $this->_resourceIndex->orderRand($select); + if ($this->isShuffled()) { + $select->order($this->getRandomOrderExpression()); } else { $select->order('link_attribute_position_int.value ASC'); } @@ -145,6 +146,20 @@ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractPr return $this->_linkCollection; } + /** + * Used to generate pseudo-random sort order to avoid using Mysql ORDER BY RAND() + * + * @return string + */ + private function getRandomOrderExpression() : string + { + $productEntityFieldsToRandomize = ['e.entity_id', 'e.attribute_set_id', 'e.sku']; + $sortOrderToRandomize = ['ASC', 'DESC']; + return $productEntityFieldsToRandomize[random_int(0, count($productEntityFieldsToRandomize) -1)] + . ' ' + . $sortOrderToRandomize[random_int(0, count($sortOrderToRandomize) -1)]; + } + /** * Get linked products * @@ -159,18 +174,23 @@ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractPr $items[$item->getEntityId()] = $item; } } + if ($this->isShuffled()) { + shuffle($items); + } return $items; } /** - * Whether rotation mode is set to "shuffle" + * Whether rotation mode is set to "weighted random" or "random" * * @return bool */ public function isShuffled() { - $rotationMode = $this->_targetRuleData->getRotationMode($this->getProductListType()); - return $rotationMode == \Magento\TargetRule\Model\Rule::ROTATION_SHUFFLE; + return in_array( + $this->_targetRuleData->getRotationMode($this->getProductListType()), + [Rule::ROTATION_SHUFFLE, Rotation::ROTATION_WEIGHTED_RANDOM] + ); } /** @@ -248,14 +268,15 @@ abstract class AbstractProduct extends \Magento\Catalog\Block\Product\AbstractPr $this->_items = []; if (in_array($behavior, $this->getSelectedBehaviorPositions())) { - $this->_items = $this->_orderProductItems($this->_getLinkProducts()); + $this->_items = $this->_getLinkProducts(); } if (in_array($behavior, $this->getRuleBasedBehaviorPositions())) { - foreach ($this->_orderProductItems($this->_getTargetRuleProducts()) as $id => $item) { + foreach ($this->_getTargetRuleProducts() as $id => $item) { $this->_items[$id] = $item; } } + $this->_sliceItems(); } diff --git a/vendor/magento/module-target-rule/Helper/Data.php b/vendor/magento/module-target-rule/Helper/Data.php index 93ae96e430b..bc6313d641f 100644 --- a/vendor/magento/module-target-rule/Helper/Data.php +++ b/vendor/magento/module-target-rule/Helper/Data.php @@ -48,7 +48,6 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper break; default: throw new \Magento\Framework\Exception\LocalizedException(__('Invalid product list type')); - break; } return $this->getMaxProductsListResult($number); @@ -84,7 +83,6 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper break; default: throw new \Magento\Framework\Exception\LocalizedException(__('Invalid product list type')); - break; } return $show; @@ -93,7 +91,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper /** * Retrieve maximum number of products can be displayed in product list * - * if number is 0 (unlimited) or great global maximum return global maximum value + * If number is 0 (unlimited) or great global maximum return global maximum value * * @param int $number * @return int @@ -137,8 +135,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper break; default: throw new \Magento\Framework\Exception\LocalizedException(__('Invalid rotation mode type')); - break; } - return $mode; + return (int)$mode; } } diff --git a/vendor/magento/module-target-rule/Model/ResourceModel/Index.php b/vendor/magento/module-target-rule/Model/ResourceModel/Index.php index cb25fe9800d..6b6d09ea110 100644 --- a/vendor/magento/module-target-rule/Model/ResourceModel/Index.php +++ b/vendor/magento/module-target-rule/Model/ResourceModel/Index.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Http\Context as HttpContext; * * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Index extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { @@ -191,7 +192,7 @@ class Index extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { $segmentsIds = array_merge([0], $this->_getSegmentsIdsFromCurrentCustomer()); - $productIds = []; + $productIdsByPriority = []; foreach ($segmentsIds as $segmentId) { $matchedProductIds = $this->_indexPool->get($object->getType()) ->loadProductIdsBySegmentId($object, $segmentId); @@ -205,16 +206,14 @@ class Index extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb $matchedProductIds ); } - - $productIds = array_merge($matchedProductIds, $productIds); + foreach ($matchedProductIds as $productId => $priority) { + if (!isset($productIdsByPriority[$productId]) || $productIdsByPriority[$productId] < $priority) { + $productIdsByPriority[$productId] = $priority; + } + } } - $productIds = array_diff(array_unique($productIds), $object->getExcludeProductIds()); - $rotationMode = $this->_targetRuleData->getRotationMode($object->getType()); - if ($rotationMode == \Magento\TargetRule\Model\Rule::ROTATION_SHUFFLE) { - shuffle($productIds); - } - return array_slice($productIds, 0, $object->getLimit()); + return array_diff_key($productIdsByPriority, array_flip($object->getExcludeProductIds())); } /** @@ -240,15 +239,17 @@ class Index extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb if (!$rule->checkDateForStore($object->getStoreId())) { continue; } - $excludeProductIds = array_merge([$object->getProduct()->getEntityId()], $productIds); + $excludeProductIds = array_keys($productIds); + $excludeProductIds[] = $object->getProduct()->getEntityId(); $resultIds = $this->_getProductIdsByRule($rule, $object, $rule->getPositionsLimit(), $excludeProductIds); - $productIds = array_merge($productIds, $resultIds); + $productIds += array_fill_keys($resultIds, $rule->getSortOrder()); } return $productIds; } /** * Retrieve found product ids by Rule action conditions + * * If rule has cached select - get it * * @param \Magento\TargetRule\Model\Rule $rule @@ -404,7 +405,8 @@ class Index extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * Retrieve SQL condition fragment by field, operator and binded value - * also modify bind array + * + * Modify bind array as well * * @param string $field * @param mixed $attribute @@ -463,7 +465,6 @@ class Index extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * Prepare bind value for LIKE condition - * Callback method * * @param string $value * @return string diff --git a/vendor/magento/module-target-rule/Model/ResourceModel/Index/Index.php b/vendor/magento/module-target-rule/Model/ResourceModel/Index/Index.php index cbfc8c48d98..717dbd86e4a 100644 --- a/vendor/magento/module-target-rule/Model/ResourceModel/Index/Index.php +++ b/vendor/magento/module-target-rule/Model/ResourceModel/Index/Index.php @@ -11,6 +11,9 @@ use Magento\Store\Model\Store; use Magento\TargetRule\Model\Cache\Index as IndexCache; use Magento\TargetRule\Model\Index as IndexModel; +/** + * Cache helper for target rule products to display + */ class Index implements IndexInterface { const CACHE_TAG_TYPE_ENTITY = 'entity'; @@ -46,7 +49,7 @@ class Index implements IndexInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function loadProductIdsBySegmentId(IndexModel $indexModel, $segmentId) { @@ -74,7 +77,7 @@ class Index implements IndexInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function saveResultForCustomerSegments(IndexModel $indexModel, $segmentId, array $productIds) { @@ -88,7 +91,7 @@ class Index implements IndexInterface $cacheKey = $this->generateKey($entityId, $storeId, $customerGroupId, $customerSegmentId); - $productTags = $this->generateTagsByProductIds($productIds); + $productTags = $productIds ? $this->generateTagsByProductIds(array_keys($productIds)) : []; $keyTags = $this->getKeyTags($entityId, $storeId, $customerGroupId, $customerSegmentId); @@ -107,7 +110,7 @@ class Index implements IndexInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function cleanIndex($store = null) { @@ -128,7 +131,7 @@ class Index implements IndexInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteProductFromIndex($entityId = null) { @@ -154,6 +157,8 @@ class Index implements IndexInterface } /** + * Get main tag name with provided suffix + * * @param string $suffix * @return string */ @@ -163,6 +168,8 @@ class Index implements IndexInterface } /** + * Get tag name for a product + * * @param string $suffix * @return string */ @@ -172,6 +179,8 @@ class Index implements IndexInterface } /** + * Get tag name based on type and suffix + * * @param string $type * @param string $suffix * @return string @@ -182,6 +191,8 @@ class Index implements IndexInterface } /** + * Generate cache key based on entity ID, store ID, customer group ID and segment ID + * * @param int $entityId * @param int $storeId * @param int $customerGroupId @@ -194,6 +205,8 @@ class Index implements IndexInterface } /** + * Get main tag prefix + * * @return string */ private function getMainPrefix() @@ -202,6 +215,8 @@ class Index implements IndexInterface } /** + * Generate tags based on product IDs + * * @param int[] $productIds * @return string[] */ @@ -216,6 +231,8 @@ class Index implements IndexInterface } /** + * Generate tags based on entity ID, store ID, customer group ID and segment ID + * * @param int $entityId * @param int $storeId * @param int $customerGroupId diff --git a/vendor/magento/module-target-rule/Model/Rotation.php b/vendor/magento/module-target-rule/Model/Rotation.php new file mode 100644 index 00000000000..b2197d24b27 --- /dev/null +++ b/vendor/magento/module-target-rule/Model/Rotation.php @@ -0,0 +1,94 @@ +random($list); + } elseif ($rotationMode == \Magento\TargetRule\Model\Source\Rotation::ROTATION_WEIGHTED_RANDOM) { + if ($limit > 0) { + $list = $this->weightedRandom($list); + $list = array_slice($list, 0, $limit, true); + } else { + $list = $this->random($list); + } + } else { + ksort($list, SORT_NUMERIC); + } + $listGroupByPriority = []; + foreach ($list as $id => $priority) { + $listGroupByPriority[$priority][] = $id; + } + ksort($listGroupByPriority, SORT_NUMERIC); + $orderedList = []; + foreach ($listGroupByPriority as $byPriority) { + array_push($orderedList, ...$byPriority); + } + $orderedList = array_replace(array_flip($orderedList), $list); + return $limit < 0 ? [] : array_slice($orderedList, 0, $limit, true); + } + + /** + * Order associative array by key in random order + * + * @param array $list + * @return array + */ + private function random(array $list): array + { + $keys = array_keys($list); + shuffle($keys); + $random = []; + foreach ($keys as $key) { + $random[$key] = $list[$key]; + } + return $random; + } + + /** + * Order associative array by key in weighted random order where values are respective priority + * + * @param array $list + * @return array + */ + private function weightedRandom(array $list): array + { + $weights = []; + $priorities = array_values(array_unique($list)); + rsort($priorities, SORT_NUMERIC); + $weight = 2; + foreach ($priorities as $priority) { + $weights[$priority] = log($weight); + $weight += 2; + } + $normalizedList = []; + foreach ($list as $key => $priority) { + $normalizedList[$key] = $weights[$priority]; + } + $random = []; + foreach ($normalizedList as $key => $weight) { + $random[$key] = pow(random_int(1, 99) / 100, 1 / $weight); + } + arsort($random, SORT_NUMERIC); + return array_replace($random, $list); + } +} diff --git a/vendor/magento/module-target-rule/Model/Source/Rotation.php b/vendor/magento/module-target-rule/Model/Source/Rotation.php index ccc9c0b199f..ad78623cbd6 100644 --- a/vendor/magento/module-target-rule/Model/Source/Rotation.php +++ b/vendor/magento/module-target-rule/Model/Source/Rotation.php @@ -5,8 +5,17 @@ */ namespace Magento\TargetRule\Model\Source; +/** + * List available rotation options + */ class Rotation implements \Magento\Framework\Option\ArrayInterface { + /** + * Weighted random + * + * The lowest priority is assigned the highest weight. Selected products have the highest weight + */ + public const ROTATION_WEIGHTED_RANDOM = 2; /** * Get data for Rotation mode selector * @@ -15,8 +24,9 @@ class Rotation implements \Magento\Framework\Option\ArrayInterface public function toOptionArray() { return [ - \Magento\TargetRule\Model\Rule::ROTATION_NONE => __('Do not rotate'), - \Magento\TargetRule\Model\Rule::ROTATION_SHUFFLE => __('Shuffle') + \Magento\TargetRule\Model\Rule::ROTATION_NONE => __('By Priority, Then by ID'), + \Magento\TargetRule\Model\Rule::ROTATION_SHUFFLE => __('By Priority, Then Random'), + self::ROTATION_WEIGHTED_RANDOM => __('Weighted Random'), ]; } } diff --git a/vendor/magento/module-target-rule/i18n/en_US.csv b/vendor/magento/module-target-rule/i18n/en_US.csv index 4c73dfbda8b..68fca247565 100644 --- a/vendor/magento/module-target-rule/i18n/en_US.csv +++ b/vendor/magento/module-target-rule/i18n/en_US.csv @@ -94,3 +94,6 @@ Rule,Rule Start,Start End,End "Applies To","Applies To" +"By Priority, Then by ID","By Priority, Then by ID" +"By Priority, Then Random","By Priority, Then Random" +"Weighted Random","Weighted Random" diff --git a/vendor/magento/module-target-rule/view/frontend/layout/catalog_product_view.xml b/vendor/magento/module-target-rule/view/frontend/layout/catalog_product_view.xml index cf66deba0cf..0eb2dc4b0f0 100644 --- a/vendor/magento/module-target-rule/view/frontend/layout/catalog_product_view.xml +++ b/vendor/magento/module-target-rule/view/frontend/layout/catalog_product_view.xml @@ -11,6 +11,7 @@ related-rule + Magento\TargetRule\Block\DataProviders\Rotation upsell-rule + Magento\TargetRule\Block\DataProviders\Rotation