PluginProbe
MONEI Payments for WooCommerce / 7.3.3
MONEI Payments for WooCommerce v7.3.3
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
monei / src / Repositories / PaymentMethodsRepository.php

PaymentMethodsRepository.php in MONEI Payments for WooCommerce 7.3.3, at src/Repositories/PaymentMethodsRepository.php

159 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Monei\Repositories;
4
5 use Monei\ApiException;
6 use Monei\MoneiClient;
7 use Exception;
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
26 private $accountId;
27 private MoneiClient $moneiClient;
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
36 public function __construct( string $accountId, MoneiClient $moneiClient ) {
37 $this->accountId = $accountId;
38 $this->moneiClient = $moneiClient;
39 }
40
41 /**
42 * Fetch payment methods from the API.
43 */
44 private function fetchFromAPI(): ?array {
45 // The account id no longer reaches the API — getAllowed() derives the account
46 // from the API key. It still gates the call because it is what separates the
47 // test cache from the live one, and because an unset one means the plugin is
48 // not configured yet.
49 if ( ! $this->accountId || $this->getBackoffUntil() ) {
50 return null;
51 }
52 try {
53 // /allowed-payment-methods, the API key authenticated replacement for the
54 // deprecated /payment-methods. Amount, currency and country are left out on
55 // purpose: this repository is a container singleton that answers admin
56 // screens as well as the checkout, so it has no one cart to describe.
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;
65 } catch ( Exception $e ) {
66 $response = null;
67 }
68
69 if ( $response ) {
70 delete_option( self::BACKOFF_OPTION );
71 }
72
73 return $response ? json_decode( $response, true ) : array();
74 }
75
76 /**
77 * Get payment methods (fetch from transient or API).
78 */
79 public function getPaymentMethods(): array {
80 $transientKey = $this->generateTransientKey( $this->accountId );
81 if ( isset( $this->memo[ $transientKey ] ) ) {
82 return $this->memo[ $transientKey ];
83 }
84 $data = get_transient( $transientKey );
85
86 if ( ! $data ) {
87 $data = $this->fetchFromAPI();
88 if ( $data ) {
89 set_transient( $transientKey, $data, 30 );
90 set_transient( $this->fallbackKey( $transientKey ), $data, HOUR_IN_SECONDS );
91 } else {
92 // An empty answer marks every gateway unavailable, which takes
93 // the payment methods off the checkout. The 30 second cache
94 // makes that one failed call away at any moment, so fall back
95 // to the last answer that worked.
96 $data = get_transient( $this->fallbackKey( $transientKey ) );
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;
103 }
104 // Without this every request during an outage repeats the
105 // failing call.
106 set_transient( $transientKey, $data, 30 );
107 }
108 }
109
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 );
143 }
144
145 /**
146 * Generate a transient key.
147 */
148 private function generateTransientKey( string $key ): string {
149 return 'payment_methods_' . md5( $key );
150 }
151
152 /**
153 * Key of the longer lived copy used when the API call fails.
154 */
155 private function fallbackKey( string $transientKey ): string {
156 return $transientKey . '_last_ok';
157 }
158 }
159