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

Steam Api Laravel Package

syntax/steam-api

Laravel wrapper for Valve's Steam API that lets you fetch player profiles, game libraries, app details, and news through a clean, expressive interface. Configurable, PSR-friendly, and easy to integrate into any Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

3.x requires PHP 8.1+ and Laravel 10+. On an older app, pin ^2.0 until you can upgrade — the constraints are enforced by Composer, so a failed composer update is the first thing you'll see.

Add the package and its config:

composer require syntax/steam-api
php artisan vendor:publish --tag=steam-api

Grab a key from https://steamcommunity.com/dev/apikey and store it in .env:

STEAM_API_KEY=YOUR_KEY_HERE

The published config/steam-api.php reads this variable; make sure config:clear runs if you change .env mid-session.

First use case — fetch a player summary and their owned games:

use Syntax\SteamApi\Facades\SteamApi;

$player  = SteamApi::player('76561197960435530');
$profile = $player->getPlayerSummaries();
$games   = $player->getOwnedGames();

As of 3.0, an empty/failed Steam response no longer comes back as null — it throws. Wrap that first call before you build anything on top of it:

try {
    $profile = SteamApi::player($steamId)->getPlayerSummaries();
} catch (\Throwable $e) {
    report($e);
    $profile = null; // private profile, bad ID, or Steam said no
}

Look at these files first, in order:

  1. config/steam-api.php — the only config surface (API key, base URIs, timeout). Nothing is cached by default; you add that yourself.
  2. src/Facades/SteamApi.php — the entry points (player(), app(), user(), item(), group()) and the only things you should call from app code.
  3. src/Contracts/ — the IPlayer, IApp, IUser, IItem, IGroup interfaces. These define the whole public API; the I prefix is just the package's naming convention.
  4. src/Player/, src/App/, etc. — concrete classes you rarely touch directly, but useful for reading what HTTP calls each method actually makes (and where 3.x now throws).

Implementation Patterns

Single-entity lookup via the facade is the normal path. Every domain object is resolved from an ID, then methods are chained:

$summary = SteamApi::player($steamId)->getPlayerSummaries();
$level   = SteamApi::player($steamId)->getSteamLevel();
$friends = SteamApi::player($steamId)->getFriendList();
$ach     = SteamApi::player($steamId)->getPlayerAchievements($appId);

Resolve a vanity URL once, store the numeric SteamID. Vanity lookups are an extra API call and are cheaply cached. In 3.x an unresolvable vanity throws rather than returning null, so cache the failure deliberately if you accept user-supplied names:

$steamId = Cache::rememberForever("steam:vanity:{$vanity}", function () use ($vanity) {
    try {
        return SteamApi::player($vanity)->resolveVanityUrl();
    } catch (\Throwable $e) {
        return null; // cached miss; don't retry a bad vanity on every request
    }
});

Wrap every call in Cache::remember, and put the try/catch inside the cache callback. The package does not cache. Steam's per-key daily quota (~100k requests) is easy to blow through when populating a profile page. Cache summaries and owned-games lists for minutes-to-hours; cache achievement schemas for days. Catching inside the closure means a transient failure isn't memoized as a permanent empty value — return a sentinel or rethrow if you'd rather not cache the miss:

$games = Cache::remember("steam:owned:{$steamId}", now()->addHour(), function () use ($steamId) {
    try {
        return SteamApi::player($steamId)->getOwnedGames();
    } catch (\Throwable $e) {
        report($e);
        return []; // treated as "no games"; or rethrow to skip caching
    }
});

App-side lookups go through SteamApi::app($appId):

$news   = SteamApi::app($appId)->getNewsForApp();
$schema = SteamApi::app($appId)->getSchemaForGame();
$global = SteamApi::app($appId)->getGlobalAchievementPercentagesForApp();

Inject the interface, not the facade, in services and jobs. Bind what you need in a service provider (usually not needed — the container resolves these), then type-hint IPlayer if you want to swap implementations:

public function __construct(private IApp $app) {}

public function news(int $appId): array {
    return $this->app->getNewsForApp();
    // or SteamApi::app($appId) inside the method
}

