| 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 |
|