Sport Line API — пример на PHP
Требования
Пример рассчитан на PHP 8.1 или новее с установленным расширением cURL.
Проверить версию PHP и наличие расширения:
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
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
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 там также
может находиться служебное сообщение.
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
Следующая функция выбирает первый вид спорта, страну и турнир, присутствующие в текущем меню. В реальном интерфейсе соответствующую ветку выбирает пользователь.
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:
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
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);
}
Запуск:
SPORTAPI_BASE_URL='https://YOUR_API_DOMAIN' \
SPORTAPI_PACKAGE_KEY='YOUR_API_KEY' \
php sportapi_example.php
Не используйте эту форму с рабочим ключом в общей истории команд или в демонстрации экрана. В рабочем окружении храните ключ в системе секретов проекта.
Получение всех турниров выбранного спорта
Если конкретный турнир выбирать не нужно, передайте tournamentId=0:
$eventsPayload = sportApiGet(
"/v1/events/{$sportId}/0/sub/50/live/ru",
);
Значение $sportId всё равно должно быть получено из актуального Live-меню.
Запрос субматча
После получения подробного основного матча выберите элемент из sub_games:
$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():
$searchText = rawurlencode('Манчестер Сити');
$searchPayload = sportApiGet(
"/v1/search/line/ru/{$searchText}",
);
$searchResult = classifyBody($searchPayload);
Не добавляйте необработанную пользовательскую строку непосредственно в URL.
Необязательные дополнительные методы
Методы ниже не являются обязательной частью основной интеграции. Используйте их только по необходимости, если соответствующая возможность нужна интерфейсу или логике проекта.
Основная рекомендуемая цепочка остаётся прежней:
menu → events → event
Назначение дополнительных запросов:
sports,countriesиtournaments— пошаговая альтернативаmenu;topmatches— готовая подборка топ-матчей по всем видам спорта;toplist— Prematch-подборка одного выбранного вида спорта;cybersport=true— отдельная выборка киберспортивных данных.
Не выполняйте все эти запросы автоматически только потому, что они существуют в API.
// Виды спорта 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
$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.
Пустой список — не ошибка
$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 и не реализует:
- периодическое обновление;
- автоматические повторные запросы;
- кэш приложения;
- модели данных;
- пользовательский интерфейс;
- систему приёма и расчёта ставок.
Рекомендуемая частота запросов описана отдельно: «Рекомендации по обновлению данных».