programmatordev / php-api-sdk
一个用于在PHP中创建SDK的库,支持PSR-18、PSR-17、PSR-6和PSR-3
Requires
- php: >=8.1
- php-http/cache-plugin: ^2.0
- php-http/client-common: ^2.7
- php-http/discovery: ^1.19
- php-http/logger-plugin: ^1.3
- php-http/message: ^1.16
- psr/cache: ^2.0 || ^3.0
- psr/http-client: ^1.0
- psr/http-client-implementation: *
- psr/http-factory: ^1.0
- psr/http-factory-implementation: *
- psr/log: ^2.0 || ^3.0
- symfony/event-dispatcher: ^6.4
- symfony/options-resolver: ^6.4
Requires (Dev)
- monolog/monolog: ^3.5
- nyholm/psr7: ^1.8
- php-http/mock-client: ^1.6
- phpunit/phpunit: ^10.0
- symfony/cache: ^6.4
- symfony/http-client: ^6.4
- symfony/var-dumper: ^6.4
Provides
README
一个用于在PHP中创建SDK的库,支持以下功能
- PSR-18 HTTP客户端;
- PSR-17 HTTP工厂;
- PSR-6 缓存;
- PSR-3 日志;
- 认证;
- 事件监听器;
- ...等等。
所有方法都是公开的,以实现完全的可修改性 🔥。
要求
- PHP 8.1或更高版本。
安装
通过Composer安装库
composer require programmatordev/php-api-sdk
基本用法
只需扩展您的API库的Api
类,并享受编码的乐趣
use ProgrammatorDev\Api\Api; class YourApi extends Api { public function __construct() { parent::__construct(); // minimum required config $this->setBaseUrl('https://api.example.com/v1'); } public function getPosts(int $page = 1): string { // GET https://api.example.com/v1/posts?page=1 return $this->request( method: 'GET', path: '/posts', query: [ 'page' => $page ] ); } }
文档
基本URL
基本URL的获取器和设置器。基本URL是API URL的公共部分,将在所有请求中使用。
$this->setBaseUrl(string $baseUrl): self
$this->getBaseUrl(): string
请求
request
此方法用于向API发送请求。
use Psr\Http\Message\StreamInterface; $this->request( string $method, string $path, array $query [], array $headers = [], StreamInterface|string $body = null ): mixed
注意
如果没有设置基本URL(即它为空),将抛出ConfigException
异常。有关更多信息,请参阅setBaseUrl
方法。
注意
如果在处理请求时发生错误,将抛出ClientException
异常。
例如,如果您想获取带分页的用户列表
use ProgrammatorDev\Api\Api; class YourApi extends Api { public function __construct() { parent::__construct(); // minimum required config $this->setBaseUrl('https://api.example.com/v1'); } public function getUsers(int $page = 1, int $perPage = 20): string { // GET https://api.example.com/v1/users?page=1&limit=20 return $this->request( method: 'GET', path: '/users', query: [ 'page' => $page, 'limit' => $perPage ] ); } }
默认情况下,此方法将返回一个string
,因为它是请求的原始响应。如果您想更改所有请求中响应的处理方式(例如,将JSON字符串解码为数组),请检查addResponseContentsListener
方法,该方法位于事件监听器部分。
buildPath
此方法的目的在于提供一个简单的方法来根据输入或参数构建正确格式的路径。
$this->buildPath(string $path, array $parameters): string;
例如,如果您想构建一个包含动态ID的路径
use ProgrammatorDev\Api\Api; class YourApi extends Api { public function __construct() { parent::__construct(); $this->setBaseUrl('https://api.example.com/v1'); } public function getPostComments(int $postId): string { // GET https://api.example.com/v1/posts/1/comments return $this->request( method: 'GET', path: $this->buildPath('/posts/{postId}/comments', [ 'postId' => $postId ]) ); } }
查询默认值
这些方法用于处理默认查询参数。默认查询参数应用于每个API请求。
$this->addQueryDefault(string $name, mixed $value): self
$this->getQueryDefault(string $name): mixed
$this->removeQueryDefault(string $name): self
例如,如果您想向所有请求中添加语言查询参数
use ProgrammatorDev\Api\Api; class YourApi extends Api { public function __construct(string $language = 'en') { // ... $this->addQueryDefault('lang', $language); } public function getPosts(): string { // GET https://api.example.com/v1/posts?lang=en return $this->request( method: 'GET', path: '/posts' ); } public function getCategories(): string { // a query parameter with the same name, passed in the request method, will overwrite a query default // GET https://api.example.com/v1/categories?lang=pt return $this->request( method: 'GET', path: '/categories', query: [ 'lang' => 'pt' ] ); } }
注意
在request
方法中传入相同名称的query
参数将覆盖查询默认值。请参阅上面的示例中的getCategories
方法。
头部默认值
这些方法用于处理默认头部。默认头部应用于每个API请求。
$this->addHeaderDefault(string $name, mixed $value): self
$this->getHeaderDefault(string $name): mixed
$this->removeHeaderDefault(string $name): self
例如,如果您想向所有请求中添加语言头部值
use ProgrammatorDev\Api\Api; class YourApi extends Api { public function __construct(string $language = 'en') { // ... $this->addHeaderDefault('X-LANGUAGE', $language); } public function getPosts(): string { // GET https://api.example.com/v1/posts with an 'X-LANGUAGE' => 'en' header value return $this->request( method: 'GET', path: '/posts' ); } public function getCategories(): string { // a header with the same name, passed in the request method, will overwrite a header default // GET https://api.example.com/v1/categories with an 'X-LANGUAGE' => 'pt' header value return $this->request( method: 'GET', path: '/categories', headers: [ 'X-LANGUAGE' => 'pt' ] ); } }
注意
在request
方法中传入相同名称的头部将覆盖头部默认值。请参阅上面的示例中的getCategories
方法。
认证
API认证的获取器和设置器。使用来自PHP HTTP的认证组件。
use Http\Message\Authentication; $this->setAuthentication(?Authentication $authentication): self;
use Http\Message\Authentication; $this->getAuthentication(): ?Authentication;
可用的认证方法
BasicAuth
用户名和密码Bearer
TokenWsse
用户名和密码QueryParam
查询参数值的数组Header
头名称和值Chain
认证实例的数组RequestConditional
请求匹配器和认证实例
您还可以 实现自己的 认证方法。
例如,如果您有一个通过查询参数进行认证的API
use ProgrammatorDev\Api\Api; use Http\Message\Authentication\QueryParam; class YourApi extends Api { public function __construct(string $applicationKey) { // ... $this->setAuthentication( new QueryParam([ 'api_token' => $applicationKey ]) ); } public function getPosts(): string { // GET https://api.example.com/v1/posts?api_token=cd982h3diwh98dd23d32j return $this->request( method: 'GET', path: '/posts' ); } }
事件监听器
addPreRequestListener
使用 addPreRequestListener
方法添加在请求发出之前被调用的函数。此事件监听器将应用于每个API请求。
$this->addPreRequestListener(callable $listener, int $priority = 0): self;
例如
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Event\PreRequestEvent; class YourApi extends Api { public function __construct() { // a PreRequestEvent is passed as an argument $this->addPreRequestListener(function(PreRequestEvent $event) { $request = $event->getRequest(); if ($request->getMethod() === 'POST') { // do something for all POST requests // ... } }); } // ... }
可用的事件方法
$this->addPreRequestListener(function(PreRequestEvent $event) { // get request data $request = $event->getRequest(); // ... // set request data $event->setRequest($request); });
addPostRequestListener
使用 addPostRequestListener
方法添加在请求发出之后被调用的函数。此函数可以用来检查发送到API并从API接收到的请求数据和响应数据。此事件监听器将应用于每个API请求。
$this->addPostRequestListener(callable $listener, int $priority = 0): self;
例如,您可以使用此事件监听器来处理API错误
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Event\PostRequestEvent; class YourApi extends Api { public function __construct() { // a PostRequestEvent is passed as an argument $this->addPostRequestListener(function(PostRequestEvent $event) { $response = $event->getResponse(); $statusCode = $response->getStatusCode(); // if there was a response with an error status code if ($statusCode >= 400) { // throw an exception match ($statusCode) { 400 => throw new BadRequestException(), 404 => throw new NotFoundException(), default => throw new UnexpectedErrorException() }; } }); } // ... }
可用的事件方法
$this->addPostRequestListener(function(PostRequestEvent $event) { // get request data $request = $event->getRequest(); // get response data $response = $event->getResponse(); // ... // set response data $event->setResponse($response); });
addResponseContentsListener
使用 addResponseContentsListener
方法操作从API接收到的响应。此事件监听器将应用于每个API请求。
$this->addResponseContentsListener(callable $listener, int $priority = 0): self;
例如,如果API响应是JSON字符串,您可以使用此事件监听器将它们解码为数组
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Event\ResponseContentsEvent; class YourApi extends Api { public function __construct() { // a ResponseContentsEvent is passed as an argument $this->addResponseContentsListener(function(ResponseContentsEvent $event) { // get response contents and decode json string into an array $contents = $event->getContents(); $contents = json_decode($contents, true); // set handled contents $event->setContents($contents); }); } public function getPosts(): array { // will return an array return $this->request( method: 'GET', path: '/posts' ); } }
可用的事件方法
$this->addResponseContentsListener(function(ResponseContentsEvent $event) { // get response body contents data $contents = $event->getContents(); // ... // set contents $event->setContents($contents); });
事件优先级
可以为同一事件添加多个监听器,并设置它们的执行顺序。默认情况下,它们将按添加顺序执行,但您可以通过设置 优先级
来控制此顺序。事件监听器将按照从高到低的优先级执行
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Event\ResponseContentsEvent; class YourApi extends Api { public function __construct() { // two event listeners are added, // but the second is executed first (higher priority) even though it was added after // executed last (lower priority) $this->addResponseContentsListener( listener: function(ResponseContentsEvent $event) { ... }, priority: 0 ); // executed first (higher priority) $this->addResponseContentsListener( listener: function(ResponseContentsEvent $event) { ... }, priority: 10 ); } }
事件传播
在某些情况下,您可能希望停止事件流并阻止调用监听器。为此,您可以使用 stopPropagation()
方法
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Event\ResponseContentsEvent; class YourApi extends Api { public function __construct() { $this->addResponseContentsListener(function(ResponseContentsEvent $event) { // stop propagation so future listeners of this event will not be called $event->stopPropagation(); }); // this listener will not be called $this->addResponseContentsListener(function(ResponseContentsEvent $event) { // ... }); } }
HTTP客户端(PSR-18)和HTTP工厂(PSR-17)
HTTP客户端和HTTP工厂适配器
默认情况下,此库使用 HTTPlug的发现 库。这意味着它会自动为您找到并安装已知的PSR-18客户端和PSR-17工厂实现(如果它们在您的项目中找不到)
use ProgrammatorDev\Api\Builder\ClientBuilder; new ClientBuilder( // a PSR-18 client ?ClientInterface $client = null, // a PSR-17 request factory ?RequestFactoryInterface $requestFactory = null, // a PSR-17 stream factory ?StreamFactoryInterface $streamFactory = null );
use ProgrammatorDev\Api\Builder\ClientBuilder; $this->setClientBuilder(ClientBuilder $clientBuilder): self;
use ProgrammatorDev\Api\Builder\ClientBuilder; $this->getClientBuilder(): ClientBuilder;
如果您不想依赖实现发现,可以设置您想要的实现
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Builder\ClientBuilder; use Symfony\Component\HttpClient\Psr18Client; use Nyholm\Psr7\Factory\Psr17Factory; class YourApi extends Api { public function __construct() { // ... $client = new Psr18Client(); $requestFactory = $streamFactory = new Psr17Factory(); $this->setClientBuilder( new ClientBuilder( client: $client, requestFactory: $requestFactory, streamFactory: $streamFactory ) ); } }
插件系统
此库允许将插件附加到HTTP客户端。插件通过拦截请求和响应流程来修改客户端的行为。
由于插件顺序很重要,因此插件会以优先级级别添加,并按从高到低的顺序执行。
use Http\Client\Common\Plugin; $this->getClientBuilder()->addPlugin(Plugin $plugin, int $priority): self;
注意
如果存在具有相同 优先级
级别的插件,将抛出 PluginException
。
重要的是要知道,此库已经使用具有不同优先级的各种插件。以下列表按优先级从高到低列出所有已实现的插件(请记住,顺序很重要)
例如,如果您希望客户端自动尝试重新发送失败(例如由于不可靠的连接和服务器)的请求,您可以添加RetryPlugin
use ProgrammatorDev\Api\Api; use Http\Client\Common\Plugin\RetryPlugin; class YourApi extends Api { public function __construct() { // ... // if a request fails, it will retry at least 3 times // priority is 20 to execute before the cache plugin // (check the above plugin order list for more information) $this->getClientBuilder()->addPlugin( plugin: new RetryPlugin(['retries' => 3]), priority: 20 ); } }
缓存(PSR-6)
这个库允许配置客户端的缓存层以进行API请求。它使用标准的PSR-6实现,并提供方法来微调HTTP缓存的行为
use ProgrammatorDev\Api\Builder\CacheBuilder; use Psr\Cache\CacheItemPoolInterface; new CacheBuilder( // a PSR-6 cache adapter CacheItemPoolInterface $pool, // default lifetime (in seconds) of cache items ?int $ttl = 60, // An array of HTTP methods for which caching should be applied $methods = ['GET', 'HEAD'], // An array of cache directives to be compared with the headers of the HTTP response, // in order to determine cacheability $responseCacheDirectives = ['max-age'] );
use ProgrammatorDev\Api\Builder\CacheBuilder; $this->setCacheBuilder(CacheBuilder $cacheBuilder): self;
use ProgrammatorDev\Api\Builder\CacheBuilder; $this->getCacheBuilder(): CacheBuilder;
例如,如果您想设置基于文件的缓存适配器
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Builder\CacheBuilder; use Symfony\Component\Cache\Adapter\FilesystemAdapter; class YourApi extends Api { public function __construct() { // ... $pool = new FilesystemAdapter(); // file-based cache adapter with a 1-hour default cache lifetime $this->setCacheBuilder( new CacheBuilder( pool: $pool, ttl: 3600 ) ); } public function getPosts(): string { // you can change the lifetime (and all other parameters) // for this specific endpoint $this->getCacheBuilder()->setTtl(600); return $this->request( method: 'GET', path: '/posts' ); } }
日志记录器(PSR-3)
这个库允许配置一个记录器以保存用于API请求的数据。它使用标准的PSR-3实现,并提供方法来微调记录的行为
use ProgrammatorDev\Api\Builder\LoggerBuilder; use Psr\Log\LoggerInterface; use Http\Message\Formatter; use Http\Message\Formatter\SimpleFormatter; new LoggerBuilder( // a PSR-3 logger adapter LoggerInterface $logger, // determines how the log entries will be formatted when they are written by the logger // if no formatter is provided, it will default to a SimpleFormatter instance ?Formatter $formatter = null );
use ProgrammatorDev\Api\Builder\LoggerBuilder; $this->setLoggerBuilder(LoggerBuilder $loggerBuilder): self;
use ProgrammatorDev\Api\Builder\LoggerBuilder; $this->getLoggerBuilder(): LoggerBuilder;
例如,如果您想将日志保存到文件中
use ProgrammatorDev\Api\Api; use ProgrammatorDev\Api\Builder\LoggerBuilder; use Monolog\Logger; use Monolog\Handler\StreamHandler; class YourApi extends Api { public function __construct() { // ... $logger = new Logger('api'); $logger->pushHandler(new StreamHandler('/logs/api.log')); $this->setLoggerBuilder( new LoggerBuilder( logger: $logger ) ); } }
配置选项
API通常提供不同的选项(如语言、时区等)。为了简化配置选项的过程,提供了OptionsResolver
。它允许您创建一组默认选项及其约束,如必填选项、默认值、允许的类型等。然后,它将这些给定的选项与默认选项进行解析,以确保它符合所有约束。
例如,如果API有语言和时区选项
use ProgrammatorDev\Api\Api; class YourApi extends Api { private array $options = []; public function __construct(array $options = []) { parent::__construct(); $this->options = $this->configureOptions($options); $this->configureApi(); } private function configureOptions(array $options): array { // set defaults values, if none were provided $this->optionsResolver->setDefault('timezone', 'UTC'); $this->optionsResolver->setDefault('language', 'en'); // set allowed types $this->optionsResolver->setAllowedTypes('timezone', 'string'); $this->optionsResolver->setAllowedTypes('language', 'string'); // set allowed values $this->optionsResolver->setAllowedValues('timezone', \DateTimeZone::listIdentifiers()); $this->optionsResolver->setAllowedValues('language', ['en', 'pt']); // return resolved options return $this->optionsResolver->resolve($options); } private function configureApi(): void { // set required base url $this->setBaseUrl('https://api.example.com/v1'); // set options as query defaults (will be included in all requests) $this->addQueryDefault('language', $this->options['language']); $this->addQueryDefault('timezone', $this->options['timezone']); } public function getPosts(int $page = 1): string { // GET https://api.example.com/v1/posts?language=en&timezone=UTC&page=1 return $this->request( method: 'GET', path: '/posts', query: [ 'page' => $page ] ); } }
使用API时,它应该看起来像这样
$api = new YourApi([ 'language' => 'pt' ]); // GET https://api.example.com/v1/posts?language=pt&timezone=UTC&page=1 $posts = $api->getPosts();
有关所有可用方法,请查看官方页面文档。
使用PHP API SDK的库
贡献
任何形式的对改进此库(包括请求)的贡献都将受到欢迎并受到赞赏。请确保打开一个拉取请求或问题。
许可证
本项目受MIT许可证的许可。有关版权和许可的更多信息,请参阅随源代码一起分发的LICENSE文件。