Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
43f6a52
Remove the unused additional args for ->walk()
RadoslavGeorgiev Oct 28, 2025
a2149a9
Add a static factory to the walker class
RadoslavGeorgiev Oct 28, 2025
509d9e9
Require integrations to define query args while creating walkers
RadoslavGeorgiev Oct 28, 2025
a60b022
Add arguments to the async generator and update actions accordingly.
RadoslavGeorgiev Oct 28, 2025
f20480d
Store arguments in the status option
RadoslavGeorgiev Oct 28, 2025
7de778e
Instead of just creating unique file names, make them unique and priv…
RadoslavGeorgiev Oct 28, 2025
238b92c
Use AS correctly: Switch to as_enqueue_async_action and add groups.
RadoslavGeorgiev Oct 29, 2025
677270a
ActionScheduler work-around
RadoslavGeorgiev Oct 29, 2025
4894f91
Merge branch 'trunk' into wooptp-41-product-feed-allow-custom-pos-par…
RadoslavGeorgiev Oct 29, 2025
46aac14
Add stronger types to the walker and inheritDoc blocks to integrations
RadoslavGeorgiev Oct 29, 2025
e68acbf
Instead of manipulating AS to run async actions, perform the action d…
RadoslavGeorgiev Oct 30, 2025
b5d3223
Simplify action names for AsyncGenerator
RadoslavGeorgiev Oct 30, 2025
5de6e1b
Forward status when forcing regeneration
RadoslavGeorgiev Oct 30, 2025
2bb6087
Prolongate feed expiry
RadoslavGeorgiev Oct 30, 2025
60bcc27
Reorganize feed initiation, make sure that deleted files ignore the `…
RadoslavGeorgiev Oct 30, 2025
796e301
Add further validation to the async generator.
RadoslavGeorgiev Oct 30, 2025
ffd167d
Add initial tests for JsonFileFeed
RadoslavGeorgiev Oct 30, 2025
97c6f1a
Further JsonFileFeed tests
RadoslavGeorgiev Oct 30, 2025
57785d8
Make MemoryManager methods non-static and mockable.
RadoslavGeorgiev Oct 30, 2025
c427b00
Add a test container and implement it for unit tests
RadoslavGeorgiev Oct 30, 2025
2239456
Add tests for the product walker
RadoslavGeorgiev Oct 30, 2025
9036660
Test for memory management
RadoslavGeorgiev Oct 30, 2025
c8f8645
Address CR nitpicks
RadoslavGeorgiev Oct 30, 2025
7123dd1
Sort normalized args
RadoslavGeorgiev Oct 30, 2025
55e5c54
Fix a typo
RadoslavGeorgiev Oct 31, 2025
b6d1f83
Settings tests
RadoslavGeorgiev Oct 31, 2025
db040d2
Fix a couple of mistakenly commited test left-overs
RadoslavGeorgiev Oct 31, 2025
6c055bf
Addressing feedback and adding proper tests for wp_hash for FileFeed
RadoslavGeorgiev Nov 3, 2025
9473691
Fix typos
RadoslavGeorgiev Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/CLI/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,25 @@ class Command extends WP_CLI_Command {
*/
private IntegrationRegistry $integration_registry;

/**
* Memory manager instance.
*
* @var MemoryManager
*/
private MemoryManager $memory_manager;

/**
* Dependency injector.
*
* @param IntegrationRegistry $integration_registry The integration registry.
* @param MemoryManager $memory_manager The memory manager.
*/
public function init( IntegrationRegistry $integration_registry ) {
public function init(
IntegrationRegistry $integration_registry,
MemoryManager $memory_manager
) {
$this->integration_registry = $integration_registry;
$this->memory_manager = $memory_manager;
}

/**
Expand Down Expand Up @@ -111,7 +123,7 @@ public function generate( $args, $assoc_args ) {

// Initialize the feed and walker, set them up.
$feed = $integration->create_feed();
$walker = new ProductWalker( $integration->get_product_mapper(), $integration->get_feed_validator(), $feed );
$walker = ProductWalker::from_integration( $integration, $feed );
$walker->set_batch_size( $batch_size );
$walker->add_time_limit( $timeout );

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

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

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" );
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" );
}
);

Expand Down
2 changes: 1 addition & 1 deletion src/Core/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class Plugin {
*
* @var Container
*/
private Container $container;
private $container;

/**
* Integration registry.
Expand Down
32 changes: 32 additions & 0 deletions src/Feed/ProductLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* Product Loader class.
*
* @package Automattic\WooCommerce\ProductFeedForOpenAI
*/

declare(strict_types=1);

namespace Automattic\WooCommerce\ProductFeedForOpenAI\Feed;

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Loader for products.
*/
class ProductLoader {
/**
* Retrieves products from WooCommerce.
*
* @see wc_get_products()
*
* @param array $args The arguments to pass to wc_get_products().
* @return array|stdClass Number of pages and an array of product objects if
* paginate is true, or just an array of values.
*/
public function get_products( $args ) {
return wc_get_products( $args );
}
}
142 changes: 104 additions & 38 deletions src/Feed/ProductWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Automattic\WooCommerce\ProductFeedForOpenAI\Feed;

use Automattic\WooCommerce\ProductFeedForOpenAI\Integrations\IntegrationInterface;
use Automattic\WooCommerce\ProductFeedForOpenAI\Utils\MemoryManager;

if ( ! defined( 'ABSPATH' ) ) {
Expand All @@ -19,26 +20,40 @@
* Walker for products.
*/
class ProductWalker {
/**
* The product loader.
*
* @var ProductLoader
*/
private ProductLoader $product_loader;

/**
* The product mapper.
*
* @var ProductMapperInterface
*/
private $mapper;
private ProductMapperInterface $mapper;

/**
* The feed.
*
* @var FeedInterface
*/
private $feed;
private FeedInterface $feed;

/**
* The feed validator.
*
* @var FeedValidatorInterface
*/
private $validator;
private FeedValidatorInterface $validator;

/**
* The memory manager.
*
* @var MemoryManager
*/
private MemoryManager $memory_manager;

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

/**
* The query arguments to apply to the product query.
*
* @var array
*/
private array $query_args;

/**
* Class constructor.
*
Expand All @@ -62,15 +84,75 @@ class ProductWalker {
* @param ProductMapperInterface $mapper The product mapper.
* @param FeedValidatorInterface $validator The feed validator.
* @param FeedInterface $feed The feed.
* @param ProductLoader $product_loader The product loader.
* @param MemoryManager $memory_manager The memory manager.
* @param array $query_args The query arguments.
*/
public function __construct(
private function __construct(
ProductMapperInterface $mapper,
FeedValidatorInterface $validator,
FeedInterface $feed
FeedInterface $feed,
ProductLoader $product_loader,
MemoryManager $memory_manager,
array $query_args
) {
$this->mapper = $mapper;
$this->validator = $validator;
$this->feed = $feed;
$this->mapper = $mapper;
$this->validator = $validator;
$this->feed = $feed;
$this->product_loader = $product_loader;
$this->memory_manager = $memory_manager;
$this->query_args = $query_args;
}

/**
* Creates a new instance of the ProductWalker class based on an integration.
*
* The walker will mostly be set up based on the integration.
* The feed is provided externally, as it might be based on the context (CLI, REST, Action Scheduler, etc.).
*
* @param IntegrationInterface $integration The integration.
* @param FeedInterface $feed The feed.
* @return self The ProductWalker instance.
*/
public static function from_integration(
IntegrationInterface $integration,
FeedInterface $feed
): self {
$query_args = array_merge(
[
'status' => [ 'publish' ],
'return' => 'objects',
],
$integration->get_product_feed_query_args()
);

/**
* Allows the base arguments for querying products for product feeds to be changed.
*
* Variable products are not included by default, as their variations will be included.
*
* @since 0.1.0
*
* @param array $query_args The arguments to pass to wc_get_products().
* @param IntegrationInterface $integration The integration that the query belongs to.
* @return array
*/
$query_args = apply_filters(
'wpfoai_product_feed_args',
$query_args,
$integration
);

$instance = new self(
$integration->get_product_mapper(),
$integration->get_feed_validator(),
$feed,
wpfoai_get_service( ProductLoader::class ),
wpfoai_get_service( MemoryManager::class ),
$query_args
);

return $instance;
}

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

/**
* Allows the base arguments for querying products for product feeds to be changed.
*
* Variable products are not included by default, as their variations will be included.
*
* @since 0.1.0
*
* @param array $args The arguments to pass to wc_get_products().
* @return array
*/
$args = apply_filters(
'wpfoai_product_feed_args',
array_merge(
[
'status' => [ 'publish' ],
'type' => [ 'simple', 'variation' ],
'return' => 'objects',
],
$additional_args
)
);

// Instruct the feed to start.
$this->feed->start();

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

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

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

// We don't want to use more than half of the available memory at the beginning of the script.
if ( $initial_available_memory - MemoryManager::get_available_memory() >= $initial_available_memory / 2 ) {
MemoryManager::flush_caches();
$current_memory = $this->memory_manager->get_available_memory();
if ( $initial_available_memory - $current_memory >= $initial_available_memory / 2 ) {
$this->memory_manager->flush_caches();
}
} while ( $iterated === $this->per_page );
} while (
// If `wc_get_products()` returns less than the batch size, it was the last page.
$iterated === $this->per_page

// For the cases where the above is true, make sure that we do not exceed the total number of pages.
&& $progress->processed_batches < $progress->total_batch_count
);

// Instruct the feed to end.
$this->feed->end();
Expand All @@ -173,7 +239,7 @@ public function walk( ?callable $callback = null, array $additional_args = [] ):
* @return object The result of the query.
*/
private function iterate( array $args = [], int $page = 1, int $limit = 100 ): object {
$result = wc_get_products(
$result = $this->product_loader->get_products(
array_merge(
$args,
[
Expand Down
16 changes: 16 additions & 0 deletions src/Integrations/IntegrationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,33 @@ public function register_hooks(): void;
/**
* Activate the integration.
*
* This method is called when the plugin is activated.
* If there is ever a setting that controls active integrations,
* this method might also be called when the integration is activated.
*
* @return void
*/
public function activate(): void;

/**
* Deactivate the integration.
*
* This method is called when the plugin is deactivated.
* If there is ever a setting that controls active integrations,
* this method might also be called when the integration is deactivated.
*
* @return void
*/
public function deactivate(): void;

/**
* Get the query arguments for the product feed.
*
* @see wc_get_products()
* @return array The query arguments.
*/
public function get_product_feed_query_args(): array;

/**
* Create a feed that is to be populated.
*
Expand Down
Loading