Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(esp-sync): queue data events sync to run once #3661

Open
wants to merge 8 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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' );
}
}
}