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(content-distribution): partial payload #205

Merged
merged 13 commits into from
Jan 29, 2025
87 changes: 78 additions & 9 deletions includes/class-content-distribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Content_Distribution {
*
* @var array Post IDs to update.
*/
private static $queued_post_updates = [];
private static $queued_distributions = [];

/**
* Initialize this class and register hooks
Expand All @@ -51,7 +51,9 @@ public static function init() {
add_filter( 'newspack_webhooks_request_priority', [ __CLASS__, 'webhooks_request_priority' ], 10, 2 );
add_filter( 'update_post_metadata', [ __CLASS__, 'maybe_short_circuit_distributed_meta' ], 10, 4 );
add_action( 'wp_after_insert_post', [ __CLASS__, 'handle_post_updated' ] );
add_action( 'updated_postmeta', [ __CLASS__, 'handle_postmeta_update' ], 10, 3 );
add_action( 'updated_post_meta', [ __CLASS__, 'handle_postmeta_update' ], 10, 3 );
add_action( 'added_post_meta', [ __CLASS__, 'handle_postmeta_update' ], 10, 3 );
add_action( 'deleted_post_meta', [ __CLASS__, 'handle_postmeta_update' ], 10, 3 );
add_action( 'before_delete_post', [ __CLASS__, 'handle_post_deleted' ] );
add_action( 'newspack_network_incoming_post_inserted', [ __CLASS__, 'handle_incoming_post_inserted' ], 10, 3 );

Expand All @@ -77,22 +79,60 @@ public static function register_data_event_actions() {
Data_Events::register_action( 'network_incoming_post_inserted' );
}

/**
* Queue post distribution to run on PHP shutdown.
*
* @param int $post_id The post ID.
* @param null|string $post_data_key The post data key to update.
* Default is null (entire post payload).
*
* @return void
*/
public static function queue_post_distribution( $post_id, $post_data_key = null ) {
// Bail if the post is already queued for a full update.
if ( isset( self::$queued_distributions[ $post_id ] ) && self::$queued_distributions[ $post_id ] === true ) {
return;
}

// Queue for a full post update.
if ( empty( $post_data_key ) ) {
self::$queued_distributions[ $post_id ] = true;
return;
}

// Queue for a partial update.
if ( ! isset( self::$queued_distributions[ $post_id ] ) ) {
self::$queued_distributions[ $post_id ] = [];
}
self::$queued_distributions[ $post_id ][] = $post_data_key;
}

/**
* Get queued post distributions.
*/
public static function get_queued_distributions() {
return self::$queued_distributions;
}

/**
* Distribute queued posts.
*/
public static function distribute_queued_posts() {
if ( empty( self::$queued_post_updates ) ) {
if ( empty( self::$queued_distributions ) ) {
return;
}
$post_ids = array_unique( self::$queued_post_updates );
foreach ( $post_ids as $post_id ) {
foreach ( self::$queued_distributions as $post_id => $post_data_keys ) {
$post = get_post( $post_id );
if ( ! $post ) {
continue;
}
self::distribute_post( $post );
if ( is_array( $post_data_keys ) ) {
self::distribute_post_partial( $post, array_unique( $post_data_keys ) );
} else {
self::distribute_post( $post );
}
}
self::$queued_post_updates = [];
self::$queued_distributions = [];
}

/**
Expand Down Expand Up @@ -170,7 +210,7 @@ public static function handle_postmeta_update( $meta_id, $object_id, $meta_key )
) {
return;
}
self::$queued_post_updates[] = $object_id;
self::queue_post_distribution( $post->ID, 'post_meta' );
}

/**
Expand All @@ -191,7 +231,7 @@ public static function handle_post_updated( $post ) {
if ( ! self::is_post_distributed( $post ) ) {
return;
}
self::$queued_post_updates[] = $post->ID;
self::queue_post_distribution( $post->ID );
}

/**
Expand Down Expand Up @@ -388,4 +428,33 @@ public static function distribute_post( $post, $status_on_create = 'draft' ) {
update_post_meta( $post->ID, self::PAYLOAD_HASH_META, $payload_hash );
}
}

/**
* Trigger a partial post distribution.
*
* @param WP_Post|Outgoing_Post|int $post The post object or ID.
* @param string[] $post_data_keys The post data keys to update.
*
* @return void|WP_Error The error if the payload is invalid.
*/
public static function distribute_post_partial( $post, $post_data_keys ) {
if ( ! class_exists( 'Newspack\Data_Events' ) ) {
return;
}
if ( is_string( $post_data_keys ) ) {
$post_data_keys = [ $post_data_keys ];
}
if ( $post instanceof Outgoing_Post ) {
$distributed_post = $post;
} else {
$distributed_post = self::get_distributed_post( $post );
}
if ( $distributed_post ) {
$payload = $distributed_post->get_partial_payload( $post_data_keys );
if ( is_wp_error( $payload ) ) {
return $payload;
}
Data_Events::dispatch( 'network_post_updated', $payload );
}
}
}
46 changes: 44 additions & 2 deletions includes/content-distribution/class-incoming-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,17 @@ public function __construct( $payload ) {
}

if ( $post ) {
$this->ID = $post->ID;
$this->post = $post;
$this->ID = $post->ID;
$this->post = $post;
}

// Handle partial payload.
if ( ! empty( $payload['partial'] ) ) {
$payload = $this->get_payload_from_partial( $payload );
if ( is_wp_error( $payload ) ) {
throw new \InvalidArgumentException( esc_html( $payload->get_error_message() ) );
}
$this->payload = $payload;
}
}

Expand Down Expand Up @@ -173,6 +182,31 @@ protected function get_post_payload() {
return get_post_meta( $this->ID, self::PAYLOAD_META, true );
}

/**
* Get payload from partial.
*
* @param array $payload The partial payload.
*
* @return array|WP_Error The full payload or WP_Error on failure.
*/
protected function get_payload_from_partial( $payload ) {
if ( ! $this->ID ) {
return new WP_Error( 'missing_post', __( 'Partial payload requires an existing post.', 'newspack-network' ) );
}

$current_payload = $this->get_post_payload();
$current_payload_error = self::get_payload_error( $current_payload );
if ( is_wp_error( $current_payload_error ) ) {
return $current_payload_error;
}

$payload['post_data'] = array_merge( $current_payload['post_data'], $payload['post_data'] );

unset( $payload['partial'] );

return $payload;
}

/**
* Get the post's original site URL.
*
Expand Down Expand Up @@ -399,6 +433,14 @@ protected function update_payload( $payload ) {
return new WP_Error( 'mismatched_post_id', __( 'Mismatched post ID.', 'newspack-network' ) );
}

// Handle partial payload.
if ( ! empty( $payload['partial'] ) ) {
$payload = $this->get_payload_from_partial( $payload );
if ( is_wp_error( $payload ) ) {
return $payload;
}
}

$this->payload = $payload;
}

Expand Down
37 changes: 37 additions & 0 deletions includes/content-distribution/class-outgoing-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,43 @@ public function get_payload( $status_on_create = 'draft' ) {
];
}

/**
* Get a partial payload for distribution.
*
* @param string[] $post_data_keys Keys in the post_data array to include in
* the partial payload.
*
* @return array|WP_Error The partial payload or WP_Error if any of the keys were not found.
*/
public function get_partial_payload( $post_data_keys ) {
if ( is_string( $post_data_keys ) ) {
$post_data_keys = [ $post_data_keys ];
}

$payload = $this->get_payload();
foreach ( $post_data_keys as $post_data_key ) {
if ( ! isset( $payload['post_data'][ $post_data_key ] ) ) {
return new WP_Error( 'key_not_found', __( 'Key not found in payload.', 'newspack-network' ) );
}
}

// Mark the payload as partial.
$payload['partial'] = true;

$post_data = [];
foreach ( $post_data_keys as $post_data_key ) {
$post_data[ $post_data_key ] = $payload['post_data'][ $post_data_key ];
}

// Always add the date and modified date to the partial payload.
$post_data['date_gmt'] = $payload['post_data']['date_gmt'];
$post_data['modified_gmt'] = $payload['post_data']['modified_gmt'];

$payload['post_data'] = $post_data;

return $payload;
}

/**
* Get the processed post content for distribution.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Test\Content_Distribution;

use Newspack_Network\Content_Distribution as Content_Distribution_Class;
use Newspack_Network\Content_Distribution\Outgoing_Post;
use Newspack_Network\Hub\Node as Hub_Node;

Expand Down Expand Up @@ -64,4 +65,29 @@ public function test_update_distributed_post_meta() {
$result = update_post_meta( $post_id, Outgoing_Post::DISTRIBUTED_POST_META, [ 'https://node.test', 'https://other-node.test' ] );
$this->assertNotFalse( $result );
}

/**
* Test queue post distribution.
*/
public function test_queue_post_distribution() {
$post_id = $this->factory->post->create();

// Queue post meta for distribution.
Content_Distribution_Class::queue_post_distribution( $post_id, 'post_meta' );
$queue = Content_Distribution_Class::get_queued_distributions();
$this->assertArrayHasKey( $post_id, $queue );
$this->assertSame( [ 'post_meta' ], $queue[ $post_id ] );

// Queue full post for distribution.
Content_Distribution_Class::queue_post_distribution( $post_id );
$queue = Content_Distribution_Class::get_queued_distributions();
// Assert that the post is queued for full distribution (= true).
$this->assertTrue( $queue[ $post_id ] );

// Queue another attribute for distribution.
Content_Distribution_Class::queue_post_distribution( $post_id, 'post_meta' );
$queue = Content_Distribution_Class::get_queued_distributions();
// Assert that the post is still queued for full distribution.
$this->assertTrue( $queue[ $post_id ] );
}
}
65 changes: 65 additions & 0 deletions tests/unit-tests/content-distribution/test-incoming-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,4 +481,69 @@ public function test_status_on_create() {
$this->incoming_post->insert( $payload );
$this->assertSame( 'draft', get_post_status( $post_id ) );
}

