PluginProbe
PostNL for WooCommerce / trunk
PostNL for WooCommerce vtrunk
5.9.11 5.9.10 5.9.9 5.9.8 5.9.7 5.9.6 trunk 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.1.4 3.1.5 3.1.6 3.1.7 4.0.0 4.0.1 4.0.2 4.3.2 4.3.3 4.4.0 4.4.1 4.4.2 All 71 releases
woo-postnl / src / Rest_API / Service_Factory.php

Service_Factory.php in PostNL for WooCommerce trunk, at src/Rest_API/Service_Factory.php

437 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Rest_API\Service_Factory file.
4 *
5 * @package PostNLWooCommerce\Rest_API
6 */
7
8 declare( strict_types = 1 );
9
10 namespace PostNLWooCommerce\Rest_API;
11
12 use PostNLWooCommerce\Logger;
13 use PostNLWooCommerce\Rest_API\Contracts\Barcode_Service_Interface;
14 use PostNLWooCommerce\Rest_API\Contracts\Label_Service_Interface;
15 use PostNLWooCommerce\Rest_API\Contracts\Pickup_Location_Service_Interface;
16 use PostNLWooCommerce\Rest_API\Contracts\Postcode_Check_Service_Interface;
17 use PostNLWooCommerce\Rest_API\Contracts\Return_Label_Service_Interface;
18 use PostNLWooCommerce\Rest_API\Contracts\Smart_Returns_Service_Interface;
19 use PostNLWooCommerce\Rest_API\Contracts\Timeframe_Service_Interface;
20 use PostNLWooCommerce\Rest_API\Legacy\Barcode_Service as Legacy_Barcode_Service;
21 use PostNLWooCommerce\Rest_API\Legacy\Checkout_Service as Legacy_Checkout_Service;
22 use PostNLWooCommerce\Rest_API\Legacy\Label_Service as Legacy_Label_Service;
23 use PostNLWooCommerce\Rest_API\Legacy\Letterbox_Service as Legacy_Letterbox_Service;
24 use PostNLWooCommerce\Rest_API\Legacy\Postcode_Check_Service as Legacy_Postcode_Check_Service;
25 use PostNLWooCommerce\Rest_API\Legacy\Return_Label_Service as Legacy_Return_Label_Service;
26 use PostNLWooCommerce\Rest_API\Legacy\Smart_Returns_Service as Legacy_Smart_Returns_Service;
27 use PostNLWooCommerce\Rest_API\SDK\Client_Factory;
28 use PostNLWooCommerce\Rest_API\SDK\Logger_Adapter;
29 use PostNLWooCommerce\Rest_API\V4\Label\Service as V4_Label_Service;
30 use PostNLWooCommerce\Rest_API\V4\Returns\Service as V4_Returns_Service;
31 use PostNLWooCommerce\Rest_API\V4\Returns\Smart_Returns_Service as V4_Smart_Returns_Service;
32 use Psr\Log\LoggerInterface;
33
34 if ( ! defined( 'ABSPATH' ) ) {
35 exit;
36 }
37
38 /**
39 * Class Service_Factory
40 *
41 * Single factory that resolves the correct service implementation per flow.
42 * Every method returns the Legacy service unless all three conditions hold:
43 * (a) a validated "New API Key" is present on the settings object,
44 * (b) Router::sdk_enabled_for() returns true for the flow, and
45 * (c) a V4 service has been registered for that flow via inject_v4_service().
46 *
47 * postcode_check_service() is permanently wired to Legacy because postcode_check
48 * is intentionally absent from Router::SUPPORTED_FLOWS.
49 *
50 * The 'shipment_and_return' SUPPORTED_FLOW has no factory method yet: no service
51 * wrapper or interface exists for it on this branch. The S&R method lands with
52 * its interface when that flow is migrated.
53 *
54 * The 'checkout' SUPPORTED_FLOW is never queried directly; the checkout endpoint
55 * is split here into the 'timeframe' and 'pickup_location' flows, both backed by
56 * the shared Legacy\Checkout_Service.
57 *
58 * Legacy services are created lazily on first access and memoised so repeated
59 * calls within a request are cheap.
60 *
61 * @since 5.9.9
62 * @package PostNLWooCommerce\Rest_API
63 */
64 class Service_Factory {
65
66 /**
67 * Plugin settings instance used to detect a V4 API key.
68 * Null when no settings object is available yet (e.g. early bootstrap).
69 *
70 * @var object|null
71 */
72 private $settings;
73
74 /**
75 * V4 service instances keyed by flow name.
76 * Populated via inject_v4_service(); future SDK implementations are registered here.
77 * Also used in unit tests to inject test doubles.
78 *
79 * @var array<string, object>
80 */
81 private $v4_services = array();
82
83 /**
84 * Memoised service instances keyed by flow name (or 'checkout' for the shared
85 * Legacy\Checkout_Service used by both timeframe and pickup_location).
86 * May be pre-seeded via set_legacy_service() in unit tests to avoid
87 * instantiating Order\Base-derived classes that require WooCommerce.
88 *
89 * @var array<string, object>
90 */
91 private $legacy_memos = array();
92
93 /**
94 * Memoised self-built V4 label service.
95 *
96 * Deliberately kept out of $v4_services so that array keeps its single meaning —
97 * "a V4 service was explicitly injected for this flow" — which barcode_from_label()
98 * depends on.
99 *
100 * @var V4_Label_Service|null
101 */
102 private $label_v4_memo = null;
103
104 /**
105 * Memoised self-built V4 returns service.
106 *
107 * Kept out of $v4_services for the same reason as $label_v4_memo: that array
108 * means "a V4 service was explicitly injected for this flow", and giving it a
109 * second meaning is how a later predicate reading it gets a wrong answer.
110 *
111 * @var V4_Returns_Service|null
112 */
113 private $return_label_v4_memo = null;
114
115 /**
116 * Memoised self-built V4 smart-returns service.
117 *
118 * Kept out of $v4_services for the same reason as $label_v4_memo: that array
119 * means "a V4 service was explicitly injected for this flow", and giving it a
120 * second meaning is how a later predicate reading it gets a wrong answer.
121 *
122 * @var V4_Smart_Returns_Service|null
123 */
124 private $smart_returns_v4_memo = null;
125
126 /**
127 * Service_Factory constructor.
128 *
129 * @param object|null $settings Plugin settings instance, or null when unavailable.
130 */
131 public function __construct( $settings = null ) {
132 $this->settings = $settings;
133 }
134
135 /**
136 * Register a V4 service for a specific flow.
137 *
138 * Called during V4 wiring once SDK service classes exist.
139 * Also used in unit tests to inject lightweight test doubles.
140 *
141 * @param string $flow Flow identifier (should be in Router::SUPPORTED_FLOWS).
142 * @param object $service V4 service instance implementing the flow's interface.
143 * @return void
144 */
145 public function inject_v4_service( string $flow, object $service ): void {
146 $this->v4_services[ $flow ] = $service;
147 }
148
149 /**
150 * Pre-seed the memoisation store with a ready-built service instance.
151 *
152 * Used exclusively in unit tests to avoid instantiating Legacy\Label_Service,
153 * Legacy\Letterbox_Service, and Legacy\Return_Label_Service, which extend
154 * Order\Base and require WooCommerce constants and Settings::get_instance()
155 * in their constructors. Not intended for production use.
156 *
157 * @param string $flow Flow identifier.
158 * @param object $service Service instance implementing the flow's interface.
159 * @return void
160 */
161 public function set_legacy_service( string $flow, object $service ): void {
162 $this->legacy_memos[ $flow ] = $service;
163 }
164
165 /**
166 * Return the barcode service for the current configuration.
167 *
168 * @return Barcode_Service_Interface
169 */
170 public function barcode_service(): Barcode_Service_Interface {
171 if ( $this->should_use_v4( 'barcode' ) && isset( $this->v4_services['barcode'] ) ) {
172 return $this->v4_services['barcode'];
173 }
174 if ( ! isset( $this->legacy_memos['barcode'] ) ) {
175 $this->legacy_memos['barcode'] = new Legacy_Barcode_Service();
176 }
177 return $this->legacy_memos['barcode'];
178 }
179
180 /**
181 * Whether the barcode is issued by the label response instead of a standalone
182 * barcode request.
183 *
184 * Gated on the same condition as label_service(), so the reorder can never select
185 * a Legacy label service that still expects a prefetched barcode.
186 *
187 * @return bool
188 *
189 * @since 6.0.0
190 */
191 public function barcode_from_label(): bool {
192 return $this->should_use_v4( 'label' ) && isset( $this->v4_services['label'] );
193 }
194
195 /**
196 * Return the timeframe (delivery options) service for the current configuration.
197 *
198 * @return Timeframe_Service_Interface
199 */
200 public function timeframe_service(): Timeframe_Service_Interface {
201 if ( $this->should_use_v4( 'timeframe' ) && isset( $this->v4_services['timeframe'] ) ) {
202 return $this->v4_services['timeframe'];
203 }
204 return $this->legacy_checkout_service();
205 }
206
207 /**
208 * Return the pickup-location service for the current configuration.
209 *
210 * @return Pickup_Location_Service_Interface
211 */
212 public function pickup_location_service(): Pickup_Location_Service_Interface {
213 if ( $this->should_use_v4( 'pickup_location' ) && isset( $this->v4_services['pickup_location'] ) ) {
214 return $this->v4_services['pickup_location'];
215 }
216 return $this->legacy_checkout_service();
217 }
218
219 /**
220 * Return the outbound shipping label service for the current configuration.
221 *
222 * @return Label_Service_Interface
223 */
224 public function label_service(): Label_Service_Interface {
225 if ( $this->should_use_v4( 'label' ) ) {
226 // A service injected via inject_v4_service() wins; otherwise build the real V4 service.
227 // The per-combination V4_Mapper gate lives inside the service, which falls back to the
228 // legacy pipeline for anything outside the happy-path domestic parcel.
229 if ( isset( $this->v4_services['label'] ) ) {
230 return $this->v4_services['label'];
231 }
232 // Memoised apart from $v4_services on purpose. That array means "deliberately
233 // injected", and barcode_from_label() reads it to decide whether Order\Base may
234 // skip the barcode prefetch. Caching a self-built instance there would flip that
235 // answer mid-request: in a bulk run the first order prefetches a barcode and
236 // builds this service, and every later order on the same Order\Bulk instance
237 // would then skip the prefetch and hand Shipping\Item_Info no main_barcode.
238 if ( null === $this->label_v4_memo ) {
239 $logger = $this->v4_logger();
240 $this->label_v4_memo = new V4_Label_Service(
241 new Client_Factory( $this->settings, $logger ),
242 (string) $this->settings->get_api_key_new(),
243 $logger
244 );
245 }
246 return $this->label_v4_memo;
247 }
248 if ( ! isset( $this->legacy_memos['label'] ) ) {
249 $this->legacy_memos['label'] = new Legacy_Label_Service();
250 }
251 return $this->legacy_memos['label'];
252 }
253
254 /**
255 * Return the letterbox label service for the current configuration.
256 *
257 * @return Label_Service_Interface
258 */
259 public function letterbox_service(): Label_Service_Interface {
260 if ( $this->should_use_v4( 'letterbox' ) && isset( $this->v4_services['letterbox'] ) ) {
261 return $this->v4_services['letterbox'];
262 }
263 if ( ! isset( $this->legacy_memos['letterbox'] ) ) {
264 $this->legacy_memos['letterbox'] = new Legacy_Letterbox_Service();
265 }
266 return $this->legacy_memos['letterbox'];
267 }
268
269 /**
270 * Return the return-label service for the current configuration.
271 *
272 * @return Return_Label_Service_Interface
273 */
274 public function return_label_service(): Return_Label_Service_Interface {
275 if ( $this->should_use_v4( 'return_label' ) ) {
276 // A service injected via inject_v4_service() wins; otherwise build the real V4
277 // service, which handles the NL retailPrint return and falls back to the legacy
278 // pipeline for the rest.
279 if ( isset( $this->v4_services['return_label'] ) ) {
280 return $this->v4_services['return_label'];
281 }
282 // Memoised apart from $v4_services for the same reason as label_service():
283 // that array means "deliberately injected", and barcode_from_label() reads it.
284 // Caching a self-built instance there would give the array two meanings, and
285 // the next predicate written against it would silently get the wrong answer.
286 if ( null === $this->return_label_v4_memo ) {
287 $logger = $this->v4_logger();
288 $this->return_label_v4_memo = new V4_Returns_Service(
289 new Client_Factory( $this->settings, $logger ),
290 (string) $this->settings->get_api_key_new(),
291 $logger
292 );
293 }
294 return $this->return_label_v4_memo;
295 }
296 if ( ! isset( $this->legacy_memos['return_label'] ) ) {
297 $this->legacy_memos['return_label'] = new Legacy_Return_Label_Service();
298 }
299 return $this->legacy_memos['return_label'];
300 }
301
302 /**
303 * Return the postcode-check service.
304 *
305 * Always returns Legacy — postcode_check is not in Router::SUPPORTED_FLOWS
306 * and is not planned for V4 routing.
307 *
308 * @return Postcode_Check_Service_Interface
309 */
310 public function postcode_check_service(): Postcode_Check_Service_Interface {
311 if ( ! isset( $this->legacy_memos['postcode_check'] ) ) {
312 $this->legacy_memos['postcode_check'] = new Legacy_Postcode_Check_Service();
313 }
314 return $this->legacy_memos['postcode_check'];
315 }
316
317 /**
318 * Return the smart-returns service for the current configuration.
319 *
320 * @return Smart_Returns_Service_Interface
321 */
322 public function smart_returns_service(): Smart_Returns_Service_Interface {
323 if ( $this->should_use_v4( 'smart_returns' ) ) {
324 // A service injected via inject_v4_service() wins; otherwise build the real V4
325 // service, which handles the NL retailPrint Smart Return and falls back to the
326 // legacy pipeline for the rest.
327 if ( isset( $this->v4_services['smart_returns'] ) ) {
328 return $this->v4_services['smart_returns'];
329 }
330 // Memoised apart from $v4_services for the same reason as label_service():
331 // that array means "deliberately injected", and barcode_from_label() reads it.
332 // Caching a self-built instance there would give the array two meanings, and
333 // the next predicate written against it would silently get the wrong answer.
334 if ( null === $this->smart_returns_v4_memo ) {
335 $logger = $this->v4_logger();
336 $this->smart_returns_v4_memo = new V4_Smart_Returns_Service(
337 new Client_Factory( $this->settings, $logger ),
338 (string) $this->settings->get_api_key_new(),
339 $logger
340 );
341 }
342 return $this->smart_returns_v4_memo;
343 }
344 if ( ! isset( $this->legacy_memos['smart_returns'] ) ) {
345 $this->legacy_memos['smart_returns'] = new Legacy_Smart_Returns_Service();
346 }
347 return $this->legacy_memos['smart_returns'];
348 }
349
350 /**
351 * Return the shared Legacy\Checkout_Service instance.
352 *
353 * Both timeframe_service() and pickup_location_service() delegate here because
354 * the PostNL checkout endpoint returns delivery options and pickup locations in
355 * a single response — one service instance covers both flows.
356 *
357 * @return Legacy_Checkout_Service
358 */
359 private function legacy_checkout_service(): Legacy_Checkout_Service {
360 if ( ! isset( $this->legacy_memos['checkout'] ) ) {
361 $this->legacy_memos['checkout'] = new Legacy_Checkout_Service();
362 }
363 return $this->legacy_memos['checkout'];
364 }
365
366 /**
367 * Build the PSR-3 logger the V4 services and the SDK transport report through.
368 *
369 * Equivalent to Main::get_logger() wrapped in a Logger_Adapter — the wiring the
370 * V4 services document — but built from the settings object this factory was
371 * handed rather than reaching back for the Settings singleton, so the factory
372 * has a single source of settings.
373 *
374 * Only called from a branch has_v4_key() already guarded, so $this->settings is
375 * never null here.
376 *
377 * @return LoggerInterface
378 */
379 private function v4_logger(): LoggerInterface {
380 return new Logger_Adapter( new Logger( (bool) $this->settings->is_logging_enabled() ) );
381 }
382
383 /**
384 * Return whether V4 routing should be used for the given flow.
385 *
386 * Short-circuits on the key check so Router (and its filter) is never consulted
387 * when no V4 key is configured.
388 *
389 * Deferred by design: product-coded flows (barcode, label, letterbox,
390 * return_label, smart_returns) must additionally gate on
391 * V4_Mapper::has_v4_equivalent(...). That gate needs a resolved product
392 * combination, which only exists once each flow builds its request, so it is
393 * wired in alongside the V4 services that need it.
394 *
395 * @param string $flow Flow identifier.
396 * @return bool
397 */
398 private function should_use_v4( string $flow ): bool {
399 return $this->has_v4_key() && Router::sdk_enabled_for( $flow );
400 }
401
402 /**
403 * Return whether a validated V4-capable API key is available on the settings object.
404 *
405 * The V4-capable key is the separate "New API Key" field, not the original key.
406 * It only becomes usable once a save-time validation call has confirmed it, so an
407 * entered-but-unvalidated key must never route traffic to V4.
408 *
409 * get_effective_api_key() is deliberately not used here: it answers "which key do
410 * we send" and falls back to the original key, so it is never empty and would
411 * report a V4 key on every site.
412 *
413 * Returns false when: no settings object was injected; the settings object does not
414 * yet expose the new-key accessors (that field ships in its own in-progress PR); the
415 * key is empty or whitespace-only; or the entered key has not passed validation.
416 *
417 * @return bool
418 */
419 private function has_v4_key(): bool {
420 if ( null === $this->settings ) {
421 return false;
422 }
423 if ( ! method_exists( $this->settings, 'get_api_key_new' )
424 || ! method_exists( $this->settings, 'is_api_key_new_validated' ) ) {
425 return false;
426 }
427
428 $key = $this->settings->get_api_key_new();
429
430 if ( ! is_string( $key ) || '' === trim( $key ) ) {
431 return false;
432 }
433
434 return true === $this->settings->is_api_key_new_validated();
435 }
436 }
437