Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Price.php
Go to the documentation of this file.
1 <?php
7 
13 use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory;
19 
26 {
30  private $indexTableStructureFactory;
31 
35  private $tableMaintainer;
36 
40  private $metadataPool;
41 
45  private $resource;
46 
50  private $fullReindexAction;
51 
55  private $connectionName;
56 
60  private $connection;
61 
67  private $dimensionToFieldMapper = [
69  CustomerGroupDimensionProvider::DIMENSION_NAME => 'cg.customer_group_id',
70  ];
71 
75  private $basePriceModifier;
76 
80  private $joinAttributeProcessor;
81 
85  private $eventManager;
86 
90  private $moduleManager;
91 
106  public function __construct(
107  IndexTableStructureFactory $indexTableStructureFactory,
108  TableMaintainer $tableMaintainer,
109  MetadataPool $metadataPool,
110  \Magento\Framework\App\ResourceConnection $resource,
111  BasePriceModifier $basePriceModifier,
112  JoinAttributeProcessor $joinAttributeProcessor,
113  \Magento\Framework\Event\ManagerInterface $eventManager,
114  \Magento\Framework\Module\Manager $moduleManager,
115  $fullReindexAction = false,
116  $connectionName = 'indexer'
117  ) {
118  $this->indexTableStructureFactory = $indexTableStructureFactory;
119  $this->tableMaintainer = $tableMaintainer;
120  $this->connectionName = $connectionName;
121  $this->metadataPool = $metadataPool;
122  $this->resource = $resource;
123  $this->fullReindexAction = $fullReindexAction;
124  $this->basePriceModifier = $basePriceModifier;
125  $this->joinAttributeProcessor = $joinAttributeProcessor;
126  $this->eventManager = $eventManager;
127  $this->moduleManager = $moduleManager;
128  }
129 
136  public function executeByDimensions(array $dimensions, \Traversable $entityIds)
137  {
138  $this->tableMaintainer->createMainTmpTable($dimensions);
139 
140  $temporaryPriceTable = $this->indexTableStructureFactory->create([
141  'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions),
142  'entityField' => 'entity_id',
143  'customerGroupField' => 'customer_group_id',
144  'websiteField' => 'website_id',
145  'taxClassField' => 'tax_class_id',
146  'originalPriceField' => 'price',
147  'finalPriceField' => 'final_price',
148  'minPriceField' => 'min_price',
149  'maxPriceField' => 'max_price',
150  'tierPriceField' => 'tier_price',
151  ]);
152 
153  $entityIds = iterator_to_array($entityIds);
154 
155  $this->prepareTierPriceIndex($dimensions, $entityIds);
156 
157  $this->prepareBundlePriceTable();
158 
159  $this->prepareBundlePriceByType(
161  $dimensions,
162  $entityIds
163  );
164 
165  $this->prepareBundlePriceByType(
167  $dimensions,
168  $entityIds
169  );
170 
171  $this->calculateBundleOptionPrice($temporaryPriceTable, $dimensions);
172 
173  $this->basePriceModifier->modifyPrice($temporaryPriceTable, $entityIds);
174  }
175 
181  private function getBundlePriceTable()
182  {
183  return $this->getTable('catalog_product_index_price_bundle_tmp');
184  }
185 
191  private function getBundleSelectionTable()
192  {
193  return $this->getTable('catalog_product_index_price_bundle_sel_tmp');
194  }
195 
201  private function getBundleOptionTable()
202  {
203  return $this->getTable('catalog_product_index_price_bundle_opt_tmp');
204  }
205 
211  private function prepareBundlePriceTable()
212  {
213  $this->getConnection()->delete($this->getBundlePriceTable());
214  return $this;
215  }
216 
222  private function prepareBundleSelectionTable()
223  {
224  $this->getConnection()->delete($this->getBundleSelectionTable());
225  return $this;
226  }
227 
233  private function prepareBundleOptionTable()
234  {
235  $this->getConnection()->delete($this->getBundleOptionTable());
236  return $this;
237  }
238 
249  private function prepareBundlePriceByType($priceType, array $dimensions, $entityIds = null)
250  {
251  $connection = $this->getConnection();
252  $select = $connection->select()->from(
253  ['e' => $this->getTable('catalog_product_entity')],
254  ['entity_id']
255  )->joinInner(
256  ['cg' => $this->getTable('customer_group')],
257  array_key_exists(CustomerGroupDimensionProvider::DIMENSION_NAME, $dimensions)
258  ? sprintf(
259  '%s = %s',
260  $this->dimensionToFieldMapper[CustomerGroupDimensionProvider::DIMENSION_NAME],
261  $dimensions[CustomerGroupDimensionProvider::DIMENSION_NAME]->getValue()
262  ) : '',
263  ['customer_group_id']
264  )->joinInner(
265  ['pw' => $this->getTable('catalog_product_website')],
266  'pw.product_id = e.entity_id',
267  ['pw.website_id']
268  )->joinInner(
269  ['cwd' => $this->getTable('catalog_product_index_website')],
270  'pw.website_id = cwd.website_id',
271  []
272  );
273  $select->joinLeft(
274  ['tp' => $this->getTable('catalog_product_index_tier_price')],
275  'tp.entity_id = e.entity_id AND tp.website_id = pw.website_id' .
276  ' AND tp.customer_group_id = cg.customer_group_id',
277  []
278  )->where(
279  'e.type_id=?',
280  \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE
281  );
282 
283  foreach ($dimensions as $dimension) {
284  if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) {
285  throw new \LogicException(
286  'Provided dimension is not valid for Price indexer: ' . $dimension->getName()
287  );
288  }
289  $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue());
290  }
291 
292  $this->joinAttributeProcessor->process($select, 'status', Status::STATUS_ENABLED);
293  if ($this->moduleManager->isEnabled('Magento_Tax')) {
294  $taxClassId = $this->joinAttributeProcessor->process($select, 'tax_class_id');
295  } else {
296  $taxClassId = new \Zend_Db_Expr('0');
297  }
298 
299  if ($priceType == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC) {
300  $select->columns(['tax_class_id' => new \Zend_Db_Expr('0')]);
301  } else {
302  $select->columns(
303  ['tax_class_id' => $connection->getCheckSql($taxClassId . ' IS NOT NULL', $taxClassId, 0)]
304  );
305  }
306 
307  $this->joinAttributeProcessor->process($select, 'price_type', $priceType);
308 
309  $price = $this->joinAttributeProcessor->process($select, 'price');
310  $specialPrice = $this->joinAttributeProcessor->process($select, 'special_price');
311  $specialFrom = $this->joinAttributeProcessor->process($select, 'special_from_date');
312  $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date');
313  $currentDate = new \Zend_Db_Expr('cwd.website_date');
314 
315  $specialFromDate = $connection->getDatePartSql($specialFrom);
316  $specialToDate = $connection->getDatePartSql($specialTo);
317  $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}";
318  $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}";
319  $specialExpr = "{$specialPrice} IS NOT NULL AND {$specialPrice} > 0 AND {$specialPrice} < 100"
320  . " AND {$specialFromExpr} AND {$specialToExpr}";
321  $tierExpr = new \Zend_Db_Expr('tp.min_price');
322 
323  if ($priceType == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED) {
324  $specialPriceExpr = $connection->getCheckSql(
325  $specialExpr,
326  'ROUND(' . $price . ' * (' . $specialPrice . ' / 100), 4)',
327  'NULL'
328  );
329  $tierPrice = $connection->getCheckSql(
330  $tierExpr . ' IS NOT NULL',
331  'ROUND((1 - ' . $tierExpr . ' / 100) * ' . $price . ', 4)',
332  'NULL'
333  );
334  $finalPrice = $connection->getLeastSql([
335  $price,
336  $connection->getIfNullSql($specialPriceExpr, $price),
337  $connection->getIfNullSql($tierPrice, $price),
338  ]);
339  } else {
340  $finalPrice = new \Zend_Db_Expr('0');
341  $tierPrice = $connection->getCheckSql($tierExpr . ' IS NOT NULL', '0', 'NULL');
342  }
343 
344  $select->columns(
345  [
346  'price_type' => new \Zend_Db_Expr($priceType),
347  'special_price' => $connection->getCheckSql($specialExpr, $specialPrice, '0'),
348  'tier_percent' => $tierExpr,
349  'orig_price' => $connection->getIfNullSql($price, '0'),
350  'price' => $finalPrice,
351  'min_price' => $finalPrice,
352  'max_price' => $finalPrice,
353  'tier_price' => $tierPrice,
354  'base_tier' => $tierPrice,
355  ]
356  );
357 
358  if ($entityIds !== null) {
359  $select->where('e.entity_id IN(?)', $entityIds);
360  }
361 
365  $this->eventManager->dispatch(
366  'catalog_product_prepare_index_select',
367  [
368  'select' => $select,
369  'entity_field' => new \Zend_Db_Expr('e.entity_id'),
370  'website_field' => new \Zend_Db_Expr('pw.website_id'),
371  'store_field' => new \Zend_Db_Expr('cwd.default_store_id')
372  ]
373  );
374 
375  $query = $select->insertFromSelect($this->getBundlePriceTable());
376  $connection->query($query);
377  }
378 
388  private function calculateBundleOptionPrice($priceTable, $dimensions)
389  {
390  $connection = $this->getConnection();
391 
392  $this->prepareBundleSelectionTable();
393  $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED);
394  $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC);
395 
396  $this->prepareBundleOptionTable();
397 
398  $select = $connection->select()->from(
399  $this->getBundleSelectionTable(),
400  ['entity_id', 'customer_group_id', 'website_id', 'option_id']
401  )->group(
402  ['entity_id', 'customer_group_id', 'website_id', 'option_id']
403  );
404  $minPrice = $connection->getCheckSql('is_required = 1', 'price', 'NULL');
405  $tierPrice = $connection->getCheckSql('is_required = 1', 'tier_price', 'NULL');
406  $select->columns(
407  [
408  'min_price' => new \Zend_Db_Expr('MIN(' . $minPrice . ')'),
409  'alt_price' => new \Zend_Db_Expr('MIN(price)'),
410  'max_price' => $connection->getCheckSql('group_type = 0', 'MAX(price)', 'SUM(price)'),
411  'tier_price' => new \Zend_Db_Expr('MIN(' . $tierPrice . ')'),
412  'alt_tier_price' => new \Zend_Db_Expr('MIN(tier_price)'),
413  ]
414  );
415 
416  $query = $select->insertFromSelect($this->getBundleOptionTable());
417  $connection->query($query);
418 
419  $this->getConnection()->delete($priceTable->getTableName());
420  $this->applyBundlePrice($priceTable);
421  $this->applyBundleOptionPrice($priceTable);
422  }
423 
433  private function calculateBundleSelectionPrice($dimensions, $priceType)
434  {
435  $connection = $this->getConnection();
436 
437  if ($priceType == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED) {
438  $selectionPriceValue = $connection->getCheckSql(
439  'bsp.selection_price_value IS NULL',
440  'bs.selection_price_value',
441  'bsp.selection_price_value'
442  );
443  $selectionPriceType = $connection->getCheckSql(
444  'bsp.selection_price_type IS NULL',
445  'bs.selection_price_type',
446  'bsp.selection_price_type'
447  );
448  $priceExpr = new \Zend_Db_Expr(
449  $connection->getCheckSql(
450  $selectionPriceType . ' = 1',
451  'ROUND(i.price * (' . $selectionPriceValue . ' / 100),4)',
452  $connection->getCheckSql(
453  'i.special_price > 0 AND i.special_price < 100',
454  'ROUND(' . $selectionPriceValue . ' * (i.special_price / 100),4)',
455  $selectionPriceValue
456  )
457  ) . '* bs.selection_qty'
458  );
459 
460  $tierExpr = $connection->getCheckSql(
461  'i.base_tier IS NOT NULL',
462  $connection->getCheckSql(
463  $selectionPriceType . ' = 1',
464  'ROUND(i.base_tier - (i.base_tier * (' . $selectionPriceValue . ' / 100)),4)',
465  $connection->getCheckSql(
466  'i.tier_percent > 0',
467  'ROUND((1 - i.tier_percent / 100) * ' . $selectionPriceValue . ',4)',
468  $selectionPriceValue
469  )
470  ) . ' * bs.selection_qty',
471  'NULL'
472  );
473 
474  $priceExpr = $connection->getLeastSql([
475  $priceExpr,
476  $connection->getIfNullSql($tierExpr, $priceExpr),
477  ]);
478  } else {
479  $price = 'idx.min_price * bs.selection_qty';
480  $specialExpr = $connection->getCheckSql(
481  'i.special_price > 0 AND i.special_price < 100',
482  'ROUND(' . $price . ' * (i.special_price / 100), 4)',
483  $price
484  );
485  $tierExpr = $connection->getCheckSql(
486  'i.tier_percent IS NOT NULL',
487  'ROUND((1 - i.tier_percent / 100) * ' . $price . ', 4)',
488  'NULL'
489  );
490  $priceExpr = $connection->getLeastSql([
491  $specialExpr,
492  $connection->getIfNullSql($tierExpr, $price),
493  ]);
494  }
495 
496  $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
497  $linkField = $metadata->getLinkField();
498  $select = $connection->select()->from(
499  ['i' => $this->getBundlePriceTable()],
500  ['entity_id', 'customer_group_id', 'website_id']
501  )->join(
502  ['parent_product' => $this->getTable('catalog_product_entity')],
503  'parent_product.entity_id = i.entity_id',
504  []
505  )->join(
506  ['bo' => $this->getTable('catalog_product_bundle_option')],
507  "bo.parent_id = parent_product.$linkField",
508  ['option_id']
509  )->join(
510  ['bs' => $this->getTable('catalog_product_bundle_selection')],
511  'bs.option_id = bo.option_id',
512  ['selection_id']
513  )->joinLeft(
514  ['bsp' => $this->getTable('catalog_product_bundle_selection_price')],
515  'bs.selection_id = bsp.selection_id AND bsp.website_id = i.website_id',
516  ['']
517  )->join(
518  ['idx' => $this->getMainTable($dimensions)],
519  'bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id' .
520  ' AND i.website_id = idx.website_id',
521  []
522  )->join(
523  ['e' => $this->getTable('catalog_product_entity')],
524  'bs.product_id = e.entity_id AND e.required_options=0',
525  []
526  )->where(
527  'i.price_type=?',
528  $priceType
529  )->columns(
530  [
531  'group_type' => $connection->getCheckSql("bo.type = 'select' OR bo.type = 'radio'", '0', '1'),
532  'is_required' => 'bo.required',
533  'price' => $priceExpr,
534  'tier_price' => $tierExpr,
535  ]
536  );
537 
538  $query = $select->insertFromSelect($this->getBundleSelectionTable());
539  $connection->query($query);
540  }
541 
550  private function prepareTierPriceIndex($dimensions, $entityIds)
551  {
552  $connection = $this->getConnection();
553  $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
554  $linkField = $metadata->getLinkField();
555  // remove index by bundle products
556  $select = $connection->select()->from(
557  ['i' => $this->getTable('catalog_product_index_tier_price')],
558  null
559  )->join(
560  ['e' => $this->getTable('catalog_product_entity')],
561  "i.entity_id=e.entity_id",
562  []
563  )->where(
564  'e.type_id=?',
565  \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE
566  );
567  $query = $select->deleteFromSelect('i');
568  $connection->query($query);
569 
570  $select = $connection->select()->from(
571  ['tp' => $this->getTable('catalog_product_entity_tier_price')],
572  ['e.entity_id']
573  )->join(
574  ['e' => $this->getTable('catalog_product_entity')],
575  "tp.{$linkField} = e.{$linkField}",
576  []
577  )->join(
578  ['cg' => $this->getTable('customer_group')],
579  'tp.all_groups = 1 OR (tp.all_groups = 0 AND tp.customer_group_id = cg.customer_group_id)',
580  ['customer_group_id']
581  )->join(
582  ['pw' => $this->getTable('store_website')],
583  'tp.website_id = 0 OR tp.website_id = pw.website_id',
584  ['website_id']
585  )->where(
586  'pw.website_id != 0'
587  )->where(
588  'e.type_id=?',
589  \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE
590  )->columns(
591  new \Zend_Db_Expr('MIN(tp.value)')
592  )->group(
593  ['e.entity_id', 'cg.customer_group_id', 'pw.website_id']
594  );
595 
596  if (!empty($entityIds)) {
597  $select->where('e.entity_id IN(?)', $entityIds);
598  }
599  foreach ($dimensions as $dimension) {
600  if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) {
601  throw new \LogicException(
602  'Provided dimension is not valid for Price indexer: ' . $dimension->getName()
603  );
604  }
605  $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue());
606  }
607 
608  $query = $select->insertFromSelect($this->getTable('catalog_product_index_tier_price'));
609  $connection->query($query);
610  }
611 
618  private function applyBundlePrice($priceTable): void
619  {
620  $select = $this->getConnection()->select();
621  $select->from(
622  $this->getBundlePriceTable(),
623  [
624  'entity_id',
625  'customer_group_id',
626  'website_id',
627  'tax_class_id',
628  'orig_price',
629  'price',
630  'min_price',
631  'max_price',
632  'tier_price',
633  ]
634  );
635 
636  $query = $select->insertFromSelect($priceTable->getTableName());
637  $this->getConnection()->query($query);
638  }
639 
646  private function applyBundleOptionPrice($priceTable): void
647  {
648  $connection = $this->getConnection();
649 
650  $subSelect = $connection->select()->from(
651  $this->getBundleOptionTable(),
652  [
653  'entity_id',
654  'customer_group_id',
655  'website_id',
656  'min_price' => new \Zend_Db_Expr('SUM(min_price)'),
657  'alt_price' => new \Zend_Db_Expr('MIN(alt_price)'),
658  'max_price' => new \Zend_Db_Expr('SUM(max_price)'),
659  'tier_price' => new \Zend_Db_Expr('SUM(tier_price)'),
660  'alt_tier_price' => new \Zend_Db_Expr('MIN(alt_tier_price)'),
661  ]
662  )->group(
663  ['entity_id', 'customer_group_id', 'website_id']
664  );
665 
666  $minPrice = 'i.min_price + ' . $connection->getIfNullSql('io.min_price', '0');
667  $tierPrice = 'i.tier_price + ' . $connection->getIfNullSql('io.tier_price', '0');
668  $select = $connection->select()->join(
669  ['io' => $subSelect],
670  'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
671  ' AND i.website_id = io.website_id',
672  []
673  )->columns(
674  [
675  'min_price' => $connection->getCheckSql("{$minPrice} = 0", 'io.alt_price', $minPrice),
676  'max_price' => new \Zend_Db_Expr('io.max_price + i.max_price'),
677  'tier_price' => $connection->getCheckSql("{$tierPrice} = 0", 'io.alt_tier_price', $tierPrice),
678  ]
679  );
680 
681  $query = $select->crossUpdateFromSelect(['i' => $priceTable->getTableName()]);
682  $connection->query($query);
683  }
684 
691  private function getMainTable($dimensions)
692  {
693  if ($this->fullReindexAction) {
694  return $this->tableMaintainer->getMainReplicaTable($dimensions);
695  }
696  return $this->tableMaintainer->getMainTable($dimensions);
697  }
698 
705  private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
706  {
707  if ($this->connection === null) {
708  $this->connection = $this->resource->getConnection($this->connectionName);
709  }
710 
711  return $this->connection;
712  }
713 
720  private function getTable($tableName)
721  {
722  return $this->resource->getTableName($tableName, $this->connectionName);
723  }
724 }
$tableName
Definition: trigger.php:13
$resource
Definition: bulk.php:12
$price
$priceType
Definition: msrp.phtml:18
executeByDimensions(array $dimensions, \Traversable $entityIds)
Definition: Price.php:136
$moduleManager
Definition: products.php:75
__construct(IndexTableStructureFactory $indexTableStructureFactory, TableMaintainer $tableMaintainer, MetadataPool $metadataPool, \Magento\Framework\App\ResourceConnection $resource, BasePriceModifier $basePriceModifier, JoinAttributeProcessor $joinAttributeProcessor, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Framework\Module\Manager $moduleManager, $fullReindexAction=false, $connectionName='indexer')
Definition: Price.php:106