11<?php
22
3+ declare(strict_types=1);
4+
35/**
4- *
6+ * This file is part of the MultiFlexi package
7+ *
8+ * https://github.com/VitexSoftware/php-vitexsoftware-rbczpremiumapi
59 *
6- * @author Vitex < vitex @hippy.cz >
7- * @copyright 2023 Vitex@hippy.cz (G)
8- *
9- * PHP 8
10+ * (c) Vítězslav Dvořák < http: // vitexsoftware.com >
11+ *
12+ * For the full copyright and license information, please view the LICENSE
13+ * file that was distributed with this source code.
1014 */
1115
1216namespace VitexSoftware\Raiffeisenbank;
1317
18+ use VitexSoftware\Raiffeisenbank\RateLimit\RateLimiter;
19+ use VitexSoftware\Raiffeisenbank\RateLimit\RateLimitExceededException;
20+
1421/**
15- * Description of ApiClient
22+ * Description of ApiClient.
1623 *
1724 * @author vitex
1825 */
1926class ApiClient extends \GuzzleHttp\Client
2027{
21-
2228 /**
2329 * ClientID obtained from Developer Portal - when you registered your app with us.
24- * @var string
2530 */
2631 protected string $xIBMClientId = ' ' ;
2732
2833 /**
29- * the end IP address of the client application (no server) in IPv4 or IPv6
30- * format. If the bank client (your user) uses a browser by which he
31- * accesses your server app, we need to know the IP address of his browser.
32- * Always provide the closest IP address to the real end-user possible.
33- * (optional)
34- *
35- * @var string
34+ * the end IP address of the client application (no server) in IPv4 or IPv6
35+ * format. If the bank client (your user) uses a browser by which he
36+ * accesses your server app, we need to know the IP address of his browser.
37+ * Always provide the closest IP address to the real end-user possible.
38+ * (optional).
3639 */
3740 protected string $pSUIPAddress = ' ' ;
3841
3942 /**
4043 * Use mocking for api calls ?
41- * @var boolean
4244 */
4345 protected bool $mockMode = false ;
46+ private RateLimiter $rateLimiter ;
4447
4548 /**
46- * @inheritDoc
47- *
48- * $config [' clientid' ] - obtained from Developer Portal - when you registered your app with us.
49- * $config [' cert' ] = [' /path/to/cert.p12' ,' certificat password' ]
50- * $config [' clientpubip' ] = the closest IP address to the real end-user
51- * $config [' mocking' ] = true to use /rbcz/premium/mock/* endpoints
52- *
53- * @param array $config
54- * @throws \Exception CERT_FILE is not set
55- * @throws \Exception CERT_PASS is not set
49+ * Initialize the API client with configuration, certificate validation, and a rate limiter.
50+ *
51+ * Accepted $config keys:
52+ * - ' clientid' : client ID from the Developer Portal.
53+ * - ' cert' : array with [pathToP12, password]; when omitted, CERT_FILE and CERT_PASS configuration values are used.
54+ * - ' clientpubip' : client public IP (nearest to the end user).
55+ * - ' mocking' : bool to enable mock endpoints.
56+ * - ' debug' : debug flag.
57+ *
58+ * @param array $config client configuration
59+ * - ' clientid' : client ID from Developer Portal
60+ * - ' cert' : [path, password]
61+ * - ' clientpubip' : client public IP
62+ * - ' mocking' : bool
63+ * - ' debug' : bool
64+ * - ' rate_limit_store' : RateLimitStoreInterface instance (optional)
65+ * - ' rate_limit_wait' : bool - wait when limited (default true )
66+ *
67+ * @throws \Exception if certificate file path (CERT_FILE) is not provided
68+ * @throws \Exception if certificate password (CERT_PASS) is not provided
5669 */
5770 public function __construct(array $config = [])
5871 {
59- if (array_key_exists(' clientid' , $config ) === false ) {
72+ if (\ array_key_exists(' clientid' , $config ) === false ) {
6073 $this -> xIBMClientId = \Ease\Shared::cfg(' XIBMCLIENTID' );
6174 } else {
6275 $this -> xIBMClientId = $config [' clientid' ];
6376 }
6477
65- if (array_key_exists('cert', $config) === false) {
78+ if (\ array_key_exists('cert', $config) === false) {
6679 $config [' cert' ] = [\Ease\Shared::cfg(' CERT_FILE' ), \Ease\Shared::cfg(' CERT_PASS' )];
80+
6781 if (empty($config [' cert' ][0])) {
6882 throw new \Exception(' Certificate (CERT_FILE) not specified' );
6983 }
84+
7085 if (empty($config['cert'][1])) {
7186 throw new \Exception(' Certificate password (CERT_PASS) not specified' );
7287 }
7388 }
7489
75- if (array_key_exists('debug', $config) === false) {
90+ if (\ array_key_exists('debug', $config) === false) {
7691 $config [' debug' ] = \Ease\Shared::cfg(' API_DEBUG' , false );
77- }
78-
79- if (array_key_exists('clientpubip', $config)){
92+ }
93+
94+ if (\ array_key_exists('clientpubip', $config)) {
8095 $this -> pSUIPAddress = $config [' clientpubip' ];
8196 }
8297
83- if (array_key_exists('mocking', $config)){
84- $this -> mockMode = boolval($config [' mocking' ]);
98+ if (\array_key_exists('mocking', $config)) {
99+ $this -> mockMode = (bool) $config [' mocking' ];
100+ }
101+
102+ if (isset($config['rate_limit_store'])) {
103+ $limitStore = $config [' rate_limit_store' ];
104+ } else {
105+ $path = $config [' rate_limit_path' ] ?? sys_get_temp_dir().' /rbczpremiumapi_rates.json' ;
106+ $limitStore = new RateLimit\JsonRateLimitStore($path );
85107 }
86-
108+
109+ $waitMode = $config['rate_limit_wait'] ?? true;
110+ $this->rateLimiter = new RateLimiter($limitStore, $waitMode);
111+
87112 parent::__construct($config);
88113 }
89114
90115 /**
91- * ClientID obtained from Developer Portal
92- *
116+ * ClientID obtained from Developer Portal.
117+ *
93118 * @return string
94119 */
95120 public function getXIBMClientId()
@@ -98,102 +123,158 @@ class ApiClient extends \GuzzleHttp\Client
98123 }
99124
100125 /**
101- * Keep user public IP here
102- *
126+ * Keep user public IP here.
127+ *
103128 * @return string
104129 */
105130 public function getpSUIPAddress()
106131 {
107132 return $this -> pSUIPAddress ;
108- }
133+ }
109134
110135 /**
111136 * Use mocking uri for api calls ?
112- *
113- * @return boolean
137+ *
138+ * @return bool
114139 */
115140 public function getMockMode()
116141 {
117142 return $this -> mockMode ;
118143 }
119-
144+
120145 /**
121- * Obtain Your current Public IP
122- *
146+ * Obtain Your current Public IP.
147+ *
123148 * @deprecated since version 0.1 - Do not use in production Environment!
124- *
149+ *
125150 * @return string
126151 */
127152 public static function getPublicIP()
128153 {
129154 $curl = curl_init();
130- curl_setopt($curl , CURLOPT_URL, " http://httpbin.org/ip" );
131- curl_setopt($curl , CURLOPT_RETURNTRANSFER, 1);
155+ curl_setopt($curl , \ CURLOPT_URL, ' http://httpbin.org/ip' );
156+ curl_setopt($curl , \ CURLOPT_RETURNTRANSFER, 1);
132157 $output = curl_exec($curl );
133158 curl_close($curl );
134159 $ip = json_decode($output , true );
160+
135161 return $ip [' origin' ];
136162 }
137-
163+
138164 /**
139- * Source Identifier
140- *
165+ * Source Identifier.
166+ *
141167 * @deprecated since version 0.1 - Do not use in production Environment!
142- *
168+ *
143169 * @return string
144170 */
145171 public static function sourceString()
146172 {
147- return substr(__FILE__ . ' @' . gethostname(), -50);
173+ return substr(__FILE__. ' @' . gethostname(), -50);
148174 }
149175
150176 /**
151- * Try to check certificate readibilty
152- *
153- * @throws Exception - Certificate file not found
154- *
155- * @param string $certFile path to certificate
156- * @param boolean $die throw exception or return false ?
157- *
158- * @return boolean certificate file
177+ * Try to check certificate readibilty.
178+ *
179+ * @param string $certFile path to certificate
180+ * @param bool $die throw exception or return false ?
181+ *
182+ * @throws \Exception - Certificate file not found
183+ *
184+ * @return bool certificate file
159185 */
160186 public static function checkCertificatePresence(string $certFile, bool $die = false): bool
161187 {
162188 $found = false ;
189+
163190 if ((file_exists($certFile ) === false ) || (is_readable($certFile ) === false )) {
164- $errMsg = ' Cannot read specified certificate file: ' . $certFile ;
165- fwrite(STDERR, $errMsg . PHP_EOL);
166- if ($die ){
191+ $errMsg = ' Cannot read specified certificate file: ' .$certFile ;
192+ fwrite(\STDERR, $errMsg .\PHP_EOL);
193+
194+ if ($die ) {
167195 throw new \Exception($errMsg );
168196 }
169197 } else {
170198 $found = true ;
171199 }
172- return $found;
200+
201+ return $found;
173202 }
174-
175- public static function checkCertificate($certFile,$password): bool {
176- return self::checkCertificatePresence($certFile ) && self::checkCertificatePassword($certFile ,$password );
203+
204+ public static function checkCertificate(string $certFile, string $password): bool
205+ {
206+ return self::checkCertificatePresence($certFile ) && self::checkCertificatePassword($certFile , $password );
177207 }
178-
179- public static function checkCertificatePassword(string $certFile, string $password): bool {
208+
209+ public static function checkCertificatePassword(string $certFile, string $password): bool
210+ {
180211 $certContent = file_get_contents($certFile );
212+
181213 if (openssl_pkcs12_read($certContent , $certs , $password ) === false ) {
182214 fwrite(\STDERR, ' Cannot read PKCS12 certificate file: ' .$certFile .\PHP_EOL);
183- exit(1);
215+
216+ throw new \Exception(' Cannot read PKCS12 certificate file: ' .$certFile );
184217 }
218+
185219 return true;
186220 }
187-
188-
221+
189222 /**
190- * Request Identifier
191- *
192- * @deprecated since version 0.1 - Do not use in production Environment!
193- *
194- * @return string
223+ * Produce a short request identifier used for diagnostics and testing.
224+ *
225+ * @deprecated since version 0.1 — Do not use in production environments.
226+ *
227+ * @return string the generated request identifier composed from a source token and the current timestamp, truncated to at most 59 characters
195228 */
196- public static function getxRequestId() {
197- return substr(self::sourceString() . ' #' . time(),-59);
229+ public static function getxRequestId()
230+ {
231+ return substr(self::sourceString().' #' .time(), -59);
232+ }
233+
234+ /**
235+ * Send an HTTP request while enforcing and updating client rate limits.
236+ *
237+ * @param \Psr\Http\Message\RequestInterface $request the HTTP request to send
238+ * @param array $options Request options to apply to the transfer. See \GuzzleHttp\RequestOptions.
239+ *
240+ * @throws \GuzzleHttp\Exception\GuzzleException
241+ * @throws RateLimitExceededException if the client is rate limited and wait mode is disabled
242+ *
243+ * @return \Psr\Http\Message\ResponseInterface the HTTP response
244+ */
245+ public function send(\Psr\Http\Message\RequestInterface $request, array $options = []): \Psr\Http\Message\ResponseInterface
246+ {
247+ $this -> rateLimiter -> checkBeforeRequest ($this -> xIBMClientId );
248+
249+ $response = parent::send($request , $options );
250+
251+ $this -> updateRateLimitsFromResponse ($response );
252+
253+ $statusCode = $response -> getStatusCode ();
254+
255+ if ($statusCode === 429) { // 429 Too Many Requests
256+ if ($this -> rateLimiter -> isWaitMode ()) {
257+ $this -> rateLimiter -> checkBeforeRequest ($this -> xIBMClientId );
258+ $response = parent::send($request , $options );
259+
260+ $this -> updateRateLimitsFromResponse ($response );
261+
262+ $statusCode = $response -> getStatusCode ();
263+ } else {
264+ throw new RateLimitExceededException(' Rate limit exceeded (HTTP 429)' );
265+ }
266+ }
267+
268+ return $response;
269+ }
270+
271+ private function updateRateLimitsFromResponse(\Psr\Http\Message\ResponseInterface $response): void
272+ {
273+ if ($response -> hasHeader (' x-ratelimit-remaining-second' ) && $response -> hasHeader (' x-ratelimit-remaining-day' )) {
274+ $remainingSecond = (int) $response -> getHeaderLine (' x-ratelimit-remaining-second' );
275+ $remainingDay = (int) $response -> getHeaderLine (' x-ratelimit-remaining-day' );
276+ $timestamp = time();
277+ $this -> rateLimiter -> handleRateLimits ($this -> xIBMClientId , $remainingSecond , $remainingDay , $timestamp );
278+ }
198279 }
199280}
0 commit comments