Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sleeper Api Symfony Bundle Laravel Package

hans-peter-ording/sleeper-api-symfony-bundle

A Symfony bundle that wraps the Sleeper API, giving your app ready-to-use services and configuration for Sleeper fantasy football data. Register the bundle, set your options, and inject the client to fetch leagues, rosters, users, and player information.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: the package is a Symfony bundle, not a Laravel package. It ships a Bundle subclass and a DependencyInjection/Extension, wires services into a Symfony container, and almost certainly has an optional Doctrine ORM dependency for the "import" half of its job. composer require hans-peter-ording/sleeper-api-symfony-bundle inside a Laravel app will install, but nothing will auto-register — there is no Illuminate\Support\ServiceProvider. Plan on one of two paths:

  1. Bridge it — add a tiny service provider that requires the bundle's Resources/config/services.php/.yaml manually or, more realistically, instantiates the bundle's client/importer class yourself and binds it into the container.
  2. Vendor the useful parts — open src/ in vendor/hans-peter-ording/sleeper-api-symfony-bundle and copy the HTTP client + DTO/entity mapping into app/Services/Sleeper/. Given 0 stars and 0 dependents, this is the lower-risk option for most Laravel teams.

Where to look first, in this order:

  • composer.json — confirms the Symfony version constraint and whether doctrine/orm is required or suggested. This tells you if you're borrowing a client or a whole persistence layer.
  • src/DependencyInjection/Configuration.php — the authoritative list of config keys (base URI, API token placeholder, entity manager, cache, timeout). This is the file that documents the package better than its README.
  • src/ service classes — look for anything named *Client, *Importer, *Api, *Fetcher. That is the actual surface area you'd use day to day.

First use case, and the one that justifies the whole exercise: resolve a Sleeper username to their leagues, then pull rosters for one league.

// Bootstrap without the Symfony container
$client = new \HansPeterOrding\SleeperApiSymfonyBundle\...\Client(
    baseUri: 'https://api.sleeper.app',
    timeout: 10,
);

$user    = $client->user('some_username');           // ->user_id, ->display_name
$leagues = $client->leagues($user->user_id, 'nfl', '2025');
$rosters = $client->rosters($leagues[0]->league_id);

If the method names don't match, the underlying calls are the plain Sleeper REST endpoints (/v1/user/{username}, /v1/user/{user_id}/leagues/nfl/{season}, /v1/league/{league_id}/rosters) and you can call them with Http::get() in five minutes. That is the honest baseline against which to judge whether the bundle earns its place.

Also verify the last-release date in composer.json/tags before committing — the metadata claims a 2026 release, which is worth confirming against Packagist rather than trusting the listing.

Implementation Patterns

Bind it behind your own interface, not the bundle's concrete class. Whether you bridge or vendor, never let HansPeterOrding\...\Client leak into controllers and jobs. One SleeperClientInterface in app/Contracts, one adapter, one binding in AppServiceProvider. That way the bundle is swappable in an afternoon when it stops being maintained.

// app/Providers/SleeperServiceProvider.php
public function register(): void
{
    $this->app->singleton(SleeperClientInterface::class, fn () =>
        new SleeperClientAdapter(
            baseUri: config('services.sleeper.base_uri', 'https://api.sleeper.app'),
            timeout: config('services.sleeper.timeout', 10),
        )
    );
}

Split reads into two tiers: the player dump and everything else. The bundle's importer is most valuable for the bulk path. /v1/players/nfl is a multi-megabyte single JSON object covering every NFL player; Sleeper explicitly asks clients to fetch it at most once per day. Everything else (leagues, rosters, users, matchups, transactions, traded_picks, drafts) is small, league-scoped, and cheap to re-fetch.

// app/Console/Commands/ImportSleeperPlayers.php
public function handle(SleeperClientInterface $sleeper): int
{
    $players = $sleeper->players('nfl'); // ~5MB associative array keyed by player_id

    DB::transaction(function () use ($players) {
        foreach (array_chunk($players, 500, true) as $chunk) {
            Player::upsert(
                collect($chunk)->map(fn ($p, $id) => [
                    'sleeper_id' => $id,
                    'full_name'  => $p['full_name'] ?? null,
                    'position'   => $p['position'] ?? null,
                    'team'       => $p['team'] ?? null,
                    'status'     => $p['status'] ?? null,
                    'payload'    => json_encode($p),
                ])->values()->all(),
                uniqueBy: ['sleeper_id'],
                update: ['full_name', 'position', 'team', 'status', 'payload'],
            );
        }
    });

    return self::SUCCESS;
}
// schedule: ->dailyAt('06:15') in routes/console.php

Everything else is a scheduled sync, not a request-time call. Sleeper has no webhooks, so there is no push channel to subscribe to. The bundle gives you pullers; you supply the cadence. Practical schedule:

