rohang27 / thesportsdb-client
PHP client for TheSportsDB API v1 and v2: typed models, rate limiting, retries, caching.
Requires
- php: >=8.2
- ext-json: *
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- psr/simple-cache: ^3.0
Suggests
- ext-curl: for the default HTTP transport
- psr/simple-cache: to cache responses in any PSR-16 cache (Psr16ResponseCache)
Provides
None
Conflicts
None
Replaces
None
README
API reference: https://rohang27.github.io/thesportsdbLibrary/api/php/index.html (phpDocumentor)
Developed in thesportsdbLibrary. RohanG27/thesportsdb-client-php is a read-only mirror that Packagist installs from; send issues and pull requests to the main repository.
A PHP client for TheSportsDB API, v1 and v2. It's the PHP sibling of the Kotlin library (kotlin/) and the Python library (python/): same behaviour, same lessons, tested against the same recorded responses.
- Every documented endpoint, plus the useful undocumented ones, one method each, for v1 and v2.
- Typed, read-only models, generated from the Kotlin models, so all three libraries have the same fields:
- values are parsed leniently, so a bad value becomes
nullinstead of failing the call; - timestamps are UTC
DateTimeImmutables; - the original fields stay available in
$record->raw.
- values are parsed leniently, so a bad value becomes
- The API's quirks are handled:
- per-endpoint record keys;
- the forms "no results" takes;
- errors sent as text where records belong;
- two timestamp formats;
yes/No/NOflags;- v2 search sending numbers where everything else sends strings.
- Safe by default:
- a rate limiter matched to your key, with retries for 429s, 5xx and network errors;
- a 30-second timeout;
- API keys never appear in exceptions or cache keys.
- Helpers for common tasks that use v2 with a premium key and v1 with a free key.
- Optional caching (in memory, or any PSR-16 cache), and a per-call event hook for logging.
- No runtime dependencies. PHP 8.2+. The default HTTP transport uses
ext-curl; implementTransportto use a PSR-18 client or anything else. PHPStan level 8 clean.
Not affiliated with TheSportsDB. Read their terms before publishing an app; artwork that isn't Creative Commons may not be used in published apps (see
Player::$creativeCommons).
Install
composer require rohang27/thesportsdb-client
The namespace is RohanG27\TheSportsDb.
Quick start
use RohanG27\TheSportsDb\SportsDb; use RohanG27\TheSportsDb\Model\ImageSize; $db = new SportsDb(); // the free key "123" ("3" also works): v1 only $arsenal = $db->v1->lookup->team(133604); echo $arsenal->name, ' ', ImageSize::Tiny->of($arsenal->badge); $table = $db->v1->lookup->table(4328); // Premier League standings
use RohanG27\TheSportsDb\Cache\InMemoryResponseCache; $db = new SportsDb(apiKey: getenv('THESPORTSDB_API_KEY'), cache: new InMemoryResponseCache()); $league = $db->v2->lookup->league(4328); $season = $db->v2->schedule->leagueSeason(4328, $league->currentSeason); // ~380 games, one call $tv = $db->v2->tv->country('Canada'); // about a week of listings
The method groups follow the API: $db->v1->search, ->lookup, ->list, ->schedule, ->tv, ->video, ->live, and $db->v2->search, ->lookup, ->list, ->all, ->schedule, ->tv, ->live. Each method's docblock names the endpoint it calls and the free key's limit. Names match the Kotlin library; the full list, the record keys and the free/premium counts are in ../docs/ENDPOINTS.md. PHP has no overloading, so Kotlin's leagues(country) and channel(id) are leaguesInCountry() and channelId() here.
Helpers
$db->helpers picks v2 with a premium key and v1 with a free key, so the same code works with either.
$h = $db->helpers; $h->currentSeason(4328); // "2026-2027" $h->seasonEvents(4328); // the current season's fixtures $h->roundEvents(4328, round: 7); // one matchday $h->upcomingLeagueEvents(4328, days: 7); $h->recentLeagueResults(4328); $h->teamSchedule(133604); // past and future, all competitions $h->eventsOnLocalDate('2026-10-04', new DateTimeZone('America/Toronto'), sport: 'Ice Hockey'); $h->liveScores(leagueId: 4328); $h->leagueTeams(4328); $h->eventChannels(2494052); $h->tvListings('Canada', sport: 'Ice Hockey', days: 3);
eventsOnLocalDateexists because the API files events under their UTC date, so evening games in the Americas land on the next UTC day. It fetches every UTC day the local day overlaps and keeps the right events.roundEventshas two routes becauseeventsround.phpis undocumented: it returns a whole round to the free keys but HTTP 404 to premium keys. With a premium key, the helper filters the season instead.
Models
- Records are
readonlyclasses extendingApiRecord:$a == $b(or$a->equals($b)) is true when the API sent the same fields;(string) $teamis a short summary,Team(id=133604, name=Arsenal);$team->rawholds every original field.
- Missing values are
null(or empty arrays). Ids and years of0mean unknown, and becomenulltoo. - Times:
timestampproperties are UTCDateTimeImmutables;- date-only fields (
date,born, …) areDateTimeImmutables at midnight UTC; - times of day (
time,localTime) areHH:MM:SSstrings.
- Status:
$event->statusCodeis the raw code;$event->status()is anEventStatus(NotStarted,InPlay,Finished,Interrupted, …), covering every sport's documented codes. Older events often have no code. - Stages:
$event->stage()reads stage codes inround(200 = final, 500 = pre-season…) as aRoundStage, ornullfor an ordinary round. - To test your own code, fake the HTTP layer with a
Transportrather than building records.
Errors
All exceptions extend RohanG27\TheSportsDb\Exception\SportsDbException:
| Exception | When |
|---|---|
InvalidApiKeyException |
HTTP 400 for the key. Only 123, 3 and paid keys work. |
PremiumRequiredException |
A v2 call with a free key, before any request. |
RateLimitException |
HTTP 429 after the retry; $retryAfter in seconds if given. |
HttpStatusException |
Any other HTTP error, after retries for 5xx. |
NetworkException |
No response at all, after retries (including timeouts). |
ResponseParseException |
The body wasn't a TheSportsDB envelope (e.g. an HTML page). |
ApiMessageException |
The API answered with a message instead of data, e.g. a rejected parameter. |
"No results" is not an error. You get an empty array, or null for a single lookup. Invalid argument combinations throw InvalidArgumentException before any request.
Settings
new SportsDb( apiKey: '...', requestsPerMinute: null, // default: 30 for free keys, 100 otherwise; 0 = off maxRetries: 2, retryBackoff: 0.5, retryOnRateLimit: true, rateLimitWait: 60.0, cache: new InMemoryResponseCache(), // or new Psr16ResponseCache($anyPsr16Cache) cachePolicy: new CachePolicy(medium: 600), timeout: 30.0, requestListener: fn (RequestEvent $e) => $logger->debug("$e->url $e->status {$e->duration}s"), transport: null, // your own Transport (e.g. wrapping a PSR-18 client) );
PHP calls are synchronous, so there's nothing in flight to de-duplicate: unlike the Kotlin and Python clients, this one has no deduplicateRequests. The rate limiter and in-memory cache last as long as the client object. To share them across web requests, use a PSR-16 cache (e.g. Redis or APCu) with Psr16ResponseCache.
Development
Run from php/:
composer install vendor/bin/phpunit # offline tests against recorded responses vendor/bin/phpunit --group live # the real API (free key; set THESPORTSDB_API_KEY for v2) vendor/bin/phpstan analyse python3 tools/gen_models.py # regenerate src/Model/ after changing the Kotlin models php tools/sync_fixtures.php # copy re-recorded fixtures from the Kotlin project
src/Model/ is generated from ../kotlin/src/main/kotlin/io/github/rohang27/thesportsdb/model/ (except the hand-written ApiRecord, Socials, LeagueRef, PlayerExternalIds, EventStatus and ImageSize), and a test fails if it's stale. Behaviour shared with the other libraries is tracked in ../docs/LIBRARY-PARITY.md.
License
MIT. See LICENSE. TheSportsDB's data and artwork are covered by its own terms.