REST-Schnittstelle des Wertungsportals

Nachfolgend finden Sie ein PHP-Beispielskript für den Zugriff auf die REST-Schnittstelle des Wertungsportals. Im Konstruktur (Zeile 24-29) müssen Sie die Ihnen zugteilten URL's und Schlüssel eintragen. In Zeile 273 muß evtl. der Funktionsaufruf ("/dwz/dwzliste/clubs") ausgetauscht werden.

Das PHP-Skript wurde zu 100% mit Claude.ai erstellt und umfasst alle nötigen Funktionen wie Anforderung des Access Tokens bzw. des Refresh Tokens, sowie das Caching der Tokens.

Das Skript wurde mit den uns zugeteilten Parametern (Zeile 24-29) erfolgreich getestet.

PHP-Code

<?php

/**
 * OAuth2 Client Credentials Flow mit Refresh-Token-Unterstützung
 * Spec: https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/
 */

class OAuth2Client
{
    // ─────────────────────────────────────────────
    //  Konfiguration (öffentliche Eigenschaften)
    // ─────────────────────────────────────────────
    public string $apiBaseUrl;
    public string $clientId;
    public string $clientSecret;
    public string $tokenEndpoint;
    public string $scope;
    public string $cacheFile;

    // ─────────────────────────────────────────────
    //  Konstruktor – initialisiert alle Konfigurationswerte
    // ─────────────────────────────────────────────
    public function __construct(
        string $apiBaseUrl    = '',
        string $clientId      = '',
        string $clientSecret  = '',
        string $tokenEndpoint = '',
        string $scope         = '',
        string $cacheFile     = ''
    ) {
        $this->apiBaseUrl    = $apiBaseUrl;
        $this->clientId      = $clientId;
        $this->clientSecret  = $clientSecret;
        $this->tokenEndpoint = $tokenEndpoint;
        $this->scope         = $scope;
        $this->cacheFile     = $cacheFile !== '' ? $cacheFile : sys_get_temp_dir() . '/oauth2_token_cache.json';
    }

    // ─────────────────────────────────────────────
    //  Cache lesen / schreiben / löschen
    // ─────────────────────────────────────────────
    public function readCache(): array
    {
        if (!file_exists($this->cacheFile)) {
            return [];
        }
        return json_decode(file_get_contents($this->cacheFile), true) ?? [];
    }

    public function writeCache(array $data): void
    {
        file_put_contents($this->cacheFile, json_encode($data));
    }

    public function clearCache(): void
    {
        if (file_exists($this->cacheFile)) {
            unlink($this->cacheFile);
            echo "🗑️  Token-Cache gelöscht.\n";
        }
    }

