Redocly/générateur-php

Conception du générateur de client PHP éjecté de Redocly. Lisez-le et mettez-le à jour avant de modifier `generators/php/`.

Qu'est-ce que générateur-php ?

Ce skill décrit la conception et les décisions architecturales du générateur de client PHP éjecté dans `generators/php/`. Il est destiné aux développeurs qui maintiennent ou personnalisent ce générateur, en garantissant que toute modification du code soit d'abord reflétée dans ce document. Le générateur produit un fichier PHP unique et autonome, sans dépendances Composer, avec des classes de modèles typées, un client avec des méthodes par opération, et un runtime embarqué. Il supporte PHP ≥ 8.1, exploite l'extension cURL pour HTTP, et génère des namespaces à partir du titre de l'API. Le skill détaille les règles de conception pour les modèles, les unions, les enumérations, la gestion des dates, des erreurs, des flux binaires, des en-têtes, de la télémétrie, et plus encore. Il sert également de guide pour migrer depuis un SDK basé sur des services vers ce style plat basé sur les operationIds.

Compatible avec~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 Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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 class avec 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): self et toArray(): array (noms de fil en ligne ; les nulls sont ignorés lors de la sérialisation) — pas de réflexion. Les schémas omit s'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 dispatcheur unmarshalX de l'union, afin que les consommateurs puissent affiner avec instanceof ; son type inclut array, 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 est array, un membre d'énumération est son scalaire sous-jacent, et un membre de date est string.
  • 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é writeOnly est 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, donc fromArray hydrate une réponse sans elle et toArray l'envoie toujours dans une requête.
  • La classe Client n'est PAS final — les suites de tests PHP mockent des classes concrètes (createMock(Client::class)), et final forcerait une interface wrapper sur chaque consommateur. Les classes de modèles restent final.
  • 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 avec uniqueIdentifiers(..., { taken: … }) : OpenAPI permet à une opération d'utiliser un nom à deux endroits (id dans 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 via self::OPERATIONS[...], jamais une const au 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 que Client existe — 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 sur match. 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 (un oneOf simple) 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 à mixed uniquement lorsqu'un membre n'a pas de propre type PHP (un objet inline, une intersection, unknown), car mixed ne peut pas apparaître à l'intérieur d'une union. La nullabilité est exprimée par |null dans une union (PHP interdit de mélanger ? avec |) et ?T pour un type unique.
  • Erreurs : les exceptions SONT le mode d'erreur (ApiError/TimeoutError étendent RuntimeException) ; errorMode ne change pas la sortie (le générateur déclare errorModes: ['throw'], donc result échoue rapidement).
  • Dates : dateType: Date type format: date/date-time comme DateTimeImmutable ; l'hydratation est new \DateTimeImmutable(...) et la sérialisation formate avec DateTimeInterface::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 $idempotencyKey sur 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 en void.
  • PHPDoc porte ce que la signature ne peut pas. Le array de PHP et Generator effacent 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 une Envelope (data, headers — convertis en int/bool/string avec des clés en camelCase, les valeurs absentes/analysables omises — et status). 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 Servers est é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 reste servers[0] avec les valeurs par défaut des variables substituées.
  • Les corps de flux passent au travers. Pour une opération multipart/form-data non typée (isUntypedMultipartBody de la boîte à outils : un schéma qui n'est ni un objet ni une $ref) ou une opération binaire (isBinaryContentType), le corps est mixed et le runtime s'en occupe (multipartBody / binaryBody) : une resource de flux est téléchargée par curl en morceaux (CURLOPT_UPLOAD + CURLOPT_INFILE, la méthode conservée par CURLOPT_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_INFILESIZE est défini uniquement lorsque le flux est seekable ET que fstat retourne un tableau (les octets restants à partir de la position actuelle) ; php://input, les pipes et les wrappers sans stream_stat vont en morceaux. Un corps string de l'une ou l'autre opération est envoyé brut, non json_encode ; toute autre valeur d'opération binaire est toujours json_encode ; un corps array d'une opération multipart non typée conserve le chemin toMultipart. L'en-tête Content-Type de l'appelant prime sur celui de la description ou de la limite multipart, comparé insensiblement à la casse dans send afin 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: null le 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 (iterSse sur une pompe curl_multi), multipart.
  • Le runtime est écrit à la main dans runtime/runtime.php dans ce dossier (propre php -l) et embarqué au moment de la préparation. curl_close n'est jamais appelé (déprécié depuis PHP 8.5, sans effet depuis 8.0). Sous --runtime module, il est écrit comme un runtime.php que le client require_once, avec son namespace réécrit vers celui du client afin qu'un seul namespace couvre les deux fichiers.
  • onSettled voit chaque appel envoyé une fois. send() appelle chaque callable Config->onSettled, puis chaque callable Config::$registeredOnSettled, après la boucle de réessai avec operationId, status (0 sans réponse), timedOut, attempts, et durationMs, 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 dans error_log. Les flux SSE s'ouvrent en dehors de send(), 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 (depuis telemetry/telemetry.php dans 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 passe new UsageTelemetry(...) dans Config->onSettled. Un propriétaire de SDK appelle register() à 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->body dé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 hook docs é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 hook sample de ce générateur, donc la page ne peut montrer que la syntaxe du SDK à côté, et la mise en page provient de renderReferencePage dans 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 par paginationRuleFor, la même aide avec laquelle ce générateur résout la pagination.

La boucle de modification

  1. Éditez ce rôle : énoncez le nouveau comportement ou la nouvelle décision.
  2. Faites correspondre generators/php/.
  3. Lancez redocly generate-client et inspectez le git diff de 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.

Individual skills in this repo

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

Skills associés