CommunityProgramación y desarrollogithub.com

Redocly/php-generator

Diseño del generador de cliente PHP de Redocly para su personalización. Léelo y actualízalo antes de modificar generators/php/.

¿Qué es php-generator?

Este skill guía la personalización del generador de cliente PHP ejectado de Redocly en `generators/php/`. Define el diseño y las decisiones de implementación: emite un único archivo PHP autónomo con modelos de constructor promovido (clases finales), un `Client` con un método tipado por operación, y un runtime embebido. Soporta PHP ≥ 8.1, curl y cero dependencias Composer. La herramienta impone reglas como que los modelos con `writeOnly` sean opcionales incluso si son requeridos, que el mapa de operaciones sea una constante de clase para evitar problemas con OPcache, que los enums sean nativos y las uniones discriminadas usen despachadores `match`. También maneja paginación con generadores, streaming SSE, telemetría opcional y paso de flujos binarios sin cargarlos en memoria. Es útil para equipos que quieran personalizar profundamente el SDK generado, manteniendo la calidad con PHPStan nivel 5 y documentación auto-generada.

Compatible con~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 Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

El generador php — su skill

Este archivo es el DISEÑO de tu generador php ejectado (generators/php/): para cambiar el generador, edita este skill primero, luego haz que el código coincida — un diff sobre generators/php/ que no tenga una oración de cobertura aquí está incompleto.

Lo que emite

Un único archivo <stem>.php autocontenido: clases de modelo con constructor promovido, un Client con un método tipado por operación y el runtime embebido. PHP ≥ 8.1, HTTP sobre la extensión curl — cero dependencias Composer. El namespace deriva del título de la API (identifierFor(title, pascal) — p. ej. CafeOrders).

