context = adp_context(); } public function withContext(Context $context) { $this->context = $context; } /** * @param ICartItem $item * @param float|null $qty * @param array|null $roles * * @return array * @throws \Exception */ public function getCache($item, $qty = null, $roles = null) { $cacheKey = $this->calculateCacheHash($item, $qty, $roles); $objects = CacheHelper::cacheGet($cacheKey, CacheHelper::GROUP_RULES_CACHE); if ( ! is_array($objects) ) { $objects = $this->calculate($item, $qty, $roles); CacheHelper::cacheSet($cacheKey, $objects, CacheHelper::GROUP_RULES_CACHE); } return $objects; } /** * @param \WC_Product $product * * @return array * @throws \Exception */ public function getCacheWithProduct($product) { $cacheKey = $this->calculateCacheHashWithProduct($product); $objects = CacheHelper::cacheGet($cacheKey, CacheHelper::GROUP_RULES_CACHE); if ( ! is_array($objects) ) { $objects = $this->calculate($product); CacheHelper::cacheSet($cacheKey, $objects, CacheHelper::GROUP_RULES_CACHE); } return $objects; } public function addRule($rows, $ruleId) { global $wpdb; $table = $wpdb->prefix . PersistentRuleModel::TABLE_NAME; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query('START TRANSACTION'); if ( ! empty($ruleId)) { $where = array('rule_id' => $ruleId); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $result = $wpdb->delete($table, $where); } /** * @var PersistentRuleCache $cache */ foreach ($rows as $cache) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $result = $wpdb->insert($table, $cache->getData()); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query('COMMIT'); } public function getAddRuleData($ruleId, Context $context) { global $wpdb; /** @var $sqlGenerator SqlGeneratorPersistent */ $sqlGenerator = Factory::get("Shortcodes_SqlGeneratorPersistent"); /** @var RuleStorage $storage */ $storage = Factory::get("Database_RuleStorage"); $storage->withContext($context); $ruleRepository = new RuleRepository(); $rows = $ruleRepository->getRules(array('id' => $ruleId)); $rulesCollection = $storage->buildPersistentRules($rows); foreach ($rulesCollection->getRules() as $rule) { /** @var PersistentRule $rule */ $sqlGenerator->applyRuleToQuery($context, $rule); } $productIds = $sqlGenerator->getProductIds(); if(!$productIds) { return []; } $data = array(); /** @var PersistentRule $rule */ $rule = $rulesCollection->getFirst(); $cartCalculator = new CartCalculatorPersistent($context, $rule); $productProcessor = new Processor($context, $cartCalculator); $cartBuilder = new CartBuilder($context); $cart = $cartBuilder->create(WC()->customer, WC()->session); $productProcessor->withCart($cart); foreach ($productIds as $productId) { $variationId = 0; if ('product_variation' === get_post_type($productId)) { $variationId = $productId; $productId = wp_get_post_parent_id($variationId); } $product = CacheHelper::getWcProduct($variationId ? $variationId : $productId); $persistentRuleCaches = $this->calculateCacheForProductWithRule($context, $productProcessor, $rule, $product); foreach ($persistentRuleCaches as $cache) { $data[] = PersistentRuleCache::fromArray($cache); $this->saveCacheInProductMetaData($product, $cache); } } return $data; } public function removeRule($ruleId) { global $wpdb; $table = $wpdb->prefix . PersistentRuleModel::TABLE_NAME; $where = array('rule_id' => $ruleId); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->delete($table, $where); } /** * @param Context $context * @param \WC_Product $product * @param array $cartItemData */ public function recalculateCacheForProduct($context, $product, $cartItemData = array()) { $objects = $this->getCacheWithProduct($product); global $wpdb; $tableCache = $wpdb->prefix . PersistentRuleModel::TABLE_NAME; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query('START TRANSACTION'); foreach ( $objects as $object ) { if ($object === null || $object->rule === null) { continue; } $rule = $object->rule; $hash = $this->calculateDbHashWithProduct($product); $where = array('rule_id' => $rule->getId(), 'product' => $hash); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $result = $wpdb->delete($tableCache, $where); $cartCalculator = new CartCalculatorPersistent($context, $rule); $productProcessor = new Processor($context, $cartCalculator); $cartBuilder = new CartBuilder($context); $cart = $cartBuilder->create(WC()->customer, WC()->session); $productProcessor->withCart($cart); foreach ($this->calculateCacheForProductWithRule($context, $productProcessor, $rule, $product, $cartItemData) as $data) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $result = $wpdb->insert($tableCache, $data); } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query('COMMIT'); } /** * @param Context $context * @param \WC_Cart $wcCart * * @return array */ public function getRulesFromWcCart($context, $wcCart) { $rules = array(); $cartBuilder = new CartBuilder($context); $cart = $cartBuilder->create(WC()->customer, WC()->session); $cartBuilder->populateCart($cart, $wcCart); foreach ($cart->getItems() as $item) { $objects = $this->getCache($item); $object = null; foreach ( $objects as $tmpObject ) { $tmpProcessor = $tmpObject->rule->buildProcessor($context); if ( $tmpProcessor->isRuleMatchedCart($cart) ) { $object = $tmpObject; } } if ($object !== null && $object->rule !== null) { $rules[] = $object->rule; } } return $rules; } public function truncate() { global $wpdb; $tableCache = $wpdb->prefix . PersistentRuleModel::TABLE_NAME; //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query("TRUNCATE TABLE $tableCache"); } /** * @param Context $context * @param Processor $productProcessor * @param PersistentRule $rule * @param \WC_Product $product * @param array $cartItemData */ protected function calculateCacheForProductWithRule( $context, $productProcessor, $rule, $product, $cartItemData = array() ) { $data = array(); $hash = $this->calculateDbHashWithProduct($product, $cartItemData); if($rolesDiscounts = $rule->getRoleDiscounts()) { $customer = $productProcessor->getCart()->getContext()->getCustomer(); $initialRoles = $customer->getRoles(); foreach ($rolesDiscounts as $rolesDiscount) { foreach ($rolesDiscount->getRoles() as $role) { $customer->setRoles([$role]); $customHash = function($key) use($role) { $key[] = "C{$role}"; return $key; }; add_filter("adp_calculate_persistent_rule_product_hash", $customHash); $hash = $this->calculateDbHashWithProduct($product, $cartItemData); $processedProduct = $productProcessor->calculateProduct($product, 1.0, $cartItemData); remove_filter("adp_calculate_persistent_rule_product_hash", $customHash); $customer->setRoles($initialRoles); if ($processedProduct === null || $processedProduct instanceof ProcessedVariableProduct || $processedProduct instanceof ProcessedGroupedProduct) { continue; } $row = [ 'product' => $hash, 'rule_id' => $rule->getId(), 'qty_start' => 1.0, 'qty_finish' => null, 'original_price' => $processedProduct->getOriginalPrice(), 'price' => $processedProduct->getCalculatedPrice(), ]; $data[] = $row; } } } else if ($rule->hasProductRangeAdjustment()) { $handler = $rule->getProductRangeAdjustmentHandler(); $ranges = $handler->getRanges(); if ( count($ranges) > 0 ) { $range = $ranges[0]; if ( $range->getFrom() !== INF && $range->getFrom() > 1.0 ) { $ranges = array_merge( array( new RangeDiscount( 1, $range->getFrom() - 1.0, new Discount($context, Discount::TYPE_PERCENTAGE, 0) ) ), $ranges ); } } foreach ($ranges as $range) { $processedProduct = $productProcessor->calculateProduct($product, $range->getFrom(), $cartItemData); if ($processedProduct === null || $processedProduct instanceof ProcessedVariableProduct || $processedProduct instanceof ProcessedGroupedProduct) { return $data; } $data[] = array( 'product' => $hash, 'rule_id' => $rule->getId(), 'qty_start' => $range->getFrom(), 'qty_finish' => $range->getTo() === INF ? null : $range->getTo(), 'original_price' => $processedProduct->getOriginalPrice(), 'price' => $processedProduct->getCalculatedPrice(), ); } } else { $processedProduct = $productProcessor->calculateProduct($product, 1.0, $cartItemData); if ($processedProduct === null || $processedProduct instanceof ProcessedVariableProduct || $processedProduct instanceof ProcessedGroupedProduct) { return $data; } $data[] = array( 'product' => $hash, 'rule_id' => $rule->getId(), 'qty_start' => 1.0, 'qty_finish' => null, 'original_price' => $processedProduct->getOriginalPrice(), 'price' => $processedProduct->getCalculatedPrice(), ); } return $data; } protected function calculateDbHashWithRoles($item, $roles) { $hash = []; if(!$roles) { return $hash; } foreach ($roles as $role) { $customHash = function($key) use($role) { $key[] = "C{$role}"; return $key; }; add_filter("adp_calculate_persistent_rule_product_hash", $customHash); $hash[] = $this->calculateDbHash($item); remove_filter("adp_calculate_persistent_rule_product_hash", $customHash); } return $hash; } /** * @param ICartItem|\WC_Product $item * @param float|null $qty * * @return array * @throws \Exception */ protected function calculate($item, $qty = null, $roles=null) { $context = $this->context; if ($item instanceof ICartItem) { $hash[] = $this->calculateDbHash($item); $hash = \array_merge($hash, $this->calculateDbHashWithRoles($item, $roles)); $qty = ($qty !== null ? (float)$qty : $item->getQty()); } elseif ($item instanceof \WC_Product) { $hash[] = $this->calculateDbHashWithProduct($item); $hash = \array_merge($hash, $this->calculateDbHashWithRoles($item, $roles)); $qty = ($qty !== null ? (float)$qty : 1.0); } else { return array(); } $hash = implode(',', array_map(function($v) { return "'" . esc_sql($v) . "'"; }, $hash)); global $wpdb; $tableCache = $wpdb->prefix . PersistentRuleModel::TABLE_NAME; //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query = $wpdb->prepare("SELECT persistent_rules_cache.rule_id, persistent_rules_cache.price FROM {$tableCache} AS persistent_rules_cache WHERE persistent_rules_cache.product IN ({$hash}) AND persistent_rules_cache.qty_start <= %s AND (persistent_rules_cache.qty_finish IS NULL OR persistent_rules_cache.qty_finish >= %s)", array($qty, $qty) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- query built with $wpdb->prepare() above $rows = $wpdb->get_results($query, ARRAY_A); if (count($rows) === 0) { return array(); } $objects = []; foreach ($rows as $row) { $price = $row['price']; $rules = CacheHelper::loadProductOnlyRules([$row['rule_id']]); if (count($rules) === 0) { return []; } $rule = reset($rules); if ($rule === null) { continue; } $objects[] = new PersistentRuleCacheObject($rule, $price); } return $objects; } /** * @param ICartItem $item */ protected function calculateDbHash($item) { $productId = $item->getWcItem()->getProductId(); $variationId = $item->getWcItem()->getVariationId(); $cartItemData = array(); $product = $item->getWcItem()->getProduct(); $variationAttributes = $product instanceof \WC_Product_Variation ? $product->get_variation_attributes() : array(); return CacheHelper::calcHashPersistentRuleProduct( $productId, $variationId, $variationAttributes, $cartItemData ); } /** * @param \WC_Product $product * @param array $cartItemData */ protected function calculateDbHashWithProduct($product, $cartItemData = array()) { $parentId = $product->get_parent_id('edit'); $productId = $parentId ?: $product->get_id(); $variationId = $parentId ? $product->get_id() : 0; $variationAttributes = $product instanceof \WC_Product_Variation ? $product->get_variation_attributes() : array(); return CacheHelper::calcHashPersistentRuleProduct( (string)$productId, (string)$variationId, $variationAttributes, $cartItemData ); } /** * @param ICartItem $item * @param float|null $qty * @param array|null $roles */ protected function calculateCacheHash($item, $qty = null, $roles = null) { return join('_', array_filter([ $this->calculateDbHash($item), $qty !== null ? (float) $qty : $item->getQty(), $roles !== null ? join(',', $roles) : '' ])); } /** * @param \WC_Product $product */ protected function calculateCacheHashWithProduct($product) { $qty = (string)(1.0); return $this->calculateDbHashWithProduct($product) . '_' . $qty; } /** * @param \WC_Product $product * @param array $cache * @return void */ protected function saveCacheInProductMetaData($product, $cache) { static $last_parent_id = false, $last_parent_min_price=0; global $wpdb; $price = $cache['price']; // save calculated price in product meta update_post_meta($product->get_id(), '_sale_price_adp', $price); //update lookup table too // phpcs:ignore WordPress.DB $wpdb->update( $wpdb->wc_product_meta_lookup, ['min_price' => $price, 'onsale'=>1], ['product_id' => $product->get_id()] ); if($product instanceof \WC_Product_Variation) { $parent_id = $product->get_parent_id('edit'); if( $last_parent_id == $parent_id){ // variations of same product processed still ? if($last_parent_min_price > $price) //use min price of variation $last_parent_min_price = $price; } else { $last_parent_id = $parent_id; $last_parent_min_price = $price; } update_post_meta($last_parent_id, '_sale_price_adp', $last_parent_min_price); // phpcs:ignore WordPress.DB $wpdb->update( $wpdb->wc_product_meta_lookup, ['min_price' => $last_parent_min_price, 'onsale'=>1], ['product_id' => $last_parent_id] ); } } public function clearCacheInProductMetaData() { delete_post_meta_by_key('_sale_price_adp'); } public function installHooksForProductLookupTable() { add_action( 'wc_update_product_lookup_tables_column', function($column){ global $wpdb; if($column == 'min_max_price') { // phpcs:ignore WordPress.DB $sql = "UPDATE {$wpdb->wc_product_meta_lookup} lookup_table INNER JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = '_sale_price_adp' SET lookup_table.min_price = meta.meta_value"; // phpcs:ignore WordPress.DB $wpdb->query($sql); } if($column == 'onsale') { // phpcs:ignore WordPress.DB $sql = "UPDATE {$wpdb->wc_product_meta_lookup} lookup_table INNER JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = '_sale_price_adp' SET lookup_table.onsale = 1"; // phpcs:ignore WordPress.DB $wpdb->query($sql); } } , 100 ); add_filter( 'woocommerce_get_catalog_ordering_args', function ($sort_args) { $orderby_value = null; // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only catalog sort parameter, no state change if ( isset( $_GET['orderby'] ) ) { $orderby_value = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); } // phpcs:enable if (!empty($orderby_value) && $orderby_value === 'on_sale_first') { add_filter( 'woocommerce_product_query_meta_query', function ($meta_query) { $meta_query = [ 'relation' => 'OR', [ 'key' => '_sale_price_adp', 'compare' => 'NOT EXISTS', ], [ 'relation' => 'OR', [ 'key' => '_sale_price_adp', 'value' => 0, 'compare' => '>=', 'type' => 'NUMERIC', ], [ 'key' => '_sale_price_adp', 'value' => '', 'compare' => '=', ], ], ]; return $meta_query; }, 100 ); } return $sort_args; }, 100); } }