Use Steamworks from PHP 8.5+ with zero C extensions — just PHP's built-in ext-ffi and the
redistributable Steamworks SDK binary.
composer require datanana-com/beamworks-php
# then put the Steam redistributable next to your composer.json:
# Windows: steam_api64.dll · Linux: libsteam_api.so (see Setup)use DatananaCom\BeamworksPhp\SteamClient;
$client = SteamClient::create();
$client->init(480); // 480 = Spacewar, Valve's test appid
echo $client->user()->getSteamId() . "\n";
$client->shutdown();That's the whole shape: create() → init() → use the typed accessors (user(), stats(),
friends(), apps(), utils()) → shutdown(). You never touch the FFI layer directly.
- Requirements
- Installation
- Setup — supply the Steam binary
- Usage
- API coverage
- Architecture
- License
- For maintainers
| Requirement | Detail |
|---|---|
| PHP | 8.5+, 64-bit only (32-bit is not supported) |
| Extension | ext-ffi enabled (ffi.enable=1) |
| Steamworks SDK | 1.64 — needs Steamworks partner access |
| OS | Linux x64 or Windows x64 (macOS best-effort) |
| Runtime | The Steam client must be running for live calls |
composer require datanana-com/beamworks-phpThe package ships the prebuilt cdef header, PHP enums, and callback registry (in generated/),
so there is nothing to generate — composer require is enough on the PHP side.
The one thing the package can't bundle is Valve's redistributable binary (their license forbids it). Provide it in any one of these ways:
| Method | What you do | Example |
|---|---|---|
| A — project root | Drop the binary next to your composer.json |
./steam_api64.dll (Win) · ./libsteam_api.so (Linux) |
| B — env var | Point STEAMWORKS_LIB_PATH at the binary |
export STEAMWORKS_LIB_PATH=/abs/path/to/steam_api64.dll |
| C — SDK layout | Keep the SDK's redistributable_bin/ under sdk/ in your project root |
sdk/redistributable_bin/win64/steam_api64.dll |
Resolution order: an explicit path passed to SteamFFI::load() → STEAMWORKS_LIB_PATH →
auto-detection (project root, then the sdk/ subtree). If none is found, create() throws
SteamLibraryNotFoundException with the same guidance.
The lifecycle, at a glance:
create() ──▶ init(appId) ──▶ start() ──▶ ┌─ loop ─────────────┐ ──▶ shutdown()
│ tick(); usleep(...) │
└────────────────────┘
start() / tick() are only needed when you consume callbacks (below). For one-shot reads
like identity, create() + init() is enough.
use DatananaCom\BeamworksPhp\SteamClient;
$client = SteamClient::create();
$client->init(480);
if ($client->user()->isLoggedOn()) {
echo "Logged in as SteamID: " . $client->user()->getSteamId() . "\n";
}use DatananaCom\BeamworksPhp\SteamClient;
$client = SteamClient::create();
$client->init(480);
$stats = $client->stats();
$ok = $stats->setAchievement('ACH_WIN_ONE_GAME') && $stats->storeStats();Important: Steam auto-requests the local user's stats during
init(), but they arrive asynchronously via theUserStatsReceived_tcallback.setAchievement()/storeStats()only take effect once those stats have been received. In a real app, pump the callback loop and wait forUserStatsReceivedbefore writing (see below). Always check the boolean return.
Callbacks are delivered as PSR-14 events while the pump runs:
tick() ──▶ Fiber resume ──▶ ManualDispatch RunFrame / GetNextCallback
──▶ SteamCallbackEvent (PSR-14) ──▶ your listener
use DatananaCom\BeamworksPhp\Events\SteamCallbackEvent;
use DatananaCom\BeamworksPhp\Events\SteamCallbackId;
use DatananaCom\BeamworksPhp\Events\SteamEventDispatcher;
use DatananaCom\BeamworksPhp\SteamClient;
$received = false;
$dispatcher = new SteamEventDispatcher();
$dispatcher->addListener(SteamCallbackEvent::class, function (SteamCallbackEvent $event) use (&$received): void {
if ($event->callbackId === SteamCallbackId::UserStatsReceived->value) {
$received = true;
}
});
$client = SteamClient::create($dispatcher);
$client->init(480);
$client->start(); // spawns the Fiber pump; nothing is delivered until tick()
while (!$received) { // wait for the stats Steam requested at init()
$client->tick();
usleep(16_000); // ~60 fps
}
// Stats are now loaded — safe to write achievements.
$client->stats()->setAchievement('ACH_WIN_ONE_GAME');
$client->stats()->storeStats();See docs/callback-guide.md for the async CallResultHandle pattern
(registerCallResult → tick → await).
| Exception | Meaning | Common cause |
|---|---|---|
SteamLibraryNotFoundException |
The redistributable couldn't be located | Binary missing or misplaced (see Setup) |
SteamInitException |
SteamAPI_Init failed |
Steam client not running, or wrong appId |
All exceptions extend SteamException (a \RuntimeException), so a single
catch (\RuntimeException) still works. Both load and init happen inside create()/init(), so
wrap both:
use DatananaCom\BeamworksPhp\Exception\SteamException;
use DatananaCom\BeamworksPhp\Exception\SteamInitException;
use DatananaCom\BeamworksPhp\Exception\SteamLibraryNotFoundException;
use DatananaCom\BeamworksPhp\SteamClient;
try {
$client = SteamClient::create(); // resolves + loads the Steam binary
$client->init(480); // starts the Steam API
} catch (SteamLibraryNotFoundException $e) {
// Binary not found — see Setup (drop it in your project root or set STEAMWORKS_LIB_PATH).
} catch (SteamInitException $e) {
// Steam not running, or the appId is wrong.
} catch (SteamException $e) {
// Anything else from the library.
}Pass any PSR-3 logger to observe init, pump, and callback dispatch:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use DatananaCom\BeamworksPhp\SteamClient;
$logger = new Logger('steam');
$logger->pushHandler(new StreamHandler('php://stderr'));
$client = SteamClient::create(logger: $logger);| Steam interface | Status | Accessor | Wrapper |
|---|---|---|---|
| User | Implemented | $client->user() |
Api\User |
| Friends | Implemented | $client->friends() |
Api\Friends |
| UserStats | Implemented | $client->stats() |
Api\UserStats |
| Apps | Implemented | $client->apps() |
Api\Apps |
| Utils | Implemented | $client->utils() |
Api\Utils |
| RemoteStorage | Implemented | $client->remoteStorage() |
Api\RemoteStorage |
| Inventory | Implemented | $client->inventory() |
Api\Inventory |
| UGC | Implemented | $client->ugc() |
Api\Ugc |
| Input | Implemented | $client->input() |
Api\Input |
| Screenshots | Implemented | $client->screenshots() |
Api\Screenshots |
| Music | Implemented | $client->music() |
Api\Music |
| RemotePlay | Implemented | $client->remotePlay() |
Api\RemotePlay |
| Timeline | Implemented | $client->timeline() |
Api\Timeline |
| ParentalSettings | Implemented | $client->parentalSettings() |
Api\ParentalSettings |
Scope: beamworks-php targets client-side Steam features — identity, friends, stats, achievements, DLC, cloud saves, inventory, Workshop, controller input, screenshots, music, Remote Play, timeline, and parental settings. Dedicated-server, networking, and matchmaking interfaces are non-goals; reach for a dedicated solution if you need those.
Three layers, each building on the one below. Never skip a layer.
SteamClient ← lifecycle, Fiber pump, PSR-14 dispatch, typed accessors
│
SteamApi\* ← typed PHP wrappers per interface (User, Friends, UserStats, …)
│ returns DTOs from src/Dto/; marshals strings/pointers
SteamFFI ← raw FFI::cdef handle; ->call() and the ->ffi() escape hatch
│
generated/ ← codegen output: cdef header, PHP enums, CallbackRegistry
(committed + shipped — never hand-edit; regenerate via bin/codegen)
See docs/architecture.md for each layer and the golden rules that govern it.
MIT — see LICENSE. MIT is a permissive license that Valve lists as compatible with shipping on Steam, matching the MIT licensing of Steamworks.NET and Facepunch.Steamworks. (Earlier releases were GPL-3.0; Valve names GPL as the "best-known example" of a license incompatible with the Steamworks SDK, so a game shipped on Steam could not have linked the GPL version.)
Steamworks SDK: The MIT license covers this project's own PHP code. The files under
generated/(steam_api.cdef.h,SteamTypes.php,CallbackRegistry.php) are derived from Valve's Steamworks SDK and ship in the package socomposer requireworks without running codegen; they remain subject to Valve's own SDK terms — see NOTICE. You must still obtain the SDK through the Steamworks partner portal. The redistributable runtime binaries (steam_api64.dll,libsteam_api.so) are not bundled here — provide them with your own application per Valve's redistribution guidelines.
Contributor workflows — testing, code style, and regenerating generated/ against a new SDK
release — live in CONTRIBUTING.md and docs/sdk-sync.md.
Consumers never run bin/sync-sdk or bin/codegen; those tools are excluded from the Composer
tarball.