sync: Zahlungsstrecke, PG-Events, Fatal-Log und PayPal-v2-Blaupause vom Live-Stand 2026-09-13
Deploy to Dev / deploy (push) Successful in 1s
Deploy to Dev / deploy (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PaypalV2;
|
||||
|
||||
/**
|
||||
* Duenner HTTP-Client fuer die PayPal-REST-API (Orders v2).
|
||||
*
|
||||
* Sicherheit:
|
||||
* - TLS-Verifikation ist ERZWUNGEN (kein CURLOPT_SSL_VERIFYPEER=false wie im Legacy-NVP).
|
||||
* - Access-Token wird ausserhalb des Webroots zwischengespeichert (chmod 600).
|
||||
*/
|
||||
final class PayPalClient
|
||||
{
|
||||
private string $base;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->base = Config::apiBase();
|
||||
}
|
||||
|
||||
/** OAuth2 client_credentials Access-Token, gecacht bis kurz vor Ablauf. */
|
||||
public function accessToken(): string
|
||||
{
|
||||
$cacheFile = self::tokenCacheFile();
|
||||
|
||||
if (\is_file($cacheFile)) {
|
||||
$c = \json_decode((string) @\file_get_contents($cacheFile), true);
|
||||
if (
|
||||
\is_array($c)
|
||||
&& ($c['mode'] ?? '') === Config::mode()
|
||||
&& isset($c['token'], $c['expires'])
|
||||
&& (int) $c['expires'] > \time() + 60
|
||||
) {
|
||||
return (string) $c['token'];
|
||||
}
|
||||
}
|
||||
|
||||
$creds = Config::creds();
|
||||
[$status, $body] = $this->raw(
|
||||
'POST',
|
||||
'/v1/oauth2/token',
|
||||
'grant_type=client_credentials',
|
||||
['Content-Type: application/x-www-form-urlencoded'],
|
||||
$creds['client_id'] . ':' . $creds['secret']
|
||||
);
|
||||
|
||||
$data = \json_decode($body, true);
|
||||
if ($status !== 200 || !\is_array($data) || empty($data['access_token'])) {
|
||||
throw new \RuntimeException('PayPal v2: OAuth2 fehlgeschlagen (HTTP ' . $status . ').');
|
||||
}
|
||||
|
||||
@\file_put_contents($cacheFile, \json_encode([
|
||||
'token' => (string) $data['access_token'],
|
||||
'expires' => \time() + (int) ($data['expires_in'] ?? 300),
|
||||
'mode' => Config::mode(),
|
||||
]), LOCK_EX);
|
||||
@\chmod($cacheFile, 0600);
|
||||
|
||||
return (string) $data['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentifizierter JSON-Request.
|
||||
*
|
||||
* @param array<string,mixed>|null $payload
|
||||
* @param list<string> $extraHeaders
|
||||
* @return array{0:int,1:array<mixed>} [HTTP-Status, dekodierter Body]
|
||||
*/
|
||||
public function request(string $method, string $path, ?array $payload = null, array $extraHeaders = []): array
|
||||
{
|
||||
$headers = \array_merge(
|
||||
[
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->accessToken(),
|
||||
],
|
||||
$extraHeaders
|
||||
);
|
||||
|
||||
$bodyStr = $payload === null ? null : (string) \json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
[$status, $body] = $this->raw($method, $path, $bodyStr, $headers);
|
||||
|
||||
$decoded = $body === '' ? [] : \json_decode($body, true);
|
||||
|
||||
return [$status, \is_array($decoded) ? $decoded : ['raw' => $body]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Roher cURL-Aufruf.
|
||||
*
|
||||
* @param list<string> $headers
|
||||
* @return array{0:int,1:string}
|
||||
*/
|
||||
private function raw(string $method, string $path, ?string $body, array $headers, ?string $basicAuth = null): array
|
||||
{
|
||||
$ch = \curl_init($this->base . $path);
|
||||
\curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
|
||||
if ($basicAuth !== null) {
|
||||
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
\curl_setopt($ch, CURLOPT_USERPWD, $basicAuth);
|
||||
}
|
||||
if ($body !== null) {
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
|
||||
$resp = \curl_exec($ch);
|
||||
if ($resp === false) {
|
||||
$err = \curl_error($ch);
|
||||
\curl_close($ch);
|
||||
throw new \RuntimeException('PayPal v2: HTTP-Transportfehler: ' . $err);
|
||||
}
|
||||
|
||||
$status = (int) \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
\curl_close($ch);
|
||||
|
||||
return [$status, (string) $resp];
|
||||
}
|
||||
|
||||
private static function tokenCacheFile(): string
|
||||
{
|
||||
return Config::dir() . '/.token_cache.json';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user