<?php
declare( strict_types=1 );
namespace WooCommerce\Facebook\Products\Sync;
defined( 'ABSPATH' ) || exit;
use WooCommerce\Facebook\Framework\Api\Exception as ApiException;
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;
use WooCommerce\Facebook\Framework\Utilities\BackgroundJobHandler;
use WooCommerce\Facebook\Products;
use WooCommerce\Facebook\Products\Sync;
/**
* The background sync handler.
*/
class Background extends BackgroundJobHandler {
/** @var string async request prefix */
protected $prefix = 'wc_facebook';
/** @var string async request action */
protected $action = 'background_product_sync';
/** @var string data key */
protected $data_key = 'requests';
/**
* Processes a job.
*
* @since 2.0.0
*
* @param \stdClass|object $job
* @param int|null $items_per_batch number of items to process in a single request (defaults to null for unlimited)
* @throws \Exception When job data is incorrect.
* @return \stdClass $job
*/
public function process_job( $job, $items_per_batch = null ) {
$profiling_logger = facebook_for_woocommerce()->get_profiling_logger();
$profiling_logger->start( 'background_product_sync__process_job' );
if ( ! $this->start_time ) {
$this->start_time = time();
}
// Indicate that the job has started processing
if ( 'processing' !== $job->status ) {
$job->status = 'processing';
$job->started_processing_at = current_time( 'mysql' );
$job = $this->update_job( $job );
}
$data_key = $this->data_key;
if ( ! isset( $job->{$data_key} ) ) {
/* translators: Placeholders: %s - user-friendly error message */
throw new \Exception( sprintf( __( 'Job data key "%s" not set', 'facebook-for-woocommerce' ), $data_key ) );
}
if ( ! is_array( $job->{$data_key} ) ) {
/* translators: Placeholders: %s - user-friendly error message */
throw new \Exception( sprintf( __( 'Job data key "%s" is not an array', 'facebook-for-woocommerce' ), $data_key ) );
}
$data = $job->{$data_key};
$job->total = count( $data );
// progress indicates how many items have been processed, it
// does NOT indicate the processed item key in any way
if ( ! isset( $job->progress ) ) {
$job->progress = 0;
}
// skip already processed items
if ( $job->progress && ! empty( $data ) ) {
$data = array_slice( $data, $job->progress, null, true );
}
// loop over unprocessed items and process them
if ( ! empty( $data ) ) {
$this->process_items( $job, $data, (int) $items_per_batch );
}
// complete current job
if ( $job->progress >= count( $job->{$data_key} ) ) {
$job = $this->complete_job( $job );
}
$profiling_logger->stop( 'background_product_sync__process_job' );
return $job;
}
/**
* Processes multiple items.
*
* @since 2.0.0
*
* @param \stdClass|object $job
* @param array $data
* @param int|null $items_per_batch number of items to process in a single request (defaults to null for unlimited)
*/
public function process_items( $job, $data, $items_per_batch = null ) {
$processed = 0;
$requests = [];
foreach ( $data as $item_id => $method ) {
try {
$request = $this->process_item( [ $item_id, $method ], $job );
if ( $request ) {
$requests[] = $request;
}
} catch ( PluginException $e ) {
facebook_for_woocommerce()->log( "Background sync error: {$e->getMessage()}" );
}
++$processed;
++$job->progress;
// update job progress
$job = $this->update_job( $job );
// job limits reached
if ( ( $items_per_batch && $processed >= $items_per_batch ) || $this->time_exceeded() || $this->memory_exceeded() ) {
break;
}
}
// send item updates to Facebook and update the job with the returned array of batch handles
if ( ! empty( $requests ) ) {
try {
$handles = $this->send_item_updates( $requests );
$job->handles = ! isset( $job->handles ) || ! is_array( $job->handles ) ? $handles : array_merge( $job->handles, $handles );
$this->update_job( $job );
} catch ( ApiException $e ) {
/* translators: Placeholders: %1$s - <string job ID, %2$s - <strong> error message */
$message = sprintf( __( 'There was an error trying sync products using the Catalog Batch API for job %1$s: %2$s', 'facebook-for-woocommerce' ), $job->id, $e->getMessage() );
facebook_for_woocommerce()->log( $message );
}
}
}
/**
* Processes a single item.
*
* @param mixed $item
* @param object|\stdClass $job
* @return array|null
* @throws PluginException In case of invalid sync request method.
*/
public function process_item( $item, $job ) {
list( $item_id, $method ) = $item;
if ( ! in_array( $method, [ Sync::ACTION_UPDATE, Sync::ACTION_DELETE ], true ) ) {
throw new PluginException( "Invalid sync request method: {$method}." );
}
if ( Sync::ACTION_UPDATE === $method ) {
$request = $this->process_item_update( $item_id );
} else {
$request = $this->process_item_delete( $item_id );
}
return $request;
}
/**
* Processes an UPDATE sync request for the given product.
*
* @since 2.0.0
*
* @param string $prefixed_product_id prefixed product ID
* @return array|null
* @throws PluginException In case no product was found.
*/
private function process_item_update( $prefixed_product_id ) {
$product_id = (int) str_replace( Sync::PRODUCT_INDEX_PREFIX, '', $prefixed_product_id );
$product = wc_get_product( $product_id );
if ( ! $product instanceof \WC_Product ) {
throw new PluginException( "No product found with ID equal to {$product_id}." );
}
$request = null;
if ( ! Products::product_should_be_deleted( $product ) && Products::product_should_be_synced( $product ) ) {
if ( $product->is_type( 'variation' ) ) {
$product_data = \WC_Facebookcommerce_Utils::prepare_product_variation_data_items_batch( $product );
} else {
$product_data = \WC_Facebookcommerce_Utils::prepare_product_data_items_batch( $product );
}
// extract the retailer_id
$retailer_id = $product_data['retailer_id'];
// NB: Changing this to get items_batch to work
// retailer_id cannot be included in the data object
unset( $product_data['retailer_id'] );
$product_data['id'] = $retailer_id;
$request = [
'method' => Sync::ACTION_UPDATE,
'data' => $product_data,
];
/**
* Filters the data that will be included in a UPDATE sync request.
*
* @since 2.0.0
*
* @param array $request request data
* @param \WC_Product $product product object
*/
$request = apply_filters( 'wc_facebook_sync_background_item_update_request', $request, $product );
}
return $request;
}
/**
* Processes a DELETE sync request for the given product.
*
* @param string $prefixed_retailer_id Product retailer ID.
*/
private function process_item_delete( $prefixed_retailer_id ) {
$retailer_id = str_replace( Sync::PRODUCT_INDEX_PREFIX, '', $prefixed_retailer_id );
$request = [
'data' => [ 'id' => $retailer_id ],
'method' => Sync::ACTION_DELETE,
];
/**
* Filters the data that will be included in a DELETE sync request.
*
* @since 2.0.0
*
* @param array $request request data
* @param string $retailer prefixed product retailer ID
*/
return apply_filters( 'wc_facebook_sync_background_item_delete_request', $request, $prefixed_retailer_id );
}
/**
* Sends item updates to Facebook.
*
* @param array $requests Array of JSON objects containing batch requests. Each batch request consists of method and data fields.
* @return array An array of handles.
* @throws ApiException In case of failed API request.
*/
private function send_item_updates( array $requests ): array {
$facebook_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id();
$response = facebook_for_woocommerce()->get_api()->send_item_updates( $facebook_catalog_id, $requests );
$response_handles = $response->handles;
$handles = ( isset( $response_handles ) && is_array( $response_handles ) ) ? $response_handles : array();
return $handles;
}
}