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 / Exception_Converter.php

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

180 lines 6.3 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\Exception_Converter 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\Exception\AuthExceptionInterface;
13 use Postnl\Sdk\Exception\Client\ValidationException;
14 use Postnl\Sdk\Exception\HttpSdkException;
15 use Postnl\Sdk\Exception\Retry\RetryExhaustedException;
16 use Postnl\Sdk\Exception\RetryableExceptionInterface;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Class Exception_Converter
24 *
25 * Translates a V4 SDK exception into a plain \Exception carrying a human-readable
26 * getMessage(), so it is consumed exactly like the errors
27 * Rest_API\Base::check_response_error() throws. Callers only read getMessage(),
28 * so the V4 path can be dropped in behind Order\Base and the frontend handlers
29 * with no error-handling changes; getCode() additionally carries the HTTP status,
30 * which the legacy path leaves at 0.
31 *
32 * The original SDK exception is preserved as the previous exception so the full
33 * cause chain stays available for logging.
34 *
35 * @since 6.0.0
36 * @package PostNLWooCommerce\Rest_API\SDK
37 */
38 class Exception_Converter {
39
40 /**
41 * Convert an SDK exception into the plugin's legacy error shape.
42 *
43 * Mapping (most specific first):
44 * - Authentication failures (HTTP 401/403 and pre-request credential/OAuth
45 * failures) collapse to a single "Invalid PostNL API credentials." message.
46 * - Validation errors (400/422) surface the field-level errors PostNL
47 * reported so the merchant can correct the request.
48 * - Transient failures (429, 408, retryable 5xx, network, retries exhausted)
49 * collapse to "PostNL is temporarily unavailable." since a retry may succeed.
50 * - Any other HTTP error, including a permanent 5xx such as 501, uses the
51 * description PostNL returned in the response body.
52 * - Anything else is an SDK-internal or PHP error whose message describes
53 * plugin internals rather than the merchant's problem, so it is replaced
54 * with a generic one. The original stays available via getPrevious().
55 *
56 * When available, the PostNL traceId is appended for support correlation.
57 *
58 * @since 6.0.0
59 *
60 * @param \Throwable $exception SDK exception (or any throwable) to convert.
61 * @return \Exception Plugin-shaped error with a preserved status code.
62 */
63 public static function convert( \Throwable $exception ): \Exception {
64 if ( $exception instanceof AuthExceptionInterface ) {
65 return self::to_error( __( 'Invalid PostNL API credentials.', 'postnl-for-woocommerce' ), $exception );
66 }
67
68 if ( $exception instanceof ValidationException ) {
69 return self::to_error( self::validation_message( $exception ), $exception );
70 }
71
72 if ( self::is_transient( $exception ) ) {
73 return self::to_error( __( 'PostNL is temporarily unavailable. Please try again.', 'postnl-for-woocommerce' ), $exception );
74 }
75
76 if ( $exception instanceof HttpSdkException ) {
77 // PostNL's own description of the failure, already cleaned by the SDK.
78 return self::to_error( $exception->getMessage(), $exception );
79 }
80
81 return self::to_error(
82 __( 'An unexpected error occurred while contacting PostNL. Check the PostNL logs for details.', 'postnl-for-woocommerce' ),
83 $exception
84 );
85 }
86
87 /**
88 * Whether a retry could plausibly succeed.
89 *
90 * RetryableExceptionInterface only declares the capability, so the predicate
91 * has to be asked rather than the type: ServerException implements it for
92 * every 5xx but reports false for permanent ones such as 501. The SDK's own
93 * retry policy gates on isRetryable() for the same reason.
94 *
95 * RetryExhaustedException does not implement the interface, so it is checked
96 * separately; the policy only ever raises it after retryable failures.
97 *
98 * @param \Throwable $exception Original SDK exception being converted.
99 * @return bool
100 */
101 private static function is_transient( \Throwable $exception ): bool {
102 if ( $exception instanceof RetryExhaustedException ) {
103 return true;
104 }
105
106 return $exception instanceof RetryableExceptionInterface && $exception->isRetryable();
107 }
108
109 /**
110 * Build the converted error, appending the traceId and preserving both the
111 * status code and the original exception as the cause.
112 *
113 * @param string $message Human-readable, already-translated message.
114 * @param \Throwable $exception Original SDK exception being converted.
115 * @return \Exception
116 */
117 private static function to_error( string $message, \Throwable $exception ): \Exception {
118 return new \Exception(
119 $message . self::trace_suffix( $exception ),
120 self::status_code( $exception ),
121 $exception
122 );
123 }
124
125 /**
126 * Flatten a ValidationException's field errors into "field: message" pairs.
127 *
128 * Falls back to the exception's own (already-cleaned) message when PostNL
129 * returned a 400/422 without any structured field errors.
130 *
131 * @param ValidationException $exception Validation exception to describe.
132 * @return string
133 */
134 private static function validation_message( ValidationException $exception ): string {
135 $parts = array();
136
137 foreach ( $exception->getFieldErrors() as $field_error ) {
138 $parts[] = sprintf( '%1$s: %2$s', $field_error->field, $field_error->message );
139 }
140
141 if ( empty( $parts ) ) {
142 return $exception->getMessage();
143 }
144
145 return implode( '; ', $parts );
146 }
147
148 /**
149 * The PostNL correlation suffix, present only on HTTP exceptions that carry a traceId.
150 *
151 * @param \Throwable $exception Original SDK exception being converted.
152 * @return string Empty string when no traceId is available.
153 */
154 private static function trace_suffix( \Throwable $exception ): string {
155 if ( $exception instanceof HttpSdkException ) {
156 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Third-party SDK DTO property.
157 $trace_id = $exception->problemDetails->traceId;
158
159 if ( null !== $trace_id && '' !== $trace_id ) {
160 return sprintf( ' (traceId: %s)', $trace_id );
161 }
162 }
163
164 return '';
165 }
166
167 /**
168 * The status code to preserve on the converted error.
169 *
170 * HTTP exceptions report their status via getCode(); pre-request failures
171 * (auth, transport) report 0, which is preserved as-is.
172 *
173 * @param \Throwable $exception Original SDK exception being converted.
174 * @return int
175 */
176 private static function status_code( \Throwable $exception ): int {
177 return (int) $exception->getCode();
178 }
179 }
180