Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions src/elements/db/ProductQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,12 @@ protected function afterPrepare(): bool
// Store dependent related joins to the sub query need to be done after the `elements_sites` is joined in the base `ElementQuery` class.
$customerId = Craft::$app->getUser()->getIdentity()?->id;

$hasStoreTables = $this->_storeTablesExist();

if (!$hasStoreTables) {
return parent::afterPrepare();
}

$this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]');

if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
Expand All @@ -771,6 +777,8 @@ protected function afterPrepare(): bool
protected function beforePrepare(): bool
{
$this->_normalizeTypeId();
$hasStoreTables = $this->_storeTablesExist();
$hasPurchasableDimensionColumns = $this->_purchasableDimensionColumnsExist();

// See if 'type' were set to invalid handles
if ($this->typeId === []) {
Expand All @@ -784,36 +792,67 @@ protected function beforePrepare(): bool
'commerce_products.typeId',
'commerce_products.postDate',
'commerce_products.expiryDate',
'purchasablesstores.basePrice as defaultBasePrice',
'purchasablesstores.basePromotionalPrice as defaultBasePromotionalPrice',
'commerce_products.defaultVariantId',
'purchasables.sku as defaultSku',
'purchasables.weight as defaultWeight',
'purchasables.length as defaultLength',
'purchasables.width as defaultWidth',
'purchasables.height as defaultHeight',
'sitestores.storeId',
]);

if ($hasPurchasableDimensionColumns) {
$this->query->addSelect([
'purchasables.weight as defaultWeight',
'purchasables.length as defaultLength',
'purchasables.width as defaultWidth',
'purchasables.height as defaultHeight',
]);
} else {
$this->query->addSelect([
new Expression('NULL as [[defaultWeight]]'),
new Expression('NULL as [[defaultLength]]'),
new Expression('NULL as [[defaultWidth]]'),
new Expression('NULL as [[defaultHeight]]'),
]);
}

if ($hasStoreTables) {
$this->query->addSelect([
'purchasablesstores.basePrice as defaultBasePrice',
'purchasablesstores.basePromotionalPrice as defaultBasePromotionalPrice',
'sitestores.storeId',
]);
} else {
$this->query->addSelect([
new Expression('NULL as [[defaultBasePrice]]'),
new Expression('NULL as [[defaultBasePromotionalPrice]]'),
new Expression('NULL as [[storeId]]'),
]);
}

// Join in sites stores to get product's store for current request
$this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->query->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]');
$this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]');
if ($hasStoreTables) {
$this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]');
}

// Tailor the query based on whether or not there is catalog pricing rules
if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
$this->query->addSelect(['subquery.price as defaultPrice']);
$this->subQuery->addSelect(['catalogprices.price']);

if (isset($this->defaultPrice)) {
$this->subQuery->andWhere(Db::parseParam('catalogprices.price', $this->defaultPrice));
}
} else {
} elseif ($hasStoreTables) {
$this->query->addSelect(['purchasablesstores.basePrice as defaultPrice']);

if (isset($this->defaultPrice)) {
$this->subQuery->andWhere(Db::parseParam('purchasablesstores.basePrice', $this->defaultPrice));
}
} else {
$this->query->addSelect([new Expression('NULL as [[defaultPrice]]')]);

if (isset($this->defaultPrice)) {
return false;
}
}

if (isset($this->postDate)) {
Expand All @@ -826,7 +865,13 @@ protected function beforePrepare(): bool

$this->_applyProductTypeIdParam();

if (isset($this->defaultHeight) || isset($this->defaultLength) || isset($this->defaultWidth) || isset($this->defaultWeight) || isset($this->defaultSku)) {
if (isset($this->defaultHeight) || isset($this->defaultLength) || isset($this->defaultWidth) || isset($this->defaultWeight)) {
if (!$hasPurchasableDimensionColumns) {
return false;
}

$this->subQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]');
} elseif (isset($this->defaultSku)) {
$this->subQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]');
}

Expand Down Expand Up @@ -1053,4 +1098,22 @@ protected function cacheTags(): array

return $tags;
}

private function _storeTablesExist(): bool
{
$db = Craft::$app->getDb();

return $db->tableExists(Table::SITESTORES) &&
$db->tableExists(Table::PURCHASABLES_STORES);
}

