Proste API Lotto w PHP

Ostatnimi czasy pisałem API do obsługi Lotto (for fun). Uparłem się, że tym razem zrobię to zgodnie ze sztuką. Dziesiątki plików, modele, interfejsy, dependency injection np. możliwość zmiany curl na guzzle czy stworzenia własnej klasy, bardzo obszerna dokumentacja doc, porządna obsługa wyjątków, unit testy itd. API zwraca ustandaryzowane odpowiedzi w formie obiektów, wybieramy typ gry ze słownika (nie ma opcji pomyłki – formalnie z typu enum).. działa to całkiem zacnie, wygląda też, ale.. co z tego? Zużyłem na ten serwis pewnie dziesiątki godzin razem z testami, a nadal nie skończyłem, bo brakuje jeszcze obsługi tzw. gierek.

Dzięki takiemu podejściu banalnie utworzyć teraz bazę danych i umieścić w niej modele, a każda odpowiedź jest przewidywalna i przeanalizowana (opisane są również błędy w samym API np. pomnożone zwracane wartości czy brak obsługi niektórych typów gry dla niektórych endpointów)… ale czy cała ta praca miała rzeczywiście sens?

Prawda jest taka, że najczęściej nie potrzebujemy całego API tylko konkretnych danych.., a nawet jeżeli to każde API ma swoją przewidywalną strukturę, a my potrzebujemy po prostu pobrać dane i np. zapisać je w bazie do dalszego przetworzenia.

W rzeczywistości jedyne co potrzeba to:

  • adres do API i endpointy
  • klucz / token – zależnie od metody autoryzacji
  • obsługi zapytań – request
  • obsługa błędów HTTP
  • obsługa błędów API (choć niekoniecznie)
  • opakowania tego w serwis tzn. wywoływanie api jako element serwisu (nie bezpośrednio)
  • obsługi limitów – jeżeli więcej procesów korzysta z api warto to przenieść do np. redis

Klient API Lotto

Dokumentacja Lotto API jest dostępna pod linkami:

Zacznijmy od klienta, którym będzie jeden plik php. Na początek definicja klasy i konstruktor.

namespace Lotto

class Client {

    private $baseUrl = 'https://developers.lotto.pl/api/open/v1/';
    private $apiKey;

    public function __construct($apiKey)
    {
        $this->apiKey = $apiKey;
    }

Musimy w konfiguracji podać klucz. Adres do API jest zapisany w kodzie. W niektórych sytuacjach możemy go przekazywać, ale w praktyce jak się zmieni to powinniśmy przetestować wszystkie metody.

Teraz dodajmy request np. na cURL. Wymagania dla API Lotto to:

  • Klucz przekazujemy w nagłówku pod Secret
  • Odpowiedzi w większości przypadków otrzymujemy w json (nie zawsze)
  • W przypadku wystąpienia błędów zwracamy wyjątek z wywołaniem i odpowiedzią (API zwraca więcej szczegółów)
    public function request($endpoint, $params = [], $accept = 'application/json')
    {
        $url = $this->baseUrl . $endpoint;
        if (!empty($params)) {
            $url .= '?' . http_build_query($params);
        }

        $headers = [
            'Accept: ' . $accept,
            'Secret: ' . $this->apiKey
        ];

        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_CUSTOMREQUEST => 'GET',
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 30,
        ]);

        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        $error = curl_error($curl);
        curl_close($curl);

        // Obsługa błędów cURL
        if ($error) {
            throw new \Exception("cURL Error: " . $error);
        }

        $res = json_decode($response, true);
        // Wszystko OK
        if ($httpCode == 200) {
            return $res;
        }

        $msg = match($httpCode) {
            400 => 'Bad Request',
            401 => 'Unauthorized',
            403 => 'Forbidden',
            404 => 'Not Found',
            422 => 'Validation Error',
            500 => 'Internal Server Error',
            default => 'Unknown status code',
        };

        throw new \Exception(
            $msg . ': ' . $endpoint . '[ ' . print_r($params, true) . " ]\n\nResponse: " . print_r($res, true),
            $httpCode
        );
    }

Na tym etapie nie ma potrzeby ograniczania odpowiedzi np. aby nie wyświetlać szczegółów w których mogą się znaleźć jakieś dane wrażliwe. To obsłużymy w drugim pliku – LottoService.

