hmennen90 / php-steamworks
PHP extension providing access to the Valve Steamworks SDK via a flat C API
Package info
github.com/hmennen90/php-steamworks
Language:C
Type:php-ext
Ext name:ext-steamworks
pkg:composer/hmennen90/php-steamworks
Requires
- php: >=8.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Native PHP extension providing access to the Steamworks SDK via a flat C API.
Requirements
- PHP 8.1+
- Steamworks SDK 1.65 (download from partner.steamgames.com) — see SDK compatibility
- macOS (arm64/x86_64), Linux (x86_64), or Windows (x86_64)
Installation
Via PIE (recommended)
PIE is the official PHP extension installer.
pie install hmennen90/php-steamworks --with-steamworks=/path/to/steamworks-sdk
After installation, copy libsteam_api.dylib (macOS) or libsteam_api.so (Linux) from the SDK's redistributable_bin/ directory next to the built extension:
# macOS cp /path/to/steamworks-sdk/redistributable_bin/osx/libsteam_api.dylib "$(php-config --extension-dir)/" # Linux x86_64 cp /path/to/steamworks-sdk/redistributable_bin/linux64/libsteam_api.so "$(php-config --extension-dir)/"
Note (Laravel Herd / custom PHP builds): If PIE fails to auto-install the
.sodue to a read-only filesystem, the build output still exists at~/.pie/php*/vendor/hmennen90/php-steamworks/modules/steamworks.so. Copy it to your extension directory manually and use an absolute path inphp.ini:extension=/full/path/to/steamworks.so
Manual build
-
Download the Steamworks SDK and extract it.
-
Build the extension:
phpize ./configure --with-steamworks=/path/to/steamworks-sdk make make install
-
Copy the Steam shared library next to the extension (see PIE section above).
-
Enable the extension in
php.ini:
extension=steamworks
See INSTALL.md for detailed platform-specific instructions.
Quick Start
<?php // steam_appid.txt with your App-ID must exist in the working directory if (!steam_init()) { die("Steam not running\n"); } echo "Player: " . steam_friends_get_name() . "\n"; echo "App ID: " . steam_utils_get_app_id() . "\n"; // Game loop while (true) { steam_run_callbacks(); // ... game logic ... usleep(16667); } steam_shutdown();
Available Functions
150 functions across 11 Steamworks interfaces. Full signatures with docblocks live in
stubs/steamworks.php (IDE autocompletion).
Core
steam_init(): boolsteam_shutdown(): voidsteam_run_callbacks(): void— call every frame
User
steam_user_get_steam_id(): int|falsesteam_user_is_logged_on(): boolsteam_user_get_player_steam_level(): int|falsesteam_user_get_auth_session_ticket(): array|falsesteam_user_begin_auth_session(string $ticket, int $steam_id): int|falsesteam_user_end_auth_session(int $steam_id): boolsteam_user_cancel_auth_ticket(int $handle): boolsteam_user_get_auth_ticket_for_web_api(?string $identity = null): int|falsesteam_user_get_web_api_ticket_result(int $handle): ?array—nulluntil the callback arrived
Friends
steam_friends_get_name(): string|falsesteam_friends_set_rich_presence(string $key, ?string $value = null): boolsteam_friends_activate_overlay(string $dialog): voidsteam_friends_activate_overlay_to_web_page(string $url, bool $modal = false): voidsteam_friends_get_persona_state(): int|falsesteam_friends_get_friend_count(int $flags = STEAM_FRIEND_FLAG_IMMEDIATE): int|falsesteam_friends_get_friend_by_index(int $index, int $flags = STEAM_FRIEND_FLAG_IMMEDIATE): int|falsesteam_friends_get_friend_relationship(int $steam_id): int|falsesteam_friends_get_friend_persona_state(int $steam_id): int|falsesteam_friends_get_friend_persona_name(int $steam_id): string|falsesteam_friends_request_user_information(int $steam_id, bool $name_only = false): boolsteam_friends_get_friend_avatar(int $steam_id, int $size = STEAM_AVATAR_MEDIUM): ?array— RGBA pixelssteam_friends_invite_user_to_game(int $friend_id, string $connect): bool— invite into the running game; the connect string (1–255 bytes) arrives at the friend's gamesteam_friends_activate_invite_dialog_connect_string(string $connect): bool— overlay invite dialogsteam_friends_activate_overlay_invite_dialog(int $lobby): bool— overlay invite dialog for a lobbysteam_friends_get_friend_rich_presence(int $friend_id, string $key): string|false— '' for a key not setsteam_friends_get_friend_game_played(int $friend_id): array|false— ['game_id', 'app_id', 'ip', 'port', 'query_port', 'lobby'], false if not in a gamesteam_friends_get_join_requests(): array— friends who accepted an invite or clicked "Join": [['friend' => int, 'connect' => string], ...], drains the queue
Stats & Achievements
steam_stats_set_achievement(string $id): boolsteam_stats_clear_achievement(string $id): boolsteam_stats_store(): boolsteam_stats_get_int(string $name): int|falsesteam_stats_set_int(string $name, int $value): boolsteam_stats_get_float(string $name): float|falsesteam_stats_set_float(string $name, float $value): boolsteam_stats_indicate_achievement_progress(string $id, int $cur, int $max): boolsteam_stats_get_achievement(string $id): ?boolsteam_stats_get_achievement_unlock_time(string $id): ?intsteam_stats_get_num_achievements(): int|falsesteam_stats_get_achievement_name(int $index): string|falsesteam_stats_get_achievement_display_attribute(string $id, string $key): string|falsesteam_stats_reset_all_stats(bool $achievements_too = false): bool
Leaderboards (async — see below)
steam_stats_find_leaderboard(string $name): int|falsesteam_stats_find_or_create_leaderboard(string $name, int $sort, int $display): int|falsesteam_stats_upload_score(int $leaderboard, int $score, int $method = STEAM_LEADERBOARD_UPLOAD_KEEP_BEST, ?array $details = null): int|false—$details= optionalint[]game-specific values stored with the score (maxSTEAM_LEADERBOARD_DETAILS_MAX= 64)steam_stats_download_leaderboard_entries(int $leaderboard, int $request, int $start, int $end): int|falsesteam_stats_get_downloaded_entry(int $entries, int $index): ?array— returns['steam_id' => int, 'global_rank' => int, 'score' => int, 'details' => int[], 'ugc' => int];ugcis the attached file,STEAM_UGC_HANDLE_INVALIDif nonesteam_stats_get_leaderboard_entry_count(int $leaderboard): intsteam_stats_attach_leaderboard_ugc(int $leaderboard, int $ugc): int|false— attach a shared file to your own entry (upload a score first) →leaderboard_ugc_set
Async CallResults
steam_get_call_result(int $handle): array|false|null— poll the result of an async call (null= pending,false= failed,array= result with atypefield)
Remote Storage (Cloud Saves)
steam_remote_file_write(string $filename, string $data): boolsteam_remote_file_read(string $filename): string|falsesteam_remote_file_exists(string $filename): boolsteam_remote_file_delete(string $filename): boolsteam_remote_file_list(): array|false
Shared files (Remote Storage UGC)
A cloud file shared with steam_remote_file_share gets a UGC handle that can be attached to a leaderboard entry; anyone who downloads the entry can fetch the file.
steam_remote_file_share(string $filename): int|false— async →remote_file_sharedwithugcsteam_remote_ugc_download(int $ugc, int $priority = 0): int|false— async →remote_ugc_downloadedwithsize,name,ownersteam_remote_ugc_read(int $ugc, int $size, int $offset = 0): string|falsesteam_remote_get_ugc_details(int $ugc): array|false—['app_id', 'name', 'size', 'owner'], after the download completed
Apps
steam_apps_is_subscribed(): boolsteam_apps_is_dlc_installed(int $dlc_id): boolsteam_apps_get_app_id(): int|falsesteam_apps_get_language(): string|falsesteam_apps_is_subscribed_app(int $app_id): boolsteam_apps_get_current_beta_name(): string|falsesteam_apps_get_earliest_purchase_time(int $app_id): int|falsesteam_apps_get_installed_depots(int $app_id): array|falsesteam_apps_get_dlc_count(): int|falsesteam_apps_get_app_build_id(): int|falsesteam_apps_get_launch_command_line(): string|false— the line Steam started the game with, e.g. after an accepted invite
Utils
steam_utils_get_app_id(): int|falsesteam_utils_is_overlay_enabled(): boolsteam_utils_get_country_code(): string|falsesteam_utils_is_steam_deck(): bool— deprecated since SDK 1.65, see belowsteam_utils_get_hardware_type(): int|false—STEAM_HARDWARE_TYPE_*steam_utils_get_hardware_default_config(): int|false—STEAM_HARDWARE_CONFIG_*steam_utils_is_running_under_proton(): boolsteam_utils_get_steam_ui_language(): string|falsesteam_utils_get_server_real_time(): int|falsesteam_utils_get_current_battery_power(): int|false— 0–100 %, 255 = on AC powersteam_utils_get_seconds_since_app_active(): int|false
Timeline (Game Recording)
Annotate the Steam Game Recording timeline. Most calls are fire-and-forget (bool);
the two does_*_recording_exist calls are async (return a handle — poll steam_get_call_result).
Verified against Steamworks SDK 1.65 (STEAMTIMELINE_INTERFACE_V004).
steam_timeline_set_game_mode(int $mode): bool—STEAM_TIMELINE_GAME_MODE_*steam_timeline_set_tooltip(string $description, float $time_delta = 0.0): boolsteam_timeline_clear_tooltip(float $time_delta = 0.0): boolsteam_timeline_add_instantaneous_event(string $title, string $description, string $icon, int $icon_priority = 0, float $start_offset_seconds = 0.0, int $possible_clip = STEAM_TIMELINE_CLIP_PRIORITY_NONE): intsteam_timeline_add_range_event(string $title, string $description, string $icon, int $icon_priority = 0, float $start_offset_seconds = 0.0, float $duration_seconds = 0.0, int $possible_clip = STEAM_TIMELINE_CLIP_PRIORITY_NONE): intsteam_timeline_start_range_event(...): int/steam_timeline_update_range_event(int $event, ...): bool/steam_timeline_end_range_event(int $event, float $end_offset_seconds = 0.0): boolsteam_timeline_remove_event(int $event): boolsteam_timeline_does_event_recording_exist(int $event): int|false— async (type=timeline_event_recording_exists)steam_timeline_start_game_phase(): bool/steam_timeline_end_game_phase(): boolsteam_timeline_set_game_phase_id(string $phase_id): boolsteam_timeline_does_game_phase_recording_exist(string $phase_id): int|false— async (type=timeline_game_phase_recording_exists)steam_timeline_add_game_phase_tag(string $tag_name, string $tag_icon, string $tag_group, int $priority = 0): boolsteam_timeline_set_game_phase_attribute(string $attribute_group, string $attribute_value, int $priority = 0): boolsteam_timeline_open_overlay_to_game_phase(string $phase_id): bool/steam_timeline_open_overlay_to_event(int $event): bool
Workshop (UGC)
Subscribe/unsubscribe, create, submit and delete are async (poll steam_get_call_result).
steam_ugc_subscribe_item(int $file_id): int|false/steam_ugc_unsubscribe_item(int $file_id): int|falsesteam_ugc_get_num_subscribed_items(bool $include_locally_disabled = false): int|falsesteam_ugc_get_subscribed_items(bool $include_locally_disabled = false): array|falsesteam_ugc_get_item_state(int $file_id): int|falsesteam_ugc_get_item_install_info(int $file_id): array|falsesteam_ugc_get_item_download_info(int $file_id): array|falsesteam_ugc_download_item(int $file_id, bool $high_priority = false): boolsteam_ugc_create_item(int $app_id, int $file_type = STEAM_UGC_FILE_TYPE_COMMUNITY): int|falsesteam_ugc_start_item_update(int $app_id, int $file_id): intsteam_ugc_set_item_title(int $handle, string $title): boolsteam_ugc_set_item_description(int $handle, string $description): boolsteam_ugc_set_item_visibility(int $handle, int $visibility): boolsteam_ugc_set_item_content(int $handle, string $content_folder): boolsteam_ugc_set_item_preview(int $handle, string $preview_file): boolsteam_ugc_set_item_tags(int $handle, array $tags): boolsteam_ugc_submit_item_update(int $handle, ?string $change_note = null): int|falsesteam_ugc_get_item_update_progress(int $handle): arraysteam_ugc_delete_item(int $file_id): int|false
Networking (P2P over the Steam relay)
steam_net_init_relay_network_access(): boolsteam_net_create_listen_socket_p2p(int $virtual_port = 0): int|falsesteam_net_connect_p2p(int $steam_id, int $virtual_port = 0): int|falsesteam_net_accept_connection(int $connection): int|falsesteam_net_close_connection(int $connection, int $reason = 0, ?string $debug = null, bool $linger = false): boolsteam_net_send_message(int $connection, string $data, bool $reliable = true): int|falsesteam_net_receive_messages(int $connection, int $max = 32): array|falsesteam_net_get_connection_events(): array— connection state changes since the last call; listen_socket names the socket an incoming connection arrived on (0 for outgoing)steam_net_close_listen_socket(int $socket): boolsteam_net_create_poll_group(): int|false/steam_net_destroy_poll_group(int $group): boolsteam_net_set_connection_poll_group(int $connection, int $group): boolsteam_net_receive_messages_on_poll_group(int $group, int $max = 32): array|false— all connections of a group in one callsteam_net_get_connection_status(int $connection): array|false— ping, quality_local/quality_remote (0–1), byte and packet rates, pending bytes, queue time
Lobbies (Matchmaking)
Creating and joining are async (poll steam_get_call_result → lobby_created / lobby_entered); everything that happens in the lobby afterwards arrives through steam_matchmaking_get_events().
steam_matchmaking_create_lobby(int $type, int $max_members): int|false— STEAM_LOBBY_TYPE_*steam_matchmaking_join_lobby(int $lobby): int|false/steam_matchmaking_leave_lobby(int $lobby): boolsteam_matchmaking_invite_user_to_lobby(int $lobby, int $user): boolsteam_matchmaking_get_num_lobby_members(int $lobby): int|false/steam_matchmaking_get_lobby_member_by_index(int $lobby, int $index): int|falsesteam_matchmaking_get_lobby_owner(int $lobby): int|false/steam_matchmaking_set_lobby_owner(int $lobby, int $owner): boolsteam_matchmaking_get_lobby_data(int $lobby, string $key): string|false/steam_matchmaking_set_lobby_data(int $lobby, string $key, string $value): bool— keys up to 255 characters, values under 8192 bytessteam_matchmaking_get_lobby_member_data(int $lobby, int $user, string $key): string|false/steam_matchmaking_set_lobby_member_data(int $lobby, string $key, string $value): boolsteam_matchmaking_set_lobby_joinable(int $lobby, bool $joinable): bool/steam_matchmaking_set_lobby_type(int $lobby, int $type): boolsteam_matchmaking_send_lobby_chat_msg(int $lobby, string $message): bool— 1–4096 bytes, binary-safesteam_matchmaking_get_events(): array— in arrival order, drains the queue: join_requested (lobby, friend), data_update (lobby, member, success), chat_update (lobby, user, changed_by, state = STEAM_CHAT_MEMBER_STATE_* bits), chat_message (lobby, user, entry_type, message)
Leaderboards & async calls
Leaderboard calls (and other SteamAPICall_t-based APIs) are asynchronous: they
return an integer handle. Poll steam_get_call_result($handle) each frame until it
stops returning null:
$handle = steam_stats_find_leaderboard('HighScores'); do { steam_run_callbacks(); $result = steam_get_call_result($handle); usleep(16667); } while ($result === null); // $result === ['type' => 'leaderboard_found', 'found' => true, 'leaderboard' => 42] $leaderboard = $result['leaderboard']; steam_stats_upload_score($leaderboard, 1500, STEAM_LEADERBOARD_UPLOAD_KEEP_BEST);
See examples/leaderboard.php for a full round-trip.
Constants
- Sort:
STEAM_LEADERBOARD_SORT_ASCENDING,STEAM_LEADERBOARD_SORT_DESCENDING - Display:
STEAM_LEADERBOARD_DISPLAY_NUMERIC,STEAM_LEADERBOARD_DISPLAY_TIME_SECONDS,STEAM_LEADERBOARD_DISPLAY_TIME_MILLISECONDS - Upload:
STEAM_LEADERBOARD_UPLOAD_KEEP_BEST,STEAM_LEADERBOARD_UPLOAD_FORCE_UPDATE - Download:
STEAM_LEADERBOARD_DATA_GLOBAL,STEAM_LEADERBOARD_DATA_GLOBAL_AROUND_USER,STEAM_LEADERBOARD_DATA_FRIENDS - Hardware type:
STEAM_HARDWARE_TYPE_NONE,STEAM_HARDWARE_TYPE_STEAM_DECK,STEAM_HARDWARE_TYPE_STEAM_MACHINE,STEAM_HARDWARE_TYPE_STEAM_FRAME - Hardware preset:
STEAM_HARDWARE_CONFIG_NONE,..._LOW,..._MEDIUM,..._HIGH,..._MAX,..._STEAM_DECK,..._STEAM_MACHINE,..._STEAM_FRAME - Shared files:
STEAM_UGC_HANDLE_INVALID(-1, a leaderboard entry without an attached file) - Lobby type:
STEAM_LOBBY_TYPE_PRIVATE,..._FRIENDS_ONLY,..._PUBLIC,..._INVISIBLE,..._PRIVATE_UNIQUE - Lobby member change (bits):
STEAM_CHAT_MEMBER_STATE_ENTERED,..._LEFT,..._DISCONNECTED,..._KICKED,..._BANNED - Lobby entry and chat:
STEAM_CHAT_ROOM_ENTER_SUCCESS,STEAM_CHAT_ENTRY_TYPE_CHAT_MSG
SDK compatibility
Built and verified against Steamworks SDK 1.65, which is the minimum: the
1.65-only ISteamUtils calls are link-time dependencies. sdk/redistributable_bin/
already ships the matching Steam runtime libraries, so a default build needs no
extra download.
Steam interfaces are resolved by version string at runtime rather than through
the SDK's versioned SteamAPI_Steam*_vNNN() accessor symbols. Those accessors
are version-locked link-time symbols, so every SDK release that bumps an
interface breaks the build outright — SDK 1.65 removed SteamAPI_SteamUtils_v010.
With the runtime lookup, an interface bump is a one-line change to a version
table instead.
Old Steam clients
Where it is safe, an older interface version is accepted as a deprecated
fallback; using one raises E_DEPRECATED once per process.
| Interface | Current | Deprecated fallback |
|---|---|---|
ISteamNetworkingSockets |
SteamNetworkingSockets013 (SDK 1.65+) |
SteamNetworkingSockets012 (SDK ≤ 1.64) |
ISteamUtils |
SteamUtils011 (SDK 1.65+) |
none — see below |
A fallback is only safe when the older interface has the same vtable layout:
the flat SteamAPI_ISteamX_* functions inside libsteam_api call fixed vtable
slots. SDK 1.65 removed two methods from the middle of ISteamUtils, so
SteamUtils010 is not layout-compatible and is deliberately not accepted. A
Steam client that offers only the old interface gets a clear
E_WARNING ("the Steam client is older than this build") and a false return,
rather than a call into the wrong vtable slot.
Since Steam clients update themselves, this only affects clients that have not run in a long time.
Deprecated functions
steam_utils_is_steam_deck() still works — it is now implemented on
IsRunningOnSteamHardware() — but SDK 1.65 removed the underlying
IsRunningOnSteamDeck() call, and it raises E_DEPRECATED. Valve's guidance is
to avoid device checks entirely: use steam_utils_get_hardware_default_config()
for graphics defaults, or a capability-specific query, so the game keeps
behaving correctly on Steam hardware that did not exist when it shipped. Use
steam_utils_get_hardware_type() only for analytics and diagnostics.
License
MIT