Skip to content

Latest commit

 

History

History
354 lines (259 loc) · 18.7 KB

File metadata and controls

354 lines (259 loc) · 18.7 KB

DiscordPHP Agent Guide

This file is the repo operating manual for AI agents. It describes how to work inside this repository without breaking its design. Specialist skills in .agents/skills/ provide deeper playbooks for specific layers.

If a change crosses layers, load multiple skills and use the playbooks in this file to keep boundaries clean.

Start here

Before changing anything:

  1. Identify the layer you are touching.
  2. Read the base abstraction for that layer before copying a concrete class.
  3. Read one representative concrete implementation from the same family.
  4. Trace how that layer connects to adjacent layers.
  5. Make the change in the narrowest layer that can own it.
  6. Update companion surfaces that define the same contract.

In this repo, companion surfaces usually matter as much as the line you edit.

Non-negotiable truths

  1. CLI-only runtime. DiscordPHP is a long-running process built on ReactPHP. Do not design around web requests, controllers, middleware stacks, or per-request state.
  2. Async first. Production I/O is Promise-based. Blocking helpers belong in tests only.
  3. Parts are canonical domain objects. They model Discord resources and expose typed magic properties.
  4. Repositories are persistence and cache boundaries. They are not generic service classes.
  5. Gateway handlers keep caches coherent. They do more than relay notifications.
  6. Builders own outbound payload rules. If a payload has meaningful shape or validation, it usually deserves a builder.
  7. Docblocks are runtime-adjacent documentation. They are not optional decoration.
  8. Traits are preferred over deep inheritance or broad interface hierarchies for shared behavior.
  9. Type maps are central dispatch points. If a Discord payload is polymorphic, there is usually one place that decides the concrete subtype.
  10. Use current library idioms. Prefer Factory::part() / Factory::repository(), prefer $part->save($reason) over repository save($part) in user-facing paths, and prefer builder ->create($repository) helpers where they exist.

Skill map

Each skill lives in .agents/skills/<name>/SKILL.md and is loaded automatically when relevant. When a task crosses layers, load multiple skills.

