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

fix: invalidate caches for menu items #272

Merged
merged 3 commits into from Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 82 additions & 11 deletions src/Cache/Invalidation.php
Expand Up @@ -108,6 +108,9 @@ public function init() {
add_action( 'deleted_term_meta', [ $this, 'on_updated_menu_meta_cb' ], 10, 4 );

// @todo: evict caches when meta on menu items are changed. This happens outside *_post_meta hooks as nav_menu_item is a "different" type of post type
add_action( 'added_term_relationship', [ $this, 'on_menu_item_added_to_menu_cb' ], 10, 3 );
add_action( 'wp_update_nav_menu_item', [ $this, 'on_menu_item_updated_cb' ], 10, 3 );
add_action( 'deleted_post', [ $this, 'on_menu_item_deleted_cb' ], 10, 2 );

add_action( 'updated_post_meta', [ $this, 'on_menu_item_change_cb' ], 10, 4 );
add_action( 'added_post_meta', [ $this, 'on_menu_item_change_cb' ], 10, 4 );
Expand Down Expand Up @@ -732,11 +735,6 @@ public function on_postmeta_change_cb( $meta_id, $post_id, $meta_key, $meta_valu
return;
}

// if the post type is not tracked, ignore it
if ( ! in_array( $post->post_type, \WPGraphQL::get_allowed_post_types(), true ) ) {
return;
}

$post_type_object = get_post_type_object( $post->post_type );

if ( ! $post_type_object instanceof \WP_Post_Type ) {
Expand Down Expand Up @@ -773,7 +771,7 @@ public function on_postmeta_change_cb( $meta_id, $post_id, $meta_key, $meta_valu
* @return bool
* @throws Exception
*/
public function is_menu_public( $menu_id ) {
public function is_menu_public( int $menu_id ): bool {
$nav_menu = get_term( $menu_id, 'nav_menu' );
if ( ! $nav_menu instanceof WP_Term ) {
return false;
Expand Down Expand Up @@ -825,8 +823,8 @@ public function on_set_nav_menu_locations_cb( $value, $old_value ) {
* @return void
* @throws Exception
*/
public function on_update_nav_menu_cb( $menu_id ) {
if ( ! $this->is_menu_public( $menu_id ) ) {
public function on_update_nav_menu_cb( int $menu_id ): void {
if ( ! $this->is_menu_public( absint( $menu_id ) ) ) {
return;
}

Expand All @@ -845,8 +843,8 @@ public function on_update_nav_menu_cb( $menu_id ) {
* @return void
* @throws Exception
*/
public function on_create_nav_menu_cb( $menu_id, array $menu_data ) {
if ( ! $this->is_menu_public( $menu_id ) ) {
public function on_create_nav_menu_cb( int $menu_id, array $menu_data ) {
if ( ! $this->is_menu_public( absint( $menu_id ) ) ) {
return;
}

Expand Down Expand Up @@ -877,7 +875,7 @@ public function on_updated_menu_meta_cb( $meta_id, $object_id, $meta_key, $meta_
}

// if the menu isn't public do nothing
if ( ! $this->is_menu_public( $term->term_id ) ) {
if ( ! $this->is_menu_public( absint( $term->term_id ) ) ) {
return;
}

Expand All @@ -888,6 +886,79 @@ public function on_updated_menu_meta_cb( $meta_id, $object_id, $meta_key, $meta_
$this->purge_nodes( 'term', $term->term_id, 'menu_meta_updated' );
}

/**
* Listen for when a term relationship has changed between nav_menu_item and nav_menu
*
* @param int $object_id The ID of the object the taxonomy is associated with
* @param int $tt_id The Term Taxonomy ID of the term
* @param string $taxonomy The name of the taxonomy the term belongs to
*
* @return void
* @throws Exception
*/
public function on_menu_item_added_to_menu_cb( int $object_id, int $tt_id, string $taxonomy ): void {

if ( 'nav_menu' !== $taxonomy ) {
return;
}

$menu_term = get_term_by( 'term_taxonomy_id', absint( $tt_id ), $taxonomy );

// if the menu isn't public do nothing
if ( ! isset( $menu_term->term_id ) || ! $this->is_menu_public( absint( $menu_term->term_id ) ) ) {
return;
}

$this->purge( 'list:menuitem', 'nav_menu_item_added' );

}

/**
* Listen for when a menu item is updated
*
* @param int $menu_id ID of the updated menu.
* @param int $menu_item_db_id ID of the updated menu item.
* @param array $args An array of arguments used to update a menu item.
*
* @return void
* @throws Exception
*/
public function on_menu_item_updated_cb( int $menu_id, int $menu_item_db_id, array $args ): void {

$menu_term = get_term_by( 'term_id', absint( $menu_id ), 'nav_menu' );

// if the menu isn't public do nothing
if ( ! isset( $menu_term->term_id ) || ! $this->is_menu_public( absint( $menu_term->term_id ) ) ) {
return;
}

$this->purge_nodes( 'post', $menu_item_db_id, 'update_menu_item' );

}

/**
* Listen for menu items being deleted and purge relevant caches
*
* @param int $post_id The ID of the post being deleted
* @param WP_Post $post The Post object that is being deleted
*
* @return void
*/
public function on_menu_item_deleted_cb( int $post_id, WP_Post $post ): void {

if ( 'nav_menu_item' !== $post->post_type ) {
return;
}

if ( 'publish' !== $post->post_status ) {
return;
}

$this->purge_nodes( 'post', $post->ID, 'nav_menu_item_deleted' );

}


/**
* Listens for changes to meta for menu items
*
Expand Down
Expand Up @@ -167,7 +167,17 @@ class WPGraphQLSmartCacheTestCaseWithSeedDataAndPopulatedCaches extends WPGraphQ
/**
* @var WP_Term
*/
public $menu;
public $public_menu;

/**
* @var WP_Term
*/
public $private_menu;

/**
* @var WP_Nav_Menu_Item
*/
public $private_menu_item;

/**
* @var WP_Nav_Menu_Item
Expand Down Expand Up @@ -424,41 +434,37 @@ public function _createSeedData() {
'post_author' => $this->admin->ID,
]);

$this->menu = self::factory()->term->create_and_get([
$this->public_menu = self::factory()->term->create_and_get([
'name' => 'test menu',
'taxonomy' => 'nav_menu'
]);

$this->menu_item_1 = self::factory()->post->create_and_get([
'post_type' => 'nav_menu_item',
'post_status' => 'publish'
$this->private_menu = self::factory()->term->create_and_get([
'name' => 'private menu',
'taxonomy' => 'nav_menu'
]);

$this->child_menu_item = self::factory()->post->create_and_get([
$this->menu_item_1 = self::factory()->post->create_and_get([
'post_type' => 'nav_menu_item',
'post_status' => 'publish'
]);

$this->approved_comment = self::factory()->comment->create_and_get([
'comment_approved' => 1,
'comment_post_ID' => $this->published_post->ID,
]);

$this->unapproved_comment = self::factory()->comment->create_and_get([
'comment_approved' => 0,
]);

// set the parent menu item
wp_update_nav_menu_item( $this->menu->term_id, $this->menu_item_1->ID, [
wp_update_nav_menu_item( $this->public_menu->term_id, $this->menu_item_1->ID, [
'menu-item-title' => 'Test Item',
'menu-item-object' => 'post',
'menu-item-object-id' => $this->published_post->ID,
'menu-item-status' => 'publish',
'menu-item-type' => 'post_type',
]);

$this->child_menu_item = self::factory()->post->create_and_get([
'post_type' => 'nav_menu_item',
'post_status' => 'publish'
]);

// set a child menu item
wp_update_nav_menu_item( $this->menu->term_id, $this->child_menu_item->ID, [
wp_update_nav_menu_item( $this->public_menu->term_id, $this->child_menu_item->ID, [
'menu-item-title' => 'Child Item',
'menu-item-object' => 'page',
'menu-item-object-id' => $this->published_page->ID,
Expand All @@ -467,11 +473,33 @@ public function _createSeedData() {
'menu-item-parent-id' => $this->menu_item_1->ID
]);

$this->private_menu_item = self::factory()->post->create_and_get([
'post_type' => 'nav_menu_item',
'post_status' => 'publish'
]);

wp_update_nav_menu_item( $this->private_menu->term_id, $this->private_menu_item->ID, [
'menu-item-title' => 'Private Menu Item',
'menu-item-object' => 'page',
'menu-item-object-id' => $this->published_page->ID,
'menu-item-status' => 'publish',
'menu-item-type' => 'post_type',
]);

// register a menu location
register_nav_menu( 'default-location', 'Test Menu Location' );

// add the menu to a default location
set_theme_mod( 'nav_menu_locations', [ 'default-location' => (int) $this->menu->term_id ] );
set_theme_mod( 'nav_menu_locations', [ 'default-location' => (int) $this->public_menu->term_id ] );

$this->approved_comment = self::factory()->comment->create_and_get([
'comment_approved' => 1,
'comment_post_ID' => $this->published_post->ID,
]);

$this->unapproved_comment = self::factory()->comment->create_and_get([
'comment_approved' => 0,
]);

// $this->assertInstanceOf( \WP_User::class, $this->admin );
// $this->assertInstanceOf( \WP_Post::class, $this->published_post );
Expand Down Expand Up @@ -1098,7 +1126,7 @@ public function getQueries() {
}
',
'variables' => [
'id' => (int) $this->menu->term_id
'id' => (int) $this->public_menu->term_id
]
],
'listMenu' => [
Expand All @@ -1114,6 +1142,20 @@ public function getQueries() {
}
'
],
'singlePrivateMenu' => [
'name' => 'singlePrivateMenu',
'query' => '
query GetMenu($id:ID!) {
menu(id:$id idType: DATABASE_ID) {
__typename
databaseId
}
}
',
'variables' => [
'id' => (int) $this->private_menu->term_id
]
],
'listMenuItem' => [
'name' => 'listMenuItem',
'query' => '
Expand Down Expand Up @@ -1158,6 +1200,21 @@ public function getQueries() {
'id' => $this->child_menu_item->ID
]
],
'singlePrivateMenuItem' => [
'name' => 'singlePrivateMenuItem',
'query' => '
query GetMenuItem($id:ID!) {
menuItem(id:$id idType: DATABASE_ID) {
__typename
databaseId
parentDatabaseId
}
}
',
'variables' => [
'id' => $this->private_menu_item->ID,
]
],
'singleMediaItem' => [
'name' => 'singleMediaItem',
'query' => '
Expand Down Expand Up @@ -1312,17 +1369,17 @@ public function executeAndCacheQueries() {
* @return array
*/
public function getEvictedCaches() {
$empty = [];
$evicted = [];
if ( ! empty( $this->query_results ) ) {
foreach ( $this->query_results as $name => $result ) {
$cache = $this->collection->get( $result['cacheKey'] );
if ( empty( $cache ) ) {
$empty[] = $name;
$evicted[] = $name;
}
}
}

return $empty;
return $evicted;
}

/**
Expand Down
27 changes: 19 additions & 8 deletions tests/wpunit/MenuCacheInvalidationTest.php
Expand Up @@ -49,7 +49,7 @@ public function testAssignNavMenuToLocationEvictsQueriesForMenus() {
$this->assertEmpty( $this->getEvictedCaches() );

// assign the menu to a location. This should evict caches for queries for menus
set_theme_mod( 'nav_menu_locations', [ $location_name => (int) $this->menu->term_id ] );
set_theme_mod( 'nav_menu_locations', [ $location_name => (int) $this->public_menu->term_id ] );

$evicted_caches = $this->getEvictedCaches();

Expand All @@ -70,8 +70,8 @@ public function testUpdateMenuThatIsAssignedToALocationShouldEvictCaches() {

$this->assertEmpty( $this->getEvictedCaches() );

wp_update_nav_menu_object( $this->menu->term_id, [
'menu-name' => $this->menu->name,
wp_update_nav_menu_object( $this->public_menu->term_id, [
'menu-name' => $this->public_menu->name,
'description' => 'updated description...',
] );

Expand Down Expand Up @@ -122,7 +122,7 @@ public function testDeleteMenuAssignedToALocationShouldEvictCache() {

$this->assertEmpty( $this->getEvictedCaches() );

wp_delete_nav_menu( $this->menu->term_id );
wp_delete_nav_menu( $this->public_menu->term_id );

$evicted = $this->getEvictedCaches();

Expand All @@ -134,7 +134,18 @@ public function testDeleteMenuAssignedToALocationShouldEvictCache() {
'singleMenu',

// deleting a menu that was in the listMenu results should evict the query
'listMenu'
'listMenu',

// deleting a menu that had this menu item in it should purge queries
// for this menu item as it will also delete this menu item
'singleChildMenuItem',

// deleting a menu that had this menu item in it should purge queries
// for this menu item as it will also delete this menu item
'singleMenuItem',

// Deleting a public menu should invalidate a query for a list of menuItems
'listMenuItem'
], $evicted );


Expand All @@ -159,7 +170,7 @@ public function testUpdateTermMetaOnMenuAssignedToALocationEvictsCache() {
$this->assertEmpty( $this->getEvictedCaches() );

// update term meta on a public menu _should_ evict cache
update_term_meta( $this->menu->term_id, 'meta_key', uniqid( null, true ) );
update_term_meta( $this->public_menu->term_id, 'meta_key', uniqid( null, true ) );

$evicted = $this->getEvictedCaches();

Expand All @@ -179,7 +190,7 @@ public function testUpdateTermMetaOnMenuAssignedToALocationEvictsCache() {
public function testDeleteTermMetaOnMenuAssignedToALocationEvictsCache() {

// setup some term meta to start with
update_term_meta( $this->menu->term_id, 'meta_key', uniqid( null, true ) );
update_term_meta( $this->public_menu->term_id, 'meta_key', uniqid( null, true ) );

// reset caches as the update above would have evicted some
$this->_populateCaches();
Expand All @@ -188,7 +199,7 @@ public function testDeleteTermMetaOnMenuAssignedToALocationEvictsCache() {
$this->assertEmpty( $this->getEvictedCaches() );

// delete term meta on a public menu _should_ evict cache
delete_term_meta( $this->menu->term_id, 'meta_key' );
delete_term_meta( $this->public_menu->term_id, 'meta_key' );

$evicted = $this->getEvictedCaches();

Expand Down