/**
* Test partial post payload on insert.
*/
public function test_partial_payload_insert() {
$post_id = $this->incoming_post->insert();

// Make the payload a partial.
$payload = $this->get_sample_payload();
$payload['partial'] = true;
$payload['post_data'] = [
'title' => 'Updated Title',
'date_gmt' => $payload['post_data']['date_gmt'],
'modified_gmt' => $payload['post_data']['modified_gmt'],
];

$this->incoming_post->insert( $payload );

// Assert that the post title was updated and the content was not.
$this->assertSame( 'Updated Title', get_the_title( $post_id ) );
$this->assertSame( 'Content', get_post_field( 'post_content', $post_id ) );
}

/**
* Test partial post payload on instantiation.
*/
public function test_partial_payload_instantiation() {
$post_id = $this->incoming_post->insert();

// Make the payload a partial.
$payload = $this->get_sample_payload();
$payload['partial'] = true;
$payload['post_data'] = [
'title' => 'Updated Title',
'date_gmt' => $payload['post_data']['date_gmt'],
'modified_gmt' => $payload['post_data']['modified_gmt'],
];

$incoming_post = new Incoming_Post( $payload );
$incoming_post->insert();

// Assert that the post title was updated and the content was not.
$this->assertSame( 'Updated Title', get_the_title( $post_id ) );
$this->assertSame( 'Content', get_post_field( 'post_content', $post_id ) );
}

