---
id: example-php
title: Sport Line API — пример на PHP
description: Пример запроса к Sport Line API на PHP 8.1 с использованием расширения cURL.
version: v1
language: ru
---

# Sport Line API — пример на PHP


## Требования

Пример рассчитан на PHP 8.1 или новее с установленным расширением cURL.

Проверить версию PHP и наличие расширения:

```bash
php --version
php -m | grep -i curl
```

## Где выполнять запросы

Пример предназначен для backend-приложения. Не помещайте ключ Sport Line API в HTML,
JavaScript или другой код, который отправляется в браузер пользователя.

Если данные нужны пользовательскому интерфейсу, интерфейс обращается к backend клиента,
а backend выполняет запрос к Sport Line API.

## Переменные окружения

Код использует две переменные:

| Переменная | Значение |
|---|---|
| `SPORTAPI_BASE_URL` | Базовый URL, полученный от менеджера SportAPI |
| `SPORTAPI_PACKAGE_KEY` | Персональный API-ключ |

Не записывайте рабочий ключ непосредственно в исходный код и не добавляйте файл с
секретами в публичный репозиторий.

## Минимальный запрос

```php
<?php

declare(strict_types=1);

$baseUrl = rtrim((string) getenv('SPORTAPI_BASE_URL'), '/');
$apiKey = (string) getenv('SPORTAPI_PACKAGE_KEY');

$curl = curl_init($baseUrl . '/v1/menu/live/ru');

curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Package: ' . $apiKey,
        'Accept: application/json',
    ],
    CURLOPT_TIMEOUT => 15,
]);

$responseText = curl_exec($curl);

if ($responseText === false) {
    throw new RuntimeException(curl_error($curl));
}

$payload = json_decode($responseText, true, 512, JSON_THROW_ON_ERROR);

print_r($payload);
```

API-ключ передаётся в HTTP-заголовке `Package`, а не в URL.

## Общая функция запроса

Следующая функция:

- добавляет заголовок `Package`;
- ограничивает время соединения и ожидания ответа;
- разбирает JSON;
- проверяет HTTP-статус;
- распознаёт `error_code` и `error_message`;
- проверяет общую оболочку `status`, `page`, `body`.

```php
<?php

declare(strict_types=1);

final class SportApiException extends RuntimeException
{
    public function __construct(
        string $message,
        public readonly int|string|null $apiCode = null,
        public readonly ?int $httpStatus = null,
        public readonly ?array $payload = null,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, 0, $previous);
    }
}

$rawBaseUrl = getenv('SPORTAPI_BASE_URL');
$apiKey = getenv('SPORTAPI_PACKAGE_KEY');

if ($rawBaseUrl === false || trim($rawBaseUrl) === '') {
    throw new RuntimeException('SPORTAPI_BASE_URL is not configured');
}

if ($apiKey === false || trim($apiKey) === '') {
    throw new RuntimeException('SPORTAPI_PACKAGE_KEY is not configured');
}

$baseUrl = rtrim($rawBaseUrl, '/');

function sportApiGet(string $path): array
{
    global $baseUrl, $apiKey;

    $curl = curl_init($baseUrl . $path);

    if ($curl === false) {
        throw new SportApiException('Could not initialize cURL');
    }

    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Package: ' . $apiKey,
            'Accept: application/json',
        ],
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15,
    ]);

    $responseText = curl_exec($curl);

    if ($responseText === false) {
        $message = curl_error($curl);
        curl_close($curl);

        throw new SportApiException(
            'Could not connect to Sport Line API: ' . $message,
        );
    }

    $httpStatus = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);

    try {
        $payload = json_decode(
            $responseText,
            true,
            512,
            JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $error) {
        throw new SportApiException(
            "Sport Line API returned invalid JSON. HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            previous: $error,
        );
    }

    if (!is_array($payload)) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
        );
    }

    if (array_key_exists('error_code', $payload)) {
        $apiErrorMessage = $payload['error_message'] ?? 'Unknown error';

        throw new SportApiException(
            "Sport Line API error {$payload['error_code']}: {$apiErrorMessage}",
            apiCode: $payload['error_code'],
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if ($httpStatus >= 400) {
        throw new SportApiException(
            "Sport Line API returned HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if (
        !array_key_exists('status', $payload)
        || !isset($payload['page'])
        || !is_string($payload['page'])
        || !array_key_exists('body', $payload)
    ) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    return $payload;
}
```

Ошибку API нужно проверять и в JSON. Не следует определять результат запроса только по
HTTP-статусу.

## Классификация `body`

В обычных методах `body` содержит массив или объект данных. В методе `event` там также
может находиться служебное сообщение.

