nietonchique/sofascore-api-bundle
Composer 安装命令:
composer require nietonchique/sofascore-api-bundle
包简介
Symfony bundle and standalone PHP client for the unofficial SofaScore API: matches, players, teams, tournaments and live per-sport statistics, with a pluggable HTTP / headless-Chrome transport.
README 文档
README
A Symfony bundle and standalone PHP client for the (unofficial) SofaScore API — matches, players, teams, tournaments and live per-sport statistics.
It is a faithful PHP port of the Python sofascore-wrapper
library, redesigned around a pluggable transport layer, typed DTOs for the core
entities, and full PHPStan (max) / Deptrac / PHPUnit coverage.
Disclaimer. This is an unofficial client and is not affiliated with, endorsed by, or supported by SofaScore. The API it talks to is undocumented and may change or block access at any time. Using it may violate SofaScore's Terms of Service — use at your own risk.
Requirements
- PHP 8.4+
- Symfony 8.0+ components (when used as a bundle)
- Optional: a Chromium/Chrome binary +
chrome-php/chromefor the headless-browser transport (Cloudflare fallback)
Installation
composer require nietonchique/sofascore-api-bundle
In a Symfony application using Symfony Flex the bundle is registered
automatically. Otherwise add it to config/bundles.php:
return [ // ... Nietonchique\SofascoreApiBundle\SofascoreApiBundle::class => ['all' => true], ];
Usage
Standalone (any PHP project)
use Nietonchique\SofascoreApiBundle\SofascoreClient; $client = SofascoreClient::create(); // default HTTP transport $results = $client->search('arsenal')->searchAll(); $event = $client->match(12436870)->getMatch(); // Dto\Event $h2h = $client->match(12436870)->h2h(); // array $team = $client->team(42)->getTeam(); // Dto\Team $games = $client->basketball()->gamesByDate('basketball', '2026-01-15');
In Symfony (dependency injection)
SofascoreClient and every endpoint group are autowired services:
use Nietonchique\SofascoreApiBundle\SofascoreClient; final class ScoresController { public function __construct(private readonly SofascoreClient $sofascore) { } public function live(): array { return $this->sofascore->match()->liveGames(); } }
Endpoint groups
Access each group via the client factory methods:
| Method | Group | Bound argument |
|---|---|---|
search(string $q, int $page = 0) |
Search |
search term + page |
match(?int $matchId = null) |
MatchEndpoint |
match (event) id |
player(int $playerId) |
Player |
player id |
playerSearch(string $query) |
PlayerSearch |
query |
team(int $teamId) |
Team |
team id |
league(int $leagueId) |
League |
unique-tournament id |
manager(int $managerId) |
Manager |
manager id |
transfers() |
Transfers |
— |
news() |
News |
— |
userData() |
UserData |
— |
flag(string $flagCode) |
Flag |
country code |
americanFootball() / baseball() / basketball() / cricket() / esports() / iceHockey() / mma() / motorsport() / rugby() / tennis() |
per-sport groups | — |
Return types
The API surface is large and SofaScore changes response fields without notice, so the return style is intentionally hybrid and predictable:
- The primary entity-detail getters return typed DTOs:
MatchEndpoint::getMatch(): Event,Player::getPlayer(): Player,Team::getTeam(): Team,League::getLeague(): Tournament. Every DTO keeps the full original payload accessible via->raw/->toArray(). - Every other method returns a decoded
array(the raw JSON), exactly as the Python library does.
Error handling
Every exception thrown by the bundle implements SofascoreExceptionInterface:
| Exception | Thrown when |
|---|---|
ApiException |
any non-2xx / undecodable response — base class, exposes getStatusCode() and getUrl() |
ApiBlockedException (extends ApiException) |
HTTP 403 (Cloudflare); triggers the chain fallback |
NotFoundException (extends ApiException) |
HTTP 404 (unknown entity) |
InvalidArgumentException |
invalid argument, e.g. an unknown sport slug |
use Nietonchique\SofascoreApiBundle\Exception\ApiBlockedException; use Nietonchique\SofascoreApiBundle\Exception\NotFoundException; use Nietonchique\SofascoreApiBundle\Exception\SofascoreExceptionInterface; try { $event = $client->match(12436870)->getMatch(); } catch (NotFoundException) { // no such match } catch (ApiBlockedException $e) { // Cloudflare blocked this IP — configure a proxy (see below) } catch (SofascoreExceptionInterface $e) { // any other error from the bundle: $e->getMessage() }
Transports & Cloudflare (403)
SofaScore sits behind Cloudflare. The bundle ships three transports behind a
single TransportInterface:
http—HttpClientTransport, a plain Symfony HTTP client with a realistic browser header set. Fast, no external dependencies.chrome—ChromeTransport, drives a headless Chromium viachrome-php/chrome(no Node.js). Warms up on the site root to obtain a Cloudflare clearance cookie before calling the API.chain(default) — tries HTTP first and falls back to Chrome on a 403.
The
X-Requested-Withheader is required. SofaScore answers every API request that lacks it with a Cloudflare403 "challenge".HttpClientTransportsends it automatically (the value is not validated — a random per-instance hex token is used to mimic the site's own XHR token), so the defaulthttptransport reaches the API directly, no browser needed.Some IPs are geo/reputation-blocked (e.g. requests originating from Russia, or certain datacenter ranges) and get a
403regardless of headers. Route through a clean exit with thehttp.proxyoption — any SOCKS5/HTTP proxy works:sofascore_api: http: proxy: 'socks5h://127.0.0.1:1080'When still blocked, the transport raises
ApiBlockedExceptionrather than returning bogus data, andChainTransportfalls back to the Chrome transport.
Configuration (bundle)
All decorators are opt-in and disabled by default:
# config/packages/sofascore_api.yaml sofascore_api: transport: chain # http | chrome | chain http: timeout: 10.0 proxy: null # 'socks5h://127.0.0.1:1080' user_agent: null # override the default browser UA x_requested_with: null # override the required header (default: random token) headers: {} # any extra headers chrome: binary: google-chrome-stable headless: true timeout_ms: 30000 warmup_url: 'https://www.sofascore.com/' # null to disable proxy: null # 'socks5://127.0.0.1:1080' retry: enabled: false max_retries: 3 delay_ms: 1000 cache: # PSR-6 response cache enabled: false pool: cache.app ttl: 300 rate_limit: enabled: false limit: 60 interval: '1 minute' logging: enabled: false service: logger
Quality
composer test # PHPUnit (network tests excluded by default) composer stan # PHPStan (level max) composer cs-check # php-cs-fixer (@PSR12 + @Symfony), dry run composer deptrac # architecture/layer rules composer qa # all of the above
Run the live integration tests (they skip automatically when Cloudflare blocks the current IP):
vendor/bin/phpunit --group network
Development
A bundle is a library — "developing" it means editing code and running the test suite (there is no app server to start). Use the host PHP (8.4+) directly:
composer install
composer qa # cs-fixer + phpstan + deptrac + phpunit
…or run everything in Docker (no PHP needed on the host; the container runs as your user, so it leaves no root-owned files):
make install # PHP 8.5 by default — `make PHP=8.4 qa` to pick a version make qa make test
Using the bundle in a Dockerized app
The bundle is pure PHP; a minimal consumer image needs only PHP + ext-curl:
FROM php:8.5-cli COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /app COPY . . RUN composer install --no-dev --prefer-dist --no-progress
For the headless-Chrome fallback, also install a Chromium binary and the optional
dependency (composer require chrome-php/chrome, then chrome.binary: chromium):
RUN apt-get update && apt-get install -y --no-install-recommends chromium \
&& rm -rf /var/lib/apt/lists/*
On a server/cloud the container's IP is a datacenter IP that Cloudflare blocks — route through a proxy (
http.proxy) to a clean residential/mobile exit.
Troubleshooting
- Every request throws
ApiBlockedException(403). Two independent causes:- the
X-Requested-Withheader is missing — it is sent automatically, so this only happens if you overrodehttp.headersand dropped it; - the exit IP is geo/reputation-blocked (e.g. Russia, some datacenter ranges).
Set
http.proxy(andchrome.proxy) to a clean exit.
- the
transport: chromefails with "class not found". Install the optional dependency and make sure a Chromium binary is available:composer require chrome-php/chromeand setchrome.binary.rate_limit/cacheenabled but the container errors. Installsymfony/rate-limiter, and pointcache.poolat a PSR-6 pool (cache.appships with FrameworkBundle).- Headless Chrome still gets 403. SofaScore's Cloudflare challenge is hard for
headless automation; use the
httptransport with a clean residential/mobile proxy instead — it only needs theX-Requested-Withheader, which is sent for you.
Credits
- Ported from
tommhe14/sofascore-wrapper(Python, MIT).
License
MIT © Aleksandr Ryzhkov
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-13