Should the cart purge trigger the various element lifecycle events?
We wrongly assumed that the cart purge logic would also call the various events that a standard delete triggers. See the current implementation, although more performant, does skip that by deleting directly in the database and relying on the cascade:
https://github.com/craftcms/commerce/blob/58c63b79d901b5b598b1caed56df502ceef935ed/src/services/Carts.php#L277
Yeah since we need to be able to purge efficiently (some sites can have a very large number of carts) so we don't do an element service delete.
We could look to put this behind a setting to change the behavior. Thoughts?
I already extended the class to include this functionality. I need to inform other services about the deleted carts so even in case Pixel and Tonic doesn't implement it you can always customize it by yourself.
However: I included a cronjob that runs every hour with a limited amount of carts so even if there are 100.000 carts to delete it takes some time but ultimately they will be deleted sooner or later.
@lukeholder Setting might be sensible, we have implemented our own logic to cover for it which is working nicely (our site has a large number of carts being created daily) we purge daily so the most we are ever having to worry about is one days worth of inactive carts.
@Anubarak We have implemented a similar setup, we just hit up a custom console command which handles everything, interested in how you have implemented?
@samhibberd
- I set the config value
purgeInactiveCartsto false in order to prevent their garbage collector trigger -> Commerce won't call their function automatically - I created a Class that extends their
Cartsclass
class Carts extends \craft\commerce\services\Carts
{
public function purgeIncompleteCarts(): int
{
// just to make sure this will only be executed during our cron
if(!Craft::$app->getRequest()->getIsConsoleRequest()) {
return 0;
}
$configInterval = ConfigHelper::durationInSeconds(Plugin::getInstance()->getSettings()->purgeInactiveCartsDuration);
$edge = new DateTime();
$interval = DateTimeHelper::secondsToInterval($configInterval);
$edge->sub($interval);
// ignore this one, we have 2 statuses that should not be purged
$notPurgeIds = [
MYSPA::getOrderStatus(OrderStatus::$wellZoneControl)->id,
MYSPA::getOrderStatus(OrderStatus::$extension)->id,
];
$carts = Order::find()
->where(['not', ['isCompleted' => true]])
->andWhere([
'or',
// ignore this line -> just to include those 2 statuses
['NOT', ['orderStatusId' => $notPurgeIds]],
['IS', 'orderStatusId', null],
])
->limit(200)
->andWhere('[[elements.dateUpdated]] <= :edge', ['edge' => Db::prepareDateForDb($edge)])
->all();
$elements = Craft::$app->getElements();
foreach ($carts as $cart) {
$elements->deleteElement($cart);
}
return count($carts);
}
}
- I used Yii2 DI to tell Commerce to use my extended class instead of their one config/app.php
return [
'container' => [
'singletons' => [
\craft\commerce\services\Carts::class => \my\namespace\Carts::class
]
]
];
- All calls to
Plugin::getInstance()->getCarts()will now return my class instead of theirs, and my custom Cronjob can just callPlugin::getInstance()->getCarts()->purgeIncompleteCarts()
Note: I did it that way because my class is actually much larger and contains more custom functions / changed behaviours.
Thanks very much for following up @Anubarak, we have implemented something very similar using a custom controller, but it was the way you suggested you extended their class which interested me! We've not used DI and have an enormous custom module! Very interesting.