Le générateur php — son rôle
Ce fichier est la CONCEPTION de votre générateur php éjecté (generators/php/) :
pour modifier le générateur, éditez d'abord ce rôle, puis faites correspondre le code — un diff
sur generators/php/ qui n'a pas de phrase de couverture ici est incomplet.
Ce qu'il émet
Un fichier <stem>.php autonome : classes de modèles avec promotion de constructeur, un Client avec une
méthode typée par opération, et le runtime embarqué. PHP ≥ 8.1, HTTP via l'extension curl
— zéro dépendance Composer. Le namespace dérive du titre de l'API
(identifierFor(title, pascal) — ex. CafeOrders).
Décisions de conception qui doivent tenir
- Les modèles sont des
final classavec promotion de propriété de constructeur, paramètres obligatoires en premier, optionnels nullables= null. L'hydratation est générée à la compilation par classe :fromArray(array $data): selfettoArray(): array(noms de fil en ligne ; les nulls sont ignorés lors de la sérialisation) — pas de réflexion. Les schémasomits'hydratent/sérialisent via leur classe de base. Une propriété ou réponse typée comme une union DISCRIMINÉE s'hydrate via le dispatcheurunmarshalXde l'union, afin que les consommateurs puissent affiner avecinstanceof; son type inclutarray, car une valeur avec un discriminateur inconnu reste le tableau brut. Toute autre union reste le JSON décodé, donc son type est celui du JSON : un membre de classe estarray, un membre d'énumération est son scalaire sous-jacent, et un membre de date eststring. - Le fichier passe PHPStan niveau 5. Il atterrit dans le propre projet de l'utilisateur, donc son
analyseur le lit. La table des opérations est une constante que PHPStan connaît exactement, donc une méthode
lit
$op['security']uniquement lorsque la table a cette entrée, et passe[]sinon. - Une propriété
writeOnlyest optionnelle même si obligatoire : les requêtes et réponses partagent une seule classe, et une réponse ne la porte jamais. Elle est nullable= null, doncfromArrayhydrate une réponse sans elle ettoArrayl'envoie toujours dans une requête. - La classe
Clientn'est PASfinal— les suites de tests PHP mockent des classes concrètes (createMock(Client::class)), etfinalforcerait une interface wrapper sur chaque consommateur. Les classes de modèles restentfinal. - Chaque paramètre est son propre argument, donc leurs noms partagent un seul namespace avec les
arguments que la méthode déclare elle-même (
$body,$headers,$idempotencyKey). Construisez-les avecuniqueIdentifiers(..., { taken: … }): OpenAPI permet à une opération d'utiliser un nom à deux endroits (iddans le chemin ET dans la requête), et PHP rejette un paramètre redéfini purement et simplement. Le nom filaire est inchangé, donc la requête est inchangée. - La carte des opérations est une constante de classe,
Client::OPERATIONS, lue viaself::OPERATIONS[...], jamais uneconstau niveau fichier. OPcache précharge les classes et les fonctions en mémoire partagée mais abandonne les constantes au niveau fichier, et un autoloader n'inclut jamais le fichier à nouveau une fois queClientexiste — donc une carte au niveau fichier est indéfinie à chaque requête après la première. Une constante de classe est stockée avec la classe. - Nommage : classes en PascalCase, propriétés/méthodes en camelCase via
identifierFor(..., RESERVED_WORDS.php); les mots réservés reçoivent un underscore final. - Les énumérations sont des énumérations natives avec valeur (string/int) ; les autres scalaires restent des alias.
Les unions discriminées sont des dispatcheurs
unmarshalX(array $data)basés surmatch. Chaque branche hydrate sa cible de mapping de la même manière qu'une propriété de ce type, donc une cible sans classe propre (unoneOfsimple) reste un tableau brut ; allOf est aplati. - Les unions conservent leurs types là où PHP 8.1 peut les exprimer. Une union de scalaires, d'énumérations,
de classes ou de tableaux devient un type d'union natif (
int|string,string|array) plutôt que de se réduire àmixed— les filtres de listes riches sont le cas courant et perdre leurs types revient à perdre l'intérêt d'un SDK typé. On tombe àmixeduniquement lorsqu'un membre n'a pas de propre type PHP (un objet inline, une intersection,unknown), carmixedne peut pas apparaître à l'intérieur d'une union. La nullabilité est exprimée par|nulldans une union (PHP interdit de mélanger?avec|) et?Tpour un type unique. - Erreurs : les exceptions SONT le mode d'erreur (
ApiError/TimeoutErrorétendentRuntimeException) ;errorModene change pas la sortie (le générateur déclareerrorModes: ['throw'], doncresultéchoue rapidement). - Dates :
dateType: Datetypeformat: date/date-timecommeDateTimeImmutable; l'hydratation estnew \DateTimeImmutable(...)et la sérialisation formate avecDateTimeInterface::ATOM(date-time) ou'Y-m-d'(date), y compris pour les paramètres de requête. - Arguments de méthode : paramètres de chemin obligatoires positionnels, corps JSON ensuite, paramètres de requête
optionnels comme arguments NOMINAUX nullables, puis
?array $headers, et?string $idempotencyKeysur les méthodes de mutation. - Corps de succès non JSON (PDFs, images, flux octet) retournent le corps brut sous forme de
string— un téléchargement binaire ne doit jamais dégrader envoid. - PHPDoc porte ce que la signature ne peut pas. Le
arrayde PHP etGeneratoreffacent leur type d'élément, donc un docblock le déclare :@return Customer[]pour les retours de collection et@return \Generator<int, Customer>sur<op>Pages()/<op>Items(). L'analyse statique et les lecteurs s'en remettent à cela ; un retour hydraté sans annotation semble non typé. - En-têtes de réponse : une opération qui DÉCLARE des en-têtes de réponse de succès gagne une
variante
<op>WithHeaders()retournant uneEnvelope(data,headers— convertis en int/bool/string avec des clés en camelCase, les valeurs absentes/analysables omises — etstatus). Les opérations sans en-têtes déclarés n'obtiennent pas de variante, et la méthode de base reste uniquement sur le corps (PHP ne peut pas faire varier un type de retour sur un indicateur). - Serveurs : lorsque la description déclare des serveurs, une classe
Serversest émise avec une méthode statique par serveur ; les VARIABLES de serveur deviennent des arguments de chaîne nommés avec des valeurs par défaut issues de la spécification (Servers::production(organizationId: 'org_x')), donc les URLs de base templatisées n'ont pas besoin de construction de chaîne manuelle. La valeur par défaut intégrée du client resteservers[0]avec les valeurs par défaut des variables substituées. - Les corps de flux passent au travers. Pour une opération
multipart/form-datanon typée (isUntypedMultipartBodyde la boîte à outils : un schéma qui n'est ni un objet ni une$ref) ou une opération binaire (isBinaryContentType), le corps estmixedet le runtime s'en occupe (multipartBody/binaryBody) : uneresourcede flux est téléchargée par curl en morceaux (CURLOPT_UPLOAD+CURLOPT_INFILE, la méthode conservée parCURLOPT_CUSTOMREQUEST), jamais lue dans une chaîne, en exactement une tentative, quelle que soit la politique de reprise, car un flux consommé ne peut pas être rejoué.CURLOPT_INFILESIZEest défini uniquement lorsque le flux est seekable ET quefstatretourne un tableau (les octets restants à partir de la position actuelle) ;php://input, les pipes et les wrappers sansstream_statvont en morceaux. Un corpsstringde l'une ou l'autre opération est envoyé brut, nonjson_encode; toute autre valeur d'opération binaire est toujoursjson_encode; un corpsarrayd'une opération multipart non typée conserve le chemintoMultipart. L'en-têteContent-Typede l'appelant prime sur celui de la description ou de la limite multipart, comparé insensiblement à la casse danssendafin que l'en-tête ne soit jamais envoyé deux fois ; cette règle s'applique à chaque opération. Le délai d'attente curl global de la requête s'applique toujours (timeout: nullle désactive). Les corps multipart typés ne sont pas affectés. - Surface de parité : authentification, réessais avec
Retry-After+ backoff avec gigue, délais d'attente curl par tentative, callables middleware, pagination (<op>Pages()/<op>Items()comme\Generators), SSE (iterSsesur une pompe curl_multi), multipart. - Le runtime est écrit à la main dans
runtime/runtime.phpdans ce dossier (proprephp -l) et embarqué au moment de la préparation.curl_closen'est jamais appelé (déprécié depuis PHP 8.5, sans effet depuis 8.0). Sous--runtime module, il est écrit comme unruntime.phpque le clientrequire_once, avec son namespace réécrit vers celui du client afin qu'un seul namespace couvre les deux fichiers. onSettledvoit chaque appel envoyé une fois.send()appelle chaque callableConfig->onSettled, puis chaque callableConfig::$registeredOnSettled, après la boucle de réessai avecoperationId,status(0 sans réponse),timedOut,attempts, etdurationMs, avant de lancer une exception ou de retourner. Chaque hook s'exécute. La première erreur de hook est lancée pour un appel réussi ; sur un appel échoué, l'erreur originale l'emporte et l'erreur du hook va danserror_log. Les flux SSE s'ouvrent en dehors desend(), donc ils n'atteignent aucun hook.- La télémétrie est facultative au moment de la génération. Avec
client.telemetry: on, le générateur écrit également<client>.telemetry.php(depuistelemetry/telemetry.phpdans ce dossier, embarqué au moment de la préparation) à côté du client, dans les deux modes de runtime, avec le namespace du client et la version du générateur intégrés. Le fichier client ne change pas. L'application le requiert et passenew UsageTelemetry(...)dansConfig->onSettled. Un propriétaire de SDK appelleregister()à la place, ce qui ajoute l'instance àConfig::$registeredOnSettled, donc chaque client compte ses appels ;UsageTelemetry::flushRegistered()vide chaque instance enregistrée. PHP n'a pas de minuteurs : un appel qui trouve sa fenêtre écoulée l'envoie d'abord, et une fonction d'arrêt envoie la fenêtre ouverte. Les constantes vivent sur la classe, car le préchargement OPcache supprime les constantes au niveau fichier. Le nom de classe est réservé pour chaque SDK, car le fichier partage le namespace avec les modèles. - Créé UNIQUEMENT avec la boîte à outils neutre — la garde dogfooding échoue sinon.
Migration depuis un SDK basé sur les services
-
Les services par ressource (
$client->customers()->get($id)) correspondent à des méthodes plates nommées d'après les operationIds ($client->getCustomer($id)) ; les paramètres de requête optionnels conservent leur style d'argument nommé (filter:,sort:,limit:). -
Les wrappers de collection exposant les EN-TÊTES de réponse de pagination (
getTotalItems(),getLimit()) correspondent à l'enveloppe<op>WithHeaders()(->headers['paginationTotal']) ; l'itération simple correspond aux générateurs<op>Items()/<op>Pages(). -
Les classes d'exception de validation dédiées exposant les erreurs de champ correspondent à
catch (ApiError $e)+$e->status === 422+ le corps$e->bodydécodé. -
Les flux de jeton session/bearer correspondent à
auth: ['bearer' => $tokenProvider]avec un callable — résolu par requête, donc un rafraîchissement ne nécessite pas de reconstruction du client. -
Il se documente lui-même. Avec
client.docs(ou--docs), le hookdocsécrit<stem>.php.md: les schémas de sécurité, puis une section par opération avec ses paramètres, son corps, son type de réponse et ses notes de comportement. Les extraits d'appel proviennent du propre hooksamplede ce générateur, donc la page ne peut montrer que la syntaxe du SDK à côté, et la mise en page provient derenderReferencePagedans la boîte à outils d'édition — accessible depuis une copie éjectée via@redocly/client-generator. La pagination sur la page est décidée parpaginationRuleFor, la même aide avec laquelle ce générateur résout la pagination.
La boucle de modification
- Éditez ce rôle : énoncez le nouveau comportement ou la nouvelle décision.
- Faites correspondre
generators/php/. - Lancez
redocly generate-clientet inspectez legit diffde la sortie générée — les fichiers générés ne sont jamais édités à la main.
Les versions intégrées plus récentes fusionnent avec redocly eject-generator php --update.