PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / trunk
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More vtrunk
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 6 days ago Customizer 6 days ago Exceptions 6 months ago Helpers 2 months ago Integrations 6 days ago Migrations 3 years ago ReviewAlerts 3 weeks ago Services 6 days ago Settings 6 days ago Support 3 months ago Traits 1 month ago UsageTracking 6 days ago Utils 2 months ago AuthorizationStatusCheck.php 1 month ago BusinessDataCache.php 6 months ago Clear_Cache.php 3 months ago Container.php 6 months ago DisplayElements.php 2 months ago Email_Notification.php 6 months ago Error_Reporter.php 3 months ago Feed.php 3 weeks ago FeedCache.php 5 months ago FeedCacheUpdater.php 3 months ago FeedDisplay.php 1 month ago Feed_Locator.php 6 months ago Parser.php 3 weeks ago PostAggregator.php 2 months ago RemoteRequest.php 6 days ago SBR_Education.php 3 months ago SBR_Schema_Service.php 3 weeks ago SBR_Settings.php 6 months ago ServiceContainer.php 6 days ago SinglePostCache.php 3 weeks ago TemplateRenderer.php 6 months ago Tooltip_Wizard.php 1 month ago Util.php 6 days ago
RemoteRequest.php
239 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 // `info` is empty on a source row that never completed a fetch, so the
191 // url can be absent. Reading it blind raised "Undefined array key".
192 $info = isset($this->args['info']) && is_array($this->args['info']) ? $this->args['info'] : [];
193 $wordpressorg_args = SBR_Feed_Saver_Manager::get_place_id_wordpressorg($info['url'] ?? null);
194 $args['type'] = $wordpressorg_args['type'];
195 $args['slug'] = $wordpressorg_args['slug'];
196 }
197
198 // SMASH-782 Phase 2 — RapidAPI providers (Airbnb, Booking, AliExpress)
199 // validate a provider-specific id param at the controller level
200 // (`property_id`/`hotel_id`/`item_id`). The relay's middleware reads
201 // place_id for source lookup, but the controller's `$request->validate()`
202 // requires the typed id and rejects the request with 422 when missing.
203 // Forward the source's business id under the correct param name so
204 // both validations pass. Mirrors the per-route alias logic in the
205 // relay's NormalizesRapidAPIParameters trait.
206 $rapidapi_id_param = [
207 'airbnb' => 'property_id',
208 'booking' => 'hotel_id',
209 'aliexpress' => 'item_id',
210 ];
211 if (
212 isset($rapidapi_id_param[$this->provider])
213 && !empty($this->args['business'])
214 ) {
215 $args[$rapidapi_id_param[$this->provider]] = (string) $this->args['business'];
216 }
217
218 if ($this->provider !== 'facebook') {
219 $api_keys = get_option('sbr_apikeys', []);
220 if (!empty($api_keys[$this->provider])) {
221 $args['api_key'] = $api_keys[$this->provider];
222 }
223 } else {
224 $args['api_key'] = !empty($this->args['access_token']) ? $this->args['access_token'] : '';
225 }
226
227 if (!empty($this->args['language']) && $this->args['language'] !== 'default') {
228 $args['language'] = $this->args['language'];
229 }
230
231 if (!empty($this->args['starsFilter']) && $this->args['starsFilter'] !== '') {
232 $args['starsFilter'] = $this->args['starsFilter'];
233 }
234
235 return $args;
236 }
237
238 }
239