PluginProbe
PostNL for WooCommerce / 5.9.12
PostNL for WooCommerce v5.9.12
5.9.12 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 All 72 releases
woo-postnl / src / Rest_API / Service_Factory.php

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

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