ict / api_one_endpoint
创建一个单端点
Requires
- php: >=8.1
- doctrine/annotations: ^2.0
- phpdocumentor/reflection-docblock: ^5.3
- phpstan/phpdoc-parser: ^1.21
- symfony/console: 6.*
- symfony/dotenv: 6.*
- symfony/flex: ^2
- symfony/framework-bundle: 6.*
- symfony/messenger: 6.*
- symfony/property-access: 6.*
- symfony/property-info: 6.*
- symfony/runtime: 6.*
- symfony/security-bundle: 6.*
- symfony/serializer: 6.*
- symfony/uid: 6.*
- symfony/validator: 6.*
- symfony/yaml: 6.*
Requires (Dev)
- phpunit/phpunit: 10.1.x-dev
- rregeer/phpunit-coverage-check: dev-master
README
api_one_endpoint
Api one endpoint bundle 允许您创建一个关注操作而不是资源的 API。一个端点用作资源,API 将查看有效载荷以提取要执行的操作以及需要哪些数据来执行该操作。
安装
使用 composer 安装此组件
composer require ict/api_one_endpoint:^1.0
输入和输出
输入和输出定义操作 I/O 流。每个操作可能需要输入(如果它需要数据来执行它),并且必须定义一个输出,该输出将返回给客户端。输入必须以 POST Json 请求的形式发送,并必须遵循以下模式
{ "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#", "type": "object", "properties": { "operation": { "type": "string" }, "data": { "type": "object" } }, "required": [ "operation", "data" ] }
以下是如何一个 SendPaymentOperation 输入可能看起来
{ "operation" : "SendPaymentOperation", "data" : { "from" : "xxxx", "to" : "yyyy", "amount" : 6.35 } }
在上面的有效载荷中,我们发送一个输入,告诉我们的 API 执行 SendPaymentOperation 并使用以下键作为数据
- from:谁发送支付
- To:谁收到支付
- Amount:xxxx 发送多少金额给 yyyy
输出必须封装在 Ict\ApiOneEndpoint\Model\Api\ApiOutput 对象中。作为参数,ApiOutput 接收将返回给客户端的数据和用于响应的 HTTP 状态码。数据可以是可迭代的(如果我们想返回数据集合)或对象。此组件依赖于 symfony serializer 向客户端发送 JSON 响应。
让我们看看以下示例
class Hero { public function __construct( public readonly string $name, public readonly string $hero ){ } } $data = [ new Hero('Peter Parker', 'spiderman'), new Hero('Clark Kent', 'superman') ]; return new ApiOutput($data, 200);
class PaymentDoneOutput { public function __construct( public readonly string paymentId, public readonly \DateTimeInmutable $date ){ } } $paymentOutput = new PaymentDoneOutput('899875557', new \DateTimeInmutable('2023-03-05 12:25'); return new ApiOutput($paymentOutput, 202);
第一个示例返回一个 Hero 对象数组作为输出,第二个示例返回 PaymentDoneOutput 对象。
如果您想在输出中使用 symfony serializer groups,则可以使用第三个 ApiOutput 参数传递组名
return new ApiOutput($paymentOutput, 202, 'admin');
定义输入操作
操作输入必须通过创建具有其获取器和设置器的基本对象来定义。您可以使用 symfony 验证约束 来定义验证规则,因此您的输入必须包含所需的有效数据。此组件将自动验证输入,并在验证失败时抛出 Ict\ApiOneEndpoint\Exception\OperationValidationException。
以下是一个支付操作输入的示例
use Symfony\Component\Validator\Constraints\GreaterThan; use Symfony\Component\Validator\Constraints\NotBlank; class SendPaymentOperationInput { #[NotBlank(message: 'From cannot be empty')] private string $from; #[NotBlank(message: 'To cannot be empty')] private string $to; #[NotBlank(message: 'Amount cannot be empty')] #[GreaterThan(0, message: 'Amount must be greater then 0')] private string $amount; // getters & setters }
定义操作
操作必须实现 Ict\ApiOneEndpoint\Contract\Operation\OperationInterface
use Ict\ApiOneEndpoint\Contract\Operation\OperationInterface; use Ict\ApiOneEndpoint\Model\Api\ApiOutput; class SendPaymentOperation implements OperationInterface { public function perform(mixed $operationData): ApiOutput { // Perform operation .... // Sending bizum, BTC ...... return new ApiOutput([], 200); } public function getName(): string { return 'SendPayment'; } public function getInput(): ?string { return SendPaymentOperationInput::class; } public function getGroup(): ?string { return null; } public function getContext() : ?array { return null; } }
每个操作都必须定义以下方法(在 OperationInterface 中声明)
- perform:执行操作
- getName:获取操作名称
- getInput:获取输入类。当操作执行时,有效载荷数据将反序列化为输入类,并执行验证。
- getGroup:获取操作组。我们将在操作授权时介绍它。
- getContext:获取操作上下文。我们将在操作上下文分离时介绍它。
保护操作
此组件依赖于 symfony 投票者 来保护操作。查看以下投票者
use Ict\ApiOneEndpoint\Model\Operation\OperationSubject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class SendPaymentVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { if(!$subject instanceof OperationSubject){ return false; } return true; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); if(!in_array('ROLE_PAYMENT', $user->getRoles())){ return false; } return true; } }
如果您想保护您的API免受身份验证上下文(JWT令牌、用户/密码)的影响,您可以使用symfony安全。
该包将Ict\ApiOneEndpoint\Model\Operation\OperationSubject作为属性传递给投票者。这使您能够访问操作名称和操作组(如果已在getGroup方法中定义)。当您想授予一组操作对特定角色或角色的访问权限时,组很有用。例如,让我们假设您有以下操作:
- 创建账户
- 更新账户
- 删除账户
如果您想限制管理员角色的访问,您必须在getGroup方法中返回相同的组,然后在您的投票者中检查该组。
use Ict\ApiOneEndpoint\Model\Operation\OperationSubject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class AccountManagementVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { if(!$subject instanceof OperationSubject){ return false; } return $subject->group === 'ACCOUNT'; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); if(!in_array('ROLE_ADMIN', $user->getRoles())){ return false; } return true; } }
操作主题包含以下信息(作为公共和只读属性):
- operation:完全限定的操作类名
- operationName:操作名称(由getName方法返回的值)
- group:操作组(由getGroup方法返回的值)
- data:执行操作的数据
此包始终检查操作授权,可能没有定义投票者或没有支持条件的投票者。为了避免在没有执行投票者时被拒绝访问,请在您的security.yaml文件中设置以下访问决策策略
security: ...... access_decision_manager: strategy: unanimous allow_if_all_abstain: true
您可以在symfony文档中找到有关访问决策策略的更多信息
将操作发送到后台
为了允许开发人员配置一些应在后台执行的操作,此包依赖于
- Symfony messenger
- Ict\ApiOneEndpoint\Model\Attribute\IsBackground 属性
让我们回到SendPayment操作
use Ict\ApiOneEndpoint\Contract\Operation\OperationInterface; use Ict\ApiOneEndpoint\Model\Api\ApiOutput; use Ict\ApiOneEndpoint\Model\Attribute\IsBackground; #[IsBackground] class SendPaymentOperation implements OperationInterface { ....... }
当操作被注解为IsBackground属性时,它的执行将在后台进行,并且客户端不需要等待它完成。对于可能需要较长时间才能完成的操作,这很有用。发送电子邮件或付款就是这类操作的例子。
将操作发送到后台后,将向客户端返回一个包含以下内容的202(已接受)http请求
{ "status" : "QUEUED" }
如果您想延迟执行一段时间,您可以在属性中添加延迟属性
use Ict\ApiOneEndpoint\Contract\Operation\OperationInterface; use Ict\ApiOneEndpoint\Model\Api\ApiOutput; use Ict\ApiOneEndpoint\Model\Attribute\IsBackground; #[IsBackground(delay: 300)] class SendPaymentOperation implements OperationInterface { ....... }
这将延迟执行300秒(5分钟)。
让我们看看您应该如何配置您的messenger.yaml文件来排队操作
messenger: transports: your_transport_name: "%env(MESSENGER_TRANSPORT_DSN)%" routing: 'Ict\ApiOneEndpoint\Message\OperationMessage': your_transport_name
使用上述配置,您将能够将操作路由到您的传输。
事件
操作执行后,此包会分派一个\Ict\ApiOneEndpoint\EventSubscriber\Event\OperationPerformedEvent,以便开发人员可以监听它并执行某些任务,例如向用户发送通知
class OperationSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ OperationPerformedEvent::class => ['onOperationPerformed'] ] } public function onOperationPerformed(OperationPerformedEvent $event): void { // events gives you access to operation name and operation result: $opName = $event->operation; $opResult = $event->operationResult; // some stuff here ..... } }
如果您对向用户发送通知感兴趣,请考虑使用此symfony通知器
操作上下文
如我们所见,OperationInterface 有一个名为 getContext 的方法,它可以返回一个包含允许上下文或null的数组。让我们假设我们想保留两个端点:一个用于客户端,另一个用于提供商。如果我们想使操作仅允许客户端上下文,我们可以将getContext方法编写如下
public function getContext(): ?array { return ['client']; }
在下一节中,我们将看到如何设置端点上下文。
控制器
设置您的控制器是一个真正简单的工作。让我们看看
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Ict\ApiOneEndpoint\Operation\OperationHandler; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\SerializerInterface; use Ict\ApiOneEndpoint\Model\Api\Context; #[Route('/api/v1')] class BackendController extends AbstractController { use \Ict\ApiOneEndpoint\Controller\OperationControllerTrait; #[Route('', name: 'api_backend_v1_process_operation', methods: ['POST'])] public function processOperation(Request $request, SerializerInterface $serializer, OperationHandler $operationHandler): JsonResponse { return $this->executeOperation($request, $serializer, $operationHandler, new Context()); } }
您只需创建您的控制器并使用trait \Ict\ApiOneEndpoint\Controller\OperationControllerTrait。然后使用executeOperation方法,将其作为参数传递给它,包括请求、序列化和操作处理器,然后您的操作将被执行。如果我们只想限制我们的端点来管理客户端上下文操作怎么办?我们只需将上下文名称传递给Context对象构造函数即可
return $this->executeOperation($request, $serializer, $operationHandler, new Context('client'));
现在,这个控制器将只执行客户端上下文操作。