first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
<?php
/**
* Feed Interface.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Feed;
/**
* Feed Interface.
*
* @since 10.5.0
*/
interface FeedInterface {
/**
* Start the feed.
* This can create an empty file, eventually put something in it, or add a database entry.
*
* @return void
*/
public function start(): void;
/**
* Add an entry to the feed.
*
* @param array $entry The entry to add.
* @return void
*/
public function add_entry( array $entry ): void;
/**
* End the feed.
*
* @return void
*/
public function end(): void;
/**
* Get the file path of the feed.
*
* @return string|null The path to the feed file, null if not ready.
*/
public function get_file_path(): ?string;
/**
* Get the URL of the feed file.
*
* @return string|null The URL of the feed file, null if not ready.
*/
public function get_file_url(): ?string;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Feed Validator Interface.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Feed;
/**
* Feed Validator Interface.
*
* @since 10.5.0
*/
interface FeedValidatorInterface {
/**
* Validate a single entry.
*
* @param array $row The entry to validate.
* @param \WC_Product $product The related product. Will be updated with validation status.
* @return string[] Validation issues.
*/
public function validate_entry( array $row, \WC_Product $product ): array;
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Product Loader class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Feed;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Loader for products.
*
* @since 10.5.0
*/
class ProductLoader {
/**
* Retrieves products from WooCommerce.
*
* @since 10.5.0
*
* @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( array $args ) {
return wc_get_products( $args );
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Product Mapper Interface.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Feed;
/**
* Product Mapper Interface.
*
* @since 10.5.0
*/
interface ProductMapperInterface {
/**
* Map a product to a feed row.
*
* @param \WC_Product $product The product to map.
* @return array The feed row.
*/
public function map_product( \WC_Product $product ): array;
}

View File

@@ -0,0 +1,288 @@
<?php
/**
* Product Walker class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Feed;
use Automattic\WooCommerce\Internal\ProductFeed\Integrations\IntegrationInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Utils\MemoryManager;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Walker for products.
*
* @since 10.5.0
*/
class ProductWalker {
/**
* The product loader.
*
* @var ProductLoader
*/
private ProductLoader $product_loader;
/**
* The product mapper.
*
* @var ProductMapperInterface
*/
private ProductMapperInterface $mapper;
/**
* The feed.
*
* @var FeedInterface
*/
private FeedInterface $feed;
/**
* The feed validator.
*
* @var FeedValidatorInterface
*/
private FeedValidatorInterface $validator;
/**
* The memory manager.
*
* @var MemoryManager
*/
private MemoryManager $memory_manager;
/**
* The number of products to iterate through per batch.
*
* @var int
*/
private int $per_page = 100;
/**
* The time limit to extend the execution time limit per batch.
*
* @var int
*/
private int $time_limit = 0;
/**
* The query arguments to apply to the product query.
*
* @var array
*/
private array $query_args;
/**
* Class constructor.
*
* This class will not be available through DI. Instead, it needs to be instantiated directly.
*
* @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.
*/
private function __construct(
ProductMapperInterface $mapper,
FeedValidatorInterface $validator,
FeedInterface $feed,
ProductLoader $product_loader,
MemoryManager $memory_manager,
array $query_args
) {
$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.).
*
* @since 10.5.0
*
* @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(
array(
'status' => array( '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 10.5.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(
'woocommerce_product_feed_args',
$query_args,
$integration
);
$instance = new self(
$integration->get_product_mapper(),
$integration->get_feed_validator(),
$feed,
wc_get_container()->get( ProductLoader::class ),
wc_get_container()->get( MemoryManager::class ),
$query_args
);
return $instance;
}
/**
* Set the number of products to iterate through per batch.
*
* @since 10.5.0
*
* @param int $batch_size The number of products to iterate through per batch.
* @return self
*/
public function set_batch_size( int $batch_size ): self {
if ( $batch_size < 1 ) {
$batch_size = 1;
}
$this->per_page = $batch_size;
return $this;
}
/**
* Set the time limit to extend the execution time limit per batch.
*
* @since 10.5.0
*
* @param int $time_limit Time limit in seconds.
* @return self
*/
public function add_time_limit( int $time_limit ): self {
if ( $time_limit < 0 ) {
$time_limit = 0;
}
$this->time_limit = $time_limit;
return $this;
}
/**
* Walks through all products.
*
* @since 10.5.0
*
* @param callable $callback The callback to call after each batch of products is processed.
* @return int The total number of products processed.
*/
public function walk( ?callable $callback = null ): int {
$progress = null;
// Instruct the feed to start.
$this->feed->start();
// Check how much memory is available at first.
$initial_available_memory = $this->memory_manager->get_available_memory();
do {
$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.
if ( is_null( $progress ) ) {
$progress = WalkerProgress::from_wc_get_products_result( $result );
}
$progress->processed_items += $iterated;
++$progress->processed_batches;
if ( is_callable( $callback ) && $iterated > 0 ) {
$callback( $progress );
}
if ( $this->time_limit > 0 ) {
set_time_limit( $this->time_limit );
}
// We don't want to use more than half of the available memory at the beginning of the script.
$current_memory = $this->memory_manager->get_available_memory();
if ( $initial_available_memory - $current_memory >= $initial_available_memory / 2 ) {
$this->memory_manager->flush_caches();
}
} 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();
return $progress->processed_items;
}
/**
* Iterates through a batch of products.
*
* @param array $args The arguments to pass to wc_get_products().
* @param int $page The page number to iterate through.
* @param int $limit The maximum number of products to iterate through.
* @return \stdClass The result of the query with properties: products, total, max_num_pages.
*/
private function iterate( array $args = array(), int $page = 1, int $limit = 100 ): \stdClass {
/**
* Result is always stdClass when paginate=true.
*
* @var \stdClass $result
*/
$result = $this->product_loader->get_products(
array_merge(
$args,
array(
'page' => $page,
'limit' => $limit,
'paginate' => true,
)
)
);
foreach ( $result->products as $product ) {
$mapped_data = $this->mapper->map_product( $product );
if ( ! empty( $this->validator->validate_entry( $mapped_data, $product ) ) ) {
continue;
}
$this->feed->add_entry( $mapped_data );
}
return $result;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Walker Progress class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Feed;
/**
* Simple class that tracks/indicates the progress of a walker.
*
* @since 10.5.0
*/
final class WalkerProgress {
/**
* Total number of items to process.
*
* @var int
*/
public int $total_count;
/**
* Total number of batches to process.
*
* @var int
*/
public int $total_batch_count;
/**
* Number of items processed so far.
*
* @var int
*/
public int $processed_items = 0;
/**
* Number of batches processed so far.
*
* @var int
*/
public int $processed_batches = 0;
/**
* Creates a WalkerProgress instance from a WooCommerce products query result.
*
* @since 10.5.0
*
* @param \stdClass $result The result object from wc_get_products() with total and max_num_pages properties.
* @return self
*/
public static function from_wc_get_products_result( \stdClass $result ): self {
$progress = new self();
$progress->total_count = $result->total;
$progress->total_batch_count = $result->max_num_pages;
$progress->processed_items = 0;
$progress->processed_batches = 0;
return $progress;
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Interface that should be implemented by all provider integrations.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\FeedInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\FeedValidatorInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\ProductMapperInterface;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* IntegrationInterface
*
* @since 10.5.0
*/
interface IntegrationInterface {
/**
* Get the ID of the provider.
*
* @return string The ID of the provider.
*/
public function get_id(): string;
/**
* Register hooks for the integration.
*
* @return void
*/
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.
*
* @return FeedInterface The feed.
*/
public function create_feed(): FeedInterface;
/**
* Get the product mapper for the provider.
*
* @return ProductMapperInterface The product mapper.
*/
public function get_product_mapper(): ProductMapperInterface;
/**
* Get the feed validator for the provider.
*
* @return FeedValidatorInterface The feed validator.
*/
public function get_feed_validator(): FeedValidatorInterface;
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Integration Registry class.
*
* Stores all provider integrations that are available.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* IntegrationRegistry
*
* @since 10.5.0
*/
class IntegrationRegistry {
/**
* List of all available Integrations.
*
* @var array<string,IntegrationInterface>
*/
private array $integrations = array();
/**
* Register an Integration.
*
* @since 10.5.0
*
* @param IntegrationInterface $integration The integration to register.
*/
public function register_integration( IntegrationInterface $integration ): void {
$this->integrations[ $integration->get_id() ] = $integration;
}
/**
* Get an Integration by ID.
*
* @since 10.5.0
*
* @param string $id The ID of the Integration.
* @return IntegrationInterface|null The Integration, or null if it is not registered.
*/
public function get_integration( string $id ): ?IntegrationInterface {
return $this->integrations[ $id ] ?? null;
}
/**
* Get all registered integrations.
*
* @since 10.5.0
*
* @return array<string,IntegrationInterface>
*/
public function get_integrations(): array {
return $this->integrations;
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* POS Catalog API Controller.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;
use Automattic\WooCommerce\Container;
use WP_REST_Request;
use WP_REST_Response;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* POS Catalog API Controller.
*
* @since 10.5.0
*/
class ApiController {
const ROUTE_NAMESPACE = 'wc/pos/v1/catalog';
/**
* Container instance.
*
* @var Container
*/
private $container;
/**
* Dependency injector.
*
* @param Container $container The container instance. Everything else will be dynamic.
* @internal
*/
final public function init( Container $container ): void {
$this->container = $container;
}
/**
* Register the routes for the API controller.
*/
public function register_routes(): void {
register_rest_route(
self::ROUTE_NAMESPACE,
'/create',
array(
'methods' => 'POST',
'callback' => array( $this, 'generate_feed' ),
'permission_callback' => array( $this, 'is_authorized' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => 'Force regeneration of the feed. NOOP if generation is in progress.',
),
'_product_fields' => array(
'type' => 'string',
'description' => 'Comma-separated list of fields to include for non-variable products.',
'required' => false,
),
'_variation_fields' => array(
'type' => 'string',
'description' => 'Comma-separated list of fields to include for variations.',
'required' => false,
),
),
)
);
}
/**
* Checks if the current user has the necessary permissions to access the API.
*
* @return bool True if the user has the necessary permissions, false otherwise.
*/
public function is_authorized() {
return is_user_logged_in() && (
current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' )
);
}
/**
* Starts generating a feed.
*
* @param WP_REST_Request<array<string, mixed>> $request The request object.
* @return WP_REST_Response The response object.
*/
public function generate_feed( WP_REST_Request $request ): WP_REST_Response { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$generator = $this->container->get( AsyncGenerator::class );
try {
$params = array();
if ( null !== $request['_product_fields'] ) {
$params['_product_fields'] = $request['_product_fields'];
}
if ( null !== $request['_variation_fields'] ) {
$params['_variation_fields'] = $request['_variation_fields'];
}
$response = $request->get_param( 'force' )
? $generator->force_regeneration( $params )
: $generator->get_status( $params );
// Use the right datetime format.
if ( isset( $response['scheduled_at'] ) ) {
$response['scheduled_at'] = wc_rest_prepare_date_response( $response['scheduled_at'] );
}
if ( isset( $response['completed_at'] ) ) {
$response['completed_at'] = wc_rest_prepare_date_response( $response['completed_at'] );
}
// Remove sensitive data from the response.
if ( isset( $response['action_id'] ) ) {
unset( $response['action_id'] );
}
if ( isset( $response['path'] ) ) {
unset( $response['path'] );
}
} catch ( \Exception $e ) {
wc_get_logger()->error(
'Feed generation failed',
array( 'error' => $e->getMessage() )
);
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'An error occurred while generating the feed.', 'woocommerce' ),
),
500
);
}
return new WP_REST_Response( $response );
}
}

View File

@@ -0,0 +1,385 @@
<?php
/**
* Async Generator class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;
use ActionScheduler_AsyncRequest_QueueRunner;
use ActionScheduler_Store;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\ProductWalker;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\WalkerProgress;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Async Generator for feeds.
*
* @since 10.5.0
*/
class AsyncGenerator {
/**
* The Action Scheduler action hook for the feed generation.
*
* @var string
*/
const FEED_GENERATION_ACTION = 'woocommerce_product_feed_generation';
/**
* The Action Scheduler action hook for the feed deletion.
*
* @var string
*/
const FEED_DELETION_ACTION = 'woocommerce_product_feed_deletion';
/**
* Feed expiry time, once completed.
* If the feed is not downloaded within this timeframe, a new one will need to be generated.
*
* @var int
*/
const FEED_EXPIRY = 20 * HOUR_IN_SECONDS;
/**
* Possible states of generation.
*/
const STATE_SCHEDULED = 'scheduled';
const STATE_IN_PROGRESS = 'in_progress';
const STATE_COMPLETED = 'completed';
const STATE_FAILED = 'failed';
/**
* Integration instance.
*
* @var POSIntegration
*/
private $integration;
/**
* Dependency injector.
*
* @param POSIntegration $integration The integration instance.
* @internal
*/
final public function init( POSIntegration $integration ): void {
$this->integration = $integration;
}
/**
* Register hooks for the async generator.
*
* @since 10.5.0
*
* @return void
*/
public function register_hooks(): void {
add_action( self::FEED_GENERATION_ACTION, array( $this, 'feed_generation_action' ) );
add_action( self::FEED_DELETION_ACTION, array( $this, 'feed_deletion_action' ), 10, 2 );
}
/**
* Returns the current feed generation status.
* Initiates one if not already running.
*
* @since 10.5.0
*
* @param array|null $args The arguments to pass to the action.
* @return array The feed generation status.
*/
public function get_status( ?array $args = null ): array {
// Determine the option key based on the integration ID and arguments.
$option_key = $this->get_option_key( $args );
$status = get_option( $option_key );
// For existing jobs, make sure that everything in the status makes sense.
if ( is_array( $status ) && ! $this->validate_status( $status ) ) {
$status = false;
}
// If the status is an array, it means that there is nothing to schedule in this method.
if ( is_array( $status ) ) {
return $status;
}
// Clear all previous actions to avoid race conditions.
as_unschedule_all_actions( self::FEED_GENERATION_ACTION, array( $option_key ), 'woo-product-feed' ); // @phpstan-ignore function.notFound
$status = array(
'scheduled_at' => time(),
'completed_at' => null,
'state' => self::STATE_SCHEDULED,
'progress' => 0,
'processed' => 0,
'total' => -1,
'args' => $args ?? array(),
);
update_option(
$option_key,
$status
);
// Start an immediate async action to generate the feed.
// @phpstan-ignore-next-line function.notFound -- Action Scheduler.
as_enqueue_async_action(
self::FEED_GENERATION_ACTION,
array( $option_key ),
'woo-product-feed',
true,
1
);
// Manually force an async request to be dispatched to process the action immediately.
if ( class_exists( ActionScheduler_AsyncRequest_QueueRunner::class ) && class_exists( ActionScheduler_Store::class ) ) {
$store = ActionScheduler_Store::instance();
$async_request = new ActionScheduler_AsyncRequest_QueueRunner( $store );
$async_request->dispatch();
}
return $status;
}
/**
* Action scheduler callback for the feed generation.
*
* @since 10.5.0
*
* @param string $option_key The option key for the feed generation status.
* @return void
*/
public function feed_generation_action( string $option_key ) {
$status = get_option( $option_key );
if ( ! is_array( $status ) || ! isset( $status['state'] ) || self::STATE_SCHEDULED !== $status['state'] ) {
wc_get_logger()->error( 'Invalid feed generation status', array( 'status' => $status ) );
return;
}
$status['state'] = self::STATE_IN_PROGRESS;
update_option( $option_key, $status );
try {
$feed = $this->integration->create_feed();
$walker = ProductWalker::from_integration( $this->integration, $feed );
// Add dynamic args to the mapper.
$args = $status['args'] ?? array();
if (
isset( $args['_product_fields'] )
&& is_string( $args['_product_fields'] ) &&
! empty( $args['_product_fields'] )
) {
$this->integration->get_product_mapper()->set_fields( $args['_product_fields'] );
}
if (
isset( $args['_variation_fields'] )
&& is_string( $args['_variation_fields'] ) &&
! empty( $args['_variation_fields'] )
) {
$this->integration->get_product_mapper()->set_variation_fields( $args['_variation_fields'] );
}
$walker->walk(
function ( WalkerProgress $progress ) use ( &$status, $option_key ) {
$status = $this->update_feed_progress( $status, $progress );
update_option( $option_key, $status );
}
);
// Store the final details.
$status['state'] = self::STATE_COMPLETED;
$status['url'] = $feed->get_file_url();
$status['path'] = $feed->get_file_path();
$status['completed_at'] = time();
update_option( $option_key, $status );
// Schedule another action to delete the file after the expiry time.
// @phpstan-ignore-next-line function.notFound -- Action Scheduler.
as_schedule_single_action(
time() + self::FEED_EXPIRY,
self::FEED_DELETION_ACTION,
array(
$option_key,
$feed->get_file_path(),
),
'woo-product-feed',
true
);
} catch ( \Throwable $e ) {
wc_get_logger()->error(
'Feed generation failed',
array(
'error' => $e->getMessage(),
'option_key' => $option_key,
)
);
$status['state'] = self::STATE_FAILED;
$status['error'] = $e->getMessage();
$status['failed_at'] = time();
update_option( $option_key, $status );
}
}
/**
* Forces a regeneration of the feed.
*
* @since 10.5.0
*
* @param array|null $args The arguments to pass to the action.
* @return array The feed generation status.
* @throws \Exception When there is a reason why the regeneration cannot be forced.
*/
public function force_regeneration( ?array $args = null ): array {
$option_key = $this->get_option_key( $args );
$status = get_option( $option_key );
// If there is no option, there is nothing to force. If the option is invalid, we can restart.
if ( ! is_array( $status ) || ! $this->validate_status( $status ) ) {
return $this->get_status( $args );
}
switch ( $status['state'] ?? '' ) {
case self::STATE_SCHEDULED:
// If generation is scheduled, we can just let it be and return the current status.
// It should start shortly.
return $status;
case self::STATE_IN_PROGRESS:
throw new \Exception( 'Feed generation is already in progress and cannot be stopped.' );
case self::STATE_COMPLETED:
// Delete the existing file, clear the option and let generation start again.
wp_delete_file( (string) $status['path'] );
delete_option( $option_key );
return $this->get_status( $args );
case self::STATE_FAILED:
// Clear the failed status and restart generation.
delete_option( $option_key );
return $this->get_status( $args );
default:
throw new \Exception( 'Unknown feed generation state.' );
}
}
/**
* Action scheduler callback for the feed deletion after expiry.
*
* @since 10.5.0
*
* @param string $option_key The option key for the feed generation status.
* @param string $path The path to the feed file.
* @return void
*/
public function feed_deletion_action( string $option_key, string $path ) {
delete_option( $option_key );
wp_delete_file( $path );
}
/**
* Returns the option key for the feed generation status.
*
* @param array|null $args The arguments to pass to the action.
* @return string The option key.
*/
private function get_option_key( ?array $args = null ): string {
$normalized_args = $args ?? array();
if ( ! empty( $normalized_args ) ) {
ksort( $normalized_args );
}
return 'feed_status_' . md5(
// WPCS dislikes serialize for security reasons, but it will be hashed immediately.
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
serialize(
array(
'integration' => $this->integration->get_id(),
'args' => $normalized_args,
)
)
);
}
/**
* Updates the feed progress while the feed is being generated.
*
* @param array $status The last previously known status.
* @param WalkerProgress $progress The progress of the walker.
* @return array Updated status of the feed generation.
*/
private function update_feed_progress( array $status, WalkerProgress $progress ): array {
$status['progress'] = $progress->total_count > 0
? round( ( $progress->processed_items / $progress->total_count ) * 100, 2 )
: 0;
$status['processed'] = $progress->processed_items;
$status['total'] = $progress->total_count;
return $status;
}
/**
* Validates the status of the feed generation.
*
* Makes sure that the file exists for completed jobs,
* that scheduled jobs are not stuck, etc.
*
* @param array $status The status of the feed generation.
* @return bool True if the status is valid, false otherwise.
*/
private function validate_status( array $status ): bool {
/**
* For completed jobs, make sure the file still exists. Regenerate otherwise.
*
* The file should typically get deleted at the same time as the status is cleared.
* However, something else could cause the file to disappear in the meantime (ex. manual delete).
*
* Also, if the cleanup job failed, the feed might appear as complete, but be expired.
*/
if ( self::STATE_COMPLETED === $status['state'] ) {
if ( ! file_exists( $status['path'] ) ) {
return false;
}
if ( ! isset( $status['completed_at'] ) ) {
return false;
}
if ( $status['completed_at'] + self::FEED_EXPIRY < time() ) {
return false;
}
}
/**
* If the job has been scheduled more than 10 minutes ago but has not
* transitioned to IN_PROGRESS yet, ActionScheduler is typically stuck.
*/
/**
* Allows the timeout for a feed to remain in `scheduled` state to be changed.
*
* @param int $stuck_time The stuck time in seconds.
* @return int The stuck time in seconds.
* @since 10.5.0
*/
$scheduled_timeout = apply_filters( 'woocommerce_product_feed_scheduled_timeout', 10 * MINUTE_IN_SECONDS );
if (
self::STATE_SCHEDULED === $status['state']
&& (
! isset( $status['scheduled_at'] )
|| time() - $status['scheduled_at'] > $scheduled_timeout
)
) {
return false;
}
// All good.
return true;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Feed Validator class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\FeedValidatorInterface;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Simple field validator for the POS catalog.
*
* @since 10.5.0
*/
final class FeedValidator implements FeedValidatorInterface {
/**
* Validate single feed row using schema.
*
* @param array $entry Product data row to validate.
* @param \WC_Product $product The related product. Will be updated with validation status.
* @return array Array of validation issues.
*/
public function validate_entry( array $entry, \WC_Product $product ): array { //phpcs:ignore VariableAnalysis
return array();
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* POS Catalog Integration class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;
use Automattic\WooCommerce\Container;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\FeedInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\FeedValidatorInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Integrations\IntegrationInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Storage\JsonFileFeed;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* POS Catalog Integration
*
* @since 10.5.0
*/
class POSIntegration implements IntegrationInterface {
/**
* Container instance.
*
* @var Container
*/
private Container $container;
/**
* Dependency injector.
*
* @param Container $container Dependency container.
* @internal
*/
final public function init( Container $container ): void {
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public function get_id(): string {
return 'pos';
}
/**
* {@inheritdoc}
*/
public function get_product_feed_query_args(): array {
return array(
'type' => array( 'simple', 'variable', 'variation' ),
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => array(
array(
'taxonomy' => 'pos_product_visibility',
'field' => 'slug',
'terms' => 'pos-hidden',
'operator' => 'NOT IN',
),
),
);
}
/**
* {@inheritdoc}
*/
public function register_hooks(): void {
add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
$this->container->get( AsyncGenerator::class )->register_hooks();
$this->container->get( POSProductVisibilitySync::class )->register_hooks();
}
/**
* Initialize the REST API.
*
* @return void
*/
public function rest_api_init(): void {
// Only load the controller when necessary.
$this->container->get( ApiController::class )->register_routes();
}
/**
* {@inheritdoc}
*/
public function activate(): void {
// At the moment, there are no activation steps for the POS catalog.
}
/**
* {@inheritdoc}
*/
public function deactivate(): void {
// At the moment, there are no deactivation steps for the POS catalog.
}
/**
* {@inheritdoc}
*/
public function create_feed(): FeedInterface {
return new JsonFileFeed( 'pos-catalog-feed' );
}
/**
* {@inheritdoc}
*/
public function get_product_mapper(): ProductMapper {
return $this->container->get( ProductMapper::class );
}
/**
* {@inheritdoc}
*/
public function get_feed_validator(): FeedValidatorInterface {
return $this->container->get( FeedValidator::class );
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* POS Product Visibility Sync class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles syncing pos_product_visibility taxonomy to products and variations.
*
* When a variable product is marked as hidden from POS, all its variations
* should also be marked as hidden. This class ensures that:
* - Products and their variations have the correct pos-hidden term
* - New variations inherit the pos-hidden term from their parent
*
* @since 10.5.0
*/
class POSProductVisibilitySync {
/**
* Register hooks for syncing POS visibility.
*
* @since 10.5.0
*
* @return void
*/
public function register_hooks(): void {
add_action( 'woocommerce_new_product_variation', array( $this, 'inherit_parent_pos_visibility' ), 10, 2 );
}
/**
* Set POS visibility for a product and its variations.
*
* This method sets or removes the pos-hidden term on the product,
* and if it's a variable product, syncs the visibility to all variations.
*
* @since 10.5.0
*
* @param int $product_id The product ID.
* @param bool $visible_in_pos Whether the product should be visible in POS.
* @return void
*/
public function set_product_pos_visibility( int $product_id, bool $visible_in_pos ): void {
$is_currently_visible = ! has_term( 'pos-hidden', 'pos_product_visibility', $product_id );
if ( $is_currently_visible === $visible_in_pos ) {
return; // No change detected.
}
if ( $visible_in_pos ) {
wp_remove_object_terms( $product_id, 'pos-hidden', 'pos_product_visibility' );
} else {
wp_set_object_terms( $product_id, 'pos-hidden', 'pos_product_visibility' );
}
$product = wc_get_product( $product_id );
if ( $product && $product->is_type( 'variable' ) ) {
$this->sync_pos_visibility_to_variations( $product, $visible_in_pos );
}
}
/**
* Sync POS visibility to all variations of a variable product.
*
* @since 10.5.0
*
* @param \WC_Product $product The variable product.
* @param bool $visible_in_pos Whether the product should be visible in POS.
* @return void
*/
private function sync_pos_visibility_to_variations( \WC_Product $product, bool $visible_in_pos ): void {
$variation_ids = $product->get_children();
foreach ( $variation_ids as $variation_id ) {
if ( $visible_in_pos ) {
wp_remove_object_terms( $variation_id, 'pos-hidden', 'pos_product_visibility' );
} else {
wp_set_object_terms( $variation_id, 'pos-hidden', 'pos_product_visibility' );
}
// Save variation to update date_modified.
$variation = wc_get_product( $variation_id );
if ( $variation ) {
$variation->save();
}
}
}
/**
* Inherit POS visibility from parent when a new variation is created.
*
* When a new variation is created, check if the parent product has the
* pos-hidden term and apply it to the variation if so.
*
* @since 10.5.0
*
* @param int $variation_id The variation ID.
* @param \WC_Product_Variation|null $variation The variation object.
* @return void
*/
public function inherit_parent_pos_visibility( $variation_id, $variation ): void {
if ( ! $variation instanceof \WC_Product_Variation ) {
return;
}
$parent_id = $variation->get_parent_id();
if ( has_term( 'pos-hidden', 'pos_product_visibility', $parent_id ) ) {
wp_set_object_terms( $variation_id, 'pos-hidden', 'pos_product_visibility' );
}
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* ProductMapper class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\ProductMapperInterface;
use WC_Product;
use WC_REST_Products_Controller;
use WC_REST_Product_Variations_Controller;
use WP_REST_Request;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Product Mapper for the POS catalog.
*
* Uses WooCommerce REST API controllers to map product data.
*
* @since 10.5.0
*/
class ProductMapper implements ProductMapperInterface {
/**
* Fields to include in the product mapping.
*
* @var string|null Fields to include in the product mapping.
*/
private ?string $fields = null;
/**
* Fields to include in the variation mapping.
*
* @var string|null Fields to include in the variation mapping.
*/
private ?string $variation_fields = null;
/**
* REST controller instance for products.
*
* @var WC_REST_Products_Controller|null
*/
private ?WC_REST_Products_Controller $products_controller = null;
/**
* REST controller instance for variations.
*
* @var WC_REST_Product_Variations_Controller|null
*/
private ?WC_REST_Product_Variations_Controller $variations_controller = null;
/**
* Cached REST request instance for products.
*
* @var WP_REST_Request<array<string, mixed>>|null
*/
private ?WP_REST_Request $products_request = null;
/**
* Cached REST request instance for variations.
*
* @var WP_REST_Request<array<string, mixed>>|null
*/
private ?WP_REST_Request $variations_request = null;
/**
* Initialize the mapper.
*
* @internal
* @return void
*/
final public function init(): void {
$this->products_controller = new WC_REST_Products_Controller();
$this->variations_controller = new WC_REST_Product_Variations_Controller();
}
/**
* Set fields to include in the product mapping.
*
* @since 10.5.0
*
* @param string|null $fields Fields to include in the product mapping.
* @return void
*/
public function set_fields( ?string $fields = null ): void {
$this->fields = $fields;
$this->products_request = null; // Invalidate the cached request.
}
/**
* Set fields to include in the variation mapping.
*
* @since 10.5.0
*
* @param string|null $fields Fields to include in the variation mapping.
* @return void
*/
public function set_variation_fields( ?string $fields = null ): void {
$this->variation_fields = $fields;
$this->variations_request = null; // Invalidate the cached request.
}
/**
* Map WooCommerce product to catalog row
*
* @since 10.5.0
*
* @param WC_Product $product Product to map.
* @return array Mapped product data array.
* @throws \RuntimeException If the controller is not initialized.
*/
public function map_product( WC_Product $product ): array {
$is_variation = $product->is_type( 'variation' );
$controller = $is_variation
? $this->variations_controller
: $this->products_controller;
// This should never be the case, as the class should be loaded through DI.
if ( null === $controller ) {
throw new \RuntimeException( 'ProductMapper::init() must be called before map_product().' );
}
$request = $is_variation ? $this->get_variations_request() : $this->get_products_request();
$response = $controller->prepare_object_for_response( $product, $request );
// Apply _fields filtering (normally done by REST server dispatch).
$fields = $is_variation ? $this->variation_fields : $this->fields;
if ( null !== $fields ) {
$response = rest_filter_response_fields( $response, rest_get_server(), $request );
}
$row = array(
'type' => $product->get_type(),
'data' => $response->get_data(),
);
/**
* Filter mapped catalog product data.
*
* @since 10.5.0
* @param array $row Mapped product data.
* @param WC_Product $product Product object.
*/
return apply_filters( 'woocommerce_pos_catalog_map_product', $row, $product );
}
/**
* Get the REST request instance for products.
*
* @return WP_REST_Request<array<string, mixed>>
*/
protected function get_products_request(): WP_REST_Request {
if ( null === $this->products_request ) {
/**
* Type hint for PHPStan generics.
*
* @var WP_REST_Request<array<string, mixed>> $request
* */
$request = new WP_REST_Request( 'GET' );
$this->products_request = $request;
$this->products_request->set_param( 'context', 'view' );
if ( null !== $this->fields ) {
$this->products_request->set_param( '_fields', $this->fields );
}
}
return $this->products_request;
}
/**
* Get the REST request instance for variations.
*
* @return WP_REST_Request<array<string, mixed>>
*/
protected function get_variations_request(): WP_REST_Request {
if ( null === $this->variations_request ) {
/**
* Type hint for PHPStan generics.
*
* @var WP_REST_Request<array<string, mixed>> $request
*/
$request = new WP_REST_Request( 'GET' );
$this->variations_request = $request;
$this->variations_request->set_param( 'context', 'view' );
if ( null !== $this->variation_fields ) {
$this->variations_request->set_param( '_fields', $this->variation_fields );
}
}
return $this->variations_request;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Plugin class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed;
use Automattic\WooCommerce\Internal\ProductFeed\Integrations\IntegrationInterface;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Integrations\IntegrationRegistry;
use Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog\POSIntegration;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main Product Feed class.
*
* @since 10.5.0
*/
final class ProductFeed implements RegisterHooksInterface {
/**
* Integration registry.
*
* @var IntegrationRegistry
*/
private IntegrationRegistry $integration_registry;
/**
* Dependency injector.
*
* @param IntegrationRegistry $integration_registry The integration registry.
* @param POSIntegration $pos_integration The POS integration.
* @internal
*/
public function init( // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingFinal
IntegrationRegistry $integration_registry,
POSIntegration $pos_integration
): void {
$this->integration_registry = $integration_registry;
$this->integration_registry->register_integration( $pos_integration );
}
/**
* Allows extensions to register integrations.
*
* @since 10.5.0
* @param IntegrationInterface $integration The integration to register.
* @return void
*/
public function register_integration( IntegrationInterface $integration ): void {
$this->integration_registry->register_integration( $integration );
}
/**
* Initialize plugin components
*
* @since 10.5.0
*/
public function register(): void {
// Let all integrations register their hooks.
foreach ( $this->integration_registry->get_integrations() as $integration ) {
$integration->register_hooks();
}
}
/**
* Plugin activation
*
* @since 10.5.0
*/
public function activate(): void {
foreach ( $this->integration_registry->get_integrations() as $integration ) {
$integration->activate();
}
}
/**
* Plugin deactivation
*
* @since 10.5.0
*/
public function deactivate(): void {
foreach ( $this->integration_registry->get_integrations() as $integration ) {
$integration->deactivate();
}
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* JSON File Feed class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Storage;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\FeedInterface;
use Exception;
// This file works directly with local files. That's fine.
// phpcs:disable WordPress.WP.AlternativeFunctions
/**
* File-backed JSON feed storage.
*
* This class writes JSON directly to a file, entry by entry, without keeping everything in memory.
*
* @since 10.5.0
*/
class JsonFileFeed implements FeedInterface {
public const UPLOAD_DIR = 'product-feeds';
/**
* Indicates if there are previous entries in the feed.
*
* @var bool
*/
private $has_entries = false;
/**
* The base name of the feed file.
*
* @var string
*/
private $base_name;
/**
* The name of the feed file, no directory.
*
* @var string
*/
private $file_name;
/**
* The path to the feed file.
*
* @var string
*/
private $file_path;
/**
* The file handle.
*
* @var resource|false|null
*/
private $file_handle = null;
/**
* Indicates if the feed file has been completed.
*
* @var bool
*/
private $file_completed = false;
/**
* The URL of the feed file.
*
* @var string|null
*/
private $file_url = null;
/**
* Indicates if the feed file is in a temp directory.
*
* @var bool
*/
private $is_temp_filepath = false;
/**
* Constructor.
*
* @param string $base_name The base name of the feed file.
*/
public function __construct( string $base_name ) {
$this->base_name = $base_name;
}
/**
* Start the feed.
*
* @return void
* @throws Exception If the feed directory cannot be created.
*/
public function start(): void {
/**
* Allows the current time to be overridden before a feed is stored.
*
* @param int $time The current time.
* @param FeedInterface $feed The feed instance.
* @return int The current time.
* @since 10.5.0
*/
$current_time = apply_filters( 'woocommerce_product_feed_time', time(), $this );
$hash_data = $this->base_name . gmdate( 'r', $current_time );
$this->file_name = sprintf(
'%s-%s-%s.json',
$this->base_name,
gmdate( 'Y-m-d', $current_time ),
wp_hash( $hash_data )
);
// Start by trying to use a temp directory to generate the feed.
$this->file_path = get_temp_dir() . DIRECTORY_SEPARATOR . $this->file_name;
$this->file_handle = fopen( $this->file_path, 'w' );
if ( false === $this->file_handle ) {
// Fall back to immediately using the upload directory for generation.
$upload_dir = $this->get_upload_dir();
$this->file_path = $upload_dir['path'] . $this->file_name;
$this->file_handle = fopen( $this->file_path, 'w' );
} else {
$this->is_temp_filepath = true;
}
if ( false === $this->file_handle ) {
throw new Exception(
esc_html(
sprintf(
/* translators: %s: directory path */
__( 'Unable to open feed file for writing: %s', 'woocommerce' ),
$this->file_path
)
)
);
}
// Open the array.
fwrite( $this->file_handle, '[' );
}
/**
* Add an entry to the feed.
*
* @param array $entry The entry to add.
* @return void
*/
public function add_entry( array $entry ): void {
if ( ! is_resource( $this->file_handle ) ) {
return;
}
if ( ! $this->has_entries ) {
$this->has_entries = true;
} else {
fwrite( $this->file_handle, ',' );
}
$json = wp_json_encode( $entry );
if ( false !== $json ) {
fwrite( $this->file_handle, $json );
}
}
/**
* End the feed.
*
* @return void
*/
public function end(): void {
if ( ! is_resource( $this->file_handle ) ) {
return;
}
// Close the array and the file.
fwrite( $this->file_handle, ']' );
fclose( $this->file_handle );
// Indicate that we have a complete file.
$this->file_completed = true;
}
/**
* {@inheritDoc}
*/
public function get_file_path(): ?string {
if ( ! $this->file_completed ) {
return null;
}
return $this->file_path;
}
/**
* {@inheritDoc}
*
* @throws Exception If the feed file cannot be moved to the upload directory.
*/
public function get_file_url(): ?string {
if ( ! $this->file_completed ) {
return null;
}
$upload_dir = $this->get_upload_dir();
// Move the file to the upload directory if it is in temp.
if ( $this->is_temp_filepath ) {
$tmp_path = $this->file_path;
$this->file_path = $upload_dir['path'] . $this->file_name;
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
if ( ! @copy( $tmp_path, $this->file_path ) ) {
$error = error_get_last();
$error_message = is_array( $error ) ? $error['message'] : 'Unknown error';
throw new Exception(
esc_html(
sprintf(
/* translators: %1$s: file path, %2$s: error message */
__( 'Unable to move feed file %1$s to upload directory: %2$s', 'woocommerce' ),
$this->file_path,
$error_message
)
)
);
}
unlink( $tmp_path );
$this->is_temp_filepath = false;
}
// Generate the URL.
$this->file_url = $upload_dir['url'] . $this->file_name;
return $this->file_url;
}
/**
* Get the upload directory for the feed.
*
* @return array {
* The upload directory for the feed. Both fields end with the right trailing slash.
*
* @type string $path The path to the upload directory.
* @type string $url The URL to the upload directory.
* }
* @throws Exception If the upload directory cannot be created.
*/
private function get_upload_dir(): array {
// Only generate everything once.
static $prepared;
if ( isset( $prepared ) ) {
return $prepared;
}
$upload_dir = wp_upload_dir( null, true );
$directory_path = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . self::UPLOAD_DIR . DIRECTORY_SEPARATOR;
// Try to create the directory if it does not exist.
if ( ! is_dir( $directory_path ) ) {
FilesystemUtil::mkdir_p_not_indexable( $directory_path );
}
// `mkdir_p_not_indexable()` returns `void`, we have to check again.
if ( ! is_dir( $directory_path ) ) {
throw new Exception(
esc_html(
sprintf(
/* translators: %s: directory path */
__( 'Unable to create feed directory: %s', 'woocommerce' ),
$directory_path
)
)
);
}
$directory_url = $upload_dir['baseurl'] . '/' . self::UPLOAD_DIR . '/';
// Follow the format, returned by `wp_upload_dir()`.
$prepared = array(
'path' => $directory_path,
'url' => $directory_url,
);
return $prepared;
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* Memory Manager class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Helper class for managing memory.
*
* @since 10.5.0
*/
class MemoryManager {
/**
* Get available memory as a percentage of the total memory limit.
*
* @since 10.5.0
*
* @return int Available memory as a percentage of the total memory limit.
*/
public function get_available_memory(): int {
$memory_limit = wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) );
if ( 0 >= $memory_limit ) {
// Some systems have "unlimited" memory.
// We should treat that as if there is none left.
return 0;
}
return (int) round( 100 - ( memory_get_usage( true ) / $memory_limit ) * 100 );
}
/**
* Flush all caches.
*
* @since 10.5.0
*/
public function flush_caches(): void {
global $wpdb, $wp_object_cache;
$wpdb->queries = array();
wp_cache_flush();
if ( ! is_object( $wp_object_cache ) ) {
return;
}
// These properties exist on various object cache implementations.
$wp_object_cache->group_ops = array(); // @phpstan-ignore property.notFound
$wp_object_cache->stats = array(); // @phpstan-ignore property.notFound
$wp_object_cache->memcache_debug = array(); // @phpstan-ignore property.notFound
$wp_object_cache->cache = array(); // @phpstan-ignore property.notFound
// This method is specific to certain memcached implementations.
if ( method_exists( $wp_object_cache, '__remoteset' ) ) {
$wp_object_cache->__remoteset(); // important.
}
$this->collect_garbage();
}
/**
* Collect garbage.
*/
private function collect_garbage(): void {
static $gc_threshold = 5000;
static $gc_too_low_in_a_row = 0;
static $gc_too_high_in_a_row = 0;
$gc_threshold_step = 2_500;
$gc_status = gc_status();
if ( $gc_threshold > $gc_status['threshold'] ) {
// If PHP managed to collect memory in the meantime and established threshold lower than ours, just use theirs.
$gc_threshold = $gc_status['threshold'];
}
if ( $gc_status['roots'] > $gc_threshold ) {
$collected = gc_collect_cycles();
if ( $collected < 100 ) {
if ( $gc_too_low_in_a_row > 0 ) {
$gc_too_low_in_a_row = 0;
// Raise GC threshold if we collected too little twice in a row.
$gc_threshold += $gc_threshold_step;
$gc_threshold = min( $gc_threshold, 1_000_000_000, $gc_status['threshold'] );
} else {
++$gc_too_low_in_a_row;
}
$gc_too_high_in_a_row = 0;
} else {
if ( $gc_too_high_in_a_row > 0 ) {
$gc_too_high_in_a_row = 0;
// Lower GC threshold if we collected more than enough twice in a row.
$gc_threshold -= $gc_threshold_step;
$gc_threshold = max( $gc_threshold, 5_000 );
} else {
++$gc_too_high_in_a_row;
}
$gc_too_low_in_a_row = 0;
}
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* String Helper class.
*
* @package Automattic\WooCommerce\Internal\ProductFeed
*/
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\ProductFeed\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* String utility helper functions
*
* @since 10.5.0
*/
class StringHelper {
/**
* Convert value to boolean string ('true' or 'false')
*
* @since 10.5.0
*
* @param mixed $value Value to convert.
* @return string 'true' or 'false'.
*/
public static function bool_string( $value ): string {
if ( is_bool( $value ) ) {
return $value ? 'true' : 'false';
}
if ( is_scalar( $value ) || null === $value ) {
$value = strtolower( (string) $value );
} else {
$value = '';
}
return ( 'true' === $value || '1' === $value || 'yes' === $value ) ? 'true' : 'false';
}
/**
* Truncate text to specified length
*
* @since 10.5.0
*
* @param string $text Text to truncate.
* @param int $max_length Maximum length.
* @return string Truncated text.
*/
public static function truncate( string $text, int $max_length ): string {
if ( mb_strlen( $text ) > $max_length ) {
return mb_substr( $text, 0, $max_length );
}
return $text;
}
}