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

Igdb Laravel Laravel Package

marcreichel/igdb-laravel

A Laravel wrapper for the IGDB (Internet Game Database) API that handles Twitch OAuth authentication automatically and provides a fluent query builder for fetching games, covers, genres, and more. Config and facade integrate cleanly with Laravel.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and configure (10 minutes)

composer require marcreichel/igdb-laravel
php artisan vendor:publish --tag=igdb-config

Create a Twitch developer application at dev.twitch.tv/console/apps (client credentials flow — no user login involved), then set:

IGDB_CLIENT_ID=xxxxxxxxxxxxxxxx
IGDB_CLIENT_SECRET=xxxxxxxxxxxxxxxx
IGDB_RETRIES=3
# optional, for webhook ingress through a relay/proxy
# IGDB_WEBHOOK_BASE_URL=https://relay.example.com

The package handles Twitch OAuth for you: it requests a client-credentials token, caches it, and as of 6.1.0 refreshes a rejected token and retries the request instead of throwing.

Where to look first

  1. config/igdb.php — credentials, retry count, webhook base URL. Nothing else should need touching.
  2. docs/ in the repository (restored in-repo as of 6.1.0) — it is the canonical reference for the query builder and webhook setup.
  3. src/Models/ — the model list is your endpoint coverage map. If a model exists, you can query it; if not, drop to raw HTTP.
  4. src/Enums/ — check @deprecated markers before using them (IGDB is migrating enums to lookup tables).

First use case: a search/browse page

Do it in php artisan tinker before writing a controller — it tells you immediately whether your Twitch app is wired up correctly:

use MarcReichel\IGDBLaravel\Models\Game;

Game::select(['name', 'rating', 'first_release_date', 'cover.image_id'])
    ->where('rating', '>', 80)
    ->orderBy('rating', 'desc')
    ->limit(10)
    ->get();

The natural first ship is a search or "top rated" page: no schema changes, no background work, and it exercises auth + query builder + relations in one go. Add webhooks and incremental sync only after the read path is proven.

Implementation Patterns

Treat it as a read path to a remote catalog, not an ORM. There is no save(), no migrations, no transactions. The models are query builders + DTOs over IGDB's Apicalypse API. The pattern that survives contact with production is: query package → map into your own DTO → persist to a local table → serve the app from your table.

Always select() explicitly. The default payload is heavy and IGDB's field selection is the main lever on both latency and rate-limit pressure. Select relation sub-fields inline (cover.image_id) or eager-load with with():

$games = Game::select(['name', 'rating', 'cover.image_id'])
    ->with(['genres', 'platforms'])
    ->whereIn('id', $ids)
    ->get();

Isolate behind an adapter. Put every package call behind one interface (GameCatalog / GameRepository) and return arrays or your own DTOs from it. This is what makes the single-maintainer risk tolerable: swapping or forking the package becomes a one-file change, and your tests can fake the adapter rather than the HTTP layer.

Cache on your side, deliberately. Reference data (genres, platforms, companies, age ratings) barely changes — cache for days. Trending/rating data changes often — cache for minutes. Do not rely on the package for caching. For anything user-facing, prefer a nightly ingest into a local table over a live call in the request cycle.

Incremental sync pattern. IGDB exposes created_at / updated_at on most endpoints as unix timestamps, which is the intended cursor:

Game::select(['id', 'name', 'updated_at'])
    ->where('updated_at', '>', $lastRunTimestamp)
    ->limit(500)          // IGDB caps results per request
    ->get();

Page with limit/offset until a short page comes back, upsert on the IGDB id, and store $lastRunTimestamp in a settings table. Run it on a queue worker with concurrency of 1 so you never exceed IGDB's ~4 requests/second.

Pagination is offset-based and uncounted. IGDB returns no total count. Design "load more" UIs rather than "page 3 of 47". limit + offset is the only mechanism; keep offsets shallow because deep offsets get progressively slower.

Use whereIn for hydration. When you have a list of IGDB ids from a webhook, a local index, or a recommendation job, fetch them in one whereIn('id', [...]) call rather than a loop of find().

