awtyklo / carve-api
为 Symfony 构建REST API端点的统一且可重用的方式
Requires
- php: >=8.1
- doctrine/orm: ^2.11
- friendsofsymfony/rest-bundle: ^3.3
- nelmio/api-doc-bundle: ^4.8
- phpoffice/phpspreadsheet: ^1.25
- sensio/framework-extra-bundle: ^6.2
- symfony/form: 6.4.*
- symfony/framework-bundle: 6.4.*
- symfony/property-access: 6.4.*
- symfony/security-bundle: 6.4.*
- symfony/serializer: 6.4.*
- symfony/translation: 6.4.*
- symfony/validator: 6.4.*
Requires (Dev)
- phpunit/phpunit: ^11
- dev-main
- 2.0.5
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- v1.x-dev
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.38
- 1.0.37
- 1.0.36
- 1.0.35
- 1.0.34
- 1.0.33
- 1.0.32
- 1.0.31
- 1.0.30
- 1.0.29
- 1.0.28
- 1.0.27
- 1.0.26
- 1.0.25
- 1.0.24
- 1.0.23
- 1.0.22
- 1.0.21
- 1.0.20
- 1.0.19
- 1.0.18
- 1.0.17
- 1.0.16
- 1.0.15
- 1.0.14
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-feat/arr
- dev-feat/symfony-6.4
- dev-feat/extend-api-resource-manager-with-role-based-groups
- dev-feat/rest-api-doc
- dev-feat/request-execution-error
- dev-hotfix/export-enum
- dev-SMA-607
- dev-SMA-579
- dev-batch
- dev-equalMultiple-fix
- dev-feat/translation-update
- dev-feat/RequestExecutionError
- dev-feat/constraint-count-unique
- dev-feat/assert-ip
- dev-feat/deprecations-fix
- dev-feat/assert-valid
- dev-hotfix/datetime-filters
- dev-hotfix/export
- dev-hotfix/export-composer
- dev-hotfix/dump-remove
- dev-feat/export
- dev-feat/abstract-deny
- dev-feat/utc-datetime
- dev-feat/api-filter-definition
- dev-hotfix/api-description-200-response
- dev-feat/api-sorting-definition
- dev-deprecations2
- dev-feat/constraints
- dev-i-2
- dev-deprecations
- dev-AssertUrl
- dev-ORM-type-guesser
- dev-asserts
This package is auto-updated.
Last update: 2024-09-25 14:21:06 UTC
README
为 Symfony 构建REST API端点的统一且可重用的方式。
重要!正在进行中。
提供统一且可重用方式来构建REST API端点 允许单个端点定制 自动生成OpenAPI文档 引入拒绝功能以简化访问控制,包括反馈消息 添加具有REST API友好信息的约束层
构建工具
- FOSRestBundle
- Symfony 序列化器
- OpenAPI
配置
在 config/packages/doctrine.yaml
中添加。这将确保 Types::DATETIME_MUTABLE
总是在UTC时区存储。
doctrine: dbal: types: datetime: Carve\ApiBundle\DBAL\Types\UTCDateTimeType
在 config/services.yaml
中添加。这将覆盖默认的 FormErrorNormalizer,以从错误消息中传递额外的参数。
services: fos_rest.serializer.form_error_normalizer: class: Carve\ApiBundle\Serializer\Normalizer\FormErrorNormalizer
在 config/services.yaml
中添加。这将覆盖默认的 ViewResponseListener,以处理视图导出。
services: fos_rest.view_response_listener: class: Carve\ApiBundle\EventListener\ViewResponseListener
在 config/packages/framework.yaml
中添加。这将添加默认的循环引用处理。
framework: serializer: circular_reference_handler: carve_api.serializer.circular_reference_handler
修改 src/Kernel.php
以覆盖 FormModelDescriber
类。
<?php namespace App; use Carve\ApiBundle\ModelDescriber\FormModelDescriber; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; class Kernel extends BaseKernel implements CompilerPassInterface { use MicroKernelTrait; public function process(ContainerBuilder $container): void { $formModelDescriberService = $container->getDefinition('nelmio_api_doc.model_describers.form'); $formModelDescriberService->setClass(FormModelDescriber::class); } }
请求执行错误报告
旨在在控制器中的操作只能部分执行时在响应中提供额外信息。抛出 RequestExecutionException
将导致 409
HTTP代码。
以下可以找到更多有关默认HTTP代码以及如何将 RequestExecutionException
与 409
结合使用的信息。
200
- 请求已成功执行。可以在响应中包含附加数据(例如更新后的对象)。204
- 请求已成功执行。响应中不包含任何附加数据。400
- 由于无效的有效负载,请求无法执行(通常与表单一起使用)。表单错误由Carve\ApiBundle\Serializer\Normalizer\FormErrorNormalizer
序列化。409
- 请求仅部分执行(有效负载正确)。在响应中包含附加信息。500
- 出现意外错误。
响应结构
以下是一个示例结构(TypeScript)。
type RequestExecutionSeverity = "warning" | "error"; // eslint-disable-next-line type RequestExecutionExceptionPayload = any; interface TranslateVariablesInterface { [index: string]: any; } interface RequestExecutionExceptionErrorType { message: string; parameters?: TranslateVariablesInterface; severity: RequestExecutionSeverity; } interface RequestExecutionExceptionType { code: number; payload: RequestExecutionExceptionPayload; severity: RequestExecutionSeverity; errors: RequestExecutionExceptionErrorType[]; }
第一层的 severity
将采用消息中最高 severity
的值。
{ "code": 409, "severity": "error", "payload": null, "errors": [ { "message": "functionality.error.processingWarning", "parameters": { "userName": "coolUser", "areaNo": 2 }, "severity": "warning" }, { "message": "functionality.error.somethingFailed", "parameters": { "deviceId": 123 }, "severity": "error" } ] }
严重性解释
error
表示在操作执行过程中发生错误,阻止了剩余步骤的执行。一个好的例子是无法连接到第三方系统(例如Google服务)。
warning
表示在操作执行过程中出现了一个问题,不应该发生,但它已被管理,并且剩余步骤已执行。一个好的例子是未从第三方系统中删除资源的操作,导致该资源缺失(我们的应用程序预计该资源存在并尝试删除它,但该资源在第三方系统中不存在)。
使用示例
待修复(目前有一些示例是过时的,其中一些是正确的。扩展mergeAsX函数示例)
error.requestExecutionFailed
- 是默认消息值 - 可以通过在 RequestExecutionException
构造函数中设置第3个参数来更改。构造函数消息(第1个参数)被添加到错误数组的第一个对象中,其他可以添加使用 addError
方法。
以下是一个示例
$exception = new RequestExecutionException('functionality.error.somethingFailed', ['userName' => 'coolUser', 'areaNo' => 2]);
$exception->addError('functionality.error.somethingElseFailed', ['deviceId' => 123]);
throw $exception;
另一个示例
throw new RequestExecutionException('functionality.error.somethingFailed', ['userName' => 'coolUser', 'areaNo' => 2]);
与 forge 集成
默认情况下,使用handleCatch
和ErrorContext
来创建ErrorDialog
前端,将显示对话框中的响应。翻译后的message
将用作对话框标题,errors
数组将以多个Alert
的形式显示错误严重性。文本将使用message
作为键,使用parameters
作为翻译参数进行翻译。ErrorDialog
需要添加到应用程序布局中。
批处理
批处理旨在处理可以通过列表端点查询的结果。
#[Rest\Post('/batch/disable')] // TODO Rest API docs public function batchDisableAction(Request $request) { $process = function (Device $device) { $device->setEnabled(false); }; return $this->handleBatchForm($process, $request); }
您可以在$process
函数中返回自定义的BatchResult
来自定义返回的结果。如果没有返回任何内容,则将返回一个带有SUCCESS
状态的BatchResult
(由getDefaultBatchProcessEmptyResult
函数控制)。
$this->handleBatchForm($process, $request, DeviceDeny::DISABLE);
use Carve\ApiBundle\Model\BatchResult; use Carve\ApiBundle\Enum\BatchResultStatus; $process = function (Device $device) { $device->setEnabled(false); // Your logic if (true) { return new BatchResult($device, BatchResultStatus::SKIPPED, 'batch.device.variableDelete.skipped.missing'); } };
您还可以使用denyKey
跳过任何不应处理的结果(将返回带有SKIPPED
状态和基于denyKey
的消息的BatchResult
)。
您可以使用以下模式来在BatchQueryType
表单中定义额外的字段(该表单仅包含sorting
和ids
字段)。
定义包含任何所需字段的表单并扩展BatchQueryType
。字段不应进行映射,否则您将需要更新表单的数据模型(这也是一个好解决方案)。
<?php declare(strict_types=1); namespace App\Form; use Carve\ApiBundle\Form\BatchQueryType; use Carve\ApiBundle\Validator\Constraints\NotBlank; use Symfony\Component\Form\FormBuilderInterface; class BatchVariableDeleteType extends BatchQueryType { public function buildForm(FormBuilderInterface $builder, array $options) { parent::buildForm($builder, $options); $builder->add('name', null, ['mapped' => false, 'constraints' => [ new NotBlank(), ]]); } }
在控制器中准备自定义逻辑。
#[Rest\Post('/batch/variable/delete')] // TODO Rest API docs public function batchVariableDeleteAction(Request $request) { $process = function (Device $device, FormInterface $form) { $name = $form->get('name')->getData(); // My custom logic }; return $this->handleBatchForm($process, $request, DeviceDeny::VARIABLE_DELETE, null, BatchVariableDeleteType::class); }
对handleBatchForm
函数的注释。
/** * Callable $process has following definition: * ($object, FormInterface $form): ?BatchResult. * Empty result from $process will be populated with getDefaultBatchProcessEmptyResult(). * By default it will be BatchResult with success status. * * Callable $postProcess has following definition: * (array $objects, FormInterface $form): void. */
导出(CSV和Excel)
当使用Carve\ApiBundle\EventListener\ViewResponseListener
并从控制器返回Carve\ApiBundle\View\ExportCsvView
或Carve\ApiBundle\View\ExportExcelView
时,结果将自动序列化并以csv
或xlsx
文件的形式返回。
示例用法
use Carve\ApiBundle\View\ExportCsvView; use Carve\ApiBundle\Model\ExportQueryField; // ... public function customExportAction() { $results = $this->getRepository(Task::class)->findAll(); $fields = []; // fields will most likely come from a POST request $field = new ExportQueryField(); // What field should be included in the export $field->setField('name'); // What label should be added for this field $field->setLabel('Name'); $fields[] = $field; $filename = 'custom_export.csv'; return new ExportCsvView($results, $fields, $filename); }
枚举翻译
默认情况下,导出中的每个枚举都将进行翻译。翻译字符串的结构如下:enum.entityName.fieldName.enumValue
。您可以通过添加一个Carve\ApiBundle\Attribute\Export\ExportEnumPrefix
属性来覆盖前缀。
在下面的示例中,翻译字符串将是enum.common.sourceType.enumValue
。
/** * Source type (upload or external url). */ #[ExportEnumPrefix('enum.common.sourceType.')] #[ORM\Column(type: Types::STRING, enumType: SourceType::class)] private ?SourceType $sourceType = null;
导出定制
您可以使用类似于Carve\ApiBundle\Serializer\ExportEnumNormalizer
的模式来定制常见的导出情况。
本地开发
将以下行添加到您的项目中的composer.json
:
"repositories": [
{
"type": "path",
"url": "/var/www/carve-api"
}
],
将"/var/www/carve-api"
更改为您的本地包路径。它应指向carve-api
的根目录(这意味着carve-api
的composer.json
位于/var/www/carve-api/composer.json
)。
之后执行
composer require "awtyklo/carve-api @dev"
它应链接本地包而不是远程包。
注意!它将更改composer.json
。请记住,在提交更改时。
TODO:如何撤销此操作
REST API文档
注意!每个端点仅支持一种方法。对端点使用多种方法可能会导致意外结果(例如,在/api/config
上同时有GET和POST)。
主题参数
一些提到的属性支持主题参数,这意味着字符串(例如摘要、描述)可以包含将用Describer\ApiDescriber
替换的参数。
主题参数根据Api\Resource
属性中的subject
进行准备。
以下为主题参数。以下为subject
= "User"的示例。
subjectLower
即"user"subjectTitle
即"User"subjectPluralLower
即"users"subjectPluralTitle
即"Users"
属性
#[Api\Summary]
- 将摘要附加到操作。摘要支持主题参数。#[Api\Parameter]
- 具有描述的参数,支持主题参数。#[Api\ParameterPathId]
- 具有描述的预配置路径ID参数,支持主题参数。#[Api\RequestBody]
- 具有描述的请求体,支持主题参数。#[Api\RequestBodyBatch]
- 支持主题参数的请求体描述。当没有内容时(期望使用Nelmio\ApiDocBundle\Annotation\Model
),则将其设置为 ApibatchFormClass>。它还会将 'sorting_field_choices' 添加到内容选项中。 #[Api\RequestBodyCreate]
- 内容设置为 ApicreateFormClass> 和支持主题参数的描述的请求体。 #[Api\RequestBodyEdit]
- 内容设置为 ApieditFormClass> 和支持主题参数的描述的请求体。 #[Api\RequestBodyList]
- 内容设置为 ApilistFormClass>(带有 'sorting_field_choices' 和 'filter_filterBy_choices' 选项)和支持主题参数的描述的请求体。 #[Api\RequestBodyExportCsv]
- 内容设置为 ApiexportCsvFormClass>(带有 'sorting_field_choices','filter_filterBy_choices' 和 'fields_field_choices' 选项)和支持主题参数的描述的请求体。 #[Api\RequestBodyExportExcel]
- 内容设置为 ApiexportExcelFormClass>(带有 'sorting_field_choices','filter_filterBy_choices' 和 'fields_field_choices' 选项)和支持主题参数的描述的请求体。 #[Api\Response200]
- 预配置的带有代码 200 的响应,并支持主题参数的描述。#[Api\Response200ArraySubjectGroups]
- 预配置的带有代码 200 的响应,默认描述支持主题参数,并将内容设置为给定类和序列化组的Nelmio\ApiDocBundle\Annotation\Model
数组。#[Api\Response200BatchResults]
- 预配置的带有代码 200 的列表响应,支持主题参数的描述,并将内容设置为Carve\ApiBundle\Model\BatchResult
数组。#[Api\Response200Groups]
- 预配置的带有代码 200 的响应,支持主题参数的描述,并将序列化组附加到内容(期望内容为Nelmio\ApiDocBundle\Annotation\Model
)。#[Api\Response200SubjectGroups]
- 预配置的带有代码 200 的响应,支持主题参数的描述,并将内容设置为具有主题类和序列化组的Nelmio\ApiDocBundle\Annotation\Model
。#[Api\Response200List]
- 预配置的带有代码 200 的列表响应,支持主题参数的描述,并将内容设置为具有rowsCount
和results
的对象,其中包含具有主题类和序列化组的项。#[Api\Response204]
- 预配置的带有代码 204 的响应,支持主题参数的描述。#[Api\Response204Delete]
- 预配置的带有代码 204 的响应,默认描述({{ subjectTitle }} 成功删除
)支持主题参数。#[Api\Response400]
- 预配置的带有代码 400 的响应,默认描述(无法处理请求,因为数据无效
)支持主题参数。#[Api\Response404]
- 预配置的带有代码 404 的响应,支持主题参数的描述。#[Api\Response404Id]
- 预配置的带有代码 404 的响应,默认描述(未找到指定 ID 的 {{ subjectTitle }}
)支持主题参数。
使用示例
#[Api\Summary('Get {{ subjectLower }} by ID')] public function getAction(int $id)
#[Api\ParameterPathId('ID of {{ subjectLower }} to return')] public function getAction(int $id)
#[Api\Parameter(name: 'serialNumber', in: 'path', schema: new OA\Schema(type: 'string'), description: 'The serial number of {{ subjectLower }} to return')] public function getAction(string $serialNumber)
#[Api\RequestBody(description: 'New data for {{ subjectTitle }}', content: new NA\Model(type: Order::class))] public function editAction()
use Nelmio\ApiDocBundle\Annotation as NA; #[Api\RequestBodyBatch(content: new NA\Model(type: BatchVariableAddType::class))] public function batchVariableAddAction()
use Nelmio\ApiDocBundle\Annotation as NA; #[Api\Response200(description: 'Returns public configuration for application', content: new NA\Model(type: PublicConfiguration::class))] public function getAction()
use Nelmio\ApiDocBundle\Annotation as NA; #[Rest\View(serializerGroups: ['public:configuration'])] #[Api\Response200Groups(description: 'Returns public configuration for application', content: new NA\Model(type: PublicConfiguration::class))] public function getAction()
use Nelmio\ApiDocBundle\Annotation as NA; #[Rest\View(serializerGroups: ['public:configuration'])] class AnonymousController extends AbstractApiController { #[Api\Response200Groups(description: 'Returns public configuration for application', content: new NA\Model(type: PublicConfiguration::class))] public function getAction() }
#[Api\Response200SubjectGroups('Returns created {{ subjectLower }}')] public function createAction(Request $request)
#[Api\Response200List('Returns list of {{ subjectPluralLower }}')] public function listAction(Request $request)
#[Api\Response204('{{ subjectTitle }} successfully enabled')] public function enableAction()
#[Api\Response404('{{ subjectTitle }} with specified serial number not found')] public function getAction()
常见用例
use Carve\ApiBundle\Attribute as Api; use Nelmio\ApiDocBundle\Annotation as NA; #[Rest\Post('/change/password')] #[Api\Summary('Change authenticated user password')] #[Api\Response204('Password successfully changed')] #[Api\RequestBody(content: new NA\Model(type: AuthenticatedChangePasswordType::class))] #[Api\Response400] public function changePasswordAction(Request $request)
use Carve\ApiBundle\Attribute as Api; use Nelmio\ApiDocBundle\Annotation as NA; #[Rest\Post('/change/password/required')] #[Api\Summary('Change authenticated user password when password change is required. Password change is required when authenticated user roles include ROLE_CHANGEPASSWORDREQUIRED')] #[Api\RequestBody(content: new NA\Model(type: AuthenticationChangePasswordRequiredType::class))] #[Api\Response200(description: 'Returns updated authentication data', content: new NA\Model(type: AuthenticationData::class))] #[Api\Response400]
use Carve\ApiBundle\Attribute as Api; #[Rest\Get('/token/extend/{refreshTokenString}')] #[Api\Summary('Extend refresh token for another access token TTL')] #[Api\Response204('Correct refresh token extended successfully')] #[Api\Parameter(in: 'path', name: 'refreshTokenString', description: 'Refresh token string')] public function extendAction(string $refreshTokenString)
use Carve\ApiBundle\Attribute as Api; use Nelmio\ApiDocBundle\Annotation as NA; #[Rest\Post('/batch/variable/add')] #[Api\Summary('Add variable to multiple {{ subjectPluralLower }}')] #[Api\RequestBodyBatch(content: new NA\Model(type: BatchVariableAddType::class))] #[Api\Response200BatchResults] #[Api\Response400] public function batchVariableAddAction(Request $request)
use OpenApi\Attributes as OA; #[Api\Response200( description: 'Progress', content: new OA\JsonContent( type: 'object', properties: [ new OA\Property(property: 'total', type: 'integer'), new OA\Property(property: 'pending', type: 'integer'), ] ), )] public function progressAction(int $id)
class OptionsController extends AbstractApiController { #[Rest\Get('/users')] #[Api\Summary('Get users')] #[Api\Response200ArraySubjectGroups(User::class)] public function usersAction() { return $this->getRepository(User::class)->findAll(); } }
开发
运行测试
使用 composer
下载包(包括 PHPUnit)。
composer install
验证 PHPUnit 安装。以下命令应返回 PHPUnit 的版本。
php vendor/bin/phpunit --version
运行测试。
php vendor/bin/phpunit