private function _purchasableDimensionColumnsExist(): bool
{
$db = Craft::$app->getDb();

return $db->columnExists(Table::PURCHASABLES, 'weight') &&
$db->columnExists(Table::PURCHASABLES, 'length') &&
$db->columnExists(Table::PURCHASABLES, 'width') &&
$db->columnExists(Table::PURCHASABLES, 'height');
}
}
161 changes: 135 additions & 26 deletions src/elements/db/PurchasableQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -655,11 +655,15 @@ public function onPromotion(bool|null $value = true): static
protected function afterPrepare(): bool
{
// Store dependent related joins to the sub query need to be done after the `elements_sites` is joined in the base `ElementQuery` class.
$this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->subQuery->leftJoin(['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]');
$hasStoreTables = $this->_storeTablesExist();

if ($hasStoreTables) {
$this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->subQuery->leftJoin(['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]');
}

// Only do the extra catalog pricing query join if we have catalog pricing rules.
if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
$customerId = $this->forCustomer;
if ($customerId === null) {
$customerId = Craft::$app->getUser()->getIdentity()?->id;
Expand All @@ -675,7 +679,9 @@ protected function afterPrepare(): bool
$this->subQuery->leftJoin(['catalogprices' => $catalogPricesQuery], '[[catalogprices.purchasableId]] = [[commerce_purchasables.id]] AND [[catalogprices.storeId]] = [[sitestores.storeId]]');
}

$this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]');
if (Craft::$app->getDb()->tableExists(Table::INVENTORYITEMS)) {
$this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]');
}

return parent::afterPrepare();
}
Expand All @@ -685,33 +691,73 @@ protected function afterPrepare(): bool
*/
protected function beforePrepare(): bool
{
$hasStoreTables = $this->_storeTablesExist();
$hasInventoryTable = Craft::$app->getDb()->tableExists(Table::INVENTORYITEMS);
$hasPurchasableDetailColumns = $this->_purchasableDetailColumnsExist();

$this->joinElementTable('commerce_purchasables');
$this->query->addSelect([
'commerce_purchasables.sku',
'commerce_purchasables.width',
'commerce_purchasables.height',
'commerce_purchasables.length',
'commerce_purchasables.weight',
'commerce_purchasables.taxCategoryId',
'purchasables_stores.availableForPurchase',
'purchasables_stores.basePrice',
'purchasables_stores.basePromotionalPrice',
'purchasables_stores.freeShipping',
'purchasables_stores.maxQty',
'purchasables_stores.minQty',
'purchasables_stores.inventoryTracked',
'purchasables_stores.allowOutOfStockPurchases',
'purchasables_stores.promotable',
'purchasables_stores.shippingCategoryId',
'inventoryitems.id as inventoryItemId',
]);

$this->query->leftJoin(Table::SITESTORES . ' sitestores', '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]');
$this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]');
if ($hasPurchasableDetailColumns) {
$this->query->addSelect([
'commerce_purchasables.width',
'commerce_purchasables.height',
'commerce_purchasables.length',
'commerce_purchasables.weight',
'commerce_purchasables.taxCategoryId',
]);
} else {
$this->query->addSelect([
new Expression('NULL as [[width]]'),
new Expression('NULL as [[height]]'),
new Expression('NULL as [[length]]'),
new Expression('NULL as [[weight]]'),
new Expression('NULL as [[taxCategoryId]]'),
]);
}

if ($hasStoreTables) {
$this->query->addSelect([
'purchasables_stores.availableForPurchase',
'purchasables_stores.basePrice',
'purchasables_stores.basePromotionalPrice',
'purchasables_stores.freeShipping',
'purchasables_stores.maxQty',
'purchasables_stores.minQty',
'purchasables_stores.inventoryTracked',
'purchasables_stores.allowOutOfStockPurchases',
'purchasables_stores.promotable',
'purchasables_stores.shippingCategoryId',
]);

$this->query->leftJoin(Table::SITESTORES . ' sitestores', '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]');
} else {
$this->query->addSelect([
new Expression('NULL as [[availableForPurchase]]'),
new Expression('NULL as [[basePrice]]'),
new Expression('NULL as [[basePromotionalPrice]]'),
new Expression('NULL as [[freeShipping]]'),
new Expression('NULL as [[maxQty]]'),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f382275. The missing-store-table fallback now selects boolean-safe defaults for availableForPurchase and freeShipping (1 and 0) instead of NULL, so hydration won’t assign null to non-nullable bool properties.

new Expression('NULL as [[minQty]]'),
new Expression('NULL as [[inventoryTracked]]'),
new Expression('NULL as [[allowOutOfStockPurchases]]'),
new Expression('NULL as [[promotable]]'),
new Expression('NULL as [[shippingCategoryId]]'),
Comment on lines +744 to +748

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f382275. The fallback selects 0 for inventoryTracked, allowOutOfStockPurchases, and promotable instead of NULL, matching the non-nullable bool properties on Purchasable.

]);
}

if ($hasInventoryTable) {
$this->query->addSelect(['inventoryitems.id as inventoryItemId']);
$this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]');
} else {
$this->query->addSelect([new Expression('NULL as [[inventoryItemId]]')]);
}

