paysera / lib-api-bundle
Symfony扩展包,允许轻松配置您的REST端点。
Requires
- php: ^7.1 || ^8.0
- ext-json: *
- doctrine/annotations: ^1.14 || ^2.0
- doctrine/persistence: ^1.3.8 || ^2.0.1 || ^3.0
- paysera/lib-dependency-injection: ^1.3.0
- paysera/lib-normalization: ^1.2
- paysera/lib-normalization-bundle: ^1.1.0
- paysera/lib-object-wrapper: ~0.1
- paysera/lib-pagination: ^1.0
- psr/log: ^1.0|^2.0
- symfony/framework-bundle: ^3.4.34|^4.3|^5.4|^6.0
- symfony/security-bundle: ^3.4.34|^4.3|^5.4|^6.0
- symfony/validator: ^3.4.34|^4.3|^5.4|^6.0
Requires (Dev)
- doctrine/doctrine-bundle: ^1.12.0|^2.1
- doctrine/orm: ^2.5.14
- mockery/mockery: ^1.3.6
- phpunit/phpunit: ^7.5 || ^9.6
- symfony/yaml: ^3.4.34|^4.3|^5.4|^6.0
README
Symfony扩展包,允许轻松配置您的REST端点。
为什么选择这个?
如果您编写了很多REST端点,一些代码或结构本身会重复。如果要在所有端点中添加一些功能,如果只是为每个端点编写一些自定义代码,这可能会变得麻烦。
与API Platform的不同之处
API Platform提供了许多API规范选项、文档生成等功能。由于它知道您对象中的所有关系和字段,因此这些功能都是可用的。但是,为此您需要配置所有这些功能的对象,包括序列化选项。
这种方法非常适合小型应用,但在大型应用或需要长期支持和可能经常更改的应用中可能会有些麻烦。
当需要一些自定义功能时,直接在代码中实现它比正确配置它(如果此类配置甚至可用)要容易得多。
此扩展包提供了一些控制
- 每个路由都明确定义,并执行相应的控制器操作。这允许更好地跟踪执行,并在需要时使用任何自定义编程代码;
- 使用序列化/规范化代码,而不是配置。这使得它也更明确和可配置。将REST接口与业务模型紧密耦合似乎不是我们想要的。
它可能有点模板化,但在需要时很容易定制。
安装
composer require paysera/lib-api-bundle
如果您不使用symfony flex,请将以下扩展包添加到您的内核中
new PayseraNormalizationBundle(),
new PayseraApiBundle(),
配置
paysera_api: locales: ['en', 'lt', 'lv'] # Optional list of accepted locales validation: property_path_converter: your_service_id # Optional service ID to use for property path converter path_attribute_resolvers: # Registered path attribute resolvers. See below for more information App\Entity\PersistedEntity: field: identifierField pagination: total_count_strategy: optional # If should we provide or allow total count of resources (by default) maximum_offset: 1000 # If we should limit offset passed to pager for performance reasons maximum_limit: 1000 # Maximum limit for one page of results default_limit: 100 # Default limit for one page of results
使用
创建资源
为了从请求和响应中规范化/反规范化数据,使用PayseraNormalizationBundle。它通过为您的每个资源编写一个类来实现。这使得它明确,并允许轻松地将映射到/从您的域模型(通常是Doctrine实体)中自定义。
规范化器示例
<?php declare(strict_types=1); use Paysera\Component\Normalization\ObjectDenormalizerInterface; use Paysera\Component\Normalization\NormalizerInterface; use Paysera\Component\Normalization\TypeAwareInterface; use Paysera\Component\Normalization\DenormalizationContext; use Paysera\Component\Normalization\NormalizationContext; use Paysera\Component\ObjectWrapper\ObjectWrapper; class UserNormalizer implements ObjectDenormalizerInterface, NormalizerInterface, TypeAwareInterface { public function denormalize(ObjectWrapper $input, DenormalizationContext $context) { return (new User()) ->setEmail($input->getRequiredString('email')) ->setPlainPassword($input->getRequiredString('password')) ->setAddress($context->denormalize($input->getObject('address'), Address::class)) ; } public function normalize($user, NormalizationContext $normalizationContext) { return [ 'id' => $user->getId(), 'email' => $user->getEmail(), 'address' => $user->getAddress(), // will be mapped automatically if type is classname ]; } public function getType(): string { return User::class; // you can use anything here, but types can be guessed if FQCN are used } }
在这种情况下,您还需要为Address
类实现规范化器。
使用注解/属性配置REST端点最简单。这也要求您在控制器注解/属性中提供路由。
使用注解的控制器示例
<?php declare(strict_types=1); use Symfony\Component\Routing\Annotation\Route; use Paysera\Bundle\ApiBundle\Annotation\Body; class ApiController { // ... /** * @Route("/users", methods="POST") * @Body(parameterName="user") * * @param User $user * @return User */ public function register(User $user) { $this->securityChecker->checkPermissions(Permissions::REGISTER_USER, $user); $this->userManager->registerUser($user); $this->entityManager->flush(); return $user; } }
使用属性的控制器示例
<?php declare(strict_types=1); use Symfony\Component\Routing\Annotation\Route; use Paysera\Bundle\ApiBundle\Attribute\Body; class ApiController { // ... #[Route(path: '/users', methods: 'POST')] #[Body(parameterName: 'user')] public function register(User $user): User { $this->securityChecker->checkPermissions(Permissions::REGISTER_USER, $user); $this->userManager->registerUser($user); $this->entityManager->flush(); return $user; } }
别忘了也将您的控制器(或Controller
目录)导入到路由配置中。例如
<!-- Resources/config/routing.xml --> <import resource="../../Controller/" type="annotation" prefix="/rest/v1/"/>
acme_something: resource: "@AcmeSomethingBundle/Controller/" type: annotation prefix: /rest/v1/
这还要求您的控制器服务ID与其FQCN相同。
HTTP示例
POST /rest/v1/users HTTP/1.1
Accept: */*
Host: api.example.com
{
"email": "user1@example.com",
"password": "that's my password",
"address": {
"country_code": "LT",
"city": "Vilnius"
"address_line": "Some street 1-2"
}
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"email": "user1@example.com",
"address": {
"country_code": "LT",
"city": "Vilnius"
"address_line": "Some street 1-2"
}
}
获取资源
使用注解的控制器示例
<?php declare(strict_types=1); use Symfony\Component\Routing\Annotation\Route; use Paysera\Bundle\ApiBundle\Annotation\PathAttribute; class ApiController { // ... /** * @Route("/users/{userId}", methods="GET") * @PathAttribute(parameterName="user", pathPartName="userId") * * @param User $user * @return User */ public function getUser(User $user) { $this->securityChecker->checkPermissions(Permissions::ACCESS_USER, $user); return $user; } }
使用属性的控制器示例
<?php declare(strict_types=1); use Symfony\Component\Routing\Annotation\Route; use Paysera\Bundle\ApiBundle\Attribute\PathAttribute; class ApiController { // ... #[Route(path: '/users/{userId}', methods: 'GET')] #[PathAttribute(parameterName: 'user', pathPartName: 'userId')] public function getUser(User $user): User { $this->securityChecker->checkPermissions(Permissions::ACCESS_USER, $user); return $user; } }
对于路径属性,应实现PathAttributeResolverInterface
,因为我们只接收标量类型(ID),而不是对象。
默认情况下,扩展包会尝试查找具有与该参数类型注册的完全限定类名相同类型的解析器。
您至少有几种方法可以使这起作用。
- 创建自己的路径属性解析器。例如
<?php declare(strict_types=1); use Paysera\Bundle\ApiBundle\Service\PathAttributeResolver\PathAttributeResolverInterface; class FindUserPathAttributeResolver implements PathAttributeResolverInterface { // ... public function resolveFromAttribute($attributeValue) { return $this->repository->find($attributeValue); } }
使用paysera_api.path_attribute_resolver
标记服务,提供FQCN作为type
属性。
- 重用
DoctrinePathAttributeResolver
类来配置所需的服务。例如
<service class="Paysera\Bundle\ApiBundle\Service\PathAttributeResolver\DoctrinePathAttributeResolver" id="find_user_denormalizer"> <tag name="paysera_api.path_attribute_resolver" type="App\Entity\User"/> <argument type="service"> <service class="App\Repository\UserRepository"> <factory service="doctrine.orm.entity_manager" method="getRepository"/> <argument>App\Entity\User</argument> </service> </argument> <argument>id</argument><!-- or any other field to search by --> </service>
- 在
config.yml
中配置支持的类和搜索字段。这实际上与前面的选项相同。
paysera_api: path_attribute_resolvers: App\Entity\User: ~ # defaults to searching by "id" App\Entity\PersistedEntity: field: identifierField
HTTP示例
GET /rest/v1/users/123 HTTP/1.1
Accept: */*
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"email": "user1@example.com",
"address": {
"country_code": "LT",
"city": "Vilnius"
"address_line": "Some street 1-2"
}
}
获取资源列表
使用注解的控制器示例
<?php declare(strict_types=1); use Symfony\Component\Routing\Annotation\Route; use Paysera\Bundle\ApiBundle\Annotation\Query; use Paysera\Pagination\Entity\Pager; use Paysera\Bundle\ApiBundle\Entity\PagedQuery; class ApiController { // ... /** * @Route("/users", methods="GET") * @Query(parameterName="filter") * @Query(parameterName="pager") * * @param UserFilter $filter * @param Pager $pager * @return PagedQuery */ public function getUsers(UserFilter $filter, Pager $pager) { $this->securityChecker->checkPermissions(Permissions::SEARCH_USERS, $filter); $configuredQuery = $this->userRepository->buildConfiguredQuery($filter); return new PagedQuery($configuredQuery, $pager); } }
使用属性的控制器示例
<?php declare(strict_types=1); use Symfony\Component\Routing\Annotation\Route; use Paysera\Bundle\ApiBundle\Attribute\Query; use Paysera\Pagination\Entity\Pager; use Paysera\Bundle\ApiBundle\Entity\PagedQuery; class ApiController { // ... #[Route(path: '/users', methods: 'GET')] #[Query(parameterName: 'filter')] #[Query(parameterName: 'pager')] public function getUsers(UserFilter $filter, Pager $pager): PagedQuery { $this->securityChecker->checkPermissions(Permissions::SEARCH_USERS, $filter); $configuredQuery = $this->userRepository->buildConfiguredQuery($filter); return new PagedQuery($configuredQuery, $pager); } }
为UserFilter
实现反规范化器
<?php declare(strict_types=1); use Paysera\Component\Normalization\ObjectDenormalizerInterface; use Paysera\Component\Normalization\TypeAwareInterface; use Paysera\Component\Normalization\DenormalizationContext; use Paysera\Component\ObjectWrapper\ObjectWrapper; class UserFilterDenormalizer implements ObjectDenormalizerInterface, TypeAwareInterface { public function denormalize(ObjectWrapper $input, DenormalizationContext $context) { return (new UserFilter()) ->setEmail($input->getString('email')) ->setCountryCode($input->getString('country_code')) ; } public function getType(): string { return UserFilter::class; // you can use anything here, but types can be guessed if FQCN are used } }
在UserRepository
中的代码
<?php declare(strict_types=1); use Paysera\Pagination\Entity\OrderingConfiguration; use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery; class UserRepository extends Repository { public function buildConfiguredQuery(UserFilter $filter) { // just an example – should add conditions only when they're set $queryBuilder = $this->createQueryBuilder('u') ->join('u.address', 'a') ->join('a.country', 'c') ->andWhere('u.email = :email') ->andWhere('c.code = :countryCode') ->setParameter('email', $filter->getEmail()) ->setParameter('countryCode', $filter->getCountryCode()) ; return (new ConfiguredQuery($queryBuilder)) ->addOrderingConfiguration('email', new OrderingConfiguration('u.email', 'email')) ->addOrderingConfiguration( 'country_code', new OrderingConfiguration('c.code', 'address.country.code') ) ; } }
如本例所示,扩展包集成了对Paysera Pagination组件的支持。
在这种情况下,实际数据库检索是在规范化器本身中执行的。这出于以下几个原因:
- 允许配置整个应用程序的总计数策略和最大偏移量(见下文);
- 支持可选的总计数和可选的项目。默认情况下,如果客户端没有明确请求资源的总计数,则不会计算。
配置示例
paysera_api: pagination: total_count_strategy: optional maximum_offset: 1000 # could be set to null for no limit maximum_limit: 500 # can be configured to any number but cannot be null default_limit: 100 # used if no limit parameter was passed
覆盖特定操作的选项
// ... begining of controller action $configuredQuery = $this->userRepository->buildConfiguredQuery($filter); $configuredQuery->setMaximumOffset(1000); // optionally override maximum offset return (new PagedQuery($configuredQuery, $pager)) // optionally override total count strategy ->setTotalCountStrategy(PagedQuery::TOTAL_COUNT_STRATEGY_OPTIONAL) ;
可用策略
always
– 默认情况下计算总计数,除非明确排除在返回字段之外;optional
– 默认情况下不计算总计数,但可以,如果明确包含在返回字段中;never
– 从不计算总计数;default
– 仅适用于PagedQuery
,回退到全局配置的策略。
当您在 ConfiguredQuery
对象中显式设置 always
策略时,最大偏移量将被忽略。如果您仍然需要它,请像配置策略一样在 ConfiguredQuery
中显式配置它。
如果您使用其他策略并配置最大偏移量,目前没有明确允许端点具有任何偏移量的方法。
请求和响应结构
分页器是从以下查询字符串字段反规范化:
limit
。限制每页中的资源数量。默认值为配置的default_limit
值;offset
。跳过一定数量的结果。应仅用于转到第 N 页。默认情况下受maximum_offset
值限制;after
。接受来自上一个结果的游标,以提供“下一页”的结果;before
。接受来自上一个结果的游标,以提供“上一页”或结果;sort
。接受由ConfiguredQuery::addOrderingConfiguration
配置的字段列表,字段之间用逗号分隔。要降序排序,请将特定项的前缀指定为-
。例如,?sort=-date_of_birth,registered_at
将产生类似于ORDER BY date_of_birth DESC, registered_at ASC
的结果。
只能提供 offset
/after
/before
中的一个。
响应结构有以下字段
items
。规范化资源的数组;_metadata.total
。整数,资源的总计数。默认情况下缺失,这取决于策略和查询字符串中的fields
参数;_metadata.has_next
。布尔值,表示当前是否可用下一页;_metadata.has_previous
。布尔值,表示当前是否可用上一页;_metadata.cursors.after
。字符串,将作为查询字符串中的after
参数传递以获取下一页;_metadata.cursors.before
。字符串,将作为查询字符串中的before
参数传递以获取上一页。
请注意,在某些罕见情况下(例如,没有结果),游标可能缺失。另一方面,即使当前没有下一页/上一页,也会提供游标。这可以用来检查是否有新资源创建 - 当与后端同步使用时非常方便。
不要对游标值的内部结构做任何假设,因为这可能会随着任何版本的变化而变化。
HTTP 示例
GET /rest/v1/users?limit=2 HTTP/1.1
Accept: */*
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{
"id": 1,
"email": "user1@example.com",
"address": {
"country_code": "LT",
"city": "Vilnius"
"address_line": "Some street 1-2"
}
},
{
"id": 2,
"email": "user2@example.com",
"address": {
"country_code": "LT",
"city": "Kaunas"
"address_line": "Some street 2-3"
}
}
],
"_metadata": {
"has_next": true,
"has_previous": false,
"cursors": {
"after": "\"2-abc\"",
"before": "\"1-abc\""
}
}
}
仅获取总计数
GET /rest/v1/users?fields=_metadata.total HTTP/1.1
Accept: */*
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{
"_metadata": {
"total": 15
}
}
这实际上只会对总计数进行 SELECT
语句,不会选择任何项目。
获取带总计数的资源页
GET /rest/v1/users?fields=*,_metadata.total HTTP/1.1
Accept: */*
Host: api.example.com
获取下一页
GET /rest/v1/users?after="2-abc" HTTP/1.1
Accept: */*
Host: api.example.com
"2-abc"
是在这种情况下从 _metadata.cursors.after
中获取的。
假设资源按创建日期降序排序。要获取是否有新资源创建
GET /rest/v1/users?before="1-abc" HTTP/1.1
Accept: */*
Host: api.example.com
在这种情况下,如果结果为零,_metadata.cursors.before
仍然相同。保存最后一个游标并以此方式迭代,直到我们得到 "has_previous": false
,这是一个可靠地同步资源的方法。
注释/属性参考
主体
指示将请求主体转换为对象并将其作为参数传递给控制器。
BodyContentType
配置允许的请求内容类型以及是否应在传递给反规范化器之前将主体 JSON 解码。
如果没有配置,则默认为JSON编码的正文,允许两种Content-Type值:""
(空)和"application/json"
。
要使此注解/属性生效,必须存在Body
注解/属性。如果您想跳过去规范化过程,请将denormalizationType
设置为plain
。
验证
配置或关闭从请求体中反规范化对象的验证。默认情况下,验证始终启用。
您可以将其关闭以适用于某个操作或整个控制器类。
如果注解/属性同时存在于类和操作中,操作中的注解/属性将生效——它们不会合并在一起。
响应规范化
配置用于方法返回值的自定义规范化类型。
默认情况下,REST端点尝试规范化控制器操作返回的任何值,除非它是Response
对象。
如果方法没有返回任何内容(void
),则提供空的HTTP状态码为204
的响应。
路径属性
配置对路径的某些具体部分的去规范化。通常用于通过ID查找实体。
可以在单个控制器操作的同一操作中使用多个此类注解/属性。
查询
指示将查询字符串转换为对象,并将其作为参数传递给控制器。
可以使用多个注解/属性映射多个不同的对象。
所需权限
指示在安全上下文中检查特定操作的权限。
它也可以添加到类级别。类和方法级别的注解/属性中的权限将合并在一起。
不使用注解/属性进行配置
还可以配置选项,以定义作为服务并带有paysera_api.request_options
标记的RestRequestOptions
。
示例
<service id="paysera_fixture_test.rest_request_options.1" class="Paysera\Bundle\ApiBundle\Entity\RestRequestOptions"> <tag name="paysera_api.request_options" controller="service_id::action"/> <tag name="paysera_api.request_options" controller="App\Controller\DefaultController::action"/> <!-- set any options similarly to this --> <call method="addQueryResolverOptions"> <argument type="service"> <service class="Paysera\Bundle\ApiBundle\Entity\QueryResolverOptions"> <call method="setDenormalizationType"> <argument>extract:parameter</argument> </call> <call method="setParameterName"> <argument>parameter</argument> </call> </service> </argument> </call> </service>
语义版本控制
此库遵循语义版本控制。
有关API中可以更改和不能更改的基本信息的详细信息,请参阅Symfony BC规则。
请勿使用未标记为public="true"
的服务以及标记为@internal
的类或方法,因为这些可能在任何版本中更改。
运行测试
composer update
composer test
贡献
请随时创建问题并提交pull请求。
您可以使用以下命令修复任何代码风格问题
composer fix-cs