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

Sportmonks Client Bundle Laravel Package

hristonev/sportmonks-client-bundle

A Symfony bundle that integrates the SportMonks football API client, wiring it via dependency injection and configuration so PHP apps can easily fetch fixtures, leagues, teams, players, standings, and live scores.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. This is a Symfony bundle (last release April 2019, zero dependents). There is no Laravel service provider, no facade, no config publishing. Don't expect composer require + php artisan vendor:publish to work. The pragmatic path is to treat it as documentation of an API client and wire the underlying HTTP client yourself, or vendor the few classes you need.

Steps:

  1. Read the bundle's composer.json and DI extension before installing. Open src/DependencyInjection/*Extension.php and DependencyInjection/Configuration.php. Those two files tell you the service IDs, the config tree (almost certainly api_token, maybe base_uri/timeout), and which HTTP client it depends on (Guzzle, Symfony HttpClient, or a sportmonks/sportmonks-php SDK). If it pins symfony/*: ^3.4|^4.x and php: ^7.1, it will fight with a modern Laravel app — install it in a scratch project, read it, then port.

  2. Get a SportMonks token from my.sportmonks.com (API token, passed as api_token query param). Put it in .env as SPORTMONKS_TOKEN, never in a committed config file.

  3. Bind it in a service provider rather than trying to load the bundle:

// app/Providers/SportMonksServiceProvider.php
$this->app->singleton(SportMonksClient::class, fn ($app) => new SportMonksClient(
    token: config('services.sportmonks.token'),
    baseUri: 'https://api.sportmonks.com/v3/football',
    timeout: 10,
));
  1. First use case: fixtures for a competition-season. Everything in SportMonks hangs off league → season → fixture. Fetch /leagues, pick a season_id, then /fixtures/between/{start}/{end}/{teamId?} with include=participants;scores;league. Cache the result — this single call is the highest-value, lowest-cost thing you can build.

The first thing to build is a read-through cache, not a domain layer. The API is the bottleneck; caching is the architecture.

Implementation Patterns

Thin client, fat repository. Keep the vendor client as a dumb transport (URL + query building + auth) and put all resource knowledge in repositories:

class FixtureRepository
{
    public function __construct(private SportMonksClient $client) {}

    public function between(Carbon $from, Carbon $to, array $includes = []): Collection
    {
        return Cache::remember(
            "sm:fixtures:{$from->toDateString()}:{$to->toDateString()}:" . implode(',', $includes),
            now()->addMinutes(15),
            fn () => collect($this->client->get('fixtures/between/...', ['include' => implode(';', $includes)]))
        );
    }
}

Cache TTL by resource volatility, not one global value:

Resource TTL
leagues, countries, continents 24h
seasons, stages, rounds 6h
teams, players, venues 12h
fixtures (future) 1–6h
fixtures (finished) 7d, effectively immutable
livescores / in-play 15–30s, or don't cache

Key the cache on sorted query params so include=a;b and include=b;a hit the same entry.

Use include aggressively but deliberately. One request with include=participants;scores;league;venue beats five requests with no includes, and it's usually cheaper against your rate limit too. Centralise the canonical include strings as constants per endpoint so you don't hand-tune them in 20 places. Watch the filters parameter similarly — filters=fixtureLeagues:8 on a livescores endpoint is how you avoid pulling 900 matches to show 12.

Handle pagination as a first-class concern. SportMonks returns data, pagination (with has_more, next_page), and rate_limit. Wrap it:

while ($page = $response->pagination['next_page'] ?? null) { ... }

Never assume a single page. For bulk imports, prefer the between/{from}/{to} and byDate style endpoints over paging through an entire season.

Queue the syncs, schedule the reads. Pattern that works well in Laravel:

  • SyncLeaguesJob (daily) → upsert into leagues
  • SyncSeasonsJob (daily) — dispatch per league, chain to fixtures
  • SyncFixturesJob (hourly for today±2 days)
  • SyncLivescoresJob (every minute, only during match windows) — ideally gated by a "is any fixture live right now?" DB check so you're not burning requests at 4am

Persist to your own tables. Do not query SportMonks from a controller on a page request. The API is a sync source, not your read model.

Rate-limit aware transport. Wrap the client with a middleware/decorator that reads the rate_limit block from every response and stores it in cache. Then a simple guard:

if (RateLimit::remaining() < 20) { throw new RateLimitGuardException(); }

Back off on 429 with exponential delay and let the queue retry ($this->release($seconds)). Give the API-calling jobs their own queue with a low --tries and a RateLimited middleware so one runaway job doesn't starve everything else.

Testing. Use Guzzle's MockHandler/Laravel's Http::fake() with recorded JSON fixtures, including the pagination and rate_limit envelopes — the bugs live in those two blocks, not in the data payload. Write at least one "has_more" test and one "empty data + single-item pagination" test.

Gotchas and Tips

The Symfony DI mismatch is the biggest one. Even if it installs, the bundle class won't be registered by Laravel, services.yaml is never read, and constructor autowiring won't resolve. Don't fight it — copy the request-building logic and own it. Same for PHP version constraints: a 2019 bundle likely requires PHP 7.1–7.3 and will block installation on Laravel 10/11 (PHP 8.1+). Use --ignore-platform-reqs in a throwaway dir to inspect, then port.

The API token travels in the query string. Guzzle's exception messages include the full URI, so a failed request dumps your token into laravel.log and into Sentry. Redact it: catch exceptions, re-throw with a sanitised message, and add a Guzzle middleware that strips api_token from RequestException messages. Also strip it from any Http::fake-free debug output.

Rate limits are per plan and change without notice. Historically free tier was ~180 req/hour with a low entity allowance; paid tiers vary. Check the rate_limit block on every response and treat it as truth — don't hardcode a number from a blog post. Includes and nested relations consume more of your allowance than a bare call.

Timezone and date semantics. All kickoff times are UTC. Store UTC in the DB, convert in the presentation layer. fixtures/between/{from}/{to} expects YYYY-MM-DD — passing datetimes silently fails or returns the wrong window. Beware fixtures that change date: a postponed match moves out of your "today" window, so your sync range must always overlap ±48h, not just today.

Fixtures mutate. state_id transitions (NS → 1H → HT → FT → AET/PST/CANC), scores update, lineups appear ~1h before kickoff, and IDs stay the same. Always upsert on the SportMonks ID with updateOrCreate, and store state_id as a foreign-key-ish integer you map to your own enum. Never delete-and-reinsert — you'll destroy relationships.

Nulls are normal. Lineups, scores, and participants.meta are frequently absent for lower leagues and future fixtures. Map defensively (data_get()), and don't assume scores is a map — it's an array of {type_id, score: {goals, participant}} objects that you have to pivot yourself.

Sparse/optional fields and deleted flags. Some responses include soft-deleted entities. Filter on the flag rather than assuming the array is clean.

Extension points, if you keep the bundle code: the client is almost certainly a single injectable service — decorate it, don't fork it. Register your own implementation against the same interface, wrap the ClientInterface, and swap the underlying HTTP client (Guzzle handler) for logging/retry. Bundle-level event dispatchers (if any) are Symfony's — you'd need symfony/event-dispatcher, which Laravel ships; it's compatible but you won't get framework events for free.

Don't build live-score polling into web requests. If you must show in-play data, push it to the client (broadcast/Echo/poll your own /api/livescores backed by cache) rather than proxying SportMonks per user. N users × 1 request/minute will exhaust your plan before lunch.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin