Skip to content

Commit

Permalink
Merge pull request #2603 from 10up/feature/term-edition
Browse files Browse the repository at this point in the history
Update posts on term changes
  • Loading branch information
felipeelia authored Mar 2, 2022
2 parents 1ecf99e + 7007b6e commit 1b5f5a2
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 15 deletions.
49 changes: 34 additions & 15 deletions includes/classes/Indexable/Post/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -611,13 +611,13 @@ public function prepare_date_terms( $date_to_prepare ) {
}

/**
* Prepare terms to send to ES.
* Get an array of taxonomies that are indexable for the given post
*
* @since 4.0.0
* @param WP_Post $post Post object
* @since 0.1.0
* @return array
* @return array Array of WP_Taxonomy objects that should be indexed
*/
private function prepare_terms( $post ) {
public function get_indexable_post_taxonomies( $post ) {
$taxonomies = get_object_taxonomies( $post->post_type, 'objects' );
$selected_taxonomies = [];

Expand All @@ -635,7 +635,36 @@ private function prepare_terms( $post ) {
* @param {WP_Post} Post object
* @return {array} New taxonomies
*/
$selected_taxonomies = apply_filters( 'ep_sync_taxonomies', $selected_taxonomies, $post );
$selected_taxonomies = (array) apply_filters( 'ep_sync_taxonomies', $selected_taxonomies, $post );

// Important we validate here to ensure there are no invalid taxonomy values returned from the filter, as just one would cause wp_get_object_terms() to fail.
$validated_taxonomies = [];
foreach ( $selected_taxonomies as $selected_taxonomy ) {
// If we get a taxonomy name, we need to convert it to taxonomy object
if ( ! is_object( $selected_taxonomy ) && taxonomy_exists( (string) $selected_taxonomy ) ) {
$selected_taxonomy = get_taxonomy( $selected_taxonomy );
}

// We check if the $taxonomy object has a valid name property. Backward compatibility since WP_Taxonomy introduced in WP 4.7
if ( ! is_a( $selected_taxonomy, '\WP_Taxonomy' ) || ! property_exists( $selected_taxonomy, 'name' ) || ! taxonomy_exists( $selected_taxonomy->name ) ) {
continue;
}

$validated_taxonomies[] = $selected_taxonomy;
}

return $validated_taxonomies;
}

/**
* Prepare terms to send to ES.
*
* @param WP_Post $post Post object
* @since 0.1.0
* @return array
*/
private function prepare_terms( $post ) {
$selected_taxonomies = $this->get_indexable_post_taxonomies( $post );

if ( empty( $selected_taxonomies ) ) {
return [];
Expand All @@ -653,16 +682,6 @@ private function prepare_terms( $post ) {
$allow_hierarchy = apply_filters( 'ep_sync_terms_allow_hierarchy', true );

foreach ( $selected_taxonomies as $taxonomy ) {
// If we get a taxonomy name, we need to convert it to taxonomy object
if ( ! is_object( $taxonomy ) && taxonomy_exists( (string) $taxonomy ) ) {
$taxonomy = get_taxonomy( $taxonomy );
}

// We check if the $taxonomy object as name property. Backward compatibility since WP_Taxonomy introduced in WP 4.7
if ( ! is_a( $taxonomy, '\WP_Taxonomy' ) || ! property_exists( $taxonomy, 'name' ) ) {
continue;
}

$object_terms = get_the_terms( $post->ID, $taxonomy->name );

if ( ! $object_terms || is_wp_error( $object_terms ) ) {
Expand Down
229 changes: 229 additions & 0 deletions includes/classes/Indexable/Post/SyncManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public function setup() {
// Called just because we need to know somehow if $delete_all is set before action_queue_meta_sync() runs.
add_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ), 10, 5 );
add_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
add_action( 'set_object_terms', array( $this, 'action_set_object_terms' ), 10, 6 );
add_action( 'edited_term', array( $this, 'action_edited_term' ), 10, 3 );
add_action( 'deleted_term_relationships', array( $this, 'action_deleted_term_relationships' ), 10, 3 );
add_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) );

add_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
Expand Down Expand Up @@ -315,6 +318,182 @@ public function action_sync_on_update( $post_id ) {
}
}

/**
* When a post's terms are changed, re-index.
*
* This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects
* and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate
* multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories),
* hence why we also have `action_deleted_term_relationships`.
*
* @see set_object_terms
* @param int $post_id Post ID.
* @param array $terms An array of object terms.
* @param array $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
* @param array $old_tt_ids Old array of term taxonomy IDs.
* @since 4.0.0
*/
public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
if ( $this->kill_sync() ) {
return;
}

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
// Bypass saving if doing autosave
return;
}

/**
* Filter to allow skipping this action in case of custom handling
*
* @hook ep_skip_action_set_object_terms
* @param {bool} $skip True means kill sync for post
* @param {int} $post_id ID of post
* @param {array} $terms An array of object terms.
* @param {array} $tt_ids An array of term taxonomy IDs.
* @param {string} $taxonomy Taxonomy slug.
* @param {bool} $append Whether to append new terms to the old terms.
* @param {array} $old_tt_ids Old array of term taxonomy IDs.
* @return {boolean} New value
*/
if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) {
return;
}

if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
return;
}