/**
* Test partial payload on missing post.
*/
public function test_partial_payload_missing_post() {
$payload = $this->get_sample_payload();

// Make the payload a partial.
$payload['partial'] = true;
$payload['post_data'] = [
'title' => 'Updated Title',
'date_gmt' => $payload['post_data']['date_gmt'],
'modified_gmt' => $payload['post_data']['modified_gmt'],
];

// Assert that instantiating a partial payload will throw an exception.
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'Partial payload requires an existing post.' );
new Incoming_Post( $payload );
}
}
34 changes: 34 additions & 0 deletions tests/unit-tests/content-distribution/test-outgoing-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,38 @@ public function test_ignored_taxonomies() {
$payload = $this->outgoing_post->get_payload();
$this->assertTrue( empty( $payload['post_data']['taxonomy'][ $taxonomy ] ) );
}

/**
* Test get partial payload.
*/
public function test_get_partial_payload() {
$partial_payload = $this->outgoing_post->get_partial_payload( 'post_meta' );

$payload = $this->outgoing_post->get_payload();
$this->assertTrue( $partial_payload['partial'] );
$this->assertSame( $payload['network_post_id'], $partial_payload['network_post_id'] );
$this->assertSame( $payload['post_data']['post_meta'], $partial_payload['post_data']['post_meta'] );
$this->assertSame( $payload['post_data']['date_gmt'], $partial_payload['post_data']['date_gmt'] );
$this->assertSame( $payload['post_data']['modified_gmt'], $partial_payload['post_data']['modified_gmt'] );
$this->assertArrayNotHasKey( 'title', $partial_payload['post_data'] );
$this->assertArrayNotHasKey( 'content', $partial_payload['post_data'] );
$this->assertArrayNotHasKey( 'taxonomy', $partial_payload['post_data'] );
}

/**
* Test get partial payload multiple keys.
*/
public function test_get_partial_payload_multiple_keys() {
$partial_payload = $this->outgoing_post->get_partial_payload( [ 'post_meta', 'taxonomy' ] );

$payload = $this->outgoing_post->get_payload();
$this->assertTrue( $partial_payload['partial'] );
$this->assertSame( $payload['network_post_id'], $partial_payload['network_post_id'] );
$this->assertSame( $payload['post_data']['post_meta'], $partial_payload['post_data']['post_meta'] );
$this->assertSame( $payload['post_data']['taxonomy'], $partial_payload['post_data']['taxonomy'] );
$this->assertSame( $payload['post_data']['date_gmt'], $partial_payload['post_data']['date_gmt'] );
$this->assertSame( $payload['post_data']['modified_gmt'], $partial_payload['post_data']['modified_gmt'] );
$this->assertArrayNotHasKey( 'title', $partial_payload['post_data'] );
$this->assertArrayNotHasKey( 'content', $partial_payload['post_data'] );
}
}