Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
39 changes: 30 additions & 9 deletions src/base/ShippingMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ abstract class ShippingMethod extends BaseModel implements ShippingMethodInterfa
*/
private ?ShippingMethodCustomerCondition $_customerCondition = null;

/**
* @var array<string, ShippingRuleInterface|false> Per-request cache of matching rule per order number.
*/
private array $_matchingRuleByOrderNumber = [];

/**
* @var DateTime|null
* @since 3.4
Expand Down Expand Up @@ -274,31 +279,47 @@ public function matchOrder(Order $order): bool
return false;
}

/** @var ShippingRuleInterface $rule */
foreach ($this->getShippingRules()->all() as $rule) {
if ($rule->matchOrder($order)) {
return true;
}
}

return false;
return $this->getMatchingShippingRule($order) !== null;
}

/**
* @inheritdoc
*/
public function getMatchingShippingRule(Order $order): ?ShippingRuleInterface
{
foreach ($this->getShippingRules() as $rule) {
if (array_key_exists($order->number, $this->_matchingRuleByOrderNumber)) {
return $this->_matchingRuleByOrderNumber[$order->number] ?: null;
}

foreach ($this->_getShippingRulesForMatching() as $rule) {
/** @var ShippingRuleInterface $rule */
if ($rule->matchOrder($order)) {
$this->_matchingRuleByOrderNumber[$order->number] = $rule;
return $rule;
}
}

$this->_matchingRuleByOrderNumber[$order->number] = false;
return null;
}

/**
* Returns an iterable of shipping rules to evaluate during order matching.
* Override in subclasses to stream rules without loading all into memory.
*/
protected function _getShippingRulesForMatching(): iterable
{
return $this->getShippingRules();
}

/**
* Clears the per-request matching rule cache. Called by ShippingMethods service after each match pass.
*/
public function clearMatchingRuleCache(): void
{
$this->_matchingRuleByOrderNumber = [];
}

public function getPriceForOrder(Order $order): float
{
$shippingRule = $this->getMatchingShippingRule($order);
Expand Down
12 changes: 12 additions & 0 deletions src/models/ShippingMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ public function getShippingRules(): Collection
return Plugin::getInstance()->getShippingRules()->getAllShippingRulesByShippingMethodId($this->id);
}

/**
* @inheritdoc
*/
protected function _getShippingRulesForMatching(): iterable
{
if ($this->id === null) {
return [];
}

return Plugin::getInstance()->getShippingRules()->getShippingRulesForMatchingByMethodId($this->id);
}

/**
* @inheritdoc
*/
Expand Down
11 changes: 7 additions & 4 deletions src/services/ShippingMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,10 @@ public function getMatchingShippingMethods(Order $order): array

/** @var ShippingMethod $method */
foreach ($event->getShippingMethods() as $method) {
$totalPrice = $method->getPriceForOrder($order);

if ($method->getIsEnabled() && $method->matchOrder($order)) {
$matchingMethods[$method->getHandle()] = [
'method' => $method,
'price' => $totalPrice, // Store the price so we can sort on it before returning
'price' => $method->getPriceForOrder($order), // Store the price so we can sort on it before returning
];
}
}
Expand All @@ -157,8 +155,13 @@ public function getMatchingShippingMethods(Order $order): array
$shippingMethods[$method->getHandle()] = $method; // Keep the key being the handle of the method for front-end use.
}

// Clear the memoized data so next time we watch to match rules, we get fresh data.
// Clear the memoized data so next time we want to match rules, we get fresh data.
$this->_serializedOrdersByNumber = [];
foreach ($event->getShippingMethods() as $method) {
if ($method instanceof ShippingMethod) {
$method->clearMatchingRuleCache();
}
}

return $shippingMethods;
}
Expand Down
50 changes: 45 additions & 5 deletions src/services/ShippingRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use craft\commerce\records\ShippingRule as ShippingRuleRecord;
use craft\commerce\records\ShippingRuleCategory as ShippingRuleCategoryRecord;
use craft\db\Query;
use Generator;
use Illuminate\Support\Collection;
use Throwable;
use yii\base\Component;
Expand Down Expand Up @@ -53,11 +54,7 @@ public function getAllShippingRules(): Collection
$allShippingRules = [];

foreach ($results as $result) {
$result['orderCondition'] ??= '';
$allShippingRules[] = Craft::createObject([
'class' => ShippingRule::class,
'attributes' => $result,
]);
$allShippingRules[] = $this->_createShippingRuleFromRow($result);
}

$this->_allShippingRules = collect($allShippingRules);
Expand All @@ -80,6 +77,38 @@ public function getAllShippingRulesByShippingMethodId(int $id): Collection
return $this->getAllShippingRules()->where('methodId', $id);
}

/**
* Yields enabled ShippingRule models one at a time for the given method, for use during order matching.
*
* @return Generator<ShippingRule>
*/
public function getShippingRulesForMatchingByMethodId(int $methodId): Generator
{
// Load category associations for all enabled rules of this method in one query.
$ruleIds = (new Query())
->select(['id'])
->from(Table::SHIPPINGRULES)
->where(['methodId' => $methodId, 'enabled' => true])
->column();

if (empty($ruleIds)) {
return;
}

$categoriesByRuleId = Plugin::getInstance()
->getShippingRuleCategories()
->getShippingRuleCategoriesByRuleIds($ruleIds);

foreach ($this->_createShippingRulesQuery()
->where(['shippingrules.methodId' => $methodId, 'shippingrules.enabled' => true])
->orderBy(['shippingrules.priority' => SORT_ASC])
->each(200) as $row) {
$rule = $this->_createShippingRuleFromRow($row);
$rule->setShippingRuleCategories($categoriesByRuleId[$row['id']] ?? []);
yield $rule;
}
}

/**
* Get a shipping rule by its ID.
*/
Expand Down Expand Up @@ -243,6 +272,17 @@ private function _createShippingRulesQuery(): Query
return $query;
}

private function _createShippingRuleFromRow(array $row): ShippingRule
{
$row['orderCondition'] ??= '';
/** @var ShippingRule $rule */
$rule = Craft::createObject([
'class' => ShippingRule::class,
'attributes' => $row,
]);
return $rule;
}

/**
* Eager loads shipping rule categories for a collection of shipping rules.
*
Expand Down
Loading