/**
* Fire before post is queued for syncing
*
* @since 4.0.0
* @hook ep_sync_on_set_object_terms
* @param {int} $post_id ID of post
* @param {array} $terms An array of object terms.
* @param {array} $tt_ids An array of term taxonomy IDs.
* @param {string} $taxonomy Taxonomy slug.
* @param {bool} $append Whether to append new terms to the old terms.
* @param {array} $old_tt_ids Old array of term taxonomy IDs.
*/
do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );

$this->add_to_queue( $post_id );
}

/**
* When a term is updated, re-index all posts attached to that term
*
* @param int $term_id Term id.
* @param int $tt_id Term Taxonomy id.
* @param string $taxonomy Taxonomy name.
* @since 4.0.0
*/
public function action_edited_term( $term_id, $tt_id, $taxonomy ) {
global $wpdb;

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
// Bypass saving if doing autosave
return;
}

// Find ID of all attached posts (query lifted from wp_delete_term())
$object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) );

if ( ! count( $object_ids ) ) {
return;
}

/**
* Filter to allow skipping this action in case of custom handling
*
* @hook ep_skip_action_edited_term
* @param {bool} $skip Current value of whether to skip running action_edited_term or not
* @param {int} $term_id Term id.
* @param {int} $tt_id Term Taxonomy id.
* @param {string} $taxonomy Taxonomy name.
* @param {array} $object_ids IDs of the objects attached to the term id.
* @return {bool} New value of whether to skip running action_edited_term or not
*/
if ( apply_filters( 'ep_skip_action_edited_term', false, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
return;
}

$indexable = Indexables::factory()->get( $this->indexable_slug );

// Add all of them to the queue
foreach ( $object_ids as $post_id ) {
if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
continue;
}

/**
* Fire before post is queued for syncing
*
* @hook ep_sync_on_edited_term
* @param {int} $post_id ID of post
* @param {int} $term_id ID of the term that was edited
* @param {int} $tt_id Taxonomy Term ID of the term that was edited
* @param {int} $taxonomy Taxonomy of the term that was edited
*/
do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );

$this->add_to_queue( $post_id );
}
}

/**
* When a term relationship is deleted, re-index all posts attached to that term
*
* @param int $post_id Post ID.
* @param array $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
* @since 4.0.0
*/
public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
if ( $this->kill_sync() ) {
return;
}

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
// Bypass saving if doing autosave
return;
}

/**
* Filter to allow skipping this action in case of custom handling
*
* @hook ep_skip_action_deleted_term_relationships
* @param {bool} $skip Current value of whether to skip running action_edited_term or not
* @param {int} $post_id Post ID.
* @param {array} $tt_ids An array of term taxonomy IDs.
* @param {string} $taxonomy Taxonomy slug.
* @return {bool} New value of whether to skip running action_deleted_term_relationships or not
*/
if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
return;
}

if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
return;
}

