filter-v3.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php
  2. /**
  3. * 从打包后的全量 OpenAPI spec 里切出仅 /v3/* 的部分,供 openapi-generator
  4. * 生成精简的 PHP 客户端(目前只测 /api/v3 路由)。
  5. *
  6. * 用法:
  7. * php tools/filter-v3.php spec/openapi.json spec/openapi-v3.json
  8. *
  9. * 逻辑:保留 /v3/* 路径,并递归收集它们引用的 components(schemas / responses /
  10. * parameters / requestBodies / examples / headers / securitySchemes / links /
  11. * callbacks),其余组件与路径一律丢弃。
  12. */
  13. if (PHP_SAPI !== 'cli') {
  14. fwrite(STDERR, "只允许 CLI 运行\n");
  15. exit(2);
  16. }
  17. [$script, $in, $out] = array_pad($argv, 3, null);
  18. if (!$in || !$out || !is_file($in)) {
  19. fwrite(STDERR, "用法: php {$script} <in.json> <out.json>\n");
  20. exit(2);
  21. }
  22. $spec = json_decode(file_get_contents($in), true);
  23. if (!is_array($spec) || !isset($spec['paths'])) {
  24. fwrite(STDERR, "无法解析 {$in}\n");
  25. exit(2);
  26. }
  27. /** 递归收集节点里所有指向 #/components/ 的 $ref */
  28. function collectComponentRefs(mixed $node, array &$refs): void
  29. {
  30. if (!is_array($node)) {
  31. return;
  32. }
  33. if (isset($node['$ref']) && is_string($node['$ref']) && str_starts_with($node['$ref'], '#/components/')) {
  34. $refs[$node['$ref']] = true;
  35. // $ref 节点本身不再展开(其内容在 components 里,会被下一轮收集)
  36. return;
  37. }
  38. foreach ($node as $value) {
  39. collectComponentRefs($value, $refs);
  40. }
  41. }
  42. /** 按 "#/components/{category}/{name}" 取节点 */
  43. function resolveComponent(array $spec, string $ref): mixed
  44. {
  45. $parts = explode('/', ltrim($ref, '#/'));
  46. // ['components', category, name]
  47. return $spec[$parts[0]][$parts[1]][$parts[2]] ?? null;
  48. }
  49. // 1) 选出 v3 路径
  50. $v3Paths = [];
  51. foreach ($spec['paths'] as $path => $item) {
  52. if (str_starts_with((string) $path, '/v3/')) {
  53. $v3Paths[$path] = $item;
  54. }
  55. }
  56. if ($v3Paths === []) {
  57. fwrite(STDERR, "spec 里没有 /v3/* 路径\n");
  58. exit(2);
  59. }
  60. // 2) 鉴权:每个 v3 operation 的 security 由生成器按路由中间件写好(auth.v3 → 必须,
  61. // 否则可选),这里只清掉全局 security,免得没写的 operation 继承成「必须」。
  62. unset($spec['security']);
  63. // 3) 递归收集引用的组件(含 security scheme)
  64. $refs = ['#/components/securitySchemes/bearerAuth' => true];
  65. collectComponentRefs($v3Paths, $refs);
  66. do {
  67. $before = count($refs);
  68. foreach (array_keys($refs) as $ref) {
  69. $node = resolveComponent($spec, $ref);
  70. if (is_array($node)) {
  71. collectComponentRefs($node, $refs);
  72. }
  73. }
  74. } while (count($refs) !== $before);
  75. // 4) 只保留被引用的组件(保留原始分类顺序)
  76. $categories = ['schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers', 'securitySchemes', 'links', 'callbacks'];
  77. $components = $spec['components'] ?? [];
  78. $keptComponents = [];
  79. foreach ($categories as $cat) {
  80. if (!isset($components[$cat])) {
  81. continue;
  82. }
  83. foreach ($components[$cat] as $name => $node) {
  84. $ref = "#/components/{$cat}/{$name}";
  85. if (isset($refs[$ref])) {
  86. $keptComponents[$cat][$name] = $node;
  87. }
  88. }
  89. }
  90. $result = [];
  91. foreach (['openapi', 'info', 'servers', 'tags', 'externalDocs'] as $key) {
  92. if (isset($spec[$key])) {
  93. $result[$key] = $spec[$key];
  94. }
  95. }
  96. $result['components'] = $keptComponents;
  97. $result['paths'] = $v3Paths;
  98. // 5) 修掉源 spec 的数组缺 items 问题:generate.php 给 query/body 参数只写了
  99. // `type: array` 没写 `items`(严格校验会拒绝)。v3 里的这类参数都是字符串数组
  100. // (page_refs / related_id),补成 string items。
  101. normalizeArraySchemas($result);
  102. // 6) 修掉「spec 与实现不一致」的 3 个端点(搜索/建议/升级):实现仍返回
  103. // {success, data:对象} 或 {data:对象},而 spec 写成了 {data:数组, meta}。
  104. // 按现实修正 200 响应 schema,否则生成的客户端反序列化会抛
  105. // "Invalid array 'object[]'"。根因在控制器还没迁到 v3 Resource 信封,
  106. // 正确的长期修法在 api-v13 侧,这里只是让测试客户端能用。
  107. applyDriftPatches($result);
  108. file_put_contents($out, json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
  109. fwrite(STDOUT, 'v3 paths: ' . count($v3Paths) . ', components refs: ' . count($refs) . " -> {$out}\n");
  110. /** 递归给缺 items 的 `type: array` schema 补 string items */
  111. function normalizeArraySchemas(mixed &$node): void
  112. {
  113. if (! is_array($node)) {
  114. return;
  115. }
  116. if (($node['type'] ?? null) === 'array' && ! isset($node['items'])) {
  117. $node['items'] = ['type' => 'string'];
  118. }
  119. foreach ($node as &$value) {
  120. normalizeArraySchemas($value);
  121. }
  122. unset($value);
  123. }
  124. /** 修正 search / search-suggest / upgrade 的 200 响应 schema,使其与实现一致 */
  125. function applyDriftPatches(array &$spec): void
  126. {
  127. $object = ['type' => 'object'];
  128. $dataObject = ['type' => 'object', 'properties' => ['data' => $object]];
  129. $successData = ['type' => 'object', 'properties' => ['success' => ['type' => 'boolean'], 'data' => $object]];
  130. foreach (['get', 'post'] as $method) {
  131. if (isset($spec['paths']['/v3/search'][$method]['responses']['200']['content']['application/json']['schema'])) {
  132. $spec['paths']['/v3/search'][$method]['responses']['200']['content']['application/json']['schema'] = $successData;
  133. }
  134. }
  135. if (isset($spec['paths']['/v3/search-suggest']['get']['responses']['200']['content']['application/json']['schema'])) {
  136. $spec['paths']['/v3/search-suggest']['get']['responses']['200']['content']['application/json']['schema'] = $successData;
  137. }
  138. if (isset($spec['paths']['/v3/upgrade']['get']['responses']['200']['content']['application/json']['schema'])) {
  139. $spec['paths']['/v3/upgrade']['get']['responses']['200']['content']['application/json']['schema'] = $dataObject;
  140. }
  141. }