Decisiones de diseño que deben mantenerse

  • Los modelos son clases final con promoción de propiedades del constructor, parámetros requeridos primero, opcionales anulables = null. La hidratación se genera en tiempo de compilación por clase: fromArray(array $data): self y toArray(): array (nombres de serialización inline; valores nulos se omiten al serializar) — sin reflexión. Los esquemas omit se hidratan/serializan a través de su clase base. Una propiedad o respuesta tipada como una unión DISCRIMINADA se hidrata a través del despachador unmarshalX de la unión, para que los consumidores puedan estrechar con instanceof; su tipo incluye array, porque un valor con un discriminador desconocido permanece como array plano. Cualquier otra unión permanece como el JSON decodificado, así que su tipo es el del JSON: un miembro de clase es array, un miembro de enum es su escalar subyacente y un miembro de fecha es string.
  • El archivo pasa PHPStan nivel 5. Aterriza en el proyecto propio del usuario, así que su analizador lo lee. La tabla de operaciones es una constante que PHPStan conoce exactamente, por lo que un método lee $op['security'] solo cuando la tabla tiene esa entrada, y pasa [] en caso contrario.
  • Una propiedad writeOnly es opcional incluso cuando es requerida: las peticiones y respuestas comparten una clase, y una respuesta nunca la lleva. Es anulable = null, así que fromArray hidrata una respuesta sin ella y toArray aún la envía en una petición.
  • La clase Client NO es final — los conjuntos de pruebas de PHP simulan clases concretas (createMock(Client::class)), y final forzaría una interfaz envolvente en cada consumidor. Las clases modelo siguen siendo final.
  • Cada parámetro es su propio argumento, así que sus nombres comparten un namespace con los argumentos que el método declara por sí mismo ($body, $headers, $idempotencyKey). Constrúyelos con uniqueIdentifiers(..., { taken: … }): OpenAPI permite que una operación use un nombre en dos ubicaciones (id en la ruta Y en la consulta), y PHP rechaza directamente un parámetro redefinido. El nombre de serialización no se toca, así que la petición no cambia.
  • El mapa de operaciones es una constante de clase, Client::OPERATIONS, leída mediante self::OPERATIONS[...], nunca una const a nivel de archivo. OPcache preload mantiene las clases y funciones en memoria compartida pero descarta las constantes a nivel de archivo, y un autoloader nunca incluye el archivo de nuevo una vez que Client existe — por lo tanto, un mapa a nivel de archivo no está definido en cada petición después de la primera. Una constante de clase se almacena con la clase.
  • Nomenclatura: clases en PascalCase, propiedades/métodos en camelCase mediante identifierFor(..., RESERVED_WORDS.php); las palabras reservadas obtienen un guion bajo al final.
  • Enums son enums nativos respaldados (string/int); otros escalares permanecen como alias. Uniones discriminadas son despachadores unmarshalX(array $data) basados en match. Cada rama hidrata su objetivo de mapeo como lo haría una propiedad de ese tipo, así que un objetivo sin clase propia (un oneOf plano) permanece como array plano; allOf se aplana.
  • Las uniones mantienen sus tipos donde PHP 8.1 puede expresarlos. Una unión de escalares, enums, clases o arrays se convierte en un tipo de unión nativo (int|string, string|array) en lugar de colapsar a mixed — los filtros de listas enriquecidas son el caso común y perder sus tipos pierde el sentido de un SDK tipado. Se degrada a mixed solo cuando un miembro no tiene un tipo PHP propio (un objeto inline, una intersección, unknown), porque mixed no puede aparecer dentro de una unión. La anulabilidad se expresa como |null en una unión (PHP prohíbe mezclar ? con |) y ?T para un único tipo.
  • Errores: las excepciones SON el modo de error (ApiError/TimeoutError extienden \RuntimeException); errorMode no cambia la salida (el generador declara errorModes: ['throw'], así que result falla rápido).
  • Fechas: dateType: Date tipifica format: date/date-time como \DateTimeImmutable; la hidratación es new \DateTimeImmutable(...) y la serialización formatea con \DateTimeInterface::ATOM (date-time) o 'Y-m-d' (date), incluso para parámetros de consulta.
  • Argumentos de método: parámetros de ruta requeridos posicionales, cuerpo JSON a continuación, parámetros de consulta opcionales como argumentos NOMBRADOS anulables, luego ?array $headers y ?string $idempotencyKey en métodos mutantes.
  • Cuerpos de éxito no JSON (PDFs, imágenes, flujos octet) devuelven el cuerpo raw como string — una descarga binaria nunca debe degradarse a void.
  • PHPDoc lleva lo que la firma no puede. El array de PHP y \Generator borran su tipo de elemento, así que un docblock lo indica: @return Customer[] para devoluciones de colección y @return \Generator<int, Customer> en <op>Pages()/<op>Items(). El análisis estático y los lectores se guían por estos; una devolución hidratada sin anotación parece no tipada.
  • Encabezados de respuesta: una operación que DECLARA encabezados de respuesta exitosa obtiene una variante <op>WithHeaders() que devuelve un Envelope (data, headers — convertidos a int/bool/string con claves en camelCase, valores ausentes/no analizables omitidos — y status). Las operaciones sin encabezados declarados no obtienen variante, y el método base permanece solo con cuerpo (PHP no puede variar un tipo de retorno con un flag).
  • Servidores: cuando la descripción declara servidores, se emite una clase Servers con un método estático por servidor; las VARIABLES de servidor se convierten en argumentos string nombrados con valores predeterminados del spec (Servers::production(organizationId: 'org_x')), así que las URLs base con plantillas no necesitan construcción manual de strings. El valor predeterminado del cliente sigue siendo servers[0] con los valores predeterminados de las variables sustituidos.
  • Los cuerpos de flujo pasan directamente. Para una operación multipart/form-data no tipada (isUntypedMultipartBody del toolkit: un esquema que no es ni un objeto ni un $ref) o una operación binaria (isBinaryContentType), el cuerpo es mixed y el runtime lo resuelve (multipartBody / binaryBody): un resource de flujo es subido por curl en fragmentos (CURLOPT_UPLOAD + CURLOPT_INFILE, el método mantenido por CURLOPT_CUSTOMREQUEST), nunca leído en un string, exactamente en un intento, sin importar la política de reintentos, porque un flujo consumido no puede reproducirse. CURLOPT_INFILESIZE se establece solo cuando el flujo es seekable Y fstat devuelve un array (los bytes restantes desde la posición actual); php://input, pipes y wrappers sin stream_stat van en fragmentados. Un cuerpo string de cualquier operación se envía raw, no json_encodeado; cualquier otro valor de operación binaria se sigue json_encodeando; un cuerpo array de una operación multipart no tipada mantiene la ruta toMultipart. El Content-Type del llamante gana sobre el de la descripción o el del límite multipart, coincidiendo sin distinción de mayúsculas/minúsculas en send para que el encabezado nunca se envíe dos veces; esa regla se aplica a todas las operaciones. El timeout curl de toda la petición sigue aplicándose (timeout: null lo desactiva). Los cuerpos multipart tipados no se ven afectados.
  • Superficie de paridad: autenticación, reintentos con Retry-After + backoff con jitter, timeouts curl por intento, callables de middleware, paginación (<op>Pages() / <op>Items() como \Generators), SSE (iterSse sobre una bomba curl_multi), multipart.
  • El runtime está escrito a mano en runtime/runtime.php en esta carpeta (limpio con php -l) y se incrusta en tiempo de preparación. curl_close nunca se llama (obsoleto desde PHP 8.5, no-op desde 8.0). Bajo --runtime module se escribe como un runtime.php que el cliente require_oncea, con su namespace reescrito al del cliente para que un namespace abarque ambos archivos.
  • onSettled ve cada llamada enviada una vez. send() llama a cada callable Config->onSettled, luego a cada callable Config::$registeredOnSettled, después del bucle de reintentos con operationId, status (0 sin respuesta), timedOut, attempts y durationMs, antes de lanzar o devolver. Cada hook se ejecuta. El primer error de hook se lanza para una llamada exitosa; en una llamada fallida, gana el error original y el error del hook va a error_log. Los flujos SSE se abren fuera de send(), así que no alcanzan ningún hook.
  • La telemetría es opt-in en tiempo de generación. Con client.telemetry: on el generador también escribe <client>.telemetry.php (desde telemetry/telemetry.php en esta carpeta, incrustado en tiempo de preparación ) junto al cliente, en ambos modos de runtime, con el namespace del cliente y la versión del generador incluidos. El archivo del cliente no cambia. La aplicación lo requiere y pasa new UsageTelemetry(...) en Config->onSettled. Un propietario del SDK llama a register() en su lugar, lo que añade la instancia a Config::$registeredOnSettled, así que cada cliente cuenta sus llamadas; UsageTelemetry::flushRegistered() vacía cada instancia registrada. PHP no tiene timers: una llamada que encuentra su ventana transcurrida la envía primero, y una función de apagado envía la ventana abierta. Las constantes viven en la clase, porque la precarga de OPcache elimina las constantes a nivel de archivo. El nombre de la clase está reservado para cada SDK, porque el archivo comparte el namespace con los modelos.
  • Escrito SOLO con el toolkit neutral — la guarda de dogfooding falla en caso contrario.

Migración desde un SDK basado en servicios

  • Los servicios por recurso ($client->customers()->get($id)) se mapean a métodos planos nombrados según los operationIds ($client->getCustomer($id)); los parámetros de consulta opcionales mantienen su estilo de argumento nombrado (filter:, sort:, limit:).

  • Los envoltorios de colección que exponen ENCABEZADOS DE RESPUESTA de paginación (getTotalItems(), getLimit()) se mapean al envoltorio <op>WithHeaders() (->headers['paginationTotal']); la iteración simple se mapea a los generadores <op>Items() / <op>Pages().

  • Las clases de excepción de validación dedicadas que exponen errores de campo se mapean a catch (ApiError $e) + $e->status === 422 + el $e->body decodificado.

  • Los flujos de token de sesión/bearer se mapean a auth: ['bearer' => $tokenProvider] con un callable — resuelto por petición, así que el refresco no necesita reconstruir el cliente.

  • Se documenta a sí mismo. Con client.docs (o --docs), el hook docs escribe <stem>.php.md: los esquemas de seguridad, luego una sección por operación con sus parámetros, cuerpo, tipo de respuesta y notas de comportamiento. Los fragmentos de código de llamada provienen del hook sample de este mismo generador, así que la página solo puede mostrar la sintaxis del SDK junto a él, y el diseño proviene de renderReferencePage en el toolkit de autoría — accesible desde una copia ejectada mediante @redocly/client-generator. La paginación en la página es decidida por paginationRuleFor, el mismo ayudante con el que este generador resuelve la paginación.

El bucle de modificación

  1. Edita este skill: indica el nuevo comportamiento o decisión.
  2. Haz que generators/php/ coincida.
  3. Ejecuta redocly generate-client e inspecciona el git diff de la salida generada — los archivos generados nunca se editan a mano.

Las versiones integradas más nuevas se fusionan con redocly eject-generator php --update.

Individual skills in this repo

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

Skills relacionados