/**
* Fire before post is queued for syncing
*
* @hook ep_sync_on_deleted_term_relationships
* @since 4.0.0
* @param {int} $post_id ID of post
* @param {array} $tt_ids An array of term taxonomy IDs.
* @param {string} $taxonomy Taxonomy of the term that was edited
*/
do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );

$this->add_to_queue( $post_id );
}

/**
* Create mapping and network alias when a new blog is created.
*
Expand Down Expand Up @@ -345,4 +524,54 @@ public function action_create_blog_index( $blog ) {

restore_current_blog();
}

/**
* Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not.
*
* @param int $post_id The post ID.
* @param string $taxonomy The taxonomy slug.
* @return boolean
*/
protected function should_reindex_post( $post_id, $taxonomy ) {
/**
* Filter to kill post sync
*
* @hook ep_post_sync_kill
* @param {bool} $skip True meanas kill sync for post
* @param {int} $object_id ID of post
* @param {int} $object_id ID of post
* @return {boolean} New value
*/
if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
return false;
}

$post = get_post( $post_id );
if ( ! is_object( $post ) ) {
return false;
}

$indexable = Indexables::factory()->get( $this->indexable_slug );

// Check post status
$indexable_post_statuses = $indexable->get_indexable_post_status();
if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
return false;
}

// Only re-index if the taxonomy is indexed for this post
$indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post );
$indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' );
if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) {
return false;
}

// Check post type
$indexable_post_types = $indexable->get_indexable_post_types();
if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) {
return false;
}

return true;
}
}
70 changes: 70 additions & 0 deletions tests/php/indexables/TestPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -6711,4 +6711,74 @@ public function testInsertPostAndDeleteAnother() {
$this->assertEquals( 1, $query->found_posts );
$this->assertEquals( $query->posts[0]->ID, $new_post_id );
}

/**
* Tests term deletion applied to posts
*
* @return void
* @group post
*/
public function testPostDeletedTerm() {
$cat = wp_create_category( 'test category' );
$tag = wp_insert_category( [ 'taxonomy' => 'post_tag', 'cat_name' => 'test-tag' ] );

$post_id = Functions\create_and_sync_post(
array(
'tags_input' => array( $tag ),
'post_category' => array( $cat ),
)
);

ElasticPress\Elasticsearch::factory()->refresh_indices();

$document = ElasticPress\Indexables::factory()->get( 'post' )->get( $post_id );
$this->assertNotEmpty( $document['terms']['category'] );
$this->assertNotEmpty( $document['terms']['post_tag'] );

ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->sync_queue = [];

wp_delete_term( $tag, 'post_tag' );
wp_delete_term( $cat, 'category' );

ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->index_sync_queue();
ElasticPress\Elasticsearch::factory()->refresh_indices();

$document = ElasticPress\Indexables::factory()->get( 'post' )->get( $post_id );
// Category will fallback to Uncategorized.
$this->assertNotContains( $cat, wp_list_pluck( $document['terms']['category'], 'term_id' ) );
$this->assertArrayNotHasKey( 'post_tag', $document['terms'] );
}

/**
* Tests term edition applied to posts
*
* @return void
* @group post
*/
public function testPostEditedTerm() {
$post_id = Functions\create_and_sync_post(
array(
'tags_input' => array( 'test-tag' ),
)
);

ElasticPress\Elasticsearch::factory()->refresh_indices();

$test_tag = get_term_by( 'name', 'test-tag', 'post_tag' );
wp_update_term(
$test_tag->term_id,
'post_tag',
[
'slug' => 'different-tag-slug',
'name' => 'Different Tag Name',
]
);

ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->index_sync_queue();
ElasticPress\Elasticsearch::factory()->refresh_indices();

$document = ElasticPress\Indexables::factory()->get( 'post' )->get( $post_id );
$this->assertEquals( 'different-tag-slug', $document['terms']['post_tag'][0]['slug'] );
$this->assertEquals( 'Different Tag Name', $document['terms']['post_tag'][0]['name'] );
}
}

0 comments on commit 1b5f5a2

Please sign in to comment.