Skip to content

Commit c2f8ae2

Browse files
committed
Merge branch '5.x' into 6.x
2 parents a1b96c9 + 5bde7f0 commit c2f8ae2

File tree

11 files changed

+218
-95
lines changed

11 files changed

+218
-95
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Added support for Craft’s batched queue jobs, so that long-running feeds can be processed in batches. ([#1598](https://github.com/craftcms/feed-me/pull/1598))
56
- Improved support for importing into Time fields. ([#1595](https://github.com/craftcms/feed-me/pull/1595))
67
- Fixed a PHP error that could occur with some plugins that were installed, but Feed Me doesn't support. ([#1596](https://github.com/craftcms/feed-me/pull/1596))
78
- Fixed a bug where that could occur that would prevent importing Commerce Variants. ([#1605](https://github.com/craftcms/feed-me/pull/1605))

src/base/DataType.php

+36-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Cake\Utility\Hash;
66
use Craft;
7+
use craft\base\Batchable;
78
use craft\base\Component;
89
use craft\helpers\UrlHelper;
910

@@ -12,8 +13,16 @@
1213
* @property-read mixed $name
1314
* @property-read mixed $class
1415
*/
15-
abstract class DataType extends Component
16+
abstract class DataType extends Component implements Batchable
1617
{
18+
// Properties
19+
// =========================================================================
20+
21+
/**
22+
* @var array
23+
*/
24+
protected array $feedData = [];
25+
1726
// Public
1827
// =========================================================================
1928

@@ -59,4 +68,30 @@ public function setupPaginationUrl($array, $feed): void
5968
// Replace the mapping value with the actual URL
6069
$feed->paginationUrl = $url;
6170
}
71+
72+
/**
73+
* @inheritdoc
74+
*/
75+
public function getSlice(int $offset, int $limit): iterable
76+
{
77+
$feedData = $this->feedData;
78+
79+
if ($offset) {
80+
$feedData = array_slice($feedData, $offset);
81+
}
82+
83+
if ($limit) {
84+
$feedData = array_slice($feedData, 0, $limit);
85+
}
86+
87+
return $feedData;
88+
}
89+
90+
/**
91+
* @inheritdoc
92+
*/
93+
public function count(): int
94+
{
95+
return count($this->feedData);
96+
}
6297
}

src/console/controllers/FeedsController.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use craft\feedme\Plugin;
66
use craft\feedme\queue\jobs\FeedImport;
77
use craft\helpers\Console;
8+
use craft\helpers\Queue;
89
use yii\console\Controller;
910
use yii\console\ExitCode;
1011

@@ -108,7 +109,7 @@ protected function queueFeed($feed, $limit = null, $offset = null, bool $continu
108109
$this->stdout($feed->name, Console::FG_CYAN);
109110
$this->stdout(' ... ');
110111

111-
$this->module->queue->push(new FeedImport([
112+
Queue::push(new FeedImport([
112113
'feed' => $feed,
113114
'limit' => $limit,
114115
'offset' => $offset,

src/controllers/FeedsController.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use craft\feedme\Plugin;
1010
use craft\feedme\queue\jobs\FeedImport;
1111
use craft\helpers\Json;
12+
use craft\helpers\Queue;
1213
use craft\helpers\StringHelper;
1314
use craft\web\Controller;
1415
use Exception;
@@ -344,7 +345,7 @@ private function _runImportTask($feed): ?bool
344345
Craft::$app->getSession()->setNotice(Craft::t('feed-me', 'Feed processing started.'));
345346

346347
// Create the import task
347-
$this->module->queue->push(new FeedImport([
348+
Queue::push(new FeedImport([
348349
'feed' => $feed,
349350
'limit' => $limit,
350351
'offset' => $offset,
@@ -363,7 +364,7 @@ private function _runImportTask($feed): ?bool
363364

364365
// Create the import task only if provided the correct passkey
365366
if ($proceed) {
366-
$this->module->queue->push(new FeedImport([
367+
Queue::push(new FeedImport([
367368
'feed' => $feed,
368369
'limit' => $limit,
369370
'offset' => $offset,

src/datatypes/Csv.php

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public function getFeed($url, $settings, bool $usePrimaryElement = true): array
112112
$array = Plugin::$plugin->data->findPrimaryElement($primaryElement, $array);
113113
}
114114

115+
$this->feedData = $array;
115116
return ['success' => true, 'data' => $array];
116117
}
117118

src/datatypes/DataBatcher.php

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
4+
namespace craft\feedme\datatypes;
5+
6+
use craft\base\Batchable;
7+
8+
class DataBatcher implements Batchable
9+
{
10+
public function __construct(
11+
private array $data,
12+
) {
13+
}
14+
15+
/**
16+
* @inheritdoc
17+
*/
18+
public function count(): int
19+
{
20+
return count($this->data);
21+
}
22+
23+
/**
24+
* @inheritdoc
25+
*/
26+
public function getSlice(int $offset, int $limit): iterable
27+
{
28+
$slice = $this->data;
29+
30+
if ($offset) {
31+
$slice = array_slice($slice, $offset);
32+
}
33+
34+
if ($limit) {
35+
$slice = array_slice($slice, 0, $limit);
36+
}
37+
38+
return $slice;
39+
}
40+
}

src/datatypes/GoogleSheet.php

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public function getFeed($url, $settings, bool $usePrimaryElement = true): array
8787
$array = Plugin::$plugin->data->findPrimaryElement($primaryElement, $array);
8888
}
8989

90+
$this->feedData = $array;
9091
return ['success' => true, 'data' => $array];
9192
}
9293
}

src/datatypes/Json.php

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public function getFeed($url, $settings, bool $usePrimaryElement = true): array
8585
$array = Plugin::$plugin->data->findPrimaryElement($primaryElement, $array);
8686
}
8787

88+
$this->feedData = $array;
8889
return ['success' => true, 'data' => $array];
8990
}
9091
}

src/datatypes/Xml.php

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public function getFeed($url, $settings, bool $usePrimaryElement = true): array
8383
$array = Plugin::$plugin->data->findPrimaryElement($primaryElement, $array);
8484
}
8585

86+
$this->feedData = $array;
8687
return ['success' => true, 'data' => $array];
8788
}
8889
}

src/queue/jobs/FeedImport.php

+99-42
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@
22

33
namespace craft\feedme\queue\jobs;
44

5+
use Cake\Utility\Hash;
56
use Craft;
7+
use craft\base\Batchable;
8+
use craft\feedme\datatypes\DataBatcher;
9+
use craft\feedme\events\FeedProcessEvent;
610
use craft\feedme\models\FeedModel;
711
use craft\feedme\Plugin;
8-
use craft\queue\BaseJob;
12+
use craft\feedme\services\Process;
13+
use craft\helpers\Queue;
14+
use craft\queue\BaseBatchedJob;
915
use Throwable;
1016
use yii\queue\RetryableJobInterface;
1117

1218
/**
1319
*
1420
* @property-read mixed $ttr
1521
*/
16-
class FeedImport extends BaseJob implements RetryableJobInterface
22+
class FeedImport extends BaseBatchedJob implements RetryableJobInterface
1723
{
1824
// Properties
1925
// =========================================================================
@@ -44,6 +50,22 @@ class FeedImport extends BaseJob implements RetryableJobInterface
4450
*/
4551
public bool $continueOnError = true;
4652

53+
/**
54+
* @var mixed The Unix timestamp with microseconds of when the feed import started being processed
55+
* @since 5.11.0
56+
*/
57+
public mixed $startTime = null;
58+
59+
/**
60+
* @var array The Feed's settings as prepared by beforeProcessFeed()
61+
*/
62+
private array $_feedSettings = [];
63+
64+
/**
65+
* @var int The index of currently processed item in current batch
66+
*/
67+
private int $_index = 0;
68+
4769
// Public Methods
4870
// =========================================================================
4971

@@ -65,73 +87,108 @@ public function canRetry($attempt, $error): bool
6587
}
6688

6789
/**
68-
* @inheritDoc
90+
* @inheritdoc
6991
*/
70-
public function execute($queue): void
92+
protected function loadData(): Batchable
7193
{
72-
try {
73-
$feedData = $this->feed->getFeedData();
94+
$feedData = $this->feed->getFeedData();
7495

75-
if ($this->offset) {
76-
$feedData = array_slice($feedData, $this->offset);
77-
}
96+
if ($this->offset) {
97+
$feedData = array_slice($feedData, $this->offset);
98+
}
7899

79-
if ($this->limit) {
80-
$feedData = array_slice($feedData, 0, $this->limit);
81-
}
100+
if ($this->limit) {
101+
$feedData = array_slice($feedData, 0, $this->limit);
102+
}
103+
104+
$data = $feedData;
82105

83-
// Do we even have any data to process?
84-
if (!$feedData) {
85-
Plugin::info('No feed items to process.');
86-
return;
106+
// Our main data-parsing function. Handles the actual data values, defaults and field options
107+
foreach ($feedData as $key => $nodeData) {
108+
if (!is_array($nodeData)) {
109+
$nodeData = [$nodeData];
87110
}
88111

89-
$feedSettings = Plugin::$plugin->process->beforeProcessFeed($this->feed, $feedData);
112+
$data[$key] = Hash::flatten($nodeData, '/');
113+
}
114+
115+
$data = array_values($data);
90116

91-
$feedData = $feedSettings['feedData'];
117+
// Fire an 'onBeforeProcessFeed' event
118+
$event = new FeedProcessEvent([
119+
'feed' => $this->feed,
120+
'feedData' => $data,
121+
]);
92122

93-
$totalSteps = count($feedData);
123+
Plugin::$plugin->process->trigger(Process::EVENT_BEFORE_PROCESS_FEED, $event);
94124

95-
$index = 0;
125+
if (!$event->isValid) {
126+
return new DataBatcher([]);
127+
}
96128

97-
foreach ($feedData as $data) {
98-
try {
99-
Plugin::$plugin->process->processFeed($index, $feedSettings, $this->processedElementIds);
100-
} catch (Throwable $e) {
101-
if (!$this->continueOnError) {
102-
throw $e;
103-
}
129+
// Allow event to modify the feed data
130+
$data = $event->feedData;
104131

105-
// We want to catch any issues in each iteration of the loop (and log them), but this allows the
106-
// rest of the feed to continue processing.
107-
Plugin::error('`{e} - {f}: {l}`.', ['e' => $e->getMessage(), 'f' => basename($e->getFile()), 'l' => $e->getLine()]);
108-
Craft::$app->getErrorHandler()->logException($e);
109-
}
132+
return new DataBatcher($data);
133+
}
110134

111-
$this->setProgress($queue, $index++ / $totalSteps);
135+
/**
136+
* @inheritdoc
137+
*/
138+
protected function processItem(mixed $item): void
139+
{
140+
try {
141+
Plugin::$plugin->process->processFeed($this->_index, $this->_feedSettings, $this->processedElementIds, $item, $this->batchIndex);
142+
} catch (Throwable $e) {
143+
if (!$this->continueOnError) {
144+
throw $e;
112145
}
113146

114-
// Check if we need to paginate the feed to run again
147+
// We want to catch any issues in each iteration of the loop (and log them), but this allows the
148+
// rest of the feed to continue processing.
149+
Plugin::error('`{e} - {f}: {l}`.', ['e' => $e->getMessage(), 'f' => basename($e->getFile()), 'l' => $e->getLine()]);
150+
Craft::$app->getErrorHandler()->logException($e);
151+
}
152+
153+
$this->_index++;
154+
}
155+
/**
156+
* @inheritDoc
157+
*/
158+
public function execute($queue): void
159+
{
160+
$processService = Plugin::$plugin->getProcess();
161+
if ($this->itemOffset == 0) {
162+
$processService->beforeProcessFeed($this->feed, (array)$this->data());
163+
}
164+
165+
if (!$this->startTime) {
166+
$this->startTime = $processService->time_start;
167+
}
168+
169+
if (empty($this->_feedSettings)) {
170+
$this->_feedSettings = $processService->getFeedSettings($this->feed, (array)$this->data());
171+
}
172+
173+
parent::execute($queue);
174+
175+
// Check if we need to paginate the feed to run again
176+
if ($this->itemOffset == $this->totalItems()) {
115177
if ($this->feed->getNextPagination()) {
116-
Plugin::getInstance()->queue->push(new self([
178+
Queue::push(new self([
117179
'feed' => $this->feed,
118180
'limit' => $this->limit,
119181
'offset' => $this->offset,
120182
'processedElementIds' => $this->processedElementIds,
183+
'startTime' => $this->startTime,
121184
]));
122185
} else {
123186
// Only perform the afterProcessFeed function after any/all pagination is done
124-
Plugin::$plugin->process->afterProcessFeed($feedSettings, $this->feed, $this->processedElementIds);
187+
$processService->afterProcessFeed($this->_feedSettings, $this->feed, $this->processedElementIds, $this->startTime);
125188
}
126-
} catch (Throwable $e) {
127-
// Even though we catch errors on each step of the loop, make sure to catch errors that can be anywhere
128-
// else in this function, just to be super-safe and not cause the queue job to die.
129-
Plugin::error('`{e} - {f}: {l}`.', ['e' => $e->getMessage(), 'f' => basename($e->getFile()), 'l' => $e->getLine()]);
130-
Craft::$app->getErrorHandler()->logException($e);
131189
}
132190
}
133191

134-
135192
// Protected Methods
136193
// =========================================================================
137194

0 commit comments

Comments
 (0)