UML diagrams
Three diagrams of how DiscordPHP is put together and how requests and events move through it. After them come two generated maps: every Discord REST route with the DiscordPHP code that sends it, and every gateway event with the class that handles it.
REST routes
Every operation in Discord's OpenAPI description, grouped by resource, with the classes and methods that send it. Routes DiscordPHP does not send yet are listed too.
Gateway events
Every dispatch event DiscordPHP handles, the handler class behind it, and what your listener is called with.
The main classes
Discord owns one HTTP client, one part factory and the gateway connection. Parts such as Guild and Channel hold repositories of their children, and every repository is a cached collection that reads and writes through the same HTTP client.
classDiagram
direction TB
class Discord {
+GuildRepository guilds
+UserRepository users
+PrivateChannelRepository private_channels
+Application application
+on(event, listener)
+run()
+getHttpClient() Http
+getFactory() Factory
+getGateway() PromiseInterface
+joinVoiceChannel(Channel channel, mute, deaf) PromiseInterface
}
class Http {
+CONCURRENT_REQUESTS = 5
+get(url, content, headers) PromiseInterface
+post(url, content, headers) PromiseInterface
+queueRequest(method, Endpoint url, content, headers) PromiseInterface
}
class Bucket {
+enqueue(Request request)
+checkQueue()
}
class DriverInterface {
<<interface>>
+runRequest(Request request) PromiseInterface
}
class React
class Guzzle
class Endpoint {
+bind(endpoint, ...args) Endpoint
+bindAssoc(args) Endpoint
}
class Factory {
+part(class, data, created) Part
+repository(class, data) AbstractRepository
}
class Handlers {
+getHandler(event) array
}
class Event {
<<abstract>>
+handle(data) Generator
}
class WebSocket
class Part {
<<abstract>>
+bool created
+fill(attributes)
}
class Widget {
+getImage(style) PromiseInterface
}
class AbstractRepository {
<<abstract>>
+freshen(queryparams) PromiseInterface
+fetch(id, fresh) PromiseInterface
+save(Part part, reason) PromiseInterface
+delete(part, reason) PromiseInterface
+cacheGet(offset) PromiseInterface
}
class Collection
class CacheWrapper
class Manager
class VoiceClient
Discord *-- Http
Discord *-- Factory
Discord *-- Handlers
Discord o-- WebSocket : gateway
Discord o-- Manager : voice
Discord *-- AbstractRepository : guilds, users, …
Http "1" *-- "many" Bucket : one per route
Http --> DriverInterface
DriverInterface <|.. React
DriverInterface <|.. Guzzle
Http ..> Endpoint
Handlers ..> Event : event name to class
Factory ..> Part : builds
Factory ..> AbstractRepository : builds
Part <|-- Widget
Widget ..> DriverInterface : PNG, not JSON
Part *-- AbstractRepository : child repositories
AbstractRepository --|> Collection
AbstractRepository *-- CacheWrapper
AbstractRepository o-- "many" Part : items
Manager "1" *-- "many" VoiceClient : one per guild
| Class | Package | Full name |
|---|---|---|
| Discord, Factory, Handlers, Event, Part, Widget, AbstractRepository, CacheWrapper | team-reflex/discord-php | Discord\Discord, Discord\Factory\Factory, Discord\WebSockets\Handlers, Discord\WebSockets\Event, Discord\Parts\Part, Discord\Parts\Guild\Widget, Discord\Repository\AbstractRepository, Discord\Helpers\CacheWrapper |
| Http, Bucket, DriverInterface, React, Guzzle, Endpoint | discord-php/http | Discord\Http\Http, Discord\Http\Bucket, Discord\Http\DriverInterface, Discord\Http\Drivers\React, Discord\Http\Drivers\Guzzle, Discord\Http\Endpoint |
| WebSocket | ratchet/pawl | Ratchet\Client\WebSocket |
| Collection | discord-php-helpers/collection | Discord\Helpers\Collection |
| Manager, VoiceClient | discord-php-helpers/voice | Discord\Voice\Manager, Discord\Voice\VoiceClient |
A REST request
Every request, whether it comes from a repository, a part or $discord->getHttpClient() directly, joins the queue of its route's bucket. The bucket waits while Discord says the route is exhausted, and the client keeps at most five requests in flight. Interaction callbacks are not bound by the global limit, so they skip the queue. The one request that bypasses Http is Widget::getImage(). Its response is a PNG rather than JSON, so it hands its request straight to the driver, outside the buckets.
sequenceDiagram
autonumber
participant App as Your code
participant Repo as Part or repository
participant Http as Http
participant Bucket as Bucket, one per route
participant Driver as Driver, React or Guzzle
participant API as Discord REST API
App->>Repo: $guild->channels->save($channel)
Repo->>Http: post(Endpoint, attributes)
Http->>Bucket: queueRequest() puts the request in its route's bucket
Bucket->>Bucket: wait while X-RateLimit-Remaining is 0
Bucket->>Http: hand the request over
Note over Http: at most 5 in flight, interaction callbacks skip the queue
Http->>Driver: runRequest(Request)
Driver->>API: HTTPS request
API-->>Driver: response and X-RateLimit headers
alt 429 Too Many Requests
Driver-->>Http: 429 with retry_after
Http-->>Bucket: RateLimit, global or for this route
Bucket->>Bucket: queue the request again and retry after retry_after
else 502 or 525
Http->>Driver: send it again
else 2xx
Driver-->>Http: response
Http-->>Bucket: remember limit, remaining and reset-after
Http-->>Repo: decoded JSON
Repo->>Repo: Factory builds or fills the Part, then cache->set()
Repo-->>App: the promise resolves with the Part
else any other status
Http-->>App: the promise rejects, with NotFoundException, NoPermissionsException, …
end
A gateway event
The gateway sends compressed frames over one WebSocket. Discord inflates each one, decodes the payload and routes dispatches through Handlers to an Event class. That class updates the cache and returns the part your listener receives. Events that arrive before init are held back until the guilds have loaded.
sequenceDiagram
autonumber
participant GW as Discord gateway
participant WS as WebSocket
participant D as Discord
participant H as Handlers
participant E as Event handler, e.g. MessageCreate
participant C as Repositories and cache
participant L as Your listeners
GW->>WS: frame, zlib-stream or plain JSON
WS->>D: handleWsMessage(Message)
D->>D: buffer until the frame ends in 00 00 FF FF, then inflate
D->>D: processWsMessage(), emit('raw'), remember the sequence number
alt op 0, DISPATCH
D->>H: getHandler(t)
H-->>D: handler class and alternative event names
opt before init, except GUILD_CREATE and GUILD_DELETE
D->>D: hold the packet until init
end
D->>E: handle(d)
E->>C: cacheGet(), build Parts, update the caches
E-->>D: the new Part, and the old one for updates
D->>L: emit(t, [new, discord, old])
opt MESSAGE_CREATE that mentions the bot
D->>L: emit('mention')
end
else op 1, 7, 9, 10 or 11
D->>D: heartbeat, reconnect, invalid session, hello or heartbeat ACK
end
Listen for any event in the list with $discord->on(Event::MESSAGE_CREATE, fn (Message $message, Discord $discord) => …). The gateway events map lists them all.