```php
function classifyBody(array $payload): array
{
    $body = $payload['body'];

    if (
        is_array($body)
        && isset($body['message'])
        && is_string($body['message'])
    ) {
        return [
            'type' => 'message',
            'message' => $body['message'],
        ];
    }

    if (is_array($body) && $body === []) {
        return [
            'type' => 'empty',
            'data' => [],
        ];
    }

    return [
        'type' => 'data',
        'data' => $body,
    ];
}
```

Возможные результаты:

| `type` | Значение |
|---|---|
| `data` | API вернул данные метода |
| `empty` | В текущей выборке нет данных |
| `message` | Метод `event` вернул `Game not found` или `Game id finished` |

## Выбор актуальной ветки из `menu`

Следующая функция выбирает первый вид спорта, страну и турнир, присутствующие в текущем
меню. В реальном интерфейсе соответствующую ветку выбирает пользователь.

```php
function findFirstTournament(array $menuBody): ?array
{
    foreach ($menuBody as $sport) {
        foreach ($sport['sub'] ?? [] as $country) {
            foreach ($country['sub'] ?? [] as $tournament) {
                return [
                    'sport_id' => $sport['id'],
                    'sport_name' => $sport['name'],
                    'country_id' => $country['id'],
                    'country_name' => $country['name'],
                    'tournament_id' => $tournament['id'],
                    'tournament_name' => $tournament['name'],
                ];
            }
        }
    }

    return null;
}
```

ID не записываются статически: функция получает их из актуального ответа `menu`.

## Выбор матча из ответа `events`

Метод `events` группирует матчи по турнирам. Чтобы получить первый доступный матч,
нужно пройти через `events_list`:

```php
function findFirstGame(array $eventsBody): ?array
{
    foreach ($eventsBody as $tournament) {
        $eventsList = $tournament['events_list'] ?? [];

        if ($eventsList !== []) {
            return $eventsList[0];
        }
    }

    return null;
}
```

## Полный пример `menu → events → event`

Сохраните следующий код в файле `sportapi_example.php`:

```php
<?php

declare(strict_types=1);

final class SportApiException extends RuntimeException
{
    public function __construct(
        string $message,
        public readonly int|string|null $apiCode = null,
        public readonly ?int $httpStatus = null,
        public readonly ?array $payload = null,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, 0, $previous);
    }
}

$rawBaseUrl = getenv('SPORTAPI_BASE_URL');
$apiKey = getenv('SPORTAPI_PACKAGE_KEY');

if ($rawBaseUrl === false || trim($rawBaseUrl) === '') {
    throw new RuntimeException('SPORTAPI_BASE_URL is not configured');
}

if ($apiKey === false || trim($apiKey) === '') {
    throw new RuntimeException('SPORTAPI_PACKAGE_KEY is not configured');
}

$baseUrl = rtrim($rawBaseUrl, '/');
$lineType = 'live';
$language = 'ru';

function sportApiGet(string $path): array
{
    global $baseUrl, $apiKey;

    $curl = curl_init($baseUrl . $path);

    if ($curl === false) {
        throw new SportApiException('Could not initialize cURL');
    }

    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Package: ' . $apiKey,
            'Accept: application/json',
        ],
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15,
    ]);

    $responseText = curl_exec($curl);

    if ($responseText === false) {
        $message = curl_error($curl);
        curl_close($curl);

        throw new SportApiException(
            'Could not connect to Sport Line API: ' . $message,
        );
    }

    $httpStatus = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);

    try {
        $payload = json_decode(
            $responseText,
            true,
            512,
            JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $error) {
        throw new SportApiException(
            "Sport Line API returned invalid JSON. HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            previous: $error,
        );
    }

    if (!is_array($payload)) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
        );
    }

    if (array_key_exists('error_code', $payload)) {
        $apiErrorMessage = $payload['error_message'] ?? 'Unknown error';

        throw new SportApiException(
            "Sport Line API error {$payload['error_code']}: {$apiErrorMessage}",
            apiCode: $payload['error_code'],
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if ($httpStatus >= 400) {
        throw new SportApiException(
            "Sport Line API returned HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if (
        !array_key_exists('status', $payload)
        || !isset($payload['page'])
        || !is_string($payload['page'])
        || !array_key_exists('body', $payload)
    ) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    return $payload;
}

function classifyBody(array $payload): array
{
    $body = $payload['body'];

    if (
        is_array($body)
        && isset($body['message'])
        && is_string($body['message'])
    ) {
        return [
            'type' => 'message',
            'message' => $body['message'],
        ];
    }

    if (is_array($body) && $body === []) {
        return [
            'type' => 'empty',
            'data' => [],
        ];
    }

    return [
        'type' => 'data',
        'data' => $body,
    ];
}

function findFirstTournament(array $menuBody): ?array
{
    foreach ($menuBody as $sport) {
        foreach ($sport['sub'] ?? [] as $country) {
            foreach ($country['sub'] ?? [] as $tournament) {
                return [
                    'sport_id' => $sport['id'],
                    'sport_name' => $sport['name'],
                    'country_id' => $country['id'],
                    'country_name' => $country['name'],
                    'tournament_id' => $tournament['id'],
                    'tournament_name' => $tournament['name'],
                ];
            }
        }
    }

    return null;
}

function findFirstGame(array $eventsBody): ?array
{
    foreach ($eventsBody as $tournament) {
        $eventsList = $tournament['events_list'] ?? [];

        if ($eventsList !== []) {
            return $eventsList[0];
        }
    }

    return null;
}

function main(): void
{
    global $lineType, $language;

    $menuPayload = sportApiGet("/v1/menu/{$lineType}/{$language}");
    $menuResult = classifyBody($menuPayload);

    if ($menuResult['type'] === 'empty') {
        echo "No sections are currently available in the selected line.\n";
        return;
    }

    if (
        $menuResult['type'] !== 'data'
        || !is_array($menuResult['data'])
        || !array_is_list($menuResult['data'])
    ) {
        throw new SportApiException(
            'The menu method returned an unexpected body.',
        );
    }

    $selection = findFirstTournament($menuResult['data']);

    if ($selection === null) {
        echo "No tournament is currently available.\n";
        return;
    }

    echo "Selected current navigation branch:\n";
    echo json_encode(
        $selection,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    ) . "\n";

    $eventsPayload = sportApiGet(
        "/v1/events/{$selection['sport_id']}"
        . "/{$selection['tournament_id']}"
        . "/sub/50/{$lineType}/{$language}",
    );
    $eventsResult = classifyBody($eventsPayload);

    if ($eventsResult['type'] === 'empty') {
        echo "The selected tournament currently has no matches.\n";
        return;
    }

    if (
        $eventsResult['type'] !== 'data'
        || !is_array($eventsResult['data'])
        || !array_is_list($eventsResult['data'])
    ) {
        throw new SportApiException(
            'The events method returned an unexpected body.',
        );
    }

    $game = findFirstGame($eventsResult['data']);

    if ($game === null) {
        echo "No match is currently available.\n";
        return;
    }

    echo "Selected current match:\n";
    echo json_encode(
        [
            'game_id' => $game['game_id'],
            'first_opponent' => $game['opp_1_name'] ?? null,
            'second_opponent' => $game['opp_2_name'] ?? null,
        ],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    ) . "\n";

    $eventPayload = sportApiGet(
        "/v1/event/{$game['game_id']}"
        . "/group/{$lineType}/{$language}",
    );
    $eventResult = classifyBody($eventPayload);

    if ($eventResult['type'] === 'message') {
        echo "The match is unavailable: {$eventResult['message']}\n";
        return;
    }

    if (
        $eventResult['type'] !== 'data'
        || !is_array($eventResult['data'])
        || array_is_list($eventResult['data'])
    ) {
        throw new SportApiException(
            'The event method returned an unexpected body.',
        );
    }

    $event = $eventResult['data'];

    echo "Detailed match:\n";
    echo json_encode(
        [
            'game_id' => $event['game_id'],
            'first_opponent' => $event['opp_1_name'] ?? null,
            'second_opponent' => $event['opp_2_name'] ?? null,
            'outcomes' => $event['game_oc_counter'] ?? null,
            'subgames' => count($event['sub_games'] ?? []),
        ],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    ) . "\n";
}

try {
    main();
} catch (SportApiException $error) {
    fwrite(STDERR, $error->getMessage() . "\n");

    if ($error->apiCode !== null) {
        fwrite(STDERR, "SportAPI error code: {$error->apiCode}\n");
    }

    if ($error->httpStatus !== null) {
        fwrite(STDERR, "HTTP status: {$error->httpStatus}\n");
    }

    exit(1);
}
```

Запуск:

```bash
SPORTAPI_BASE_URL='https://YOUR_API_DOMAIN' \
SPORTAPI_PACKAGE_KEY='YOUR_API_KEY' \
php sportapi_example.php
```

Не используйте эту форму с рабочим ключом в общей истории команд или в демонстрации
экрана. В рабочем окружении храните ключ в системе секретов проекта.

## Получение всех турниров выбранного спорта

Если конкретный турнир выбирать не нужно, передайте `tournamentId=0`:

```php
$eventsPayload = sportApiGet(
    "/v1/events/{$sportId}/0/sub/50/live/ru",
);
```

Значение `$sportId` всё равно должно быть получено из актуального Live-меню.

## Запрос субматча

После получения подробного основного матча выберите элемент из `sub_games`:

```php
$match = $eventResult['data'];
$subgames = $match['sub_games'] ?? [];

if ($subgames !== [] && isset($subgames[0]['game_id'])) {
    $subgamePayload = sportApiGet(
        "/v1/event/{$subgames[0]['game_id']}"
        . "/group/{$lineType}/{$language}",
    );
    $subgameResult = classifyBody($subgamePayload);

    if ($subgameResult['type'] === 'data') {
        echo 'Selected subgame: '
            . ($subgameResult['data']['game_dop_name'] ?? '')
            . "\n";
    }
}
```

Коэффициенты ответа относятся только к выбранному субматчу.

## Поиск матчей

Пользовательский текст обязательно кодируется через `rawurlencode()`:

```php
$searchText = rawurlencode('Манчестер Сити');
$searchPayload = sportApiGet(
    "/v1/search/line/ru/{$searchText}",
);
$searchResult = classifyBody($searchPayload);
```

Не добавляйте необработанную пользовательскую строку непосредственно в URL.

## Необязательные дополнительные методы

Методы ниже не являются обязательной частью основной интеграции. Используйте их только
по необходимости, если соответствующая возможность нужна интерфейсу или логике проекта.

Основная рекомендуемая цепочка остаётся прежней:

```text
menu → events → event
```

Назначение дополнительных запросов:

- `sports`, `countries` и `tournaments` — пошаговая альтернатива `menu`;
- `topmatches` — готовая подборка топ-матчей по всем видам спорта;
- `toplist` — Prematch-подборка одного выбранного вида спорта;
- `cybersport=true` — отдельная выборка киберспортивных данных.

Не выполняйте все эти запросы автоматически только потому, что они существуют в API.

```php
// Виды спорта Live
$sports = sportApiGet('/v1/sports/live/ru');

// Страны актуального вида спорта
$countries = sportApiGet(
    "/v1/countries/{$sportId}/live/ru",
);

// Турниры актуальных спорта и страны
$tournaments = sportApiGet(
    "/v1/tournaments/{$sportId}/{$countryId}/live/ru",
);

// Топ-матчи Live с расширенными карточками
$topmatches = sportApiGet('/v1/topmatches/live/ru?full=true');

// Топ-матчи выбранного спорта, только Prematch
$toplist = sportApiGet(
    "/v1/toplist/{$prematchSportId}/ru?full=true",
);

// Киберспортивное Live-меню
$cybersportMenu = sportApiGet(
    '/v1/menu/live/ru?cybersport=true',
);
```

Переменные `$sportId`, `$countryId` и `$prematchSportId` в этих фрагментах должны быть
получены из актуальных ответов соответствующего типа линии.

## Обработка `Game id finished`

```php
$payload = sportApiGet(
    "/v1/event/{$gameId}/group/live/ru",
);
$result = classifyBody($payload);

if ($result['type'] === 'message') {
    if ($result['message'] === 'Game id finished') {
        echo "Stop updating this game_id and refresh the Live events list.\n";
    } else {
        echo "SportAPI event message: {$result['message']}\n";
    }
}
```

`Game id finished` не позволяет определить, завершился матч, был отменён или перешёл из
Prematch в Live. После сообщения прекратите запросы по старому ID и обновите `events`.

## Пустой список — не ошибка

```php
$payload = sportApiGet(
    '/v1/events/SPORT_ID_FROM_MENU/0/sub/50/live/ru',
);
$result = classifyBody($payload);

if ($result['type'] === 'empty') {
    echo "There are currently no matches in this selection.\n";
}
```

Не заменяйте пустой актуальный ответ ранее сохранёнными матчами.

## Что логировать при ошибке

Можно записывать:

- время запроса;
- путь метода без ключа;
- HTTP-статус;
- `error_code` и `error_message`;
- `body.message`;
- используемые ID и тип линии.

Не записывайте значение заголовка `Package` в логи.

## Что не делает пример

Код не является готовым SDK и не реализует:

- периодическое обновление;
- автоматические повторные запросы;
- кэш приложения;
- модели данных;
- пользовательский интерфейс;
- систему приёма и расчёта ставок.

Рекомендуемая частота запросов описана отдельно:
[«Рекомендации по обновлению данных»](../getting-started/update-guidelines.md).

## Связанные документы

- [Примеры cURL](curl.md)
- [Пример на JavaScript](javascript.md)
- [Пример на Python](python.md)
- [Быстрый старт](../getting-started/quick-start.md)
- [Общий формат ответа](../data-models/response-format.md)
- [Обработка ошибок](../getting-started/error-handling.md)
- [Методы Sport Line API](../api-reference/README.md)
