simple-as-fuck / php-api-toolkit
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.4
- guzzlehttp/psr7: ^2.1
- illuminate/bus: ^8.81|^9.0|^10.0|^11.0
- illuminate/database: ^8.81|^9.0|^10.0|^11.0
- illuminate/log: ^8.81|^9.0|^10.0|^11.0
- illuminate/queue: ^8.81|^9.0|^10.0|^11.0
- illuminate/support: ^8.81|^9.0|^10.0|^11.0
- kayex/http-codes: ^1.1
- psr/http-factory: ^1.0
- psr/http-message: ^1.0
- psr/http-server-handler: ^1.0
- psr/log: ^2.0|^3.0
- simple-as-fuck/php-validator: ^0.5.2|^0.6.0
- symfony/http-foundation: ^5.4|^6.0|^7.0
- symfony/http-kernel: ^5.4|^6.0|^7.0
- symfony/psr-http-message-bridge: ^2.1|^6.0|^7.0
Requires (Dev)
- ext-pdo: *
- phpstan/phpstan: ^1.2
- phpstan/phpstan-deprecation-rules: ^1.0
- phpstan/phpstan-strict-rules: ^1.1
- phpunit/phpunit: ^10.0
README
提供一系列服务,以尽可能简化的方式实现API,并带有标准化的依赖。
安装
composer require simple-as-fuck/php-api-toolkit
支持
如果composer.json中列出的任何PHP平台要求以安全支持结尾,请将包版本视为不受支持,除非是最新版本。
使用方法
API客户端服务
API客户端需要guzzle客户端,psr客户端接口不够好,因为没有异步请求。第二个主要依赖是一些配置,你可以实现自己的配置加载。可选地,你可以添加弃用记录器以自动记录弃用或日落响应头。
Laravel将从services.php配置文件中自动加载配置。
'some_api_name' => [ // this key is value of first parameter ApiClient::request method 'base_url' => 'https://some-host/some-base-url', 'token' => 'tokenexample', // optional default null, authentication token for https://swagger.org.cn/docs/specification/authentication/bearer-authentication/ 'verify' => true, // optional default true, turn on/off certificates verification 'deprecated_header' => 'Deprecated', // optional default 'Deprecated', define name of deprecated response header logged into deprecation log ],
如果你在Laravel中定义了logging.deprecations配置键,弃用或日落头将被记录到指定的日志通道。
/** * @var \SimpleAsFuck\ApiToolkit\Service\Client\Config $config * @var \Psr\Log\LoggerInterface $logger */ /** @var \SimpleAsFuck\ApiToolkit\Service\Client\DeprecationsLogger|null $deprecationsLogger */ $deprecationsLogger = new \SimpleAsFuck\ApiToolkit\Service\Client\DeprecationsLogger( $config, $logger, new \GuzzleHttp\Psr7\HttpFactory() ); $client = new \SimpleAsFuck\ApiToolkit\Service\Client\ApiClient( $config, new \GuzzleHttp\Client(), new \GuzzleHttp\Psr7\HttpFactory(), $deprecationsLogger ); /** * with transformer, YourClass can be converted into different api structure * * @implements \SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer<YourClass> */ final class YourTransformer implements \SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer { /** * @param YourClass $transformed */ public function toApi($transformed): \stdClass { $apiData = new \stdClass(); $apiData->some_property = $transformed->someProperty; return $apiData; } } /** * @var YourClass $yourModelForRequestBody * @var \SimpleAsFuck\Validator\Rule\Custom\UserClassRule<YourOtherClass> $classRuleForResponseModel */ try { $responseObject = $client->requestObject('some_api_name', 'POST', '/to-some-action', $yourModelForRequestBody, new YourTransformer()); /* * response has getter for json decoded body which is validated after decoding by rule chain * request method return object rule, so you can easily validate response json structure * is recommended use some you class rule documented here: https://github.com/simple-as-fuck/php-validator#user-class-rule * and convert api data structure into some your concrete object instance */ $yourModelFromResponseBody = $responseObject->class($classRuleForResponseModel)->notNull(); } catch (\SimpleAsFuck\ApiToolkit\Model\Client\ApiException $exception) { /* * if anything go wrong in request/response processing or response json parsing * \SimpleAsFuck\ApiToolkit\Model\Client\ApiException is thrown, * and you can handle any error from communication */ $exception->getCode(); // if exception contains http response, http status is here, otherwise zero is returned $exception->getMessage(); // if http response contains json object with message string property, json message overwrite exception message }
API客户端钩子工具
API客户端服务提供两个辅助方法来注册和注销钩子监听URL。辅助方法调用与这些控制器兼容的数据结构AddListener,RemoveListener。
/** * @var \SimpleAsFuck\ApiToolkit\Service\Client\ApiClient $client */ // method call POST /webhook request $webhook = $client->addWebhookListener('some_api_name', 'some_webhook_event_type', 'https://some-client/listening-url'); // you can register listener URL with priority and required webhook attributes, // it means than listener SHOULD be called only if server dispatch webhook type // with attributes containing all specified attributes in webhook registration // (webhook dispatch can contain more attributes than required) // priority specified which webhook listener SHOULD be called first, // if on server is more than one listener for same webhook type, // this behaviours is implemented in server services in this package, // but other server implementations can behave differently // or webhook functionality may not be implemented, so always read specific API documentation! $webhook = $client->addWebhookListener( 'some_api_name', 'some_webhook_event_type', 'https://some-client/listening-url', \SimpleAsFuck\ApiToolkit\Model\Webhook\Priority::NORMAL, ['some_key' => '89'] ); // you can save webhook identifier for future use // deletion while listening is no longer needed, or some data loading in listening url $webhook->id();
/** * @var \SimpleAsFuck\ApiToolkit\Service\Client\ApiClient $client * @var non-empty-string $webhookId */ // method call DELETE /webhook request $client->removeWebhookListener('some_api_name', $webhookId);
为了监听分发的钩子,你需要在服务器可访问的URL上准备一些POST操作。操作将接收在JSON体中分发的钩子实例。你注册的URL不应被服务器修改,这是此包中服务器服务默认行为。
你可以在钩子监听操作之前添加一些身份验证中间件,此包中的服务器服务允许使用自定义HTTP头分发,并支持自动添加https://swagger.org.cn/docs/specification/authentication/bearer-authentication/。
class YourListeningController { public function handle( \Psr\Http\Message\ServerRequestInterface $request //\Symfony\Component\HttpFoundation\Request $request ): \Psr\Http\Message\ResponseInterface { //): \Symfony\Component\HttpFoundation\Response { $webhook = \SimpleAsFuck\ApiToolkit\Factory\Server\Validator::make($request) //$webhook = \SimpleAsFuck\ApiToolkit\Factory\Symfony\Validator::make($request) ->json() ->object() ->class(new \SimpleAsFuck\ApiToolkit\Service\Webhook\WebhookTransformer()) ->notNull() ; // run some you logic // you should expect than listening action can be called multiple times // because of some network error or another failure $result = new \SimpleAsFuck\ApiToolkit\Model\Webhook\Result(); $result = new \SimpleAsFuck\ApiToolkit\Model\Webhook\Result( // you can inform server site application to stop // dispatching webhook for another listener after current listener // which has less priority // server services in this package support this functionality stopDispatching: true ) // you SHOULD return valid json object, // server services in this package expect to receive result object, // otherwise dispatch can be detected as failed because of some syntax error // and server can dispatch webhook agan return \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson( //return \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson( $result, new \SimpleAsFuck\ApiToolkit\Service\Webhook\ResultTransformer(), // you MUST return successful response, otherwise // server site application can send webhook agan // because error response will look like failed dispatch \Kayex\HttpCodes::HTTP_OK ); } }
API服务器控制器工具
为了请求处理,已准备验证器和响应工厂。有关验证规则的更多信息,请参阅Simple as fuck / Php Validator的README。
如果你使用symfony请求和响应,你可以使用来自不同命名空间的工厂,如示例中注释所示。
// star of your action $rules = \SimpleAsFuck\ApiToolkit\Factory\Server\Validator::make($request); //$rules = \SimpleAsFuck\ApiToolkit\Factory\Symfony\Validator::make($request); // validate some query parameter $someQueryValidValue = $rules->query()->key('someKey')->string()->parseInt()->min(1)->notNull(); /** @var \SimpleAsFuck\ApiToolkit\Service\Server\UserQueryRule<YourClass> $yourQueryRule */ $yourObjectFromRequestQuery = $rules->query()->class($yourQueryRule)->notNull(); // validate something from request body with json format $someJsonValidValue = $rules->json()->object()->property('someProperty')->string()->notEmpty()->max(255)->notNull(); /** @var \SimpleAsFuck\Validator\Rule\Custom\UserClassRule<YourClass> $yourClassRule */ $yourObjectFromRequestBody = $rules->json()->object()->class($yourClassRule)->notNull(); // end of your action /** * @var YourClass $yourModelForResponseBody * @var \SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer<YourClass> $transformer */ // response with one object $response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson($yourModelForResponseBody, $transformer, \Kayex\HttpCodes::HTTP_OK); //$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson($yourModelForResponseBody, $transformer, \Kayex\HttpCodes::HTTP_OK); // response with some array or collection (avoiding out of memory problem recommended some lazy loading iterator) $response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJsonStream(new \ArrayIterator([$yourModelForResponseBody]), $transformer); //$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJsonStream(new \ArrayIterator([$yourModelForResponseBody]), $transformer); //$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJsonStream([$yourModelForResponseBody], $transformer);
API服务器中间件工具
如果出现任何问题,你可以在异常捕获中间件或某些异常处理程序中使用异常转换器。
为Laravel准备了一个Laravel配置适配器,它将自动加载ExceptionTransformer的配置,你可以轻松地从DI中获取此转换器,无需任何新配置(使用Laravel的标准配置)。
/** * @var \SimpleAsFuck\ApiToolkit\Service\Config\Repository $configRepository */ try { // some breakable logic } catch(\SimpleAsFuck\ApiToolkit\Model\Server\ApiException $exception) { //catch(\Symfony\Component\HttpKernel\Exception\HttpException $exception) { $response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson( //$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson( $exception, // transformer will convert exception in to json object with message property with original exception message new \SimpleAsFuck\ApiToolkit\Service\Server\ApiExceptionTransformer(), $exception->getStatusCode() ); } catch (\Throwable $exception) { $response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson( //$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson( $exception, // transformer will convert exception in to json object with message property // if application has turned off debug, message property contain only "Internal server error" // but with enabled debug message contains exception type, message, file and line where was exception thrown new \SimpleAsFuck\ApiToolkit\Service\Server\ExceptionTransformer($configRepository), \Kayex\HttpCodes::HTTP_INTERNAL_SERVER_ERROR ); }
API服务器钩子工具
从服务器端向客户端进行webhook派发时,这里准备了WebhookDispatcher。WebhookDispatcher将通过抽象的webhook Repository 查找必要的webhook,然后通过抽象的webhook Client 来调用它们。
您需要实现webhook Repository、webhook Client,并为持久化webhook准备一些存储,还需要为webhook调用重试准备一些队列。
对于Laravel,已经准备了使用Laravel queues 的 LaravelMysqlRepository 和 LaravelClient。
Laravel webhook实现会自动从 webhook.php 配置文件中加载配置,该配置文件可以通过此包发布。
php artisan vendor:publish --tag=api-toolkit-config
Webhook存储在MySql数据库表中,它们在Laravel迁移中定义,可以从此包发布。
php artisan vendor:publish --tag=api-toolkit-migration
/** * @var \SimpleAsFuck\ApiToolkit\Service\Webhook\Repository $webhookRepository * @var \SimpleAsFuck\ApiToolkit\Service\Webhook\Client $webhookClient */ $dispatcher = new \SimpleAsFuck\ApiToolkit\Service\Webhook\WebhookDispatcher($webhookRepository, $webhookClient); // simplest dispatch, when something happened on the server side, // webhooks calls are added into a queue $dispatcher->dispatch('some_webhook_event_type'); // webhook call with some attribute, // for example, you can dispatch an event type with some concrete entity id $dispatcher->dispatch('some_webhook_event_type', ['some_attribute' => '1256']); // webhook dispatch with third parameter $synchronouslyFirstTry as true // will first webhook call try immediately without adding call into queue // only if the first call fails, webhook call is added into queue for retry $dispatcher->dispatch('some_webhook_event_type', [], synchronouslyFirstTry: true); // simple dispatch when the first call will try after 1 minute $dispatcher->dispatchWithDelay('some_webhook_event_type', [], 60);
对于服务器端上的webhook监听器注册,您可以使用控制器 AddListener、RemoveListener、Symfony 等价控制器,或者只需在任意操作中使用webhook Repository,并使用此处提供的模型和控制器作为灵感。
由于安全原因,此包中的控制器没有发布功能或不提供自动注册到您的路由器。您应该始终完全控制您的应用程序,什么将被监听!
您可以将控制器手动复制到您的应用程序中,并将它们放在其他控制器之间。您应该为webhook操作添加与之前操作相同的身份验证中间件,以确保相同的安全性。
如果您在POST /webhook路由上注册了AddListener,并在DELETE /webhook上注册了RemoveListener,则您的webhook操作将与API客户端的webhook辅助方法兼容。