Skip to content

Commit

Permalink
feat(esp-sync): queue data events sync to run once (#3661)
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelpeixe authored and dkoo committed Feb 17, 2025
1 parent 1c29e17 commit 5646a72
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 17 deletions.
31 changes: 31 additions & 0 deletions includes/data-events/class-data-events.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ final class Data_Events {
*/
private static $queued_dispatches = [];

/**
* Current action event.
*
* @var string|null
*/
private static $current_event = null;

/**
* Initialize hooks.
*/
Expand Down Expand Up @@ -95,6 +102,9 @@ public static function maybe_handle() {
* @param string $client_id Client ID.
*/
public static function handle( $action_name, $timestamp, $data, $client_id ) {
// Set current event.
self::set_current_event( $action_name );

// Execute global handlers.
Logger::log(
sprintf( 'Executing global action handlers for "%s".', $action_name ),
Expand Down Expand Up @@ -145,6 +155,27 @@ public static function handle( $action_name, $timestamp, $data, $client_id ) {
* @param string $client_id Client ID.
*/
\do_action( 'newspack_data_event', $action_name, $timestamp, $data, $client_id );

// Unset current event.
self::set_current_event( null );
}

/**
* Get the current event being handled.
*
* @return string|null Current event.
*/
public static function current_event() {
return self::$current_event;
}

/**
* Set the current event being handled.
*
* @param string|null $name Event name.
*/
private static function set_current_event( $name ) {
self::$current_event = $name;
}

/**
Expand Down
90 changes: 73 additions & 17 deletions includes/reader-activation/sync/class-esp-sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Newspack\Reader_Activation;

use Newspack\Reader_Activation;
use Newspack\Data_Events;

defined( 'ABSPATH' ) || exit;

Expand All @@ -22,11 +23,20 @@ class ESP_Sync extends Sync {
* @var string
*/
protected static $context = 'ESP Sync';

/**
* Queued syncs containing their contexts keyed by email address.
*
* @var array[]
*/
protected static $queued_syncs = [];

/**
* Initialize hooks.
*/
public static function init_hooks() {
add_action( 'newspack_scheduled_esp_sync', [ __CLASS__, 'scheduled_sync' ], 10, 2 );
add_action( 'shutdown', [ __CLASS__, 'run_queued_syncs' ] );
}

/**
Expand Down Expand Up @@ -98,6 +108,15 @@ public static function sync( $contact, $context = '' ) {
$context = static::$context;
}

// If we're running in a data event, queue the sync to run on shutdown.
if ( Data_Events::current_event() && ! did_action( 'shutdown' ) ) {
if ( ! isset( self::$queued_syncs[ $contact['email'] ] ) ) {
self::$queued_syncs[ $contact['email'] ] = [];
}
self::$queued_syncs[ $contact['email'] ][] = $context;
return;
}

$master_list_id = Reader_Activation::get_esp_master_list_id();

/**
Expand Down Expand Up @@ -163,24 +182,12 @@ public static function scheduled_sync( $user_id, $context ) {
}

/**
* Given a user ID or WooCommerce Order, sync that reader's contact data to
* the connected ESP.
* Get contact data for syncing.
*
* @param int|\WC_order $user_id_or_order User ID or WC_Order object.
* @param bool $is_dry_run True if a dry run.
*
* @return true|\WP_Error True if the contact was synced successfully, WP_Error otherwise.
* @param int $user_id The user ID.
*/
public static function sync_contact( $user_id_or_order, $is_dry_run = false ) {
$can_sync = static::can_esp_sync( true );
if ( ! $is_dry_run && $can_sync->has_errors() ) {
return $can_sync;
}

$is_order = $user_id_or_order instanceof \WC_Order;
$order = $is_order ? $user_id_or_order : false;
$user_id = $is_order ? $order->get_customer_id() : $user_id_or_order;
$user = \get_userdata( $user_id );
protected static function get_contact_data( $user_id ) {
$user = \get_userdata( $user_id );

$customer = new \WC_Customer( $user_id );
if ( ! $customer || ! $customer->get_id() ) {
Expand All @@ -200,7 +207,29 @@ public static function sync_contact( $user_id_or_order, $is_dry_run = false ) {
$customer->save();
}

$contact = $is_order ? Sync\WooCommerce::get_contact_from_order( $order ) : Sync\WooCommerce::get_contact_from_customer( $customer );
return Sync\WooCommerce::get_contact_from_customer( $customer );
}

/**
* Given a user ID or WooCommerce Order, sync that reader's contact data to
* the connected ESP.
*
* @param int|\WC_order $user_id_or_order User ID or WC_Order object.
* @param bool $is_dry_run True if a dry run.
*
* @return true|\WP_Error True if the contact was synced successfully, WP_Error otherwise.
*/
public static function sync_contact( $user_id_or_order, $is_dry_run = false ) {
$can_sync = static::can_esp_sync( true );
if ( ! $is_dry_run && $can_sync->has_errors() ) {
return $can_sync;
}

$is_order = $user_id_or_order instanceof \WC_Order;
$order = $is_order ? $user_id_or_order : false;
$user_id = $is_order ? $order->get_customer_id() : $user_id_or_order;

$contact = $is_order ? Sync\WooCommerce::get_contact_from_order( $order ) : self::get_contact_data( $user_id );
$result = $is_dry_run ? true : self::sync( $contact );

if ( $result && ! \is_wp_error( $result ) ) {
Expand All @@ -219,5 +248,32 @@ public static function sync_contact( $user_id_or_order, $is_dry_run = false ) {

return $result;
}

/**
* Run queued syncs.
*
* @return void
*/
public static function run_queued_syncs() {
if ( empty( self::$queued_syncs ) ) {
return;
}

foreach ( self::$queued_syncs as $email => $contexts ) {
$user = get_user_by( 'email', $email );
if ( ! $user ) {
continue;
}

$contact = self::get_contact_data( $user->ID );
if ( ! $contact ) {
continue;
}

self::sync( $contact, implode( '; ', $contexts ) );
}

self::$queued_syncs = [];
}
}
ESP_Sync::init_hooks();
43 changes: 43 additions & 0 deletions tests/unit-tests/data-events.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,47 @@ function( $timestamp, $data, $client_id ) use ( &$parsed_data ) {
$parsed_data
);
}

/**
* Test the current event is set and available during handler execution.
*/
public function test_current_event() {
Data_Events::register_action( 'test_action' );
Data_Events::register_action( 'test_action2' );

$handler = function() {
$this->assertEquals( 'test_action', Data_Events::current_event(), 'Current event should be set and equal to the action name' );
};
Data_Events::register_handler( $handler, 'test_action' );
Data_Events::handle( 'test_action', time(), [], 'test-client-id' );

$this->assertNull( Data_Events::current_event(), 'Current event should be null after handling' );

$handler2 = function() {
$this->assertEquals( 'test_action2', Data_Events::current_event(), 'Current event should be set and equal to the action name' );
};
Data_Events::register_handler( $handler2, 'test_action2' );
Data_Events::handle( 'test_action2', time(), [], 'test-client-id' );

$this->assertNull( Data_Events::current_event(), 'Current event should be null after handling' );
}

/**
* Test that the current event is set to null even if a handler throws an exception.
*/
public function test_current_event_exception() {
Data_Events::register_action( 'test_action' );

$handler = function() {
$this->assertEquals( 'test_action', Data_Events::current_event(), 'Current event should be set and equal to the action name' );
throw new Exception( 'Test exception' );
};
Data_Events::register_handler( $handler, 'test_action' );

try {
Data_Events::handle( 'test_action', time(), [], 'test-client-id' );
} catch ( Exception $e ) {
$this->assertNull( Data_Events::current_event(), 'Current event should be null after handling' );
}
}
}

0 comments on commit 5646a72

Please sign in to comment.