diff --git a/src/Base/RestAPI/ItemFields/FeaturedImageField.php b/src/Base/RestAPI/ItemFields/FeaturedImageField.php index d1ec048..5c944e9 100644 --- a/src/Base/RestAPI/ItemFields/FeaturedImageField.php +++ b/src/Base/RestAPI/ItemFields/FeaturedImageField.php @@ -7,6 +7,13 @@ class FeaturedImageField extends CreatesFields { + private const CACHE_KEY = 'openpub_featured_image_%d'; + + /** + * Attachment meta that ends up in the cached field. + */ + private const CACHED_META_KEYS = ['_wp_attachment_metadata', '_wp_attached_file', '_wp_attachment_image_alt']; + /** * Gets the featured image of a post. * @@ -21,7 +28,7 @@ public function create(WP_Post $post): array } $id = \get_post_thumbnail_id($post->ID); - $cacheKey = sprintf('openpub_featured_image_%d', $id); + $cacheKey = sprintf(self::CACHE_KEY, $id); $cached = get_transient($cacheKey); if (is_array($cached)) { @@ -50,6 +57,21 @@ public function create(WP_Post $post): array return $result; } + public function clearCache(int $attachmentId): void + { + delete_transient(sprintf(self::CACHE_KEY, $attachmentId)); + } + + /** + * Image editor saves (crop, rotate, scale, restore) only touch attachment meta. + */ + public function clearCacheOnMetaChange(int $metaId, int $objectId, string $metaKey): void + { + if (in_array($metaKey, self::CACHED_META_KEYS, true)) { + $this->clearCache($objectId); + } + } + /** * Get meta data of an attachment. */ diff --git a/src/Base/RestAPI/RestAPIServiceProvider.php b/src/Base/RestAPI/RestAPIServiceProvider.php index c9c1d74..4e10d25 100644 --- a/src/Base/RestAPI/RestAPIServiceProvider.php +++ b/src/Base/RestAPI/RestAPIServiceProvider.php @@ -7,6 +7,7 @@ use OWC\OpenPub\Base\RestAPI\Controllers\SearchController; use OWC\OpenPub\Base\RestAPI\Controllers\SettingsController; use OWC\OpenPub\Base\RestAPI\Controllers\ThemeController; +use OWC\OpenPub\Base\RestAPI\ItemFields\FeaturedImageField; use WP_REST_Server; class RestAPIServiceProvider extends ServiceProvider @@ -22,6 +23,13 @@ public function register(): void $this->plugin->loader->addFilter('rest_api_init', $this, 'registerRoutes'); $this->plugin->loader->addFilter('owc/config-expander/rest-api/whitelist', $this, 'whitelist', 10, 1); + // Before cache purges on the default priority, so a portal refetch never rebuilds from the stale image. + $featuredImageField = new FeaturedImageField($this->plugin); + $this->plugin->loader->addAction('updated_post_meta', $featuredImageField, 'clearCacheOnMetaChange', 5, 3); + $this->plugin->loader->addAction('added_post_meta', $featuredImageField, 'clearCacheOnMetaChange', 5, 3); + $this->plugin->loader->addAction('edit_attachment', $featuredImageField, 'clearCache', 5, 1); + $this->plugin->loader->addAction('delete_attachment', $featuredImageField, 'clearCache', 5, 1); + $this->registerModelFields(); } diff --git a/tests/Unit/Base/RestAPI/ItemFields/FeaturedImageFieldTest.php b/tests/Unit/Base/RestAPI/ItemFields/FeaturedImageFieldTest.php index fb518e1..55e9e51 100644 --- a/tests/Unit/Base/RestAPI/ItemFields/FeaturedImageFieldTest.php +++ b/tests/Unit/Base/RestAPI/ItemFields/FeaturedImageFieldTest.php @@ -224,4 +224,45 @@ public function it_returns_a_correct_featured_image_response_with_multiple_size( ]; $this->assertEquals($expected, $actual); } + + /** @test */ + public function it_clears_the_cached_image_of_an_attachment() + { + WP_Mock::userFunction('delete_transient', [ + 'args' => ['openpub_featured_image_2'], + 'times' => 1, + ]); + + (new FeaturedImageField($this->plugin))->clearCache(2); + + $this->assertTrue(true); + } + + /** @test */ + public function it_clears_the_cached_image_when_the_attachment_is_edited() + { + WP_Mock::userFunction('delete_transient', [ + 'args' => ['openpub_featured_image_2'], + 'times' => 3, + ]); + + $featuredImageField = new FeaturedImageField($this->plugin); + $featuredImageField->clearCacheOnMetaChange(10, 2, '_wp_attachment_metadata'); + $featuredImageField->clearCacheOnMetaChange(10, 2, '_wp_attached_file'); + $featuredImageField->clearCacheOnMetaChange(10, 2, '_wp_attachment_image_alt'); + + $this->assertTrue(true); + } + + /** @test */ + public function it_keeps_the_cached_image_on_unrelated_meta_changes() + { + WP_Mock::userFunction('delete_transient', [ + 'times' => 0, + ]); + + (new FeaturedImageField($this->plugin))->clearCacheOnMetaChange(10, 2, '_edit_lock'); + + $this->assertTrue(true); + } } diff --git a/tests/Unit/Base/RestAPI/RestAPIServiceProviderTest.php b/tests/Unit/Base/RestAPI/RestAPIServiceProviderTest.php index f75aa84..09cf2b7 100644 --- a/tests/Unit/Base/RestAPI/RestAPIServiceProviderTest.php +++ b/tests/Unit/Base/RestAPI/RestAPIServiceProviderTest.php @@ -6,6 +6,7 @@ use OWC\OpenPub\Base\Foundation\Config; use OWC\OpenPub\Base\Foundation\Loader; use OWC\OpenPub\Base\Foundation\Plugin; +use OWC\OpenPub\Base\RestAPI\ItemFields\FeaturedImageField; use OWC\OpenPub\Base\RestAPI\RestAPIServiceProvider; use OWC\OpenPub\Tests\TestCase; use WP_Mock; @@ -69,6 +70,26 @@ public function check_registration_of_rest_endpoints() 'registerRoutes' ])->once(); + foreach (['updated_post_meta', 'added_post_meta'] as $hook) { + $plugin->loader->shouldReceive('addAction')->withArgs([ + $hook, + m::type(FeaturedImageField::class), + 'clearCacheOnMetaChange', + 5, + 3 + ])->once(); + } + + foreach (['edit_attachment', 'delete_attachment'] as $hook) { + $plugin->loader->shouldReceive('addAction')->withArgs([ + $hook, + m::type(FeaturedImageField::class), + 'clearCache', + 5, + 1 + ])->once(); + } + $configRestAPIFields = [ 'posttype1' => [ 'endpoint_field1' =>