*/ private $memo = array(); /** * PSR-3 logger handed to the SDK, or null to leave SDK logging off. * * @var LoggerInterface|null */ private $logger; /** * Client_Factory constructor. * * The logger is optional because this factory is shared infrastructure that * every V4 service builds on, and a caller with nothing to log should not have * to construct a null object. Callers that do want SDK request/response * logging pass a Logger_Adapter. * * @since 6.0.0 Added the optional $logger parameter. * * @param object $settings Plugin settings instance. * @param LoggerInterface|null $logger PSR-3 logger for SDK request/response logging. */ public function __construct( object $settings, ?LoggerInterface $logger = null ) { $this->settings = $settings; $this->logger = $logger; } /** * Return a configured SDK client for the given V4 API key and environment. * * Repeated calls with the same arguments return the same memoized instance. * * The raw V4 key is hashed before it is used as the cache key, so it is not * exposed by dumping the memo. The key parameter is additionally marked * SensitiveParameter so PHP redacts it from exception stack traces and * backtraces; the SDK applies the same protection at its own boundary and * redacts the key from logs and var_dump output. * * @param string $v4_key PostNL V4 API key. Must not be empty. * @param bool $is_sandbox Whether to target the sandbox environment. * @return PostnlClientInterface * @throws \Postnl\Sdk\Exception\InvalidArgumentSdkException When $v4_key is empty or whitespace-only. */ public function build( #[\SensitiveParameter] string $v4_key, bool $is_sandbox ): PostnlClientInterface { // Cast at the boundary: the Settings getters are untyped and may return non-string option data, // which would TypeError against make_builder( string ... ) under strict_types. $customer_number = (string) $this->settings->get_customer_num(); $customer_code = (string) $this->settings->get_customer_code(); // Any future with*() value sourced from settings or request data must be added to this key, // or memoization will return a client configured for a different combination. // // Every variable segment is hashed to a fixed width before being joined. Concatenating the // raw values would let a '|' inside one of them shift the boundary, so that ( '1|2', '3' ) // and ( '1', '2|3' ) produced the same key and collided onto one client. The customer // values are country-scoped settings, so they can legitimately differ within a request. $cache_key = implode( '|', array( sha1( $v4_key ), $is_sandbox ? '1' : '0', sha1( $customer_number ), sha1( $customer_code ), ) ); if ( ! isset( $this->memo[ $cache_key ] ) ) { $this->memo[ $cache_key ] = $this->make_builder( $v4_key, $is_sandbox, $customer_number, $customer_code )->make(); } return $this->memo[ $cache_key ]; } /** * Build a client with extra transport plugins attached (e.g. HTTP caching). * * Unlike build(), this is not memoized: plugin instances are request-scoped * (a caching plugin carries its own cache backend), so a fresh client is * assembled each call. The shared auth/version/customer/retry configuration * still comes from make_builder(), so callers add only the plugins they need. * * The key parameter is marked SensitiveParameter for the same reason build() * marks its own: this method throws, Exception_Converter keeps the original * as the previous exception, and an unredacted argument would ride the full * trace into debug.log and fatal-error emails. * * @since 6.0.0 * * @param string $v4_key PostNL V4 API key. * @param bool $is_sandbox Whether to target the sandbox environment. * @param HttpPluginInterface ...$plugins Transport plugins to attach in order. * @return PostnlClientInterface */ public function build_with_plugins( #[\SensitiveParameter] string $v4_key, bool $is_sandbox, HttpPluginInterface ...$plugins ): PostnlClientInterface { $builder = $this->make_builder( $v4_key, $is_sandbox, (string) $this->settings->get_customer_num(), (string) $this->settings->get_customer_code() ); foreach ( $plugins as $plugin ) { $builder = $builder->withPlugin( $plugin ); } return $builder->make(); } /** * Create and configure a ClientBuilder ready to call make() on. * * Extracted as a protected method so test subclasses can intercept builder * construction and inspect its configuration via reflection without * triggering a real network call. * * @since 6.0.0 Attaches the configured logger, when one was supplied. * * @param string $v4_key PostNL V4 API key. * @param bool $is_sandbox Route requests to the sandbox environment. * @param string $customer_number PostNL customer number from settings. * @param string $customer_code PostNL customer code from settings. * @return ClientBuilder */ protected function make_builder( #[\SensitiveParameter] string $v4_key, bool $is_sandbox, string $customer_number, string $customer_code ): ClientBuilder { $builder = new ClientBuilder(); $builder = $builder ->withApiVersion( Version::V4 ) ->withAuth( Auth::apiKey( $v4_key ) ) ->withSandbox( $is_sandbox ) ->withSourceSystem( self::SOURCE_SYSTEM ) ->withCustomerCredentials( $customer_number, $customer_code ) ->withRetry( RetryConfig::exponentialBackoff() ); // Left unset without a logger: the SDK then installs its own NullLogger, // so this must not hand it one that only looks configured. if ( null !== $this->logger ) { $builder = $builder->withLogger( $this->logger ); } return $builder; } }