Job Cadence Endpoint family
Player dump daily /v1/players/nfl
NFL state (season/week) hourly /v1/state/nfl
Leagues/rosters/users every 6h in season, daily off-season /v1/league/*
Matchups every 15–30 min during game windows /v1/league/{id}/matchups/{week}
Transactions every 15–30 min, backfill rounds /v1/league/{id}/transactions/{round}

Drive the whole schedule off /v1/state/nfl — it returns season, week, season_type, and leg. If season_type !== 'regular' or week === 0, skip the matchup job entirely. Hardcoding "week 1 = September" is the single most common way these integrations rot.

Upsert on Sleeper's IDs, never on names. league_id and user_id are digit strings, roster_id is an int, player_id is a string. Keep them as the natural key and store the raw payload as a JSON column so you can re-derive fields the bundle doesn't map without re-fetching:

League::upsert($leagues->map(fn ($l) => [
    'sleeper_id'         => $l->league_id,
    'name'               => $l->name,
    'season'             => $l->season,
    'previous_league_id' => $l->previous_league_id,
    'payload'            => json_encode($l),
])->all(), uniqueBy: ['sleeper_id'], update: ['name', 'season', 'payload']);

Respect the rate limit centrally. Sleeper's documented ceiling is roughly 1,000 calls/minute; you will brush against it if you loop rosters over many leagues or iterate transaction rounds 1–18. Put a RateLimiter::attempt() or a ->middleware('throttle:...') on the job rather than inside the bundle's client, so the limit survives if you swap the client out.

Test with recorded fixtures, not live calls. Capture one real response per endpoint into tests/Fixtures/sleeper/*.json and Http::fake() them. The bundle, if it uses Symfony HttpClient, is not faked by Http::fake() — another argument for the adapter, whose Http:: calls you can fake. Never point CI at the real API; it's unauthenticated, so a test suite becomes an unintentional load generator.

Gotchas and Tips

  • No auth, but not no rules. Sleeper's API is fully public — there's no token to configure despite what a config key named api_key or token might suggest. If the bundle's Configuration.php demands one, it's a placeholder and you can pass any string. The real constraints are the ~1,000 req/min ceiling and the once-daily player-dump guidance.
  • Framework mismatch is the dominant gotcha. Symfony-isms you'll hit while bridging: autowired constructor args that Laravel can't resolve, EntityManagerInterface type-hints where you want Eloquent, Symfony\Component\HttpClient\* (not fakeable by Http::fake()), and exceptions extending Symfony\Contracts\HttpClient\Exception\* rather than Illuminate\Http\Client\RequestException. Catch broadly and re-throw as your own exception type at the adapter boundary.
  • League IDs churn every season. A league's league_id is new each year and links backwards via previous_league_id. Storing one ID and assuming permanence breaks in January. Walk the chain to build league history, and guard against cycles (previous_league_id can be "0" at the origin).
  • roster_id is not user_id. Rosters are keyed by roster_id; the owning user is in owner_id, and co-owners live in a separate co_owners array. Joining rosters to users on the wrong field silently produces "empty team" bugs that look like API failures.
  • Nulls are everywhere and mean different things. team: null means free agent; status: "Active" vs missing; injury_status absent vs "Questionable". Coalesce deliberately ($p['team'] ?? null) rather than trusting presence, and never ->team on a typed DTO without a null check.
  • Timezones. Sleeper returns epoch-millisecond timestamps in many places, not ISO strings. Carbon::createFromTimestampMs() — not createFromTimestamp() — or you'll land in 1970.
  • Config quirks. Bundle config lives under its own root key (something like sleeper_api_symfony:); in Laravel you'll be hand-merging it. Publish nothing, mirror the keys into config/services.php under sleeper, and read via config('services.sleeper.*') so you get config:cache compatibility for free.
  • Debugging: log the raw body before parsing. Wrap the adapter's request in a Log::debug('sleeper.response', ['path' => $path, 'status' => $status, 'body' => Str::limit($body, 2000)]). When the bundle's DTO mapping throws a TypeError on a field Sleeper changed, that log line is the difference between a 2-minute and a 2-hour fix. Telescope's HTTP watcher won't see Symfony HttpClient traffic — another adapter argument.
  • Extension points, in order of preference: (1) decorate the client interface rather than subclassing the bundle's class; (2) decorate the importer to add post-persist events (LeagueSynced, RosterSynced) so your app can react without forking; (3) override the service definition if you must stay in the Symfony container. Avoid editing vendor/.
  • Bus factor. 0 stars, 0 dependents, single author, no community. Pin an exact version in composer.json, keep the adapter thin so replacement is a one-file change, and be prepared to vendor the source into app/ if the repo goes quiet. Treat the bundle as a reference implementation of Sleeper's schema — that's genuinely its highest-value asset — rather than as a long-lived dependency.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle