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
finalcon 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): selfytoArray(): array(nombres de serialización inline; valores nulos se omiten al serializar) — sin reflexión. Los esquemasomitse 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 despachadorunmarshalXde la unión, para que los consumidores puedan estrechar coninstanceof; su tipo incluyearray, 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 esarray, un miembro de enum es su escalar subyacente y un miembro de fecha esstring. - 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
writeOnlyes opcional incluso cuando es requerida: las peticiones y respuestas comparten una clase, y una respuesta nunca la lleva. Es anulable= null, así quefromArrayhidrata una respuesta sin ella ytoArrayaún la envía en una petición. - La clase
ClientNO esfinal— los conjuntos de pruebas de PHP simulan clases concretas (createMock(Client::class)), yfinalforzaría una interfaz envolvente en cada consumidor. Las clases modelo siguen siendofinal. - 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 conuniqueIdentifiers(..., { taken: … }): OpenAPI permite que una operación use un nombre en dos ubicaciones (iden 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 medianteself::OPERATIONS[...], nunca unaconsta 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 queClientexiste — 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 enmatch. Cada rama hidrata su objetivo de mapeo como lo haría una propiedad de ese tipo, así que un objetivo sin clase propia (unoneOfplano) 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 amixed— los filtros de listas enriquecidas son el caso común y perder sus tipos pierde el sentido de un SDK tipado. Se degrada amixedsolo cuando un miembro no tiene un tipo PHP propio (un objeto inline, una intersección,unknown), porquemixedno puede aparecer dentro de una unión. La anulabilidad se expresa como|nullen una unión (PHP prohíbe mezclar?con|) y?Tpara un único tipo. - Errores: las excepciones SON el modo de error (
ApiError/TimeoutErrorextienden\RuntimeException);errorModeno cambia la salida (el generador declaraerrorModes: ['throw'], así queresultfalla rápido). - Fechas:
dateType: Datetipificaformat: date/date-timecomo\DateTimeImmutable; la hidratación esnew \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 $headersy?string $idempotencyKeyen 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 avoid. - PHPDoc lleva lo que la firma no puede. El
arrayde PHP y\Generatorborran 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 unEnvelope(data,headers— convertidos a int/bool/string con claves en camelCase, valores ausentes/no analizables omitidos — ystatus). 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
Serverscon 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 siendoservers[0]con los valores predeterminados de las variables sustituidos. - Los cuerpos de flujo pasan directamente. Para una operación
multipart/form-datano tipada (isUntypedMultipartBodydel toolkit: un esquema que no es ni un objeto ni un$ref) o una operación binaria (isBinaryContentType), el cuerpo esmixedy el runtime lo resuelve (multipartBody/binaryBody): unresourcede flujo es subido por curl en fragmentos (CURLOPT_UPLOAD+CURLOPT_INFILE, el método mantenido porCURLOPT_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_INFILESIZEse establece solo cuando el flujo es seekable Yfstatdevuelve un array (los bytes restantes desde la posición actual);php://input, pipes y wrappers sinstream_statvan en fragmentados. Un cuerpostringde cualquier operación se envía raw, nojson_encodeado; cualquier otro valor de operación binaria se siguejson_encodeando; un cuerpoarrayde una operación multipart no tipada mantiene la rutatoMultipart. ElContent-Typedel llamante gana sobre el de la descripción o el del límite multipart, coincidiendo sin distinción de mayúsculas/minúsculas ensendpara 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: nulllo 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 (iterSsesobre una bomba curl_multi), multipart. - El runtime está escrito a mano en
runtime/runtime.phpen esta carpeta (limpio conphp -l) y se incrusta en tiempo de preparación.curl_closenunca se llama (obsoleto desde PHP 8.5, no-op desde 8.0). Bajo--runtime modulese escribe como unruntime.phpque el clienterequire_oncea, con su namespace reescrito al del cliente para que un namespace abarque ambos archivos. onSettledve cada llamada enviada una vez.send()llama a cada callableConfig->onSettled, luego a cada callableConfig::$registeredOnSettled, después del bucle de reintentos conoperationId,status(0 sin respuesta),timedOut,attemptsydurationMs, 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 aerror_log. Los flujos SSE se abren fuera desend(), así que no alcanzan ningún hook.- La telemetría es opt-in en tiempo de generación. Con
client.telemetry: onel generador también escribe<client>.telemetry.php(desdetelemetry/telemetry.phpen 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 pasanew UsageTelemetry(...)enConfig->onSettled. Un propietario del SDK llama aregister()en su lugar, lo que añade la instancia aConfig::$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->bodydecodificado. -
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 hookdocsescribe<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 hooksamplede este mismo generador, así que la página solo puede mostrar la sintaxis del SDK junto a él, y el diseño proviene derenderReferencePageen el toolkit de autoría — accesible desde una copia ejectada mediante@redocly/client-generator. La paginación en la página es decidida porpaginationRuleFor, el mismo ayudante con el que este generador resuelve la paginación.
El bucle de modificación
- Edita este skill: indica el nuevo comportamiento o decisión.
- Haz que
generators/php/coincida. - Ejecuta
redocly generate-cliente inspecciona elgit diffde 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.