Maintain Builder classes — outbound payload construction, validation, serialization, component handling, and fromPart symmetry. Use when changing Builders or outbound Discord API payloads.
Maintain Builder classes — outbound payload construction, validation, serialization, component handling, and fromPart symmetry. Use when changing Builders or outbound Discord API payloads.
Skill: builder-payload-smith
Use this skill when work touches:
src/Discord/Builders/*
src/Discord/Builders/Components/*
outbound payload construction paths in parts or interaction responses
This is outbound-shape skill. Load it when a change affects how callers build data to send to Discord.
Goal
Keep builders as the repo's safe way to author complex outbound payloads:
Matching part or repository family that consumes builder output
Core contract
Builders are not parts. Builders:
extend Builder
often implement JsonSerializable
keep mutable fluent state for outbound payloads
validate limits and allowed values in setters or adders
serialize to request-ready arrays
sometimes provide create($repository) helpers for smoother public API
If a builder starts acting like cached domain object or repository, wrong layer.
Base patterns to preserve
Builder::fromPart(Part $part)
This is bridge from stored part data into editable payload state. Preserve it when adding new builder properties or part fields. Edit flows depend on this symmetry.
Not part-style getContentAttribute(). Keep that distinction.
new() factory
Most builders expose new() for userland ergonomics. Preserve pattern when adding new top-level builders.
create($repository) helper
Newer builders often let callers hand the builder to a repository directly. Prefer keeping that path because it nudges users away from raw arrays.
Validation rules
Validation should live as close as possible to setter/add method that introduces invalid state.
Good examples already in repo:
message content length limits
modal title and custom id length limits
max embeds/components counts
valid channel type enums
valid video quality modes
Do not rely on far-away repository methods to catch simple builder invariants.
Serialization rules
jsonSerialize() is canonical outbound shape
This is where builder translates fluent internal state into Discord payload array. Keep it:
explicit
ordered enough to read
faithful to API docs
selective about optionals
Omit unset optionals when possible
Many Discord APIs treat missing field differently from explicit null. Builders should usually omit properties that were never set unless API specifically wants null.
Normalize nested builder or part inputs
If builder accepts nested objects:
serialize nested builders
convert parts to raw attributes or IDs where appropriate
keep final payload JSON-friendly
Boundaries with parts and repositories
Builder vs part
part = canonical resource model received from Discord or stored in memory
builder = request payload authoring tool
Do not move outbound validation rules into parts if there is already a builder abstraction.
Builder vs repository
builder prepares payload
repository performs REST call
Repositories should not become payload authoring zones full of hand-built nested arrays if builder exists.
Component-specific rules
Component system exists in both builder and part worlds:
builder components under src/Discord/Builders/Components/*
inbound message component parts under src/Discord/Parts/Channel/Message/*
Keep these worlds aligned conceptually, but do not collapse them into one class family. Outbound builders and inbound parts solve different problems.
ComponentObject::TYPES
If new outbound component subtype appears:
add constant
add map entry
add class
check matching inbound component family if relevant
update builders using components
Usage contexts
Component usage constraints matter:
message
modal
interaction contexts
Where repo already encodes allowed contexts, keep validation there instead of free-form component insertion.
Builder selection guide
Add or extend a builder when:
payload has multiple related fields
Discord API has length/count/value limits
nested structures are easy to get wrong by hand
same shape is authored from several call sites
Raw array may be enough when:
payload is tiny
shape is stable and local
repo does not already have builder convention for that family
But if users will author it directly or repeatedly, bias toward builder.
Existing patterns worth copying
Message builder
Best example for:
content validation
embeds/files/stickers/components combination
fluent helpers
repository create() bridge
Channel builder
Best example for:
type-specific optional fields
enum validation
payload fields that differ by channel subtype
Command builder
Best example for:
optional field omission
payload shape depending on command type
Modal builder
Best example for:
strict component usage constraints
response payload shape nested under type + data
Smells
Stop if you see:
raw arrays duplicated across many callers when builder exists
validation postponed until after HTTP call
builder storing cache/domain state
repository assembling complicated payload shape that should live in builder
mismatch between builder and part edit flow because fromPart() path no longer reflects real fields
new component type added only on one side of builder/part system
Builder change checklist
new property has setter/getter if public
validation lives near write path
jsonSerialize() includes it only when appropriate
fromPart() behavior still makes sense
any create($repository) helper remains correct
component/type maps updated if subtype introduced
tests cover invalid and valid boundary cases
docs/examples updated if preferred public usage changed
Bottom line
Builders in this repo exist to keep outbound payload rules from leaking everywhere. If a caller has to memorize Discord payload trivia instead of relying on builder methods, builder design is unfinished.