The php generator — its skill
This file is the DESIGN of your ejected php generator (generators/php/):
to change the generator, edit this skill first, then make the code match it — a diff
to generators/php/ that has no covering sentence here is incomplete.
What it emits
One self-contained <stem>.php: promoted-constructor model classes, a Client with one
typed method per operation, and the embedded runtime. PHP ≥ 8.1, HTTP over the curl
extension — zero Composer dependencies. The namespace derives from the API title
(identifierFor(title, pascal) — e.g. CafeOrders).
Design decisions that must hold
- Models are
final classes with constructor property promotion, required parameters first, optionals nullable= null. Hydration is compile-time generated per class:fromArray(array $data): selfandtoArray(): array(wire names inline; nulls skipped on serialize) — no reflection.omitschemas hydrate/serialize through their base class. A property or response typed as a DISCRIMINATED union hydrates through the union'sunmarshalXdispatcher, so consumers can narrow withinstanceof; undiscriminated unions stay raw arrays. - The
Clientclass is NOTfinal— PHP test suites mock concrete classes (createMock(Client::class)), andfinalwould force a wrapper interface on every consumer. Model classes stayfinal. - Every parameter is its own argument, so their names share one namespace with the
arguments the method declares itself (
$body,$headers,$idempotencyKey). Build them withuniqueIdentifiers(..., { taken: … }): OpenAPI lets one operation use a name in two locations (idin the path AND in the query), and PHP rejects a redefined parameter outright. The wire name is untouched, so the request is unchanged. - Naming: classes PascalCase, properties/methods camelCase via
identifierFor(..., RESERVED_WORDS.php); reserved words get a trailing underscore. - Enums are native backed enums (string/int); other scalars stay aliases.
Discriminated unions are
match-basedunmarshalX(array $data)dispatchers; allOf is flattened. - Unions keep their types where PHP 8.1 can express them. A union of scalars, enums,
classes, or arrays becomes a native union type (
int|string,PromotionType|array) rather than collapsing tomixed— rich list filters are the common case and losing their types loses the point of a typed SDK. It falls back tomixedonly when a member has no PHP type of its own (an inline object, an intersection,unknown), becausemixedcannot appear inside a union. Nullability is expressed as|nullin a union (PHP forbids mixing?with|) and?Tfor a single type. - Errors: exceptions ARE the error mode (
ApiError/TimeoutErrorextend\RuntimeException);errorModedoes not change the output (the generator declareserrorModes: ['throw'], soresultfails fast). - Dates:
dateType: Datetypesformat: date/date-timeas\DateTimeImmutable; hydration isnew \DateTimeImmutable(...)and serialization formats with\DateTimeInterface::ATOM(date-time) or'Y-m-d'(date), including for query parameters. - Method arguments: required path params positional, JSON body next, optional query
params as nullable NAMED arguments, then
?array $headers, and?string $idempotencyKeyon mutating methods. - Non-JSON success bodies (PDFs, images, octet streams) return the raw body as
string— a binary download must never degrade tovoid. - PHPDoc carries what the signature cannot. PHP's
arrayand\Generatorerase their element type, so a docblock states it:@return Customer[]for collection returns and@return \Generator<int, Customer>on<op>Pages()/<op>Items(). Static analysis and readers go by these; a hydrated return with no annotation looks untyped. - Response headers: an operation that DECLARES success-response headers gains a
<op>WithHeaders()variant returning anEnvelope(data,headers— coerced to int/bool/string with camelCase keys, absent/unparsable values omitted — andstatus). Operations without declared headers get no variant, and the base method stays body-only (PHP cannot vary a return type on a flag). - Servers: when the description declares servers, a
Serversclass is emitted with one static method per server; server VARIABLES become named string arguments defaulting to the spec's defaults (Servers::production(organizationId: 'org_x')), so templated base URLs need no manual string building. The client's baked default staysservers[0]with variable defaults substituted. - Parity surface: auth, retries with
Retry-After+ jittered backoff, per-attempt curl timeouts, middleware callables, pagination (<op>Pages()/<op>Items()as\Generators), SSE (iterSseover a curl_multi pump), multipart. - The runtime is hand-written in
runtime/runtime.phpin this folder (php -l-clean) and embedded at prepare time.curl_closeis never called (deprecated since PHP 8.5, no-op since 8.0). Under--runtime moduleit is written as aruntime.phpthe clientrequire_onces, with its namespace rewritten to the client's so one namespace spans both files. - Authored ONLY with the neutral toolkit — the dogfooding guard fails otherwise.
Migrating from a service-based SDK
Per-resource services (
$client->customers()->get($id)) map to flat methods named after operationIds ($client->getCustomer($id)); optional query params keep their named-argument style (filter:,sort:,limit:).Collection wrappers exposing pagination RESPONSE HEADERS (
getTotalItems(),getLimit()) map to the<op>WithHeaders()envelope (->headers['paginationTotal']); plain iteration maps to<op>Items()/<op>Pages()generators.Dedicated validation-exception classes exposing field errors map to
catch (ApiError $e)+$e->status === 422+ the decoded$e->body.Session/bearer token flows map to
auth: ['bearer' => $tokenProvider]with a callable — resolved per request, so refresh needs no client rebuild.It documents itself. With
client.docs(or--docs), thedocshook writes<stem>.php.md: the security schemes, then one section per operation with its parameters, body, response type, and behavior notes. The call snippets come from this generator's ownsamplehook, so the page can only show the syntax of the SDK beside it, and the layout comes fromrenderReferencePagein the authoring toolkit — reachable from an ejected copy through@redocly/client-generator. Pagination on the page is decided bypaginationRuleFor, the same helper this generator resolves pagination with.
The modify loop
- Edit this skill: state the new behavior or decision.
- Make
generators/php/match it. - Run
redocly generate-clientand inspect thegit diffof the generated output — generated files are never hand-edited.
Newer built-in versions merge in with redocly eject-generator php --update.