vzgcoders / twitchphp
An async ReactPHP framework for the Twitch Helix API, EventSub, and chat — built like DiscordPHP.
Requires
- php: ^8.4
- discord-php-helpers/collection: dev-main
- evenement/evenement: ^3.0
- nesbot/carbon: ^3.8 || ^2.72
- psr/log: ^1.1 || ^2.0 || ^3.0
- ratchet/pawl: ^0.4.1
- react/event-loop: ^1.5
- react/promise: ^3.2 || ^2.11
- react/socket: ^1.16
- symfony/options-resolver: ^6.4 || ^7.0
- twitchphp/http: dev-main
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- monolog/monolog: ^3.0
- phpunit/phpunit: ^11.0 || ^10.5
- react/async: ^4.3 || ^3.2
Suggests
- ext-uv: A faster ReactPHP event loop
- monolog/monolog: A PSR-3 logger for the client's debug output
Provides
None
Conflicts
None
Replaces
None
README
An async ReactPHP framework for the Twitch Helix API, EventSub over WebSocket, and chat (IRC over WebSocket) — designed and built like DiscordPHP.
use Twitch\Twitch; $twitch = new Twitch([ 'client_id' => getenv('TWITCH_CLIENT_ID'), 'client_secret' => getenv('TWITCH_CLIENT_SECRET'), // app token, or: 'token' => getenv('TWITCH_ACCESS_TOKEN'), // a user token 'refresh_token' => getenv('TWITCH_REFRESH_TOKEN'), // refreshed automatically on 401 ]); $twitch->on('ready', function (Twitch $twitch) { $twitch->users->fetchByLogin('twitchdev')->then(function ($user) { echo "twitchdev is user {$user->id}, created {$user->created_at->diffForHumans()}\n"; }); }); $twitch->run();
Requirements
- PHP 8.4+
- Composer
- A registered application at https://dev.twitch.tv/console/apps for the
client_id/client_secret
Installation
composer require vzgcoders/twitchphp
The HTTP transport lives in its own package, twitchphp/http,
and is pulled in automatically.
Windows and SSL
PHP on Windows has no access to the system certificate store. Download a
CA bundle and point openssl.cafile in your php.ini at it,
otherwise every TLS connection fails silently.
Authentication
Twitch has several OAuth flows; TwitchPHP supports the ones a bot or service needs.
| You have… | Pass | Behaviour |
|---|---|---|
| A client id + secret only | client_id, client_secret |
Requests an app access token (client-credentials). Good for public data and EventSub. |
| A user access token | token (+ optional refresh_token) |
Validated on startup. With a refresh_token, an invalid/expired token is refreshed transparently, and any Helix 401 triggers one refresh-and-retry. |
Twitch\Http\OAuth also exposes authorizationUrl(), exchangeCode(), deviceCode(), validate()
and revoke() for building the authorization-code / device-code flows in your own app.
The Helix client
Resources hang off the client as repositories, mirroring DiscordPHP. 29 of them:
$twitch->users; $twitch->channels; $twitch->streams; $twitch->games; $twitch->chat; $twitch->moderation; $twitch->clips; $twitch->videos; $twitch->polls; $twitch->predictions; $twitch->channelPoints; $twitch->subscriptions; $twitch->eventSubscriptions; $twitch->teams; $twitch->schedule; $twitch->charity; $twitch->goals; $twitch->raids; $twitch->bits; $twitch->hypeTrain; $twitch->search; $twitch->whispers; $twitch->ads; $twitch->contentLabels; $twitch->conduits; $twitch->entitlements; $twitch->analytics; $twitch->guestStar; $twitch->extensions;
Every call returns a React\Promise\PromiseInterface. Collections are
Discord\Helpers\Collection instances of
hydrated Twitch\Parts\* models.
// Look users up by login and cache them $twitch->users->byLogins(['twitchdev', 'twitch'])->then(function ($users) { foreach ($users as $user) { echo "{$user->display_name} ({$user->id})\n"; } }); // Who is live among a set of channels $twitch->streams->live(['userLogins' => ['twitchdev']])->then(function ($streams) { echo $streams->count() . " live\n"; }); // Update your channel $twitch->channels->modify($broadcasterId, [ 'title' => 'Now with 20% more PHP', 'game_id' => '509670', 'tags' => ['php', 'reactphp'], ]); // Moderation $twitch->moderation->ban($broadcasterId, $modId, $userId, 'spam'); $twitch->moderation->timeout($broadcasterId, $modId, $userId, 600, 'chill');
Repositories share a common surface from Twitch\Repository\AbstractRepository:
freshen(), all() (auto-follows pagination.cursor), fetch($id), save($part),
delete($part), plus get(), first(), filter() and iteration.
EventSub
Enable the WebSocket transport with 'eventsub' => true, then subscribe once the session is ready:
$twitch = new Twitch([ 'client_id' => getenv('TWITCH_CLIENT_ID'), 'token' => getenv('TWITCH_ACCESS_TOKEN'), 'refresh_token' => getenv('TWITCH_REFRESH_TOKEN'), 'eventsub' => true, ]); use Twitch\EventSub\SubscriptionTypes; $twitch->on('ready', function (Twitch $twitch) use ($broadcasterId) { $es = $twitch->getEventSub(); // Typed helpers for the common ones — right condition, right version. $es->onChatMessage($broadcasterId, $twitch->getUserId()); $es->onFollow($broadcasterId, $twitch->getUserId()); // channel.follow v2 $es->onStreamChange($broadcasterId); // online + offline // Generic form — version is looked up from SubscriptionTypes unless given. $es->subscribe(SubscriptionTypes::CHANNEL_CHEER, ['broadcaster_user_id' => $broadcasterId]); $es->subscribeMany([ [SubscriptionTypes::CHANNEL_RAID, ['to_broadcaster_user_id' => $broadcasterId]], [SubscriptionTypes::CHANNEL_AD_BREAK_BEGIN, ['broadcaster_user_id' => $broadcasterId]], ]); }); // Every notification fires twice: a generic event and a per-type event. $twitch->on('eventsub', function (string $type, array $event, array $meta, Twitch $twitch) { echo "eventsub {$type}\n"; }); $twitch->on('eventsub.stream.online', function (array $event) { echo "{$event['broadcaster_user_name']} went live\n"; });
SubscriptionTypes has a constant for every EventSub type and knows the version
Twitch currently wants for it. Session reconnects (session_reconnect),
keepalives and revocation are handled for you, and the desired subscription set
(deduplicated) is replayed automatically onto a fresh session after a drop.
Chat (IRC)
Pass a nick and channels to connect the chat client. It speaks IRCv3 over
wss://irc-ws.chat.twitch.tv and parses tags into Twitch\Parts\ChatMessage parts.
$twitch = new Twitch([ 'client_id' => getenv('TWITCH_CLIENT_ID'), 'token' => getenv('TWITCH_ACCESS_TOKEN'), // needs chat:read + chat:edit 'refresh_token' => getenv('TWITCH_REFRESH_TOKEN'), 'nick' => 'valgorithms', 'channels' => ['twitchdev'], 'command_prefix' => '!', ]); $twitch->on('ready', function (Twitch $twitch) { $irc = $twitch->getIrc(); $irc->on('chat', function ($message) { if (str_contains(strtolower($message->content), 'hello')) { $message->reply('hey!'); } }); $irc->registerCommand('ping', fn ($message) => $message->say('pong')); });
Richer commands
Twitch\Chat\CommandClient layers aliases, per-user cooldowns, permission
levels and an auto !help on top of the plain command event:
use Twitch\Chat\Command; use Twitch\Chat\CommandClient; $cc = new CommandClient($twitch); $cc->command('dice', fn ($m) => $m->reply('🎲 ' . random_int(1, 6)), cooldown: 10, description: 'Roll a die'); $cc->command('so', function ($m, $args) use ($twitch) { $twitch->chat->shoutout($m->tags['room-id'], $args[0], $twitch->getUserId()); }, aliases: ['shoutout'], permission: Command::MODERATOR, cooldown: 30, description: 'Shout a channel out');
Moderators and the broadcaster bypass cooldowns; permission also accepts a
fn (ChatMessage $m): bool predicate.
Events
| Event | Payload | Notes |
|---|---|---|
init / ready |
Twitch |
Authenticated and connected. |
error |
\Throwable, Twitch |
|
token_refreshed |
string $token, Twitch |
|
closed |
Twitch |
After close(). |
eventsub |
string $type, array $event, array $meta, Twitch |
Every notification. |
eventsub.<type> |
array $event, array $meta, Twitch |
e.g. eventsub.channel.chat.message. |
eventsub.ready / eventsub.keepalive / eventsub.revoked / eventsub.disconnected |
||
chat |
ChatMessage |
Any chat line. |
command |
string $name, array $args, ChatMessage |
Prefix commands. |
Configuration
| Option | Default | |
|---|---|---|
client_id |
— | Required. |
client_secret |
'' |
Enables the app-token flow. |
token / refresh_token |
null |
A user token and its refresh token. |
scopes |
[] |
Informational. |
loop |
Loop::get() |
A React\EventLoop\LoopInterface. |
logger |
NullLogger |
Any PSR-3 logger. |
socket_options |
[] |
Passed to React\Socket\Connector. |
nick |
null |
Set to connect the IRC client. |
channels |
[] |
Normalised to #lowercase. |
command_prefix |
'!' |
|
eventsub |
false |
Set to connect the EventSub client. |
Development
composer install composer test # phpunit composer cs # php-cs-fixer
License
MIT — see LICENSE.