PluginProbe
MONEI Payments for WooCommerce / trunk
MONEI Payments for WooCommerce vtrunk
7.3.3 7.3.2 7.3.1 7.3.0 7.2.4 7.2.3 7.2.2 7.2.0 7.2.1 7.1.3 2.1.0 3.0.0 3.1.0 3.1.1 4.0.0 4.1.0 4.1.1 4.2.0 4.2.1 5.0 5.1.0 5.1.1 5.1.2 5.2.2 5.2.3 All 87 releases
← All changes | src/Repositories/PaymentMethodsRepository.php +82 -7 7.2.1trunk View file →
@@ -1,15 +1,39 @@
1 1 <?php
2 2
3 3 namespace Monei\Repositories;
4 4
5 +use Monei\ApiException;
5 6 use Monei\MoneiClient;
6 7 use Exception;
7 8
8 9 class PaymentMethodsRepository implements PaymentMethodsRepositoryInterface {
10 + /**
11 + * Cached marker for "the API said no". Distinct from an empty array so the
12 + * cache can hold it: an empty array is falsy and would be re-fetched.
13 + */
14 + private const UNAVAILABLE = array(
15 + 'paymentMethods' => array(),
16 + 'metadata' => array(),
17 + );
18 +
19 + /**
20 + * Option that holds off the API after it rejected the key. An option, not a
21 + * transient: on the hosts that made this necessary transients do not persist,
22 + * so a transient-backed marker is gone by the next request.
23 + */
24 + public const BACKOFF_OPTION = 'monei_payment_methods_backoff';
25 +
9 26 private $accountId;
10 27 private MoneiClient $moneiClient;
11 28
29 + /**
30 + * Answers already resolved during this request, keyed by transient key. Every
31 + * gateway asks several times per render; without this each ask re-reads the
32 + * transient, and on a host where transients do not persist, calls the API again.
33 + */
34 + private array $memo = array();
35 +
12 36 public function __construct( string $accountId, MoneiClient $moneiClient ) {
13 37 $this->accountId = $accountId;
14 38 $this->moneiClient = $moneiClient;
15 39 }
@@ -21,9 +45,9 @@
21 45 // The account id no longer reaches the API — getAllowed() derives the account
22 46 // from the API key. It still gates the call because it is what separates the
23 47 // test cache from the live one, and because an unset one means the plugin is
24 48 // not configured yet.
25 - if ( ! $this->accountId ) {
49 + if ( ! $this->accountId || $this->getBackoffUntil() ) {
26 50 return null;
27 51 }
28 52 try {
29 53 // /allowed-payment-methods, the API key authenticated replacement for the
@@ -30,12 +54,23 @@
30 54 // deprecated /payment-methods. Amount, currency and country are left out on
31 55 // purpose: this repository is a container singleton that answers admin
32 56 // screens as well as the checkout, so it has no one cart to describe.
33 57 $response = $this->moneiClient->paymentMethods->getAllowed();
58 + } catch ( ApiException $e ) {
59 + // A rejected key does not fix itself, so retrying every 30 seconds only
60 + // costs. Anything else (network, 5xx) keeps the short retry.
61 + if ( in_array( $e->getCode(), array( 401, 403 ), true ) ) {
62 + $this->extendBackoff();
63 + }
64 + $response = null;
34 65 } catch ( Exception $e ) {
35 66 $response = null;
36 67 }
37 68
69 + if ( $response ) {
70 + delete_option( self::BACKOFF_OPTION );
71 + }
72 +
38 73 return $response ? json_decode( $response, true ) : array();
39 74 }
40 75
41 76 /**
@@ -42,9 +77,12 @@
42 77 * Get payment methods (fetch from transient or API).
43 78 */
44 79 public function getPaymentMethods(): array {
45 80 $transientKey = $this->generateTransientKey( $this->accountId );
46 - $data = get_transient( $transientKey );
81 + if ( isset( $this->memo[ $transientKey ] ) ) {
82 + return $this->memo[ $transientKey ];
83 + }
84 + $data = get_transient( $transientKey );
47 85
48 86 if ( ! $data ) {
49 87 $data = $this->fetchFromAPI();
50 88 if ( $data ) {
@@ -55,17 +93,54 @@
55 93 // the payment methods off the checkout. The 30 second cache
56 94 // makes that one failed call away at any moment, so fall back
57 95 // to the last answer that worked.
58 96 $data = get_transient( $this->fallbackKey( $transientKey ) );
59 - if ( $data ) {
60 - // Without this every request during an outage repeats the
61 - // failing call.
62 - set_transient( $transientKey, $data, 30 );
97 + if ( ! $data ) {
98 + // No answer has ever worked: a wrong or missing API key. An
99 + // empty array is falsy, so it never reached the cache and
100 + // every checkout render repeated the failing call. One
101 + // store did this 15 times a second for a week.
102 + $data = self::UNAVAILABLE;
63 103 }
104 + // Without this every request during an outage repeats the
105 + // failing call.
106 + set_transient( $transientKey, $data, 30 );
64 107 }
65 108 }
66 109
67 - return $data ?: array();
110 + $this->memo[ $transientKey ] = $data === self::UNAVAILABLE ? array() : ( $data ?: array() );
111 +
112 + return $this->memo[ $transientKey ];
113 + }
114 +
115 + /**
116 + * When the plugin will next ask the API after it rejected the key, as a Unix
117 + * timestamp. Null while the plugin is asking normally.
118 + */
119 + public function getBackoffUntil(): ?int {
120 + $backoff = get_option( self::BACKOFF_OPTION );
121 + $until = is_array( $backoff ) ? (int) ( $backoff['until'] ?? 0 ) : 0;
122 +
123 + return $until > time() ? $until : null;
124 + }
125 +
126 + /**
127 + * Hold off the API for an hour, doubling on every further rejection up to a day.
128 + */
129 + private function extendBackoff(): void {
130 + $previous = get_option( self::BACKOFF_OPTION );
131 + $delay = HOUR_IN_SECONDS;
132 + if ( is_array( $previous ) ) {
133 + $delay = min( max( $delay, 2 * (int) ( $previous['delay'] ?? 0 ) ), DAY_IN_SECONDS );
134 + }
135 + update_option(
136 + self::BACKOFF_OPTION,
137 + array(
138 + 'until' => time() + $delay,
139 + 'delay' => $delay,
140 + ),
141 + false
142 + );
68 143 }
69 144
70 145 /**
71 146 * Generate a transient key.