If task touches... Skill to load
Discord.php, startup, intents, cache, loop, gateway connection runtime-bootstrap-keeper
Parts/*, domain modeling, mutators, typed nested data part-model-maintainer
Repository/*, endpoint vars, cache, CRUD, fetch/save/delete repository-cache-keeper
WebSockets/Handlers.php, WebSockets/Event.php, WebSockets/Events/* gateway-cache-sync-keeper
Builders/*, Builders/Components/*, outbound payload rules builder-payload-smith
subtype maps like Channel::TYPES or Interaction::TYPES type-map-keeper
interactions, slash commands, resolved data, autocomplete, modals interaction-flow-keeper
DiscordCommandClient or prefix-command behavior legacy-command-client-keeper
tests, guides, docblocks, generated reference expectations async-test-and-doc-sync
Voice/*, audio streaming, encryption, voice gateway protocol voice-subsystem-keeper
Helpers/*, Exceptions/*, cache wrappers, Endpoint::bind(), Collection helpers-and-infra-keeper

Architecture map

Layer Owns Primary files What to preserve
Runtime process lifecycle, options, loop, gateway, HTTP, root repos src/Discord/Discord.php __construct() wires dependencies and connects; run() only starts loop
Factory part/repository instantiation src/Discord/Factory/Factory.php callers should not construct repo families ad hoc
Parts domain objects, mutators, typed nested data, high-level operations src/Discord/Parts/Part.php, src/Discord/Parts/PartTrait.php, src/Discord/Parts/**/* $fillable, mutators, PHPDoc, save() semantics, created lifecycle
Repositories typed collections, cache, REST endpoints, CRUD src/Discord/Repository/AbstractRepository.php, src/Discord/Repository/**/* $class, $endpoints, $vars, cache writes, Promise-based API
Builders outbound payload construction and validation src/Discord/Builders/**/* fluent setters, validation, jsonSerialize(), fromPart() symmetry
Gateway events payload hydration, cache mutation, emitted return shapes src/Discord/WebSockets/Handlers.php, src/Discord/WebSockets/Event.php, src/Discord/WebSockets/Events/* typed part creation, related cache updates, event contract shape
Helpers cross-cutting utilities: cache wrappers, BigInt math, multipart uploads, property mutator trait, domain exceptions src/Discord/Helpers/*, src/Discord/Exceptions/* no domain logic here; utilities only
Voice internal voice protocol types and encryption; runtime integration in Discord.php; audio client in external discord-php-helpers/voice package src/Discord/Voice/* Old* files are legacy — do not extend them; keep protocol and crypto layers separate
Optional command layer message-prefix command UX src/Discord/DiscordCommandClient.php, src/Discord/CommandClient/Command.php keep it layered on top of core client, not inside it
Tests and docs behavioral contract tests/*, guide/*, README.md, docs/* async testing patterns, public guidance, docblock reference surface

External packages

Three external packages are tightly coupled to core runtime behavior. Treat them as first-class parts of the architecture:

Package Why it matters Local touchpoints
discord-php/http HTTP client and Endpoint URL template binding used in every repository; handles rate-limiting via Bucket $endpoints arrays in all repositories; use Discord\Http\Endpoint imports
discord-php-helpers/collection Collection class is the base for every AbstractRepository; discriminator-keyed typed collections AbstractRepository extends Collection; $discrim property
discord-php-helpers/voice Manager and VoiceClient implement the actual audio client; src/Discord/Voice/* contains only internal protocol types and encryption Discord::joinVoiceChannel(), voice event handlers in Discord.php

When editing endpoint bindings, REST routes, cache storage, or voice audio, you are necessarily touching these packages' contracts.

Repo worldview

Runtime to domain flow

Discord owns bootstrapping. Gateway dispatch goes through Handlers into a dedicated event class. Event classes translate payloads into typed parts and update repositories. Repositories provide cache and REST persistence. Parts expose that state through magic properties and typed helpers. Builders assemble outbound payloads for operations that would otherwise become fragile arrays.

Why the split matters

  • If you put transport logic in parts, parts stop being stable domain objects.
  • If you put domain validation in repositories, repository APIs stop being predictable.
  • If you skip builders, payload rules spread across unrelated methods.
  • If gateway handlers do not update caches correctly, every downstream relation becomes stale.

Common class patterns

Parts

Expect these elements on real resource models:

  • large class-level @property and @property-read docblocks
  • protected $fillable = [...]
  • optional protected $repositories = [...]
  • constants mirroring Discord enums or flags
  • getXAttribute() and setXAttribute() mutators
  • overrides for getCreatableAttributes(), getUpdatableAttributes(), getRepository(), save(), fetch(), or getRepositoryAttributes() when the part is persistable

Semantic rules:

  • raw attribute names stay snake_case to match Discord payloads
  • convenience relations (guild, channel, owner, member) are usually computed, not directly stored
  • nested typed data should become a Part, typed collection, or Carbon value through helper methods
  • permission checks for high-level mutations belong on the part before repository delegation
  • created tells you whether the object already exists remotely

Repositories

Expect these elements:

  • extends AbstractRepository
  • protected $class = SomePart::class
  • protected $endpoints = [...]
  • optional constructor normalization for route vars
  • occasional domain-specific convenience methods

Semantic rules:

  • repositories are typed collections plus REST/cache wrappers
  • create() builds a local part; save() persists it
  • $vars carries parent route context like guild_id or channel_id
  • cache writes must stay aligned with REST writes and gateway updates
  • special methods should still return typed parts or repositories, not loose payloads

Builders

Expect these elements:

  • extends Builder
  • implements JsonSerializable
  • fluent setX() / getX() methods
  • eager validation in setters or adders
  • new() factory method on most builders
  • create($repository) helper on newer builders

Semantic rules:

  • builders are not parts and should not own persistence or cache logic
  • validation belongs here when it describes outgoing payload shape
  • jsonSerialize() should omit unset optionals when Discord distinguishes missing from explicit null
  • fromPart() should make edit flows symmetrical

Gateway events

Expect these elements:

  • one class per event type under src/Discord/WebSockets/Events
  • matching constant in src/Discord/WebSockets/Event.php
  • matching registration in src/Discord/WebSockets/Handlers.php
  • handle($data) method returning typed semantic values

Semantic rules:

  • event handlers should hydrate the right subtype on first read
  • event handlers are responsible for repository/cache coherence
  • update events often return both new and old state
  • delete events often return cached removed state, not only the raw payload id
  • related user/member caches usually need updating too

Cross-layer rules

1. Parts delegate persistence; repositories own REST

If a part can be saved:

  • the part decides whether the action is allowed and which repository owns it
  • the repository decides how to call Discord and update cache

Smell: a part method manually building endpoints and PATCH payloads when a repository already exists for that family.

2. Parts own semantics; builders own payload ergonomics

If userland needs to construct a non-trivial outbound payload:

  • add or extend a builder
  • keep raw-array construction as an implementation detail only when a builder would be needless overhead

Smell: multiple methods assembling the same nested array by hand.

3. Gateway events own reactive cache updates

If Discord can tell us something through gateway dispatch:

  • prefer updating cache from the event instead of forcing later REST refetches
  • keep parent/child repository relationships coherent at the same time

Smell: event handler creates a part but does not update the repository that should own it.

4. Traits carry horizontal behavior

The codebase prefers traits for shared capabilities across sibling models.

Examples:

  • PartTrait for universal part mechanics
  • ChannelTrait for channel/thread behavior
  • GuildTrait for guild asset and feature helpers
  • DynamicPropertyMutatorTrait for builder/property mutators (src/Discord/Helpers/DynamicPropertyMutatorTrait.php)
  • AbstractRepositoryTrait for repository collection mechanics
  • ComponentsTrait for shared component helpers across builders (src/Discord/Builders/ComponentsTrait.php)
  • VoiceGroupCryptoTrait for voice channel encryption/decryption

Smell: new abstract intermediate base class that only exists to share a few methods between peers that already have a common root.

5. Type maps beat repeated branching

When a Discord discriminator chooses a subtype:

  • extend the relevant TYPES map
  • update all materialization sites that depend on that family

Smell: one event handler special-cases a new subtype but the central type map still does not know it.

Common companion surfaces

If you touch one of these, inspect the companions too:

Touching Also inspect
src/Discord/Parts/Part.php or PartTrait.php representative parts, PartInterface, generated docblocks
a concrete Part owning repository, related trait, event handlers, tests, docs
a nested repository relationship parent part $repositories, getRepositoryAttributes(), repository constructor vars
src/Discord/Repository/AbstractRepository* representative repos, cache wrapper behavior, part save/fetch overrides
a gateway event class Event.php, Handlers.php, related part/repository, tests
a builder matching part, callers, tests, docs/examples
Channel::TYPES, Interaction::TYPES, component/ embed maps event handlers, typed collection helpers, builder mirrors
DiscordCommandClient CommandClient/Command.php, examples/docs
public magic properties class PHPDoc, guide docs if user-facing behavior changed
Helpers/CacheWrapper.php or cache config AbstractRepository cache behavior, Discord.php options['cache'] wiring
Voice/* or voice event in Discord.php VoiceGroupCrypto, VoicePacket, external voice package entry points, voice opcodes

Change playbooks

Playbook: editing a Part

  1. Update $fillable.
  2. Add or adjust mutators for typed nested data, computed properties, or normalization.
  3. Update class docblocks.
  4. Update $repositories if a new child repository is exposed.
  5. Update getRepositoryAttributes() if child route vars changed.
  6. Update getCreatableAttributes() / getUpdatableAttributes() if persistence shape changed.
  7. Update getRepository() or save() if ownership or permission rules changed.
  8. Check gateway events and repositories that hydrate or cache the part.
  9. Add or update tests.

Playbook: editing a Repository

  1. Confirm $class, $discrim, and $endpoints still describe the family correctly.
  2. Confirm parent route vars are complete and in the correct shape.
  3. Keep REST writes and cache writes in sync.
  4. Return typed values.
  5. Check owning parts for getRepository() and getRepositoryAttributes() assumptions.
  6. Update tests and docs if public behavior changed.

Playbook: editing a Builder

  1. Put payload validation in setters/adders.
  2. Keep fluent chaining style.
  3. Keep jsonSerialize() aligned with Discord docs and existing payload semantics.
  4. Keep fromPart() edit symmetry in mind.
  5. Update tests for validation limits and payload shape.
  6. Update docs/examples if the preferred public construction path changed.

Playbook: editing a gateway event

  1. Add or update the event constant in Event.php if needed.
  2. Add or update the handler registration in Handlers.php.
  3. Hydrate the correct subtype.
  4. Update every affected repository and relation cache.
  5. Preserve event return shape expected by userland listeners.
  6. Cache related users/members if the payload supplies them.
  7. Re-check any intent-gated or partial-data behavior.

Playbook: editing interactions or commands

  1. Keep application-command and prefix-command layers separate.
  2. Keep interaction typing and resolved-data hydration intact.
  3. Keep builders as the outbound authoring path for commands, components, and modals.
  4. Avoid slow interaction-time work that can delay a response.
  5. Update both inbound event handling and outbound builder/docs surfaces if public behavior shifts.

Design tripwires

If you see one of these, slow down:

  • a new raw nested array where a typed part already exists
  • a repository method returning raw decoded payload instead of a part
  • a part saving itself with hand-built endpoints even though an owning repository exists
  • a new subtype without a TYPES map entry
  • a magic property added in code but not in docblocks
  • a gateway handler that updates one cache but leaves related repositories stale
  • a synchronous wait or loop-stop trick outside tests
  • a new abstraction layer that duplicates what parts, repositories, events, or builders already do
  • web-framework terminology creeping into core runtime code
  • extending OldVoiceClient or any Old* class in src/Discord/Voice/ unless deliberately fixing legacy behavior
  • bypassing Endpoint::bind() with hand-assembled raw URL strings

Preferred reference files

When you need an example worth imitating, start here:

  • Runtime orchestration: src/Discord/Discord.php
  • Base part mechanics: src/Discord/Parts/Part.php, src/Discord/Parts/PartTrait.php
  • Rich part model: src/Discord/Parts/Guild/Guild.php
  • Channel/resource semantics: src/Discord/Parts/Channel/Channel.php
  • Message semantics and repository binding: src/Discord/Parts/Channel/Message.php
  • Repository baseline: src/Discord/Repository/AbstractRepository.php, src/Discord/Repository/AbstractRepositoryTrait.php
  • Simple repo specialization: src/Discord/Repository/Channel/MessageRepository.php
  • Rich repo specialization: src/Discord/Repository/GuildRepository.php
  • Outbound builder style: src/Discord/Builders/MessageBuilder.php
  • Interaction typing: src/Discord/Parts/Interactions/Interaction.php
  • Gateway cache mutation: src/Discord/WebSockets/Events/MessageCreate.php, src/Discord/WebSockets/Events/GuildCreate.php
  • Optional command layer: src/Discord/DiscordCommandClient.php, src/Discord/CommandClient/Command.php
  • Cache infrastructure: src/Discord/Helpers/CacheWrapper.php, src/Discord/Helpers/CacheConfig.php
  • Voice protocol and encryption: src/Discord/Voice/VoiceGroupCrypto.php, src/Discord/Voice/VoicePacket.php

Testing and docs workflow

Tests

  • Prefer plain PHPUnit\Framework\TestCase when logic is isolated.
  • Use DiscordTestCase only when real Discord integration matters.
  • Use wait() from tests/functions.php to bridge promises into test assertions.
  • Keep semantic tests focused on behavior, not on incidental implementation details.

Docs

  • Public magic properties, repositories, and helpers should be reflected in PHPDoc.
  • Long-form guides live in guide/.
  • Gatsby docs live in docs/.
  • Keep docs in sync when preferred usage or public contracts change.

Useful commands

Purpose Command
main PHPUnit suite composer unit
static analysis composer run-script mago-lint
formatter contributors run composer run-script cs
non-mutating Pint check ./vendor/bin/pint --test --config ./pint.json ./src
Pint formatter (auto-fix) composer pint
test coverage report composer coverage
docs build cd docs && yarn install && yarn build

Integration tests expect .env values for DISCORD_TOKEN, TEST_CHANNEL, and TEST_CHANNEL_NAME.

Final rule

When unsure where code belongs, choose the layer that already owns the same kind of knowledge elsewhere in the repo. Matching the existing ownership model matters more than shaving a few lines off one class.