Unreleased
Section titled “Unreleased”Added
$connector->users()->summaries()andbans()take any number of Steam IDs and cut a list over 100 into requests of 100 thatpool()sendsconcurrencyat a time, 5 by default, each once. The first failure is thrown after the chunks in flight have answered, and no chunk goes out after it (#121).Testing\SteamFake, a SaloonMockClientfaking every endpoint through the resource chain, withTesting\SteamResponsefor each envelope and failure Steam sends and a factory per DTO underTesting\Factories, moved from the Laravel bridge. A new fake clearsMemoryStore, and fakedsummaries()andbans()answer each request with the players it asks for (#120).
Changed
- BC break
SteamConnectoris final. A subclass moves its overrides toSteamConfig:rateLimits,rateLimitStore,senderortooManyRequestsCooldown(#123). - DTO constructors and
fromArray(), theRequestSending,ResponseReceivedandSteamSenderconstructors, and the exceptions’ named constructors are@internal, outside SemVer from 1.0.0. Tests build DTOs with theTestingfactories, and PHPStan with bleeding edge reports a call from outside the SDK (#123).
Fixed
- A request sent without the key (
SendsNoApiKey) is limited apart from a configured key, as a connector without one would limit it: it no longer spends the daily budget or is refused while the key is spent or locked, and a429on it locks only requests without the key, underSteamConnector:anonymous.rateLimitscount it apart as well (#138).
Added
GetServerInfoRequest(ISteamWebAPIUtil), returning Steam’s clock as aDateTimeImmutablein UTC, reached anonymously through the newWebApiResourceas$connector->webApi()->serverTime()(#98).GetSupportedApiListRequest(ISteamWebAPIUtil) with theWebApiInterface,WebApiMethodandWebApiParameterDTOs, listing every Web API method Steam lets the caller see as$connector->webApi()->supportedApis(). It implements the newSendsOptionalApiKeycontract, so the configured key goes along and adds the methods it can call, a connector without one gets the anonymous list, and a rejected key raisesInvalidApiKeyException(#99).rateLimitsonSteamConfig, a list of the rate limit plugin’sLimits that replaces the daily budget of 100 000, with a key or without;[]meters nothing locally, and a429from Steam still raisesSteamRateLimitException. Two limits sharing a name throw the newInvalidRateLimitExceptionas the config is built (#101).senderonSteamConfig, any SaloonSenderin place of the defaultGuzzleSender. Whatever is injected, a request Steam never answered, a PSR-18 network exception included, raisesSteamConnectionExceptionand is retried (#102).tooManyRequestsCooldownonSteamConfig, the seconds a429withoutRetry-Afterrefuses requests locally, 60 by default, with a key or without and whateverrateLimitsis. ARetry-Afterfrom Steam still wins, and a value under 1 throwsInvalidRateLimitExceptionas the config is built (#117).
Changed
- BC break
SteamConnector::sender()returnsSteamSender, which wraps the sender in use, instead of aGuzzleSender, soaddMiddleware(),getHandlerStack()andgetGuzzleClient()are gone from it. Callers that need Guzzle pass aGuzzleSenderof their own assender(#102). - GitHub Actions are pinned to full commit SHAs with the version in a trailing comment, and the docs workflow runs
@fkrzski/docs-schema0.1.0 instead of the latest release. A tag can be moved to another commit after the fact, so the version tags used since 0.3.0 did not fix what runs (#103).
Fixed
- The daily budget resets at 00:00 UTC instead of 24 hours after the first request of its window. The limit is now named
100000_every_utc_midnight, so counters already held in a persistent store restart from zero once (#100). PlayerSummarymaps thecommunityvisibilitystate2that Steam returns for friends-only profiles to the newCommunityVisibility::FriendsOnly, where it threwUnexpectedValueExceptionand failed the wholeGetPlayerSummariesRequestbatch. Any other value outside1–3still throws (#106).
Added
GetNewsForAppRequest(ISteamNews) with theAppNewsandNewsItemDTOs, returning a page of a game’s news with Steam’s total for the filter, reached anonymously through the newNewsResourceas$connector->news()->appNews(). Steam answers an app ID it does not know exactly like some apps that exist, so both raise the newAppNewsUnavailableException(#77).SteamConnector::onFailure(), fired once per call undersend(),sendAsync()andpool()with the exception the caller gets: after the retries, as the SDK exception, and also for a request refused locally for the quota or a missing key. An exception thrown in the hook is dropped, so the caller still gets the original (#80).SteamConnector::onRequest()andonResponse(), fired on every attempt including undersendAsync()andpool(), with the key-freeRequestSendingandResponseReceivedpayloads: the Steam method, the query, the attempt number and, on the response, the status and thedurationin seconds. A429reachesonResponsebefore it becomesSteamRateLimitException, and an exception thrown in a hook reaches the caller (#79).symfony/var-dumperinsuggest, because the default dumpers behinddebug()need it and Composer does not surface Saloon’s own suggestion. Without it the first debugged request fails on a missing class (#78).
Changed
- BC break
getPrevious()isnullonSteamConnectionExceptionand on aSteamApiExceptionraised from a4xxor5xx, where 0.7.0 chained Saloon’sFatalRequestExceptionorRequestException, so callers read the status fromgetCode()and the payload fromresponse. Guzzle 7 quotes the request URI, API key included, in the exception beneath, and error trackers store every exception on the chain (#86). - BC break
ProfileNotPublicException::forSteamId(),ProfileNotPublicException::forPrivateOrMissing()andSteamUserNotFoundException::forVanity()require the SaloonResponse, so callers building them by hand have to pass one.GetOwnedGamesRequestandResolveVanityUrlRequestnow do, which gives their failures aresponse, code200rather than0and the Steam method in the message (#90). - BC break
send(),sendAsync()andpool()raise the failure Steam reports in a200payload forGetOwnedGamesRequest,GetSteamLevelRequest,GetBadgesRequest,GetRecentlyPlayedGamesRequest,GetCommunityBadgeProgressRequest,ResolveVanityUrlRequest,UpToDateCheckRequestandGetServersAtAddressRequest, where they used to return theResponseand leave the exception todto(), so callers catch it around the call itself. That brings it toonFailure()and to thepool()exception handler, while the resource methods throw what they did before (#94). - Every exception built from a Steam response, and
SteamConnectionException, opens its message with the Steam method called, as inGetFriendList: Steam API request failed with HTTP 500.Classes and codes are unchanged, so only code matching on the message text is affected (#81).
Fixed
SteamConnector::debugRequest(), anddebug()through it, pass the callable a copy of the PSR request with the API key masked askey=***, while the request sent to Steam keeps it. A pasted dump or a logging callback used to leak the key (#78).SteamConnectionExceptionmasks the API key askey=***in the Guzzle message it quotes (#86).SteamConfigmarksapiKeyas#[\SensitiveParameter], so an invalid timeout or retry setting no longer puts the key among the stack-trace arguments whenzend.exception_ignore_argsis off (#86).sendAsync()andpool()count a4xxor5xxagainst the daily budget and turn a429intoSteamRateLimitException, refusing the requests after it locally, where they used to count only a2xxand raiseSteamApiExceptionwith code429. A failed response runs the response pipeline as undersend(), sodebugResponse()sees it too (#85).sendAsync()andpool()return the promise their response pipeline settles, so a middleware throwing on a2xxrejects the call and aResponsea middleware swaps in reaches the caller. Saloon ran that pipeline on a branch it dropped, losing both (#79).
Added
ApiKeyNotConfiguredExceptionfor a request that needs an API key sent from a config carrying none, and for a blank key handed toSteamConfig. Both are raised locally, so neither costs a round trip or a slot of the daily budget (#59).GetSdrConfigRequest(ISteamApps) with theSdrConfig,SdrPointOfPresenceandSdrRelayDTOs, mapping the Steam Datagram Relay network a game connects through, keyed by point-of-presence code and reached anonymously as$connector->apps()->sdrConfig(). An app ID Steam does not know answers500, which raisesAppNotFoundExceptionat once rather than being retried (#66).GetServersAtAddressRequest(ISteamApps) with theGameServerDTO and theServerRegionenum, listing the game servers Steam knows at an IPv4 address, reached anonymously as$connector->apps()->serversAtAddress(); an address with none returns an empty list. An address Steam rejects raises the newInvalidServerAddressException, and any other refusal raises the rootSteamApiExceptioncarrying Steam’s own message (#65).SteamConnectionExceptionfor a request that never reached Steam — a timeout, a DNS failure, a TLS error, a refused connection. It carries no response and a0code, with Saloon’sFatalRequestExceptionongetPrevious()(#60).UpToDateCheckRequest(ISteamApps) with theAppVersionCheckDTO, telling whether a game server’s version is current and which one Steam requires, reached anonymously through the newAppsResourceas$connector->apps()->upToDateCheck(). Steam answers an app ID it does not know exactly like a game running no versioned servers, so both raise the newAppVersionUnavailableException(#64).connectTimeoutandrequestTimeoutonSteamConfig, in seconds, falling back to Saloon’s 10 and 30 when leftnull.0disables the limit and a negative value throwsInvalidTimeoutException(#61).tries,retryInterval(in milliseconds) andexponentialBackoffonSteamConfig, defaulting to a single attempt; fewer than one try or a negative interval throwsInvalidRetryException. Only a request Steam never answered or a5xxis retried, never a4xxorSteamRateLimitException, because every attempt spends a slot of the daily budget (#62).
Changed
- BC break
SteamConfig::$apiKeyis a?stringdefaulting tonull, which makesGetNumberOfCurrentPlayersRequestandGetGlobalAchievementPercentagesForAppRequestreachable with no key configured. Callers reading the property as astringhave to handlenull(#59). - BC break Transport failures out of
send(),sendAsync()andpool()surface asSteamConnectionExceptionrather than Saloon’sFatalRequestException. Callers catching the Saloon exception have to catch the SDK one instead (#60). - A connector without an API key meters no daily budget, because Steam bills the 100 000 requests to the key; a
429from Steam still raisesSteamRateLimitException(#59). InvalidApiKeyException::missing()reads “Steam received no API key” rather than “Steam API key is missing”, so Steam’s verdict no longer reads like the local configuration error (#59).- A status the SDK gives no meaning of its own raises the root
SteamApiExceptionwith Saloon’sRequestExceptionongetPrevious(), which is what lets the retry loop see it (#62).
0.6.0
Added
AppNotFoundExceptionfor an app ID Steam does not know, thrown only where the response separates it from an app that merely exposes no stats. The400onGetPlayerAchievementsRequestdoes not, so it keepsStatsUnavailableException(#47).GetGlobalAchievementPercentagesForAppRequest(ISteamUserStats) with theGlobalAchievementDTO, listing how much of the playerbase has unlocked each achievement in a game. Steam spells the parametergameidrather thanappidand the SDK follows, so this one method takes$gameId;403comes back identically for a game carrying no achievements and for a game ID Steam does not know, so both raiseStatsUnavailableException(#50).GetNumberOfCurrentPlayersRequest(ISteamUserStats) returning a game’s concurrent player count as a plainint. Steam serves the endpoint anonymously, so requests implementing the newSendsNoApiKeycontract go out with the configured key stripped from the query (#49).GetSchemaForGameRequest(ISteamUserStats) with theGameSchema,SchemaAchievementandSchemaStatDTOs, listing every stat and achievement a game publishes, localised through theLanguageenum. An app publishing no schema comes back as an emptyGameSchemarather than a failure, because Steam answers400for an app ID it does not know and that separates the two (#48).Languageenum backing Steam’slquery parameter, and alanguageonSteamConfigthat applies it to every request whose endpoint localises its payload. Steam’s codes are Valve’s own rather than ISO, so the enum transcribes their table instead of mapping to one (#46).
Changed
- BC break
$languageis a?Languageinstead of a?stringonGetPlayerAchievementsRequest,GetUserStatsForGameRequestand bothStatsResourcemethods. Callers passing a raw code swap'english'forLanguage::English(#46).
0.5.0
Added
- Fluent resources on the connector:
PlayersResource,UsersResourceandStatsResource, reached through$connector->players(),->users()and->stats(). Each method sends its request and returns the DTO; the request classes are unchanged and stay the way to reach theResponseitself or Saloon’spool()(#40). GetBadgesRequest(IPlayerService) with thePlayerBadgesandBadgeDTOs, covering every earned badge alongside the experience behind the community level. An account with no badges answers exactly like a SteamID64 that belongs to nobody, so neither is treated as a failure (#38).GetCommunityBadgeProgressRequest(IPlayerService) with theCommunityBadgeQuestDTO, listing the quests behind the Steam community badge and whether each one is done. Steam’sbadgeidfilter is not exposed, because every ID other than0answers with the same empty payload a withheld profile gets (#39).GetRecentlyPlayedGamesRequest(IPlayerService) with theRecentlyPlayedGamesandRecentlyPlayedGameDTOs and an optionalcountlimit.RecentlyPlayedGames::$totalCountcarries Steam’s unlimited total, so a list truncated bycountstays distinguishable from a complete one (#36).GetSteamLevelRequest(IPlayerService) returning the player’s Steam community level as a plainint. Level0is also what Steam answers for a SteamID64 that belongs to no account, so it is not treated as a failure (#37).- Testing guidance in the docs: faking the connector with Saloon’s
MockClient, and clearing theMemoryStoredaily counter between tests.
Changed
- Documentation pages are
.mdxand use Starlight components — tabs for alternatives, asides for caveats, steps for setup, badges for what each request throws, and filename bars on code blocks.
Fixed
InvalidApiKeyExceptionalso covers a401whose HTML names thekeyparameter, not just a403.GetCommunityBadgeProgressRequestanswers401for a broken or absent key, which used to surface asProfileNotPublicException.- BC break
PlayerSummary::$timeCreatedis nullable, because Steam omitstimecreatedfor a hidden profile and reading it unguarded fataled the wholeGetPlayerSummariesRequestbatch. Callers reading the date need a null check (#33).
0.4.0
Added
GetPlayerBansRequest(ISteamUser) with thePlayerBanDTO andEconomyBanenum. Steam’s value set is open, so unknown values fall back toUnknowninstead of throwing (#18).GetUserGroupListRequest(ISteamUser) with theUserGroupDTO (#19).InvalidApiKeyExceptionfor a missing (400) or rejected (403) API key. Steam reports both as HTML, which is what tells them apart from a private profile on the same status.StatsUnavailableException, thrown byGetPlayerAchievementsRequestwhen Steam withholds achievements. It names both causes, because theRequested app has no statsbody is identical for an app without achievements and for a private profile.SteamApiException::$responseexposing the response behind an HTTP failure.JsonSerializableonSteamId, which now encodes to a bare string instead of{"value":"<id>"}(#25).
Changed
- BC break
SteamApiExceptiondeclares its own constructor,__construct(string $message, ?Response $response = null).getCode()is now the HTTP status, or0when the failure was raised before a request went out. - BC break HTTP failures raise SDK exceptions instead of Saloon’s
RequestExceptionsubclasses, so catchingSteamApiExceptioncovers every failure (#20). - BC break
TooManySteamIdsException::forCount()takes a requiredstring $endpoint, so the message names the request that hit the cap. - BC break
CommentPermissionandCommunityVisibilityare backed enums:CommunityVisibilitybyintusing Steam’s owncommunityvisibilitystatecodes (Hidden = 1,Visible = 3),CommentPermissionbystring('everyone','nobody','friends_only'), because Steam omitscommentpermissionentirely for the friends-only case and so has no wire integer for it. Case names andfromApiValue()are unchanged; onlyUnitEnumtype hints andinstanceof UnitEnumchecks need updating. GetPlayerAchievementsRequest,GetUserStatsForGameRequestandGetUserGroupListRequestno longer detect a private profile from the response body. Steam answers with an error status in all three cases (400, 400 and 403), so none of those checks ever ran.
Fixed
PlayerSummarycan be passed tojson_encode(). Both enums it exposes were pure, so encoding it — or any structure containing it — returnedfalsewithNon-backed enums have no default serialization(#25).DateTimeImmutableproperties on the DTOs still serialize as PHP’s internal shape and remain open there.SteamId::tryFromInput()andSteamId::extractVanityName()parse profile and vanity URLs carrying a sub-path, query string or fragment (e.g./profiles/<id>/stats/,/id/<nick>?snr=…). PreviouslytryFromInput()returnednullandextractVanityName()returned an unusable slug.- Rate limit counters are scoped to the API key instead of shared by every connector in the process, so one key can no longer spend another’s daily budget. The key is hashed into the store key, never written in plaintext, and counters already held in a persistent store restart from zero once (#30).
0.3.0
Added
GetFriendListRequest(ISteamUser) with theFriendDTO andFriendRelationshipenum.- Documentation site under
docs/, published at docs.fkrzski.dev/php-steam-api-sdk, with a CI job validating the docs frontmatter.
Changed
- BC break Request classes are grouped into per-interface subnamespaces (
Http\Requests\ISteamUser,Http\Requests\ISteamUserStats,Http\Requests\IPlayerService). Update imports when upgrading. - Test fixtures are grouped into per-interface, per-request folders (
tests/Fixtures/Saloon/<Interface>/<Request>/) with shortened variant filenames (default,empty,private, …), mirroring the request and test layout. - GitHub Actions are pinned to version tags instead of commit hashes, for readability.
- The PHPUnit cache lives under
.cache/phpunit, so PHPStan, Rector and PHPUnit share one.cachedirectory.
Removed
- Dead
.gitignoreentries for unused tooling (.php-cs-fixer.php,.php-cs-fixer.cache,.phpunit.result.cache,.phpunit.cache).
0.2.0
Added
- Mutation testing via Pest (
composer test:mutate), enforcing a 100% MSI threshold in CI. laravel/paoas a development dependency.
Changed
- PHPStan and Rector are cached in the formats workflow, to speed up CI.
- Pull requests no longer trigger duplicate workflow runs.
.gitattributescovers distribution archives and line-ending handling.- The README carries build, coverage and version badges.
- Bumped
shivammathur/setup-php(2.37.0 → 2.37.1) andactions/checkout(6.0.2 → 6.0.3).
0.1.0
Added
SteamConnector, built on Saloon v4, with the daily 100 000-request rate limit baked in viasaloonphp/rate-limit-plugin(SteamRateLimitExceptiononce exhausted).SteamConfigfor the API key and an optional customRateLimitStore.SteamIdvalue object, with strictfromSteamId64(), lenienttryFromInput()andextractVanityName()helpers.- Requests with readonly DTO responses:
ResolveVanityUrlRequest,GetPlayerSummariesRequest(batch, ≤100 IDs),GetOwnedGamesRequest,GetUserStatsForGameRequestandGetPlayerAchievementsRequest. - Domain exception hierarchy rooted at
SteamApiException(InvalidSteamIdException,SteamUserNotFoundException,ProfileNotPublicException,TooManySteamIdsException,SteamRateLimitException). - Enums:
PersonaState,CommunityVisibility,CommentPermission. - Test suite (Pest) with Saloon
MockClientfixtures, PHPStan max, 100% type coverage, Pint and Rector.