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 / SDK / Client_Factory.php

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

216 lines 7.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\SDK\Client_Factory file.
4 *
5 * @package PostNLWooCommerce\Rest_API\SDK
6 */
7
8 declare( strict_types = 1 );
9
10 namespace PostNLWooCommerce\Rest_API\SDK;
11
12 use Postnl\Sdk\Auth\Auth;
13 use Postnl\Sdk\Client\ClientBuilder;
14 use Postnl\Sdk\Client\PostnlClientInterface;
15 use Postnl\Sdk\Enums\Version;
16 use Postnl\Sdk\Transport\HttpPluginInterface;
17 use Postnl\Sdk\Transport\Retry\RetryConfig;
18 use Psr\Log\LoggerInterface;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Class Client_Factory
26 *
27 * Builds a configured PostNL V4 SDK client from the installed SDK.
28 * Clients are memoized per (key-hash, sandbox, customer) combination so
29 * checkout does not reconstruct the SDK client on every request within a
30 * single PHP request cycle.
31 *
32 * @since 5.9.9
33 * @package PostNLWooCommerce\Rest_API\SDK
34 */
35 class Client_Factory {
36
37 /**
38 * SourceSystem identifier reserved for this plugin.
39 * PostNL confirmed on 2026-05-21 that SourceSystem 35 is reused for V4.
40 */
41 private const SOURCE_SYSTEM = '35';
42
43 /**
44 * Plugin settings instance.
45 *
46 * @var object
47 */
48 private $settings;
49
50 /**
51 * Memoized SDK client instances keyed by configuration hash.
52 *
53 * Keys use sha1(v4_key)|sandbox|customer_number|customer_code so the raw
54 * API key is never stored in memory as a visible array key.
55 *
56 * @var array<string, PostnlClientInterface>
57 */
58 private $memo = array();
59
60 /**
61 * PSR-3 logger handed to the SDK, or null to leave SDK logging off.
62 *
63 * @var LoggerInterface|null
64 */
65 private $logger;
66
67 /**
68 * Client_Factory constructor.
69 *
70 * The logger is optional because this factory is shared infrastructure that
71 * every V4 service builds on, and a caller with nothing to log should not have
72 * to construct a null object. Callers that do want SDK request/response
73 * logging pass a Logger_Adapter.
74 *
75 * @since 6.0.0 Added the optional $logger parameter.
76 *
77 * @param object $settings Plugin settings instance.
78 * @param LoggerInterface|null $logger PSR-3 logger for SDK request/response logging.
79 */
80 public function __construct( object $settings, ?LoggerInterface $logger = null ) {
81 $this->settings = $settings;
82 $this->logger = $logger;
83 }
84
85 /**
86 * Return a configured SDK client for the given V4 API key and environment.
87 *
88 * Repeated calls with the same arguments return the same memoized instance.
89 *
90 * The raw V4 key is hashed before it is used as the cache key, so it is not
91 * exposed by dumping the memo. The key parameter is additionally marked
92 * SensitiveParameter so PHP redacts it from exception stack traces and
93 * backtraces; the SDK applies the same protection at its own boundary and
94 * redacts the key from logs and var_dump output.
95 *
96 * @param string $v4_key PostNL V4 API key. Must not be empty.
97 * @param bool $is_sandbox Whether to target the sandbox environment.
98 * @return PostnlClientInterface
99 * @throws \Postnl\Sdk\Exception\InvalidArgumentSdkException When $v4_key is empty or whitespace-only.
100 */
101 public function build(
102 #[\SensitiveParameter]
103 string $v4_key,
104 bool $is_sandbox
105 ): PostnlClientInterface {
106 // Cast at the boundary: the Settings getters are untyped and may return non-string option data,
107 // which would TypeError against make_builder( string ... ) under strict_types.
108 $customer_number = (string) $this->settings->get_customer_num();
109 $customer_code = (string) $this->settings->get_customer_code();
110
111 // Any future with*() value sourced from settings or request data must be added to this key,
112 // or memoization will return a client configured for a different combination.
113 //
114 // Every variable segment is hashed to a fixed width before being joined. Concatenating the
115 // raw values would let a '|' inside one of them shift the boundary, so that ( '1|2', '3' )
116 // and ( '1', '2|3' ) produced the same key and collided onto one client. The customer
117 // values are country-scoped settings, so they can legitimately differ within a request.
118 $cache_key = implode(
119 '|',
120 array(
121 sha1( $v4_key ),
122 $is_sandbox ? '1' : '0',
123 sha1( $customer_number ),
124 sha1( $customer_code ),
125 )
126 );
127
128 if ( ! isset( $this->memo[ $cache_key ] ) ) {
129 $this->memo[ $cache_key ] = $this->make_builder( $v4_key, $is_sandbox, $customer_number, $customer_code )->make();
130 }
131
132 return $this->memo[ $cache_key ];
133 }
134
135 /**
136 * Build a client with extra transport plugins attached (e.g. HTTP caching).
137 *
138 * Unlike build(), this is not memoized: plugin instances are request-scoped
139 * (a caching plugin carries its own cache backend), so a fresh client is
140 * assembled each call. The shared auth/version/customer/retry configuration
141 * still comes from make_builder(), so callers add only the plugins they need.
142 *
143 * The key parameter is marked SensitiveParameter for the same reason build()
144 * marks its own: this method throws, Exception_Converter keeps the original
145 * as the previous exception, and an unredacted argument would ride the full
146 * trace into debug.log and fatal-error emails.
147 *
148 * @since 6.0.0
149 *
150 * @param string $v4_key PostNL V4 API key.
151 * @param bool $is_sandbox Whether to target the sandbox environment.
152 * @param HttpPluginInterface ...$plugins Transport plugins to attach in order.
153 * @return PostnlClientInterface
154 */
155 public function build_with_plugins(
156 #[\SensitiveParameter]
157 string $v4_key,
158 bool $is_sandbox,
159 HttpPluginInterface ...$plugins
160 ): PostnlClientInterface {
161 $builder = $this->make_builder(
162 $v4_key,
163 $is_sandbox,
164 (string) $this->settings->get_customer_num(),
165 (string) $this->settings->get_customer_code()
166 );
167
168 foreach ( $plugins as $plugin ) {
169 $builder = $builder->withPlugin( $plugin );
170 }
171
172 return $builder->make();
173 }
174
175 /**
176 * Create and configure a ClientBuilder ready to call make() on.
177 *
178 * Extracted as a protected method so test subclasses can intercept builder
179 * construction and inspect its configuration via reflection without
180 * triggering a real network call.
181 *
182 * @since 6.0.0 Attaches the configured logger, when one was supplied.
183 *
184 * @param string $v4_key PostNL V4 API key.
185 * @param bool $is_sandbox Route requests to the sandbox environment.
186 * @param string $customer_number PostNL customer number from settings.
187 * @param string $customer_code PostNL customer code from settings.
188 * @return ClientBuilder
189 */
190 protected function make_builder(
191 #[\SensitiveParameter]
192 string $v4_key,
193 bool $is_sandbox,
194 string $customer_number,
195 string $customer_code
196 ): ClientBuilder {
197 $builder = new ClientBuilder();
198
199 $builder = $builder
200 ->withApiVersion( Version::V4 )
201 ->withAuth( Auth::apiKey( $v4_key ) )
202 ->withSandbox( $is_sandbox )
203 ->withSourceSystem( self::SOURCE_SYSTEM )
204 ->withCustomerCredentials( $customer_number, $customer_code )
205 ->withRetry( RetryConfig::exponentialBackoff() );
206
207 // Left unset without a logger: the SDK then installs its own NullLogger,
208 // so this must not hand it one that only looks configured.
209 if ( null !== $this->logger ) {
210 $builder = $builder->withLogger( $this->logger );
211 }
212
213 return $builder;
214 }
215 }
216