Die REST-Schnittstelle in den Bereichen Spieler, Vereine und Verbände ist öffentlich zugänglich. Wichtig: Die Daten im Livesystem sind echt, die Daten im Demosystem sind "Spieldaten" und nicht zur Veröffentlichung gedacht. Das Demosystem wird auch nach der Projektphase als "Spielwiese" weiter existieren.
Livesystem:
Demosystem:
# ==============================================
# Livesystem
# ==============================================
# ZIP-Datei für Nordrhein-Westfalen
curl 'https://schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/download/LV-6-dwzliste.zip --output LV-6-dwzliste.zip';
# Die ersten zehn Verbände
curl 'https://schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/federations?limit=10&offset=0';
# Der aktuelle DWZ-Wert für Andre Martin
curl 'https://schachde-appso.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons?lastname=Martin&firstname=Andre';
# Oder direkt über die nuLigaPersonId
curl 'https://schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons/NU4211295';
# ==============================================
# Demosystem
# ==============================================
# ZIP-Datei für Nordrhein-Westfalen
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/download/LV-6-dwzliste.zip --output LV-6-dwzliste.zip';
# Die ersten zehn Verbände
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/federations?limit=10&offset=0';
# Der aktuelle DWZ-Wert für Andre Martin
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons?lastname=Martin&firstname=Andre';
# Oder direkt über die nuLigaPersonId
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons/NU4211295';
Da die DWZ-Listen-API nun ohne Authentifizierung erreichbar ist, könnet ihr die URL's auch mit jedem Webbrowser direkt ausprobieren. Für den ZIP-Download: schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/download/LV-0-dwzliste.zip
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
/**
* 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);
}
Die Anpassungen sind mit Claude leicht umzusetzen und man vermeidet dadurch auch Fehler die durch eigene Arbeit am Code entstehen können.