Redocly/gerador-php

Design do gerador de cliente PHP ejetado do Redocly. Leia e atualize este documento antes de modificar generators/php/.

O que é gerador-php?

Este arquivo descreve o design do gerador de cliente PHP (`generators/php/`) ejetado do Redocly. Ele detalha as decisões de implementação para gerar um SDK PHP autossuficiente a partir de uma especificação OpenAPI. O gerador produz um único arquivo `<stem>.php` contendo classes de modelo (final class com construtor promovido), um `Client` com métodos tipados por operação, e o runtime embutido, usando PHP ≥ 8.1 e extensão cURL — sem dependências Composer. O namespace deriva do título da API. As decisões de design incluem: modelos finais para segurança de tipo, hidratação compilada (sem reflection), suporte a uniões discriminadas, enums nativos, tratamento de datas com `DateTimeImmutable`, parâmetros nomeados e separados, mapa de operações como constante de classe para compatibilidade com OPcache, e telemetria opcional em tempo de geração. O arquivo também documenta como o gerador lida com fluxos de arquivos, autenticação, retry com backoff, paginação, SSE e erros via exceções. A seção de migração orienta usuários de SDKs baseados em serviço a adaptarem seu código para a nova estrutura plana de métodos. O processo de modificação exige editar este skill primeiro, depois ajustar o código em `generators/php/` e validar com `redocly generate-client`.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Redocly/redocly-cli/tree/HEAD/packages/client-generator/eject-assets/skills/php-generator

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O gerador php — sua habilidade

Este arquivo é o DESIGN do seu gerador php ejetado (generators/php/): para alterar o gerador, edite esta habilidade primeiro, depois faça o código corresponder a ela — uma diferença para generators/php/ que não tenha uma sentença de cobertura aqui está incompleta.

O que ele emite

Um único arquivo <stem>.php autossuficiente: classes de modelo com construtor promovido, um Client com um método tipado por operação e o runtime embutido. PHP ≥ 8.1, HTTP sobre a extensão curl — zero dependências Composer. O namespace deriva do título da API (identifierFor(title, pascal) — ex.: CafeOrders).

