velocity-sports-labs/datacenter-php-sdk
A PHP SDK for Velocity Sports Labs’ DataCenter API, providing a clean client wrapper, typed request and response handling, and straightforward integration for Laravel or standalone PHP applications.
This is a thin, framework-agnostic PHP client for FSL's Data Center API — not a Laravel package with a service provider, config file, or facade. Treat it as a typed HTTP wrapper you wire up yourself.
Install and inspect first
composer require velocity-sports-labs/datacenter-php-sdk
Before writing application code, spend ten minutes in the repo:
composer.json — PHP version constraint and whether it pulls Guzzle, Saloon, or Laravel's HTTP client. This determines how you mock it in tests.src/ — find the root client class (likely DataCenter, Client, or similar) and the resource classes hanging off it (Leagues, Teams, Players, Fixtures, Stats). Method names here are the real documentation.README.md and any tests/ — usually the only place showing auth and a working call.CHANGELOG — the package is young and low-traffic; expect breaking changes between minors.Credentials
Get an API key from the FSL Data Center dashboard and put it in .env, never in code:
FSL_DATACENTER_KEY=your-key-here
First use case: pull a league's teams and fixtures
use VelocitySportsLabs\DataCenter\DataCenter; // confirm exact class in src/
$client = new DataCenter(apiKey: config('services.fsl.key'));
$leagues = $client->leagues()->all();
$teams = $client->teams()->forLeague($leagueId);
$fixtures = $client->fixtures()->forLeague($leagueId);
Start with read-only reference data (leagues, teams, venues) — it's static, cacheable, and lets you validate auth and response shapes before touching live stats.
Bind as a singleton in Laravel
// app/Providers/DataCenterServiceProvider.php
$this->app->singleton(DataCenter::class, fn () => new DataCenter(
apiKey: config('services.fsl.key'),
));
Add services.fsl.key to config/services.php. Don't resolve the client with new inside controllers — you lose connection reuse and make mocking painful.
Put a repository between the SDK and your app
The SDK's return types are the vendor's problem; your domain shouldn't depend on them. Wrap it:
class FixtureRepository
{
public function __construct(private DataCenter $client) {}
public function upcomingForLeague(string $leagueId): Collection
{
return Cache::remember("fsl:fixtures:{$leagueId}", now()->addMinutes(15), function () use ($leagueId) {
return collect($this->client->fixtures()->forLeague($leagueId))
->map(fn (array $f) => FixtureData::fromArray($f));
});
}
}
This gives you one place to change when the SDK breaks, and one place to fake in tests.
Cache aggressively, tiered by volatility
Key your cache by league + season + date so invalidation is obvious.
Queue the sync work, don't do it in requests
Schedule a job per league/season with withoutOverlapping() or Cache::lock(), and store raw payloads plus a normalized table. Sports data is append-heavy: persist fixtures as rows rather than re-fetching per page view.
Handle pagination explicitly
If resource methods accept page/perPage or return a paginated envelope, write one iterator and reuse it:
foreach ($client->players()->paginate(perPage: 100) as $player) { ... }
Don't assume a single call returns everything; list endpoints almost always cap.
Testing
Define a narrow interface (FixtureSource) that your repository depends on, implement it with the SDK for production and with an array stub for tests. If the SDK accepts a PSR-18/Guzzle client in its constructor, inject a MockHandler instead and assert on the request path and auth header. Avoid hitting the live API in CI.
Error handling and retries
Catch the SDK's exception type (check src/Exceptions), convert to a domain exception, and retry only on 429/5xx with exponential backoff. Don't retry 4xx — a bad key or unknown league ID will just burn quota.
Rate limiting
Wrap calls in a job middleware or a small token-bucket guard. If you run multiple workers, use a shared Redis limiter; in-process sleep won't coordinate across processes.
composer.json, commit the lockfile, and keep the SDK behind your repository interface so you can fork or replace it in an afternoon.config/datacenter.php or a facade. If composer.json lacks a extra.laravel block, wiring is entirely on you.Authorization: Bearer, others an api_key query param or X-API-Key header. A 401 with an HTML body usually means a missing Accept: application/json header rather than bad credentials — check the raw response.guzzlehttp/guzzle than your Laravel version, Composer will either resolve to something surprising or refuse. Run composer why guzzlehttp/guzzle after install.int and don't use them as array keys without normalizing.$data['stats']['passing']['yards'] ?? null) beats assuming a schema, and log unknown keys during integration.null returns and [] for "no fixtures" — they are different states and your UI should distinguish them.now() is within a few hours of a fixture, or bust the cache after each poll.Middleware::log() and a Middleware::history() so you can dump the last request/response. Otherwise add temporary var_dump around the raw call before your mapping layer. Redact the auth header when logging.X-RateLimit-* and Retry-After headers on every response during integration; they're your only source of truth. Back off on 429 rather than hammering.How can I help you explore Laravel packages today?