Skip to content

Commit d984a9f

Browse files
authored
Merge branch '2.4-develop' into patch-12
2 parents 3b704a2 + 11be3df commit d984a9f

File tree

9 files changed

+597
-46
lines changed

9 files changed

+597
-46
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Persistent\Model;
9+
10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot;
12+
use Magento\Store\Api\Data\StoreInterface;
13+
use Magento\Store\Model\StoreManagerInterface;
14+
use Magento\Persistent\Model\ResourceModel\ExpiredPersistentQuotesCollection;
15+
use Magento\Quote\Model\QuoteRepository;
16+
use Psr\Log\LoggerInterface;
17+
use Exception;
18+
19+
/**
20+
* Cleaning expired persistent quotes from the cron
21+
*/
22+
class CleanExpiredPersistentQuotes
23+
{
24+
/**
25+
* @param StoreManagerInterface $storeManager
26+
* @param ExpiredPersistentQuotesCollection $expiredPersistentQuotesCollection
27+
* @param QuoteRepository $quoteRepository
28+
* @param Snapshot $snapshot
29+
* @param LoggerInterface $logger
30+
* @param int $batchSize
31+
*/
32+
public function __construct(
33+
private readonly StoreManagerInterface $storeManager,
34+
private readonly ExpiredPersistentQuotesCollection $expiredPersistentQuotesCollection,
35+
private readonly QuoteRepository $quoteRepository,
36+
private readonly Snapshot $snapshot,
37+
private readonly LoggerInterface $logger,
38+
private readonly int $batchSize
39+
) {
40+
}
41+
42+
/**
43+
* Execute the cron job
44+
*
45+
* @param int $websiteId
46+
* @return void
47+
* @throws LocalizedException
48+
*/
49+
public function execute(int $websiteId): void
50+
{
51+
$stores = $this->storeManager->getWebsite($websiteId)->getStores();
52+
foreach ($stores as $store) {
53+
$this->processStoreQuotes($store);
54+
}
55+
}
56+
57+
/**
58+
* Process store quotes in batches
59+
*
60+
* @param StoreInterface $store
61+
* @return void
62+
*/
63+
private function processStoreQuotes(StoreInterface $store): void
64+
{
65+
$lastProcessedId = $count = 0;
66+
67+
while (true) {
68+
$quotesToProcess = $this->expiredPersistentQuotesCollection
69+
->getExpiredPersistentQuotes($store, $lastProcessedId, $this->batchSize);
70+
71+
if (!$quotesToProcess->count()) {
72+
break;
73+
}
74+
75+
foreach ($quotesToProcess as $quote) {
76+
$count++;
77+
try {
78+
$this->quoteRepository->delete($quote);
79+
$lastProcessedId = (int)$quote->getId();
80+
} catch (Exception $e) {
81+
$this->logger->error(sprintf(
82+
'Unable to delete expired quote (ID: %s): %s',
83+
$quote->getId(),
84+
(string)$e
85+
));
86+
}
87+
if ($count % $this->batchSize === 0) {
88+
$this->snapshot->clear($quote);
89+
}
90+
$quote->clearInstance();
91+
unset($quote);
92+
}
93+
94+
$quotesToProcess->clear();
95+
unset($quotesToProcess);
96+
}
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Persistent\Model\ResourceModel;
9+
10+
use Magento\Framework\DB\Select;
11+
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
12+
use Magento\Persistent\Helper\Data;
13+
use Magento\Framework\App\Config\ScopeConfigInterface;
14+
use Magento\Quote\Model\ResourceModel\Quote\Collection;
15+
use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory;
16+
use Magento\Store\Api\Data\StoreInterface;
17+
use Magento\Store\Model\ScopeInterface;
18+
19+
/**
20+
* Handles the collection of expired persistent quotes.
21+
*/
22+
class ExpiredPersistentQuotesCollection
23+
{
24+
/**
25+
* @param ScopeConfigInterface $scopeConfig
26+
* @param CollectionFactory $quoteCollectionFactory
27+
*/
28+
public function __construct(
29+
private readonly ScopeConfigInterface $scopeConfig,
30+
private readonly CollectionFactory $quoteCollectionFactory
31+
) {
32+
}
33+
34+
/**
35+
* Retrieves the collection of expired persistent quotes.
36+
*
37+
* Filters and returns all quotes that have expired based on the persistent lifetime threshold.
38+
*
39+
* @param StoreInterface $store
40+
* @param int $lastId
41+
* @param int $batchSize
42+
* @return AbstractCollection
43+
*/
44+
public function getExpiredPersistentQuotes(StoreInterface $store, int $lastId, int $batchSize): AbstractCollection
45+
{
46+
$lifetime = $this->scopeConfig->getValue(
47+
Data::XML_PATH_LIFE_TIME,
48+
ScopeInterface::SCOPE_WEBSITE,
49+
$store->getWebsiteId()
50+
);
51+
52+
$lastLoginCondition = gmdate("Y-m-d H:i:s", time() - $lifetime);
53+
54+
/** @var $quotes Collection */
55+
$quotes = $this->quoteCollectionFactory->create();
56+
57+
$additionalQuotes = clone $quotes;
58+
$additionalQuotes->addFieldToFilter('main_table.store_id', (int)$store->getId());
59+
$additionalQuotes->addFieldToFilter('main_table.updated_at', ['lt' => $lastLoginCondition]);
60+
$additionalQuotes->addFieldToFilter('main_table.is_persistent', 1);
61+
$additionalQuotes->addFieldToFilter('main_table.entity_id', ['gt' => $lastId]);
62+
$additionalQuotes->setOrder('entity_id', Collection::SORT_ORDER_ASC);
63+
$additionalQuotes->setPageSize($batchSize);
64+
65+
$select1 = clone $additionalQuotes->getSelect();
66+
$select2 = clone $additionalQuotes->getSelect();
67+
68+
//case 1 - customer logged in and logged out
69+
$select1->reset(Select::COLUMNS)
70+
->columns('main_table.entity_id')
71+
->joinLeft(
72+
['cl1' => $additionalQuotes->getTable('customer_log')],
73+
'cl1.customer_id = main_table.customer_id',
74+
[]
75+
)->where('cl1.last_login_at < cl1.last_logout_at
76+
AND cl1.last_logout_at IS NOT NULL');
77+
78+
//case 2 - customer logged in and not logged out but session expired
79+
//case 3 - customer logged in, logged out, logged in and then session expired
80+
$select2->reset(Select::COLUMNS)
81+
->columns('main_table.entity_id')
82+
->joinLeft(
83+
['cl2' => $additionalQuotes->getTable('customer_log')],
84+
'cl2.customer_id = main_table.customer_id',
85+
[]
86+
)->where('cl2.last_login_at < "' . $lastLoginCondition . '"
87+
AND (cl2.last_logout_at IS NULL OR cl2.last_login_at > cl2.last_logout_at)');
88+
89+
$selectQuoteIds = $additionalQuotes
90+
->getConnection()
91+
->select()
92+
->union(
93+
[
94+
$select1,
95+
$select2
96+
],
97+
Select::SQL_UNION_ALL
98+
);
99+
100+
$quotes->getSelect()->where('main_table.entity_id IN (' . $selectQuoteIds . ')');
101+
102+
return $quotes;
103+
}
104+
}

app/code/Magento/Persistent/Observer/ClearExpiredCronJobObserver.php

+31-16
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,63 @@
11
<?php
22
/**
3-
*
4-
* Copyright © Magento, Inc. All rights reserved.
5-
* See COPYING.txt for license details.
3+
* Copyright 2017 Adobe
4+
* All Rights Reserved.
65
*/
6+
declare(strict_types=1);
7+
78
namespace Magento\Persistent\Observer;
89

9-
use Magento\Framework\Event\ObserverInterface;
10+
use Magento\Cron\Model\Schedule;
11+
use Magento\Persistent\Model\CleanExpiredPersistentQuotes;
12+
use Magento\Persistent\Model\SessionFactory;
13+
use Magento\Store\Model\ResourceModel\Website\CollectionFactory;
1014

1115
class ClearExpiredCronJobObserver
1216
{
1317
/**
14-
* Website collection factory
18+
* A property for website collection factory
19+
*
20+
* @var CollectionFactory
21+
*/
22+
protected CollectionFactory $_websiteCollectionFactory;
23+
24+
/**
25+
* A property for session factory
1526
*
16-
* @var \Magento\Store\Model\ResourceModel\Website\CollectionFactory
27+
* @var SessionFactory
1728
*/
18-
protected $_websiteCollectionFactory;
29+
protected SessionFactory $_sessionFactory;
1930

2031
/**
21-
* Session factory
32+
* A property for clean expired persistent quotes
2233
*
23-
* @var \Magento\Persistent\Model\SessionFactory
34+
* @var CleanExpiredPersistentQuotes
2435
*/
25-
protected $_sessionFactory;
36+
private CleanExpiredPersistentQuotes $cleanExpiredPersistentQuotes;
2637

2738
/**
28-
* @param \Magento\Store\Model\ResourceModel\Website\CollectionFactory $websiteCollectionFactory
29-
* @param \Magento\Persistent\Model\SessionFactory $sessionFactory
39+
* @param CollectionFactory $websiteCollectionFactory
40+
* @param SessionFactory $sessionFactory
41+
* @param CleanExpiredPersistentQuotes $cleanExpiredPersistentQuotes
3042
*/
3143
public function __construct(
32-
\Magento\Store\Model\ResourceModel\Website\CollectionFactory $websiteCollectionFactory,
33-
\Magento\Persistent\Model\SessionFactory $sessionFactory
44+
CollectionFactory $websiteCollectionFactory,
45+
SessionFactory $sessionFactory,
46+
CleanExpiredPersistentQuotes $cleanExpiredPersistentQuotes
3447
) {
3548
$this->_websiteCollectionFactory = $websiteCollectionFactory;
3649
$this->_sessionFactory = $sessionFactory;
50+
$this->cleanExpiredPersistentQuotes = $cleanExpiredPersistentQuotes;
3751
}
3852

3953
/**
4054
* Clear expired persistent sessions
4155
*
42-
* @param \Magento\Cron\Model\Schedule $schedule
56+
* @param Schedule $schedule
4357
* @return $this
4458
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
4559
*/
46-
public function execute(\Magento\Cron\Model\Schedule $schedule)
60+
public function execute(Schedule $schedule)
4761
{
4862
$websiteIds = $this->_websiteCollectionFactory->create()->getAllIds();
4963
if (!is_array($websiteIds)) {
@@ -52,6 +66,7 @@ public function execute(\Magento\Cron\Model\Schedule $schedule)
5266

5367
foreach ($websiteIds as $websiteId) {
5468
$this->_sessionFactory->create()->deleteExpired($websiteId);
69+
$this->cleanExpiredPersistentQuotes->execute((int) $websiteId);
5570
}
5671

5772
return $this;

0 commit comments

Comments
 (0)