Skip to content

Commit 9f87e74

Browse files
committed
feat: add cached elementor woocommerce page discovery and invalidation on product updates
1 parent fbbae68 commit 9f87e74

3 files changed

Lines changed: 399 additions & 0 deletions

File tree

src/Configuration/EventManagementConfiguration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public function modify(Container $container)
6060
// Plugin compatibility subscribers
6161
new Subscriber\Compatibility\ActionSchedulerSubscriber(),
6262
new Subscriber\Compatibility\DiviSubscriber(),
63+
new Subscriber\Compatibility\ElementorSubscriber($container['cloudfront_client']),
6364
new Subscriber\Compatibility\LifterLmsSubscriber(),
6465
new Subscriber\Compatibility\WooCommerceSubscriber($container['cloudfront_client'], $container['site_url'], $container['assets_url'], $container['ymir_cdn_image_processing_enabled'], $container['page_caching_options']),
6566
new Subscriber\Compatibility\WpAllImportSubscriber(),
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir WordPress plugin.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Plugin\Subscriber\Compatibility;
15+
16+
use Ymir\Plugin\EventManagement\SubscriberInterface;
17+
use Ymir\Plugin\PageCache\ContentDeliveryNetworkPageCacheClientInterface;
18+
use Ymir\Plugin\Support\Collection;
19+
20+
/**
21+
* Subscriber that handles Elementor compatibility.
22+
*/
23+
class ElementorSubscriber implements SubscriberInterface
24+
{
25+
/**
26+
* Transient key containing Elementor page IDs with WooCommerce loops.
27+
*
28+
* @var string
29+
*/
30+
private const LOOP_PAGE_IDS_TRANSIENT = 'ymir_elementor_wc_loop_page_ids';
31+
32+
/**
33+
* Client interacting with the content delivery network handling page caching.
34+
*
35+
* @var ContentDeliveryNetworkPageCacheClientInterface
36+
*/
37+
private $pageCacheClient;
38+
39+
/**
40+
* The page caching options.
41+
*
42+
* @var array
43+
*/
44+
private $pageCachingOptions;
45+
46+
/**
47+
* Constructor.
48+
*/
49+
public function __construct(ContentDeliveryNetworkPageCacheClientInterface $pageCacheClient, array $pageCachingOptions = [])
50+
{
51+
$this->pageCacheClient = $pageCacheClient;
52+
$this->pageCachingOptions = $pageCachingOptions;
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public static function getSubscribedEvents(): array
59+
{
60+
return [
61+
'woocommerce_update_product' => 'clearElementorLoopPages',
62+
'woocommerce_update_product_variation' => 'clearElementorLoopPages',
63+
'save_post_product' => ['clearElementorLoopPagesOnSave', 20, 3],
64+
'save_post_product_variation' => ['clearElementorLoopPagesOnSave', 20, 3],
65+
'save_post_page' => ['clearElementorLoopPageCache', 10, 3],
66+
'save_post_elementor_library' => ['clearElementorLoopPageCache', 10, 3],
67+
'deleted_post' => ['clearElementorLoopPageCacheOnDelete', 10, 1],
68+
];
69+
}
70+
71+
/**
72+
* Clear loop page discovery transient when Elementor content can change.
73+
*/
74+
public function clearElementorLoopPageCache(int $postId)
75+
{
76+
if ($this->isAutosaveOrRevision($postId)) {
77+
return;
78+
}
79+
80+
$this->clearElementorLoopPagesCacheTransient();
81+
}
82+
83+
/**
84+
* Clear loop page discovery transient when relevant posts are deleted.
85+
*/
86+
public function clearElementorLoopPageCacheOnDelete(int $postId)
87+
{
88+
if (!in_array(get_post_type($postId), ['page', 'elementor_library'], true)) {
89+
return;
90+
}
91+
92+
$this->clearElementorLoopPagesCacheTransient();
93+
}
94+
95+
/**
96+
* Clear cache for Elementor loop pages.
97+
*/
98+
public function clearElementorLoopPages()
99+
{
100+
if (empty($this->pageCachingOptions['invalidation_enabled'])) {
101+
return;
102+
}
103+
104+
$loopPagesIds = $this->getElementorLoopPageIds();
105+
106+
if ($loopPagesIds->isEmpty()) {
107+
return;
108+
}
109+
110+
$urlsToClear = $loopPagesIds->map(function (int $pageId) {
111+
return get_permalink($pageId);
112+
})->filter(function ($url) {
113+
return is_string($url) && '' !== $url;
114+
});
115+
116+
if ($urlsToClear->isEmpty()) {
117+
return;
118+
}
119+
120+
$this->pageCacheClient->clearUrls($urlsToClear);
121+
}
122+
123+
/**
124+
* Clear Elementor loop page cache when a product is saved directly.
125+
*/
126+
public function clearElementorLoopPagesOnSave(int $postId)
127+
{
128+
if ($this->isAutosaveOrRevision($postId)) {
129+
return;
130+
}
131+
132+
$this->clearElementorLoopPages();
133+
}
134+
135+
/**
136+
* Clear Elementor loop pages discovery transient.
137+
*/
138+
private function clearElementorLoopPagesCacheTransient(): void
139+
{
140+
delete_transient(self::LOOP_PAGE_IDS_TRANSIENT);
141+
}
142+
143+
/**
144+
* Get Elementor page IDs likely containing WooCommerce loops.
145+
*/
146+
private function getElementorLoopPageIds(): Collection
147+
{
148+
$loopPagesIds = get_transient(self::LOOP_PAGE_IDS_TRANSIENT);
149+
150+
if (!is_array($loopPagesIds)) {
151+
$loopPagesIds = (new Collection(get_posts([
152+
'post_type' => 'page',
153+
'post_status' => 'publish',
154+
'posts_per_page' => -1,
155+
'fields' => 'ids',
156+
'no_found_rows' => true,
157+
'meta_query' => [
158+
'relation' => 'AND',
159+
[
160+
'key' => '_elementor_data',
161+
'compare' => 'EXISTS',
162+
],
163+
[
164+
'relation' => 'OR',
165+
[
166+
'key' => '_elementor_data',
167+
'value' => 'woocommerce-products',
168+
'compare' => 'LIKE',
169+
],
170+
[
171+
'key' => '_elementor_data',
172+
'value' => 'archive-products',
173+
'compare' => 'LIKE',
174+
],
175+
[
176+
'key' => '_elementor_data',
177+
'value' => 'loop-grid',
178+
'compare' => 'LIKE',
179+
],
180+
[
181+
'key' => '_elementor_data',
182+
'value' => 'product_cat',
183+
'compare' => 'LIKE',
184+
],
185+
[
186+
'key' => '_elementor_data',
187+
'value' => 'product_tag',
188+
'compare' => 'LIKE',
189+
],
190+
],
191+
],
192+
])))->filter(function ($pageId) {
193+
return is_int($pageId) || ctype_digit((string) $pageId);
194+
})->map(function ($pageId) {
195+
return (int) $pageId;
196+
})->filter(function (int $pageId) {
197+
$elementorData = get_post_meta($pageId, '_elementor_data', true);
198+
199+
if (empty($elementorData) || !is_string($elementorData)) {
200+
return false;
201+
}
202+
203+
return str_contains($elementorData, 'archive-products')
204+
|| str_contains($elementorData, 'woocommerce-products')
205+
|| (str_contains($elementorData, 'loop-grid')
206+
&& (str_contains($elementorData, '"product"')
207+
|| str_contains($elementorData, 'product_cat')
208+
|| str_contains($elementorData, 'product_tag')));
209+
})->unique()->all();
210+
211+
set_transient(self::LOOP_PAGE_IDS_TRANSIENT, $loopPagesIds, 10 * MINUTE_IN_SECONDS);
212+
}
213+
214+
return new Collection($loopPagesIds);
215+
}
216+
217+
/**
218+
* Check if a post save is an autosave or revision.
219+
*/
220+
private function isAutosaveOrRevision(int $postId): bool
221+
{
222+
return (bool) wp_is_post_autosave($postId) || (bool) wp_is_post_revision($postId);
223+
}
224+
}

0 commit comments

Comments
 (0)