// Only do the extra catalog pricing query join if we have catalog pricing rules.
if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
$this->query->addSelect([
'subquery.price',
'subquery.promotionalPrice as promotionalPrice',
Expand Down Expand Up @@ -743,7 +789,7 @@ protected function beforePrepare(): bool
if (isset($this->salePrice)) {
$this->subQuery->andWhere(Db::parseNumericParam('catalogprices.salePrice' , $this->salePrice));
}
} else {
} elseif ($hasStoreTables) {
// If Catalog pricing rules are not being used
$this->query->addSelect([
'purchasables_stores.basePrice as price',
Expand Down Expand Up @@ -777,6 +823,17 @@ protected function beforePrepare(): bool
if (isset($this->salePrice)) {
$this->subQuery->andWhere(Db::parseNumericParam(new Expression('CASE WHEN [[purchasables_stores.basePromotionalPrice]] < [[purchasables_stores.basePrice]] THEN [[purchasables_stores.basePromotionalPrice]] ELSE [[purchasables_stores.basePrice]] END') , $this->salePrice));
}
} else {
$this->query->addSelect([
new Expression('NULL as [[price]]'),
new Expression('NULL as [[promotionalPrice]]'),
new Expression('NULL as [[salePrice]]'),
new Expression('NULL as [[catalogPricingRuleId]]'),
]);

if ($this->_hasStoreTableParams()) {
return false;
}
}

if (isset($this->sku)) {
Expand Down Expand Up @@ -811,6 +868,10 @@ protected function beforePrepare(): bool
}

if (isset($this->taxCategoryId)) {
if (!$hasPurchasableDetailColumns) {
return false;
}

if ($this->taxCategoryId instanceof Query) {
$taxCategoryWhere = ['exists', $this->taxCategoryId];
} else {
Expand All @@ -821,6 +882,10 @@ protected function beforePrepare(): bool
}

if ($this->width !== false) {
if (!$hasPurchasableDetailColumns) {
return false;
}

if ($this->width === null) {
$this->subQuery->andWhere(['commerce_purchasables.width' => $this->width]);
} else {
Expand All @@ -829,6 +894,10 @@ protected function beforePrepare(): bool
}

if ($this->height !== false) {
if (!$hasPurchasableDetailColumns) {
return false;
}

if ($this->height === null) {
$this->subQuery->andWhere(['commerce_purchasables.height' => $this->height]);
} else {
Expand All @@ -837,6 +906,10 @@ protected function beforePrepare(): bool
}

if ($this->length !== false) {
if (!$hasPurchasableDetailColumns) {
return false;
}

if ($this->length === null) {
$this->subQuery->andWhere(['commerce_purchasables.length' => $this->length]);
} else {
Expand All @@ -845,6 +918,10 @@ protected function beforePrepare(): bool
}

if ($this->weight !== false) {
if (!$hasPurchasableDetailColumns) {
return false;
}

if ($this->weight === null) {
$this->subQuery->andWhere(['commerce_purchasables.weight' => $this->weight]);
} else {
Expand Down Expand Up @@ -880,7 +957,7 @@ protected function beforePrepare(): bool
*/
public function populate($rows): array
{
if (!empty($rows) && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
if (!empty($rows) && $this->_storeTablesExist() && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) {
$row = ArrayHelper::firstValue($rows);
$store = Plugin::getInstance()->getStores()->getStoreBySiteId($row['siteId']);
$purchasableIds = ArrayHelper::getColumn($rows, 'id');
Expand Down Expand Up @@ -920,4 +997,36 @@ public function populate($rows): array

return parent::populate($rows);
}

private function _storeTablesExist(): bool
{
$db = Craft::$app->getDb();

return $db->tableExists(Table::SITESTORES) &&
$db->tableExists(Table::PURCHASABLES_STORES);
}

private function _hasStoreTableParams(): bool
{
return isset($this->price) ||
isset($this->promotionalPrice) ||
isset($this->onPromotion) ||
isset($this->salePrice) ||
isset($this->stock) ||
isset($this->inventoryTracked) ||
isset($this->availableForPurchase) ||
isset($this->shippingCategoryId) ||
isset($this->hasStock);
}

private function _purchasableDetailColumnsExist(): bool
{
$db = Craft::$app->getDb();

return $db->columnExists(Table::PURCHASABLES, 'width') &&
$db->columnExists(Table::PURCHASABLES, 'height') &&
$db->columnExists(Table::PURCHASABLES, 'length') &&
$db->columnExists(Table::PURCHASABLES, 'weight') &&
$db->columnExists(Table::PURCHASABLES, 'taxCategoryId');
}
}
Loading
Loading