commerce icon indicating copy to clipboard operation
commerce copied to clipboard

Should the cart purge trigger the various element lifecycle events?

Open samhibberd opened this issue 3 years ago • 5 comments

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

samhibberd avatar Mar 03 '22 13:03 samhibberd

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?

lukeholder avatar Jul 03 '23 09:07 lukeholder

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.

Anubarak avatar Jul 06 '23 14:07 Anubarak

@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 avatar Jul 18 '23 08:07 samhibberd

@samhibberd

  1. I set the config value purgeInactiveCarts to false in order to prevent their garbage collector trigger -> Commerce won't call their function automatically
  2. I created a Class that extends their Carts class
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);
    }
}
  1. 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
        ]
    ]
];
  1. All calls to Plugin::getInstance()->getCarts() will now return my class instead of theirs, and my custom Cronjob can just call Plugin::getInstance()->getCarts()->purgeIncompleteCarts()

Note: I did it that way because my class is actually much larger and contains more custom functions / changed behaviours.

Anubarak avatar Jul 18 '23 09:07 Anubarak

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.

samhibberd avatar Jul 18 '23 10:07 samhibberd