Dla porządku przetestujmy czy nasze API działa. Na tym etapie dodajmy wywołajmy najpierw API pod klasą (metoda bez parametrów i z parametrem). Sprawdźmy odpowiedź.

$client = new Client('KLUCZ_DO_API');
$response = $client->request('lotteries/info');

print_r ($response);

i już na starcie przetestowaliśmy wyjątek typu Validation Error. API zwróciło błąd, choć według oficjalnej dokumentacji parametr nie był tutaj wymagany.

(
    [errors] => Array
        (
            [gameType] => Array
                (
                    [0] => Pole 'Game Type' nie może być puste.
                )

        )

    [code] => 422
)

Lotto\Client->request($endpoint = 'lotteries/info', $params = ???, $accept = ???)

Dodajmy parametr np. Lotto.

$client = new Client('KLUCZ_DO_API');
$response = $client->request('lotteries/info', ['gameType' => 'Lotto']);

print_r ($response);

W odpowiedzi otrzymaliśmy

Array
(
    [gameType] => Lotto
    [nextDrawDate] => 2025-06-07T20:00:00Z
    [closestPrizeValue] => 3000000
    [draws] => Wtorki, czwartki i soboty o 22:00
    [couponPrice] => 3 zł za zakład
    [closestPrizePoolType] => Cumulation
)

Sprawdźmy jeszcze metodę dla której nie ma na pewno parametru.

$client = new Client('KLUCZ_DO_API');
$response = $client->request('lotteries/draw-results/last-results');

print_r ($response);

i w odpowiedzi otrzymamy listę ostatnich wyników losowania dla wszystkich gier.

Array
(
    [0] => Array
        (
            [drawSystemId] => 7205
            [drawDate] => 2025-06-05T20:00:00Z
            [gameType] => Lotto
            [results] => Array
                (
                    [0] => Array
                        (
                            [gameType] => Lotto
                            [resultsJson] => Array
                                (
                                    [0] => 21
                                    [1] => 17
                                    [2] => 26
                                    [3] => 31
                                    [4] => 22
                                    [5] => 1
                                )
                        )
                    [1] => Array
                        (
                            [gameType] => LottoPlus
                            [resultsJson] => Array
                                (
                                    [0] => 46
                                    [1] => 35
                                    [2] => 19
                                    [3] => 1
                                    [4] => 24
                                    [5] => 30
                                )
                        )
                )
        )
    [1] => ...  [gameType] => EuroJackpot ...
    [2] => ...
    ...
)

I to tyle w kontekście klienta, choć jeszcze mała uwaga odnośnie dat. Te są w formacie UTC, czyli uniwersalnym. W praktyce oznacza to, że musimy stosować konwersję na czas lokalny jak chcemy poznać dokładne daty / godziny losowań.

Oczywiście teraz usuwamy te testy.

Co dalej?

Możemy utworzyć serwis w którym zawrzemy wszystkie metody dostępne w API. Możemy też dodać metody do naszego API (pamiętajmy by zmienić wtedy metodę request na private). Można też się pokusić o obsługę typu null w request tzn. jak przekażemy parametr z null to nie zostanie dodany do wywołania…

    // Informacje na temat Oddziałów Totalizatora Sportowego.
    public function departmets(string $name, string $city) : array { 
        return $this->request("departments", ['Name' => $name, 'City' => $city]); 
    }

    // Liczba wygranych każdego stopnia dla wybranej gry liczbowej lub loterii pieniężnej 
    public function drawPrizes(...) : array { 
        return $this->request(...

Możemy dodać np. testy integracyjne w php unit, albo po prostu testować dostępność itd. Wtedy jak coś się zmieni to dostaniemy informacje..

class ApiAvailabilityTest extends TestCase
{
    public function testApiIsAvailable()
    {
        $response = $client->request('lotteries/info', ['gameType' => 'Lotto']);
        $expectedKeys = [
            'gameType',
            'nextDrawDate',
            'closestPrizeValue',
            'draws',
            'couponPrice',
            'closestPrizePoolType',
        ];

        foreach ($expectedKeys as $key) {
            $this->assertArrayHasKey($key, $response , "Brak klucza: $key");
        }
    }
}

Tradycyjnie kod w repozytorium GitHub