    // ─────────────────────────────────────────────
    //  Token-Request (flexibel für beide Grant-Typen)
    // ─────────────────────────────────────────────
    public function requestToken(array $postFields): array
    {
        $ch = curl_init($this->tokenEndpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => http_build_query($postFields),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/x-www-form-urlencoded',
                'Accept: application/json',
            ],
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 5,
            CURLOPT_POSTREDIR      => 3,
        ]);

        $response     = curl_exec($ch);
        $httpCode     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        $curlError    = curl_error($ch);
        curl_close($ch);

        if ($effectiveUrl !== $this->tokenEndpoint) {
            echo "ℹ️  Weitergeleitet zu: $effectiveUrl\n";
        }

        if ($curlError) {
            throw new RuntimeException("cURL-Fehler: $curlError");
        }

        $data = json_decode($response, true);

        if ($httpCode !== 200 || empty($data['access_token'])) {
            $errorMsg = $data['error_description'] ?? $data['error'] ?? 'Unbekannter Fehler';
            throw new RuntimeException("Token-Anfrage fehlgeschlagen (HTTP $httpCode): $errorMsg");
        }

        return $data;
    }

    // ─────────────────────────────────────────────
    //  Neuen Token via Client Credentials holen
    // ─────────────────────────────────────────────
    public function fetchNewToken(): array
    {
        echo "🔑 Hole neuen Access Token (client_credentials) ...\n";

        $tokenData = $this->requestToken([
            'grant_type'    => 'client_credentials',
            'client_id'     => $this->clientId,
            'client_secret' => $this->clientSecret,
            'scope'         => $this->scope,
        ]);

        $this->saveTokenToCache($tokenData);
        echo "✅ Neuer Token erhalten (gültig für {$tokenData['expires_in']} Sekunden).\n";
        return $tokenData;
    }

    // ─────────────────────────────────────────────
    //  Bestehenden Token via Refresh-Token erneuern
    // ─────────────────────────────────────────────
    public function refreshToken(string $refreshToken): array
    {
        echo "🔄 Erneuere Access Token via Refresh-Token ...\n";

        $tokenData = $this->requestToken([
            'grant_type'    => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id'     => $this->clientId,
            'client_secret' => $this->clientSecret,
        ]);

        $this->saveTokenToCache($tokenData);
        echo "✅ Token erneuert (gültig für {$tokenData['expires_in']} Sekunden).\n";
        return $tokenData;
    }

    // ─────────────────────────────────────────────
    //  Token-Daten im Cache speichern
    // ─────────────────────────────────────────────
    public function saveTokenToCache(array $tokenData): void
    {
        $expiresIn = $tokenData['expires_in'] ?? 3600;
        $this->writeCache([
            'access_token'  => $tokenData['access_token'],
            'refresh_token' => $tokenData['refresh_token'] ?? null,
            'expires_at'    => time() + $expiresIn,
        ]);
    }

    // ─────────────────────────────────────────────
    //  Gültigen Access Token liefern
    //  Reihenfolge:
    //    1. Gecachter Token noch gültig   → direkt verwenden
    //    2. Refresh-Token vorhanden       → Token erneuern
    //    3. Kein Cache                    → neuen Token holen
    // ─────────────────────────────────────────────
    public function getValidToken(): string
    {
        $cache = $this->readCache();

        // 1. Access Token noch gültig? (30 Sekunden Puffer)
        if (!empty($cache['access_token']) && isset($cache['expires_at'])
            && time() < ($cache['expires_at'] - 30)) {
            echo "ℹ️  Verwende gecachten Access Token.\n";
            return $cache['access_token'];
        }

        // 2. Refresh-Token vorhanden → erneuern statt neu anfordern
        if (!empty($cache['refresh_token'])) {
            try {
                $tokenData = $this->refreshToken($cache['refresh_token']);
                return $tokenData['access_token'];
            } catch (RuntimeException $e) {
                // Refresh-Token ungültig → Cache leeren und neu starten
                echo "⚠️  Refresh fehlgeschlagen ({$e->getMessage()}), hole neuen Token ...\n";
                $this->clearCache();
            }
        }

        // 3. Komplett neuen Token holen
        $tokenData = $this->fetchNewToken();
        return $tokenData['access_token'];
    }

    // ─────────────────────────────────────────────
    //  API-Aufruf mit Bearer Token
    // ─────────────────────────────────────────────
    public function callApi(string $accessToken, string $apiUrl, string $method = 'GET', ?array $body = null): array
    {
        $ch = curl_init($apiUrl);

        $headers = [
            'Authorization: Bearer ' . $accessToken,
            'Accept: application/json',
        ];

        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_SSL_VERIFYPEER => true,
        ];

        if (strtoupper($method) === 'POST') {
            $options[CURLOPT_POST]       = true;
            $options[CURLOPT_POSTFIELDS] = json_encode($body ?? []);
            $headers[]                   = 'Content-Type: application/json';
            $options[CURLOPT_HTTPHEADER] = $headers;
        }

        curl_setopt_array($ch, $options);

        $response  = curl_exec($ch);
        $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            throw new RuntimeException("cURL-Fehler beim API-Aufruf: $curlError");
        }

        return [
            'http_code' => $httpCode,
            'body'      => json_decode($response, true) ?? $response,
        ];
    }

    // ─────────────────────────────────────────────
    //  API-Aufruf mit automatischem Token-Refresh
    //  bei HTTP 401 (Token abgelaufen / ungültig)
    // ─────────────────────────────────────────────
    public function callApiWithRefresh(string $apiUrl, string $method = 'GET', ?array $body = null): array
    {
        $token  = $this->getValidToken();
        $result = $this->callApi($token, $apiUrl, $method, $body);

        // Bei 401: Token per Refresh-Token erneuern und einmal wiederholen
        if ($result['http_code'] === 401) {
            echo "⚠️  HTTP 401 – Token wird erneuert ...\n";
            $cache = $this->readCache();
            $this->clearCache();

            if (!empty($cache['refresh_token'])) {
                $tokenData = $this->refreshToken($cache['refresh_token']);
                $token     = $tokenData['access_token'];
            } else {
                $tokenData = $this->fetchNewToken();
                $token     = $tokenData['access_token'];
            }

            $result = $this->callApi($token, $apiUrl, $method, $body);
        }

        return $result;
    }
}

// ─────────────────────────────────────────────
//  Hauptprogramm
// ─────────────────────────────────────────────
try {
    $client = new OAuth2Client();

    $result = $client->callApiWithRefresh($client->apiBaseUrl . '/dwz/dwzliste/clubs');

    echo "\n📦 API-Antwort (HTTP {$result['http_code']}):\n";
    echo json_encode($result['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

} catch (RuntimeException $e) {
    echo "\n❌ Fehler: " . $e->getMessage() . "\n";
    exit(1);
}