The common day-to-day shape is: a small SteamService class that takes int|string $steamId, wraps facade calls in Cache::remember, catches the package's empty-response exception, normalizes failures into arrays/DTOs, and is what your controllers and jobs actually call. Keep the facade out of controllers — you'll want to swap it in tests, and you'll want exactly one place where 3.x's exceptions get converted into domain results.

Queue anything that iterates. Enriching a user's library (owned game → schema → achievements) is a fan-out of dozens of HTTP calls. Use a job per app ID, throttle with Redis::throttle() or RateLimited, and write into a local table. Don't do it in a request — and with 3.x's throwing behaviour, one bad app ID inside a fan-out will now fail the job rather than quietly writing nulls, which is usually what you want.

Multi-ID calls. Steam's GetPlayerSummaries supports up to 100 IDs at once, but this package exposes the single-ID method. If you need bulk, batch at your layer and either call the method per ID inside a job, or drop to Guzzle for the batch endpoint and keep the package for the rest.

Gotchas and Tips

  • 3.0 throws on empty responses. This is the headline behavioural change and the one that breaks upgrades. Code that did if ($games === null) or relied on ?? [] silently working on the package's return value will now hit an exception. Audit for private-profile paths, getPlayerAchievements() on games without stats, and any endpoint you call for arbitrary user input. Wrap at the service boundary and convert to your own result type.
  • getSteamLevelDetails() error handling changed (PR #112). If you were parsing the old shape or relying on partial data from a malformed response, re-read the method and re-test it — this endpoint got specific attention because Steam's payload is inconsistent.
  • Upgrading from 2.x: bump PHP to 8.1 and Laravel to 10 in the same PR, run the test suite against mocked responses, and grep for === null / ?? [] around every package call. The exception type matters — catch narrowly if the package exposes a dedicated exception class, or \Throwable if not, but don't let it escape to your global handler as a 500 for a merely-private profile.
  • The package is still not cached. Every method is a live HTTP call. If you see slow pages or 403/429 responses, you've hit rate limits — add Cache::remember before anything else.
  • Use Cache::rememberForever for the app list. getAppList() returns the entire Steam catalog (tens of MB). Never call it in a request; run a scheduled command and store to DB.
  • Steam IDs are strings. Passing an integer can silently lose precision on 32-bit or get coerced oddly by Guzzle. Always pass (string) $steamId.
  • Not every "empty" is an error. Treat a thrown exception on a private profile as a normal domain outcome, not an incident — log at info, not error, or your monitoring will be noise.
  • Vanity URL resolution is the most rate-limited call. Cache aggressively and prefer accepting numeric IDs in your own endpoints.
  • Timeouts and retries live in Guzzle, not the package. Publish/override the config and set sensible connect/read timeouts; wrap calls in Laravel's retry() helper for transient 5xx — but make sure the package's empty-response exception doesn't get retried pointlessly. Retry transport failures, not "this profile is private".
  • Mock the facade in feature tests. SteamApi::shouldReceive('player')->andReturn($fakePlayer) (implement IPlayer in a test double) keeps tests fast and offline. Never let tests hit the real Steam API. Also add a test case for the throwing path — the 3.0 upgrade is exactly the kind of change that a happy-path-only suite misses.
  • The I-prefixed interfaces are the extension point. If you need a custom return shape or a batch endpoint, implement the interface and bind it; the facade doesn't know or care which concrete class backs it.
  • Config and cache both need clearing after .env changes — php artisan config:clear is the usual fix for "key is set but package says it's missing."
  • getAssetPrices and currency/region. Price endpoints are region-sensitive; pass the currency explicitly and cache per-currency-per-app, otherwise you'll mix USD and EUR responses.
  • Achievement endpoints are per-app. Don't assume a game supports them; check getGlobalAchievementPercentagesForApp first, and expect the 3.x exception for games without stats — treat it as "not supported", not as a broken integration.
  • Log the raw response on unexpected shapes. A debug tap on Guzzle (->getHandlerStack()) reveals whether Steam changed a payload, the package failed to map it, or it was genuinely empty and thrown — faster than guessing which of the three you're looking at.
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