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.

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
ClassPackageFull name
Discord, Factory, Handlers, Event, Part, Widget, AbstractRepository, CacheWrapperteam-reflex/discord-phpDiscord\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, Endpointdiscord-php/httpDiscord\Http\Http, Discord\Http\Bucket, Discord\Http\DriverInterface, Discord\Http\Drivers\React, Discord\Http\Drivers\Guzzle, Discord\Http\Endpoint
WebSocketratchet/pawlRatchet\Client\WebSocket
Collectiondiscord-php-helpers/collectionDiscord\Helpers\Collection
Manager, VoiceClientdiscord-php-helpers/voiceDiscord\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.