遵循PSR标准的第三方API SDK开发两类技术问题咨询
第三方API SDK开发的PSR规范相关问题解答
问题1:接口类存放目录规范
PSR标准并未强制要求接口必须放在独立的Contracts目录,两种存放方式均合规,需结合项目场景选择:
- 同目录存放:接口与对应实现类放在同一目录,便于开发时快速关联查看,适合接口与实现一一绑定、逻辑高度耦合的场景。当前你的目录结构中每个接口都有唯一对应实现,只要命名空间与目录结构匹配(符合PSR-4自动加载规范),这种方式完全合规。
- 独立Contracts目录:将所有抽象接口统一归类到
src/Contracts目录,更突出接口的契约属性,适合接口可能被多个实现复用、或需要对外暴露SDK契约的场景,能更清晰地区分抽象层与实现层。
结论:若当前项目接口仅对应单一实现,同目录存放无任何问题;若未来有扩展多实现的计划,迁移到Contracts目录会更利于长期维护。
问题2:API响应修改的合规实现方式
在PSR标准下修改API响应是合规的,需遵循单一职责原则,两种实现路径各有适用场景:
方式1:在ResponseMediator类中实现
当前ResponseMediator的职责是解析响应内容,在此类中添加全局通用的响应修改逻辑(如字段格式统一、类型转换、冗余字段清理)完全合理,符合PSR-1(单一职责)与PSR-12(编码规范)。
示例修改(以统一字段命名为例):
final class ResponseMediator { /** * @throws ApiException|InvalidJsonException */ public static function getContent(ResponseInterface $response): array { $body = $response->getBody()->__toString(); if (!str_starts_with($response->getHeaderLine('Content-Type'), 'application/json')) { throw new ApiException(sprintf('The response is not JSON: %s', $body)); } try { /** @var array<string|int, string|int> $content */ $content = json_decode($body, true, 512, JSON_THROW_ON_ERROR); // 新增:将snake_case字段转换为camelCase return array_map(function ($value) { if (is_array($value)) { return array_change_key_case($value, CASE_CAMEL); } return $value; }, array_change_key_case($content, CASE_CAMEL)); } catch (JsonException $e) { throw new InvalidJsonException( sprintf('Invalid JSON: %s', $e->getMessage()) ); } } }
这种方式适合无需动态调整的全局统一处理,实现简单直接。
方式2:通过HttpClient Plugin实现
若修改逻辑是场景化、可插拔的(如仅针对特定接口修改响应、需根据配置开关启用/禁用),则使用HttpPlug的Plugin机制更合适。HttpPlug插件本身就是为在HTTP请求/响应链路中插入可复用逻辑设计的,完全符合PSR-7与PSR-18规范。
示例响应修改插件:
use Http\Client\Common\Plugin; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use GuzzleHttp\Psr7\Stream; class ResponseTransformPlugin implements Plugin { public function handleRequest(RequestInterface $request, callable $next, callable $first): ResponseInterface { $response = $next($request); // 仅针对成就接口修改响应字段 if (str_contains($request->getUri()->getPath(), '/achievements')) { $bodyContent = $response->getBody()->__toString(); $content = json_decode($bodyContent, true); // 将count字段重命名为total if (isset($content['count'])) { $content['total'] = $content['count']; unset($content['count']); } $newBody = new Stream(fopen('data://text/plain,' . json_encode($content), 'r')); return $response->withBody($newBody); } return $response; } }
这种方式更灵活,符合开闭原则,适合需要动态调整的响应处理需求。
结论:全局通用的响应修改放在ResponseMediator;场景化、可插拔的修改用HttpClient Plugin实现,两种方式均符合PSR标准。
附:项目相关信息
Composer依赖配置
{ "require": { "php": ">=8.2", "php-http/client-common": "^2.5", "php-http/discovery": "^1.14.3", "php-http/httplug": "^2.3", "php-http/multipart-stream-builder": "^1.2", "psr/http-client-implementation": "^1.0", "psr/http-factory-implementation": "^1.0", "psr/http-message": "^1.0.1" }, "require-dev": { "phpunit/phpunit": "^8.5", "guzzlehttp/guzzle": "^7.5", "guzzlehttp/psr7": "^2.4.1", "http-interop/http-factory-guzzle": "^1.2" } }
项目目录结构
./src/ ├── Api │ ├── AbstractApi.php │ ├── Achievement.php │ ├── AchievementInterface.php │ ├── Clan.php │ ├── ClanInterface.php │ ├── Game.php │ ├── GameInterface.php │ ├── Rating.php │ ├── RatingInterface.php │ ├── User.php │ ├── UserInterface.php │ ├── Weapon.php │ └── WeaponInterface.php ├── Client.php ├── ClientExtraInterface.php ├── Enum │ ├── GameClassEnum.php │ ├── GameClassList.php │ ├── Http │ │ └── RegionList.php │ └── Leagues.php ├── Exception │ ├── Response │ │ ├── ApiException.php │ │ └── InvalidJsonException.php │ ├── UndefinedBranchException.php │ └── UnknownMethodCallException.php ├── Helper │ └── QueryParamsHelper.php └── HttpClient ├── Builder.php ├── Message │ └── ResponseMediator.php └── Plugin ├── BypassTimeoutResponsePlugin.php └── ServerSupportPlugin.php
ResponseMediator类代码
final class ResponseMediator { /** * @throws ApiException|InvalidJsonException */ public static function getContent(ResponseInterface $response): array { $body = $response->getBody()->__toString(); if (!str_starts_with($response->getHeaderLine('Content-Type'), 'application/json')) { throw new ApiException(sprintf('The response is not JSON: %s', $body)); } try { /** @var array<string|int, string|int> $content */ return json_decode($body, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new InvalidJsonException( sprintf('Invalid JSON: %s', $e->getMessage()) ); } } }
内容的提问来源于stack exchange,提问作者hyperplural
相关产品推荐
相关产品推荐

