Skip to content

Commit 8b6c22b

Browse files
Allow integrations to have custom parameters for queries (#26)
* Remove the unused additional args for ->walk() * Add a static factory to the walker class * Require integrations to define query args while creating walkers * Add arguments to the async generator and update actions accordingly. * Store arguments in the status option We will decide what to do with them later. * Instead of just creating unique file names, make them unique and private. Use the current time in the file name for uniqueness, but also `wp_hash()` (salt) to make it impossible to guess the last part of the filename. * Use AS correctly: Switch to as_enqueue_async_action and add groups. * ActionScheduler work-around * Add stronger types to the walker and inheritDoc blocks to integrations * Instead of manipulating AS to run async actions, perform the action directly. * Simplify action names for AsyncGenerator * Forward status when forcing regeneration * Prolongate feed expiry * Reorganize feed initiation, make sure that deleted files ignore the `COMPLETED` status. * Add further validation to the async generator. * Add initial tests for JsonFileFeed * Further JsonFileFeed tests * Make MemoryManager methods non-static and mockable. * Add a test container and implement it for unit tests * Add tests for the product walker - Memory management is not tested yet. - Add a ProductLoader that can be mocked - Fix a bug in the walker where a round number of procucts (ex. 100 products and batch size 10) would require a useless additional iteration. * Test for memory management * Address CR nitpicks * Sort normalized args * Fix a typo * Settings tests * Fix a couple of mistakenly commited test left-overs * Addressing feedback and adding proper tests for wp_hash for FileFeed * Fix typos
1 parent 4857d4c commit 8b6c22b

20 files changed

+1158
-176
lines changed

src/CLI/Command.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,25 @@ class Command extends WP_CLI_Command {
3232
*/
3333
private IntegrationRegistry $integration_registry;
3434

35+
/**
36+
* Memory manager instance.
37+
*
38+
* @var MemoryManager
39+
*/
40+
private MemoryManager $memory_manager;
41+
3542
/**
3643
* Dependency injector.
3744
*
3845
* @param IntegrationRegistry $integration_registry The integration registry.
46+
* @param MemoryManager $memory_manager The memory manager.
3947
*/
40-
public function init( IntegrationRegistry $integration_registry ) {
48+
public function init(
49+
IntegrationRegistry $integration_registry,
50+
MemoryManager $memory_manager
51+
) {
4152
$this->integration_registry = $integration_registry;
53+
$this->memory_manager = $memory_manager;
4254
}
4355

4456
/**
@@ -111,7 +123,7 @@ public function generate( $args, $assoc_args ) {
111123

112124
// Initialize the feed and walker, set them up.
113125
$feed = $integration->create_feed();
114-
$walker = new ProductWalker( $integration->get_product_mapper(), $integration->get_feed_validator(), $feed );
126+
$walker = ProductWalker::from_integration( $integration, $feed );
115127
$walker->set_batch_size( $batch_size );
116128
$walker->add_time_limit( $timeout );
117129

@@ -136,7 +148,7 @@ function ( WalkerProgress $progress ) use ( $silent, &$iteration_time, &$total_i
136148

137149
$per_item = round( ( $duration / $items_count ) * 1000, 2 );
138150

139-
WP_CLI::log( "Batch $progress->processed_batches/$progress->total_batch_count: Processed $progress->processed_items/$progress->total_count products. Available memory: " . MemoryManager::get_available_memory() . "%. Time per item: $per_item ms" );
151+
WP_CLI::log( "Batch $progress->processed_batches/$progress->total_batch_count: Processed $progress->processed_items/$progress->total_count products. Available memory: " . $this->memory_manager->get_available_memory() . "%. Time per item: $per_item ms" );
140152
}
141153
);
142154

src/Core/Plugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class Plugin {
2828
*
2929
* @var Container
3030
*/
31-
private Container $container;
31+
private $container;
3232

3333
/**
3434
* Integration registry.

src/Feed/ProductLoader.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
/**
3+
* Product Loader class.
4+
*
5+
* @package Automattic\WooCommerce\ProductFeedForOpenAI
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Automattic\WooCommerce\ProductFeedForOpenAI\Feed;
11+
12+
if ( ! defined( 'ABSPATH' ) ) {
13+
exit;
14+
}
15+
16+
/**
17+
* Loader for products.
18+
*/
19+
class ProductLoader {
20+
/**
21+
* Retrieves products from WooCommerce.
22+
*
23+
* @see wc_get_products()
24+
*
25+
* @param array $args The arguments to pass to wc_get_products().
26+
* @return array|stdClass Number of pages and an array of product objects if
27+
* paginate is true, or just an array of values.
28+
*/
29+
public function get_products( $args ) {
30+
return wc_get_products( $args );
31+
}
32+
}

src/Feed/ProductWalker.php

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace Automattic\WooCommerce\ProductFeedForOpenAI\Feed;
1111

12+
use Automattic\WooCommerce\ProductFeedForOpenAI\Integrations\IntegrationInterface;
1213
use Automattic\WooCommerce\ProductFeedForOpenAI\Utils\MemoryManager;
1314

1415
if ( ! defined( 'ABSPATH' ) ) {
@@ -19,26 +20,40 @@
1920
* Walker for products.
2021
*/
2122
class ProductWalker {
23+
/**
24+
* The product loader.
25+
*
26+
* @var ProductLoader
27+
*/
28+
private ProductLoader $product_loader;
29+
2230
/**
2331
* The product mapper.
2432
*
2533
* @var ProductMapperInterface
2634
*/
27-
private $mapper;
35+
private ProductMapperInterface $mapper;
2836

2937
/**
3038
* The feed.
3139
*
3240
* @var FeedInterface
3341
*/
34-
private $feed;
42+
private FeedInterface $feed;
3543

3644
/**
3745
* The feed validator.
3846
*
3947
* @var FeedValidatorInterface
4048
*/
41-
private $validator;
49+
private FeedValidatorInterface $validator;
50+
51+
/**
52+
* The memory manager.
53+
*
54+
* @var MemoryManager
55+
*/
56+
private MemoryManager $memory_manager;
4257

4358
/**
4459
* The number of products to iterate through per batch.
@@ -54,6 +69,13 @@ class ProductWalker {
5469
*/
5570
private int $time_limit = 0;
5671

72+
/**
73+
* The query arguments to apply to the product query.
74+
*
75+
* @var array
76+
*/
77+
private array $query_args;
78+
5779
/**
5880
* Class constructor.
5981
*
@@ -62,15 +84,75 @@ class ProductWalker {
6284
* @param ProductMapperInterface $mapper The product mapper.
6385
* @param FeedValidatorInterface $validator The feed validator.
6486
* @param FeedInterface $feed The feed.
87+
* @param ProductLoader $product_loader The product loader.
88+
* @param MemoryManager $memory_manager The memory manager.
89+
* @param array $query_args The query arguments.
6590
*/
66-
public function __construct(
91+
private function __construct(
6792
ProductMapperInterface $mapper,
6893
FeedValidatorInterface $validator,
69-
FeedInterface $feed
94+
FeedInterface $feed,
95+
ProductLoader $product_loader,
96+
MemoryManager $memory_manager,
97+
array $query_args
7098
) {
71-
$this->mapper = $mapper;
72-
$this->validator = $validator;
73-
$this->feed = $feed;
99+
$this->mapper = $mapper;
100+
$this->validator = $validator;
101+
$this->feed = $feed;
102+
$this->product_loader = $product_loader;
103+
$this->memory_manager = $memory_manager;
104+
$this->query_args = $query_args;
105+
}
106+
107+
/**
108+
* Creates a new instance of the ProductWalker class based on an integration.
109+
*
110+
* The walker will mostly be set up based on the integration.
111+
* The feed is provided externally, as it might be based on the context (CLI, REST, Action Scheduler, etc.).
112+
*
113+
* @param IntegrationInterface $integration The integration.
114+
* @param FeedInterface $feed The feed.
115+
* @return self The ProductWalker instance.
116+
*/
117+
public static function from_integration(
118+
IntegrationInterface $integration,
119+
FeedInterface $feed
120+
): self {
121+
$query_args = array_merge(
122+
[
123+
'status' => [ 'publish' ],
124+
'return' => 'objects',
125+
],
126+
$integration->get_product_feed_query_args()
127+
);
128+
129+
/**
130+
* Allows the base arguments for querying products for product feeds to be changed.
131+
*
132+
* Variable products are not included by default, as their variations will be included.
133+
*
134+
* @since 0.1.0
135+
*
136+
* @param array $query_args The arguments to pass to wc_get_products().
137+
* @param IntegrationInterface $integration The integration that the query belongs to.
138+
* @return array
139+
*/
140+
$query_args = apply_filters(
141+
'wpfoai_product_feed_args',
142+
$query_args,
143+
$integration
144+
);
145+
146+
$instance = new self(
147+
$integration->get_product_mapper(),
148+
$integration->get_feed_validator(),
149+
$feed,
150+
wpfoai_get_service( ProductLoader::class ),
151+
wpfoai_get_service( MemoryManager::class ),
152+
$query_args
153+
);
154+
155+
return $instance;
74156
}
75157

76158
/**
@@ -99,42 +181,19 @@ public function add_time_limit( int $time_limit ): self {
99181
* Walks through all products.
100182
*
101183
* @param callable $callback The callback to call after each batch of products is processed.
102-
* @param array $additional_args Optional. Additional arguments to merge into the base query args.
103184
* @return int The total number of products processed.
104185
*/
105-
public function walk( ?callable $callback = null, array $additional_args = [] ): int {
186+
public function walk( ?callable $callback = null ): int {
106187
$progress = null;
107188

108-
/**
109-
* Allows the base arguments for querying products for product feeds to be changed.
110-
*
111-
* Variable products are not included by default, as their variations will be included.
112-
*
113-
* @since 0.1.0
114-
*
115-
* @param array $args The arguments to pass to wc_get_products().
116-
* @return array
117-
*/
118-
$args = apply_filters(
119-
'wpfoai_product_feed_args',
120-
array_merge(
121-
[
122-
'status' => [ 'publish' ],
123-
'type' => [ 'simple', 'variation' ],
124-
'return' => 'objects',
125-
],
126-
$additional_args
127-
)
128-
);
129-
130189
// Instruct the feed to start.
131190
$this->feed->start();
132191

133192
// Check how much memory is available at first.
134-
$initial_available_memory = MemoryManager::get_available_memory();
193+
$initial_available_memory = $this->memory_manager->get_available_memory();
135194

136195
do {
137-
$result = $this->iterate( $args, $progress ? $progress->processed_batches + 1 : 1, $this->per_page );
196+
$result = $this->iterate( $this->query_args, $progress ? $progress->processed_batches + 1 : 1, $this->per_page );
138197
$iterated = count( $result->products );
139198

140199
// Only done when the progress is not set. Will be modified otherwise.
@@ -153,10 +212,17 @@ public function walk( ?callable $callback = null, array $additional_args = [] ):
153212
}
154213

155214
// We don't want to use more than half of the available memory at the beginning of the script.
156-
if ( $initial_available_memory - MemoryManager::get_available_memory() >= $initial_available_memory / 2 ) {
157-
MemoryManager::flush_caches();
215+
$current_memory = $this->memory_manager->get_available_memory();
216+
if ( $initial_available_memory - $current_memory >= $initial_available_memory / 2 ) {
217+
$this->memory_manager->flush_caches();
158218
}
159-
} while ( $iterated === $this->per_page );
219+
} while (
220+
// If `wc_get_products()` returns less than the batch size, it was the last page.
221+
$iterated === $this->per_page
222+
223+
// For the cases where the above is true, make sure that we do not exceed the total number of pages.
224+
&& $progress->processed_batches < $progress->total_batch_count
225+
);
160226

161227
// Instruct the feed to end.
162228
$this->feed->end();
@@ -173,7 +239,7 @@ public function walk( ?callable $callback = null, array $additional_args = [] ):
173239
* @return object The result of the query.
174240
*/
175241
private function iterate( array $args = [], int $page = 1, int $limit = 100 ): object {
176-
$result = wc_get_products(
242+
$result = $this->product_loader->get_products(
177243
array_merge(
178244
$args,
179245
[

src/Integrations/IntegrationInterface.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,33 @@ public function register_hooks(): void;
3838
/**
3939
* Activate the integration.
4040
*
41+
* This method is called when the plugin is activated.
42+
* If there is ever a setting that controls active integrations,
43+
* this method might also be called when the integration is activated.
44+
*
4145
* @return void
4246
*/
4347
public function activate(): void;
4448

4549
/**
4650
* Deactivate the integration.
4751
*
52+
* This method is called when the plugin is deactivated.
53+
* If there is ever a setting that controls active integrations,
54+
* this method might also be called when the integration is deactivated.
55+
*
4856
* @return void
4957
*/
5058
public function deactivate(): void;
5159

60+
/**
61+
* Get the query arguments for the product feed.
62+
*
63+
* @see wc_get_products()
64+
* @return array The query arguments.
65+
*/
66+
public function get_product_feed_query_args(): array;
67+
5268
/**
5369
* Create a feed that is to be populated.
5470
*

0 commit comments

Comments
 (0)