Webhooks: register, queue, reconcile. Webhooks give you push-based freshness (release alerts, newly added games, catalog edits). Register the subscription, let the package resolve the incoming payload to a model and dispatch the Created/Updated/Deleted event, and then — critically — have the listener dispatch a queued job that re-fetches the entity by id and upserts it. Never do the write inline in the webhook request. Set IGDB_WEBHOOK_BASE_URL when you want deliveries to land on a relay (Hookdeck, a queue front door) or a staging host instead of APP_URL; the package still appends its own path.

Retries are per-query tunable. IGDB_RETRIES covers the default. For a bulk ingest job where a failed request loses more work, raise it locally:

Game::retries(5)->where('updated_at', '>', $cursor)->limit(500)->get();

Version pin and read the changelog before minor bumps. IGDB itself churns (enum→lookup-table migration in progress) and the package chases it. Pin the version, and treat composer update on this dependency as a task with a regression pass, not a drive-by.

Gotchas and Tips

"Eloquent-style" will fool you once. $game->genres does not lazy-load. If you don't with(['genres']), the relation is empty — it's not a missing-data bug. Same for $game->cover: if you didn't select it, it's null.

Attributes you didn't select simply don't exist. A null field is almost always a missing select(), not a missing IGDB value. This is the single most common debugging detour with this package.

6.1.0 changed two things that break assumptions. $game->tags is a plain attribute again (not a relation Collection) — code doing $game->tags->pluck(...) breaks. And the GameTimeToBeat.game cast was removed because IGDB only returns game_id; use game_id and join locally.

Pin at least 6.1.0 if you use artwork. The Game::with(['artworks']) relation was broken before that (artwork vs artworks). Several other relations (AgeRating.rating_category, Character.character_species, *Website.type, *.release_region) also resolved to nothing earlier.

Deprecated enums still work — and that's the trap. Game\Category, Game\Status, PlatformType, WebsiteType, Webhook\Category and friends are marked @deprecated with unchanged case values, pointing at replacement models (GameType, GameStatus, PlatformType, ExternalGameSource, DateFormat, …). Nothing breaks today, so the migration silently never happens. If you store enum values in your own database, store the integer and keep a local label mapping table rather than hardcoding case names — that absorbs the enum→lookup-table transition without a data migration.

Webhooks need CSRF exemption and idempotency. Add the webhook route to your CSRF exceptions or IGDB's POSTs will 419. And assume at-least-once delivery with no ordering guarantee: key your upserts on the IGDB id and make the listener idempotent. Webhooks are also best-effort — pair them with a periodic reconciliation sync, especially for anything user-visible like release alerts.

Rate limits are your problem, not the package's. There is no built-in throttle. A naive foreach over 5,000 ids will get you 429s. Throttle ingestion jobs with Laravel's RateLimiter or a sleep, and run the sync worker with --max-jobs / concurrency 1.

Token cache in multi-server setups. The access token is cached; if your cache store is per-node (array, file on ephemeral containers), every node does its own Twitch handshake. 6.1.0's refresh-and-retry covers rejection, but use a shared cache (Redis) to avoid the churn in the first place.

Images are ids, not URLs. IGDB returns image_id; you build https://images.igdb.com/igdb/image/upload/t_cover_big/{image_id}.jpg. Wrap this in one helper or accessor and never scatter the string across Blade templates — the size tokens (t_cover_big, t_thumb, t_1080p) are your bandwidth lever.

Testing seam. Fake at your adapter boundary first — it's stable and fast. If you must test the package layer, its requests go through Laravel's HTTP client, so Http::fake() with recorded IGDB payloads is the fallback. Record a handful of real responses early; they double as fixtures and as documentation of the shape you depended on.

Log the raw failure. IGDB errors (bad Apicalypse syntax, unknown field, invalid sort) come back as 400s that surface as generic exceptions. When a query misbehaves, dump the built query before sending it. Sorting and where on a field you didn't select is the second most common cause of a 400 after typos in attribute names.

Budget an upgrade slot. IGDB is mid-migration from enums to lookup tables, and this is effectively a single-maintainer package with minimal external validation. Keep it behind the adapter, pin the version, watch releases, and schedule a dependency-review task each quarter rather than discovering the next breaking minor bump in production.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
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