PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.8.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.8.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.7 2.6.8 2.6.5 2.6.4 2.6.3 2.6.2 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2.0 2.0 2.1.0 2.1.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1
reviews-feed / class / Common / RemoteRequest.php
reviews-feed / class / Common Last commit date
Admin 1 month ago Builder 1 month ago Customizer 1 month ago Exceptions 1 month ago Helpers 1 month ago Integrations 1 month ago Migrations 1 month ago ReviewAlerts 1 month ago Services 1 month ago Settings 1 month ago Support 1 month ago Traits 1 month ago Utils 1 month ago AuthorizationStatusCheck.php 1 month ago BusinessDataCache.php 1 month ago Clear_Cache.php 1 month ago Container.php 1 month ago DisplayElements.php 1 month ago Email_Notification.php 1 month ago Error_Reporter.php 1 month ago Feed.php 1 month ago FeedCache.php 1 month ago FeedCacheUpdater.php 1 month ago FeedDisplay.php 1 month ago Feed_Locator.php 1 month ago Parser.php 1 month ago PostAggregator.php 1 month ago RemoteRequest.php 1 month ago SBR_Education.php 1 month ago SBR_Settings.php 1 month ago ServiceContainer.php 1 month ago SinglePostCache.php 1 month ago TemplateRenderer.php 1 month ago Tooltip_Wizard.php 1 month ago Util.php 1 month ago
RemoteRequest.php
236 lines
1 <?php
2
3 /**
4 * Class RemoteRequest
5 *
6 * @since 1.0
7 */
8
9 namespace SmashBalloon\Reviews\Common;
10
11 use SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager;
12 use SmashBalloon\Reviews\Common\Integrations\SBRelay;
13 use SmashBalloon\Reviews\Common\Services\SettingsManagerService;
14
15 /**
16 * Summary of RemoteRequest
17 */
18 class RemoteRequest
19 {
20 public const BASE_URL = \SBR_RELAY_BASE_URL;
21
22 /**
23 * Per-request memoization for relay fetches. Key = sha256 of the
24 * (endpoint, provider, place_id, type, slug, language, starsFilter,
25 * api_key) tuple — the same fields that determine the upstream response
26 * shape. Two callers within the SAME PHP request asking for the same
27 * (provider, place_id) collapse to ONE relay round-trip.
28 *
29 * Why this exists: SMASH-1360 Phase 2. The customizer-load path on
30 * `feed_customizer_fly_preview` and the Save path on `builder_update`
31 * both build a `Feed` and call `Feed::do_remote_requests()`, which can
32 * iterate the same source twice with subtly different `info` arrays
33 * (one carrying `relay_source_id`, one not). The relay-side cache (PR #84
34 * `UpstreamResponseCache`) catches these at the upstream-billable
35 * boundary, but they still cost the WP host a full HTTP roundtrip to
36 * the relay. This in-process memo skips even the WP-side roundtrip when
37 * the same data was already fetched milliseconds ago.
38 *
39 * Key INTENTIONALLY excludes `source_id` and any `info[*]` discriminators
40 * that don't reach upstream (matches the relay-side EXCLUDED_PARAMS
41 * design). The two URL shapes the wp_lhr_log captured on demo-wp2
42 * 2026-05-06 (`?source_id=N&place_id=X` vs `?place_id=X`) hash to the
43 * same memo key under this design.
44 *
45 * Lifetime: a single PHP request. Cleared automatically when PHP-FPM
46 * recycles the worker. NOT persisted across requests — that's the
47 * relay-side cache's job. Bounded at 64 keys (typical Pro fleet has
48 * 1-30 sources per feed; we never need more than that in a single
49 * request).
50 *
51 * @var array<string, mixed>
52 */
53 private static $memo = [];
54
55 /**
56 * Memo cap — defensive upper bound so a pathological feed with hundreds
57 * of sources doesn't grow the static unbounded.
58 */
59 private const MEMO_MAX_KEYS = 64;
60
61 private $provider;
62
63 private $args;
64
65 private $endpoint;
66
67 /**
68 * Summary of __construct
69 * @param mixed $provider
70 * @param mixed $args
71 * @param mixed $endpoint
72 */
73 public function __construct($provider, $args, $endpoint = 'reviews')
74 {
75 $this->provider = $provider;
76 $this->args = $args;
77 $this->endpoint = $endpoint;
78 }
79
80 /**
81 * Summary of fetch
82 * @return array|string
83 */
84 public function fetch()
85 {
86 if (empty($this->args['business'])) {
87 return '';
88 }
89
90 $business = $this->args['business'];
91
92 // Build request args - always include place_id for fallback
93 // If source_id is available, include it too (preferred - encoding-immune)
94 // Relay middleware will use source_id first, fall back to place_id if needed
95 $args = [
96 'place_id' => $business,
97 ];
98
99 if (!empty($this->args['info']['relay_source_id'])) {
100 $args['source_id'] = (int) $this->args['info']['relay_source_id'];
101 }
102
103 // Add additional parameters
104 $args = array_merge($args, $this->buildBaseArgs());
105
106 // SMASH-1360 Phase 2: per-request memo — collapse identical
107 // (provider, place_id) lookups within a single PHP request.
108 // `source_id` is deliberately EXCLUDED from the memo key because
109 // it's plugin-side tracking and doesn't change what upstream returns.
110 $memo_key = $this->memo_key($args);
111 if ($memo_key !== null && isset(self::$memo[$memo_key])) {
112 return self::$memo[$memo_key];
113 }
114
115 $settings = new SettingsManagerService();
116 $relay = new SBRelay($settings);
117
118 $response = $relay->call($this->endpoint . '/' . $this->provider, $args, 'GET', true);
119
120 if ($memo_key !== null) {
121 // Defensive cap: if the memo grew past the bound (unlikely in
122 // practice — typical Pro feed has <30 sources × <2 endpoints =
123 // <60 keys per request), drop the oldest half to prevent
124 // unbounded growth in a long-running PHP-FPM worker.
125 if (count(self::$memo) >= self::MEMO_MAX_KEYS) {
126 self::$memo = array_slice(self::$memo, (int) (self::MEMO_MAX_KEYS / 2), null, true);
127 }
128 self::$memo[$memo_key] = $response;
129 }
130
131 return $response;
132 }
133
134 /**
135 * Build the memo key from the args that actually affect the upstream
136 * response. Returns null if the args contain non-scalar values (e.g.,
137 * an array under `info` that we'd hash unstably) — those bypass the
138 * memo and call upstream directly, mirroring the relay-side cache's
139 * conservative array-param policy.
140 *
141 * Public so unit tests can pin the contract.
142 *
143 * @param array $args The post-buildBaseArgs() argument set passed to
144 * SBRelay::call().
145 * @return string|null sha256 hex string, or null when args aren't
146 * safely hashable.
147 */
148 public function memo_key(array $args): ?string
149 {
150 $relevant = $args;
151
152 // Plugin-side tracking — NOT in upstream response shape.
153 unset($relevant['source_id']);
154
155 foreach ($relevant as $value) {
156 if (is_array($value) || is_object($value)) {
157 return null;
158 }
159 }
160
161 ksort($relevant);
162
163 return hash('sha256', $this->endpoint . '|' . $this->provider . '|' . http_build_query($relevant));
164 }
165
166 /**
167 * Test-only helper to flush the static memo between tests. PHPUnit
168 * doesn't reset class-level static state automatically; tests that
169 * exercise the memo need to call this in setUp/tearDown.
170 *
171 * Not part of the public plugin API — keep `@internal`.
172 *
173 * @internal
174 */
175 public static function flush_memo(): void
176 {
177 self::$memo = [];
178 }
179
180 /**
181 * Build base arguments that are common to all requests
182 *
183 * @return array
184 */
185 private function buildBaseArgs()
186 {
187 $args = [];
188
189 if ($this->provider === 'wordpress.org') {
190 $wordpressorg_args = SBR_Feed_Saver_Manager::get_place_id_wordpressorg($this->args['info']['url']);
191 $args['type'] = $wordpressorg_args['type'];
192 $args['slug'] = $wordpressorg_args['slug'];
193 }
194
195 // SMASH-782 Phase 2 — RapidAPI providers (Airbnb, Booking, AliExpress)
196 // validate a provider-specific id param at the controller level
197 // (`property_id`/`hotel_id`/`item_id`). The relay's middleware reads
198 // place_id for source lookup, but the controller's `$request->validate()`
199 // requires the typed id and rejects the request with 422 when missing.
200 // Forward the source's business id under the correct param name so
201 // both validations pass. Mirrors the per-route alias logic in the
202 // relay's NormalizesRapidAPIParameters trait.
203 $rapidapi_id_param = [
204 'airbnb' => 'property_id',
205 'booking' => 'hotel_id',
206 'aliexpress' => 'item_id',
207 ];
208 if (
209 isset($rapidapi_id_param[$this->provider])
210 && !empty($this->args['business'])
211 ) {
212 $args[$rapidapi_id_param[$this->provider]] = (string) $this->args['business'];
213 }
214
215 if ($this->provider !== 'facebook') {
216 $api_keys = get_option('sbr_apikeys', []);
217 if (!empty($api_keys[$this->provider])) {
218 $args['api_key'] = $api_keys[$this->provider];
219 }
220 } else {
221 $args['api_key'] = !empty($this->args['access_token']) ? $this->args['access_token'] : '';
222 }
223
224 if (!empty($this->args['language']) && $this->args['language'] !== 'default') {
225 $args['language'] = $this->args['language'];
226 }
227
228 if (!empty($this->args['starsFilter']) && $this->args['starsFilter'] !== '') {
229 $args['starsFilter'] = $this->args['starsFilter'];
230 }
231
232 return $args;
233 }
234
235 }
236