Decisões de design que devem ser mantidas

  • Modelos são final class com promoção de propriedades do construtor, parâmetros obrigatórios primeiro, opcionais anuláveis = null. A hidratação é gerada em tempo de compilação por classe: fromArray(array $data): self e toArray(): array (nomes de campo inline; nulos ignorados na serialização) — sem reflection. Esquemas omit hidratam/serializam através de sua classe base. Uma propriedade ou resposta tipada como uma união DISCRIMINADA hidrata através do despachante unmarshalX da união, para que consumidores possam restringir com instanceof; seu tipo inclui array, porque um valor com um discriminador desconhecido permanece como array bruto. Qualquer outra união permanece como JSON decodificado, então seu tipo é o do JSON: um membro de classe é array, um membro de enum é seu escalar de suporte e um membro de data é string.
  • O arquivo passa no PHPStan nível 5. Ele cai no projeto do próprio usuário, então seu analisador o lê. A tabela de operações é uma constante que o PHPStan conhece exatamente, então um método lê $op['security'] apenas quando a tabela tem essa entrada e passa [] caso contrário.
  • Uma propriedade writeOnly é opcional mesmo quando obrigatória: requisições e respostas compartilham uma classe, e uma resposta nunca a carrega. Ela é anulável = null, então fromArray hidrata uma resposta sem ela e toArray ainda a envia em uma requisição.
  • A classe Client NÃO é final — suítes de teste PHP simulam classes concretas (createMock(Client::class)), e final forçaria uma interface wrapper em todo consumidor. Classes de modelo permanecem final.
  • Cada parâmetro é seu próprio argumento, então seus nomes compartilham um namespace com os argumentos que o próprio método declara ($body, $headers, $idempotencyKey). Construa-os com uniqueIdentifiers(..., { taken: … }): o OpenAPI permite que uma operação use um nome em dois locais (id no caminho E na query), e o PHP rejeita um parâmetro redefinido diretamente. O nome na rede não é alterado, então a requisição permanece inalterada.
  • O mapa de operações é uma constante de classe, Client::OPERATIONS, lida através de self::OPERATIONS[...], nunca uma const no nível do arquivo. O OPcache pré-carregado mantém classes e funções em memória compartilhada, mas descarta constantes no nível do arquivo, e um autoloader nunca inclui o arquivo novamente quando Client existe — então um mapa no nível do arquivo é indefinido em toda requisição após a primeira. Uma constante de classe é armazenada com a classe.
  • Nomenclatura: classes PascalCase, propriedades/métodos camelCase via identifierFor(..., RESERVED_WORDS.php); palavras reservadas recebem um sublinhado final.
  • Enums são enums nativos com suporte (string/int); outros escalares permanecem como aliases. Uniões discriminadas são despachantes unmarshalX(array $data) baseados em match. Cada braço hidrata seu alvo de mapeamento da mesma forma que uma propriedade desse tipo faria, então um alvo sem classe própria (um oneOf simples) permanece como array bruto; allOf é achatado.
  • Uniões mantêm seus tipos onde o PHP 8.1 pode expressá-los. Uma união de escalares, enums, classes ou arrays se torna um tipo de união nativo (int|string, string|array) em vez de colapsar para mixed — listas de filtro ricas são o caso comum e perder seus tipos perde o propósito de um SDK tipado. Ele recai para mixed apenas quando um membro não tem tipo PHP próprio (um objeto inline, uma intersecção, unknown), porque mixed não pode aparecer dentro de uma união. A anulabilidade é expressa como |null em uma união (o PHP proíbe misturar ? com |) e ?T para um tipo único.
  • Erros: exceções SÃO o modo de erro (ApiError/TimeoutError estendem \RuntimeException); errorMode não altera a saída (o gerador declara errorModes: ['throw'], então result falha rapidamente).
  • Datas: dateType: Date tipifica format: date/date-time como \DateTimeImmutable; a hidratação é new \DateTimeImmutable(...) e a serialização formata com \DateTimeInterface::ATOM (date-time) ou 'Y-m-d' (date), inclusive para parâmetros de query.
  • Argumentos de método: parâmetros de caminho obrigatórios posicionais, corpo JSON em seguida, parâmetros de query opcionais como argumentos NOMEADOS anuláveis, depois ?array $headers e ?string $idempotencyKey em métodos mutantes.
  • Corpos de sucesso não-JSON (PDFs, imagens, fluxos octet) retornam o corpo bruto como string — um download binário nunca deve degradar para void.
  • PHPDoc carrega o que a assinatura não pode. O array do PHP e \Generator apagam seus tipos de elementos, então um docblock declara: @return Customer[] para retornos de coleção e @return \Generator<int, Customer> em <op>Pages()/<op>Items(). Análise estática e leitores se guiam por isso; um retorno hidratado sem anotação parece não tipado.
  • Cabeçalhos de resposta: uma operação que DECLARA cabeçalhos de resposta de sucesso ganha uma variante <op>WithHeaders() retornando um Envelope (data, headers — convertidos para int/bool/string com chaves camelCase, valores ausentes/inescrutáveis omitidos — e status). Operações sem cabeçalhos declarados não recebem variante, e o método base permanece apenas com corpo (o PHP não pode variar um tipo de retorno em uma flag).
  • Servidores: quando a descrição declara servidores, uma classe Servers é emitida com um método estático por servidor; VARIÁVEIS de servidor se tornam argumentos string nomeados com padrão para os padrões da especificação (Servers::production(organizationId: 'org_x')), então URLs base com template não precisam de construção manual de string. O padrão embutido do cliente permanece servers[0] com substituições de variáveis padrão.
  • Corpos de stream passam através. Para uma operação multipart/form-data não tipada (isUntypedMultipartBody do toolkit: um esquema que não é nem objeto nem $ref) ou uma operação binária (isBinaryContentType), o corpo é mixed e o runtime resolve (multipartBody / binaryBody): um resource de stream é enviado pelo curl em chunks (CURLOPT_UPLOAD + CURLOPT_INFILE, o método mantido por CURLOPT_CUSTOMREQUEST), nunca lido em uma string, em exatamente uma tentativa, qualquer que seja a política de retry, porque um stream consumido não pode ser reproduzido. CURLOPT_INFILESIZE é definido apenas quando o stream é seekable E fstat retorna um array (os bytes restantes da posição atual); php://input, pipes e wrappers sem stream_stat vão em chunked. Um corpo string de qualquer operação é enviado bruto, não json_encoded; qualquer outro valor de operação binária é ainda json_encoded; um corpo array de uma operação multipart não tipada mantém o caminho toMultipart. O Content-Type do chamador vence sobre o da descrição ou do limite multipart, combinado case-insensitively em send para que o cabeçalho nunca seja enviado duas vezes; essa regra vale para toda operação. O timeout curl de toda a requisição ainda se aplica (timeout: null o desabilita). Corpos multipart tipados não são afetados.
  • Superfície de paridade: autenticação, retries com Retry-After + backoff com jitter, timeouts curl por tentativa, callables de middleware, paginação (<op>Pages() / <op>Items() como \Generators), SSE (iterSse sobre uma bomba curl_multi), multipart.
  • O runtime é escrito à mão em runtime/runtime.php nesta pasta (limpo com php -l) e embutido em tempo de preparação. curl_close nunca é chamado (obsoleto desde PHP 8.5, no-op desde 8.0). Sob --runtime module ele é escrito como um runtime.php que o cliente require_once, com seu namespace reescrito para o namespace do cliente para que um namespace abranja ambos os arquivos.
  • onSettled vê cada chamada enviada uma vez. send() chama cada callable Config->onSettled, depois cada callable Config::$registeredOnSettled, após o loop de retry com operationId, status (0 sem resposta), timedOut, attempts e durationMs, antes de lançar ou retornar. Todo hook é executado. O primeiro erro de hook é lançado para uma chamada bem-sucedida; em uma chamada falha, o erro original vence e o erro do hook vai para error_log. Streams SSE abrem fora de send(), então eles não atingem nenhum hook.
  • Telemetria é opt-in em tempo de geração. Com client.telemetry: on o gerador também escreve <client>.telemetry.php (de telemetry/telemetry.php nesta pasta, embutido em tempo de preparação) ao lado do cliente, em ambos os modos de runtime, com o namespace do cliente e a versão do gerador embutidos. O arquivo do cliente não muda. A aplicação o requer e passa new UsageTelemetry(...) em Config->onSettled. Um proprietário de SDK chama register() em vez disso, que adiciona a instância a Config::$registeredOnSettled, então todo cliente conta suas chamadas; UsageTelemetry::flushRegistered() libera cada instância registrada. PHP não tem timers: uma chamada que encontra sua janela expirada a envia primeiro, e uma função de shutdown envia a janela aberta. Constantes vivem na classe, porque o pré-carregamento OPcache descarta constantes no nível do arquivo. O nome da classe é reservado para todo SDK, porque o arquivo compartilha o namespace com os modelos.
  • Criado APENAS com o toolkit neutro — o guardião de dogfooding falha caso contrário.

Migrando de um SDK baseado em serviço

  • Serviços por recurso ($client->customers()->get($id)) mapeiam para métodos planos nomeados após operationIds ($client->getCustomer($id)); parâmetros de query opcionais mantêm seu estilo de argumento nomeado (filter:, sort:, limit:).

  • Wrappers de coleção expondo CABEÇALHOS DE RESPOSTA de paginação (getTotalItems(), getLimit()) mapeiam para o envelope <op>WithHeaders() (->headers['paginationTotal']); iteração simples mapeia para geradores <op>Items() / <op>Pages().

  • Classes de exceção de validação dedicadas expondo erros de campo mapeiam para catch (ApiError $e) + $e->status === 422 + o $e->body decodificado.

  • Fluxos de token de sessão/bearer mapeiam para auth: ['bearer' => $tokenProvider] com um callable — resolvido por requisição, então refresh não precisa de reconstrução do cliente.

  • Ele se documenta. Com client.docs (ou --docs), o hook docs escreve <stem>.php.md: os esquemas de segurança, depois uma seção por operação com seus parâmetros, corpo, tipo de resposta e notas de comportamento. Os trechos de chamada vêm do próprio hook sample deste gerador, então a página pode mostrar apenas a sintaxe do SDK ao lado, e o layout vem de renderReferencePage no toolkit de autoria — acessível de uma cópia ejetada através de @redocly/client-generator. A paginação na página é decidida por paginationRuleFor, o mesmo helper com o qual este gerador resolve paginação.

O loop de modificação

  1. Edite esta habilidade: declare o novo comportamento ou decisão.
  2. Faça generators/php/ corresponder a ele.
  3. Execute redocly generate-client e inspecione o git diff da saída gerada — arquivos gerados nunca são editados manualmente.

Versões embutidas mais novas são mescladas com redocly eject-generator php --update.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Habilidades Relacionadas