wizzaq / rest-bundle
Wizzaq RestBundle
Requires
- php: ^8.0
- ext-json: *
- symfony/cache: ^6.0
- symfony/config: ^6.0
- symfony/dependency-injection: ^6.0
- symfony/framework-bundle: ^6.0
- symfony/polyfill-php81: 1.x-dev
Requires (Dev)
- doctrine/coding-standard: ^9.0
- doctrine/orm: ^2.10 || ^3.0
- friendsofphp/proxy-manager-lts: ^1.0
- phpunit/phpunit: ^7.5 || ^8.0 || ^9.3 || ^10.0
- psalm/plugin-phpunit: ^0.16.1
- psalm/plugin-symfony: ^3
- psr/log: ^1.1.4|^2.0|^3.0
- symfony/deprecation-contracts: ^2.1|^3
- symfony/doctrine-bridge: ^6.0
- symfony/form: ^6.0
- symfony/phpunit-bridge: ^6.0
- symfony/property-info: ^6.0
- symfony/proxy-manager-bridge: ^6.0
- symfony/security-bundle: ^6.0
- symfony/twig-bridge: ^6.0
- symfony/validator: ^6.0
- symfony/web-profiler-bundle: ^6.0
- symfony/yaml: ^6.0
- twig/twig: ^1.34|^2.12|^3.0
- vimeo/psalm: ^4.7
Suggests
- symfony/doctrine-bridge: To use ProcessForm argument resolver.
- symfony/form: To use ProcessForm argument resolver.
README
这是您一直在寻找的REST bundle。
使用方法
ProcessForm 参数
将典型表单处理包装成更干净的行动代码。
配置
# config/packages/wizzaq_rest.yaml wizzaq_rest: use_resolvers: true # set to false to completely disable resolvers
简单示例
典型控制器的简单示例。
<?php namespace App\Controller; use App\Form\Filter\MyEntityFilterType; use App\Form\Dto\Filter\MyEntityFilter; use App\Repository\MyEntityRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Wizzaq\RestBundle\Attribute\ProcessForm; class MyEntityController extends AbstractController { #[Route('/my-entity', methods: 'GET')] public function list( #[ProcessForm(MyEntityFilterType::class, mapEntity: false, throwOnNotValid: false)] ?MyEntityFilter $filter = null, MyEntityRepository $repository ): Response { return $this->json(['list' => $repository->list($filter ?? new MyEntityFilter())]); } #[Route('/my-entity', methods: 'POST')] #[Route('/my-entity/{id}', methods: 'PUT')] public function edit( #[ProcessForm(MyEntityType::class)] MyEntity $entity, EntityManagerInterface $em ): Response { // $entity is processed by form and validated // Wizzaq\RestBundle\Exception\FormValidationException will be thrown if validation failed $em->persist($entity); $em->flush(); return new JsonResponse(['id' => $entity->getId()]); } }
所有选项
在 ProcessForm
属性上有许多选项可供控制行为
form
:string
(必填)要处理的表单类型类名
#[Route('/my-entity/{id}', methods: 'PUT')] public function edit( #[ProcessForm(form: MyEntityType::class)] MyEntity $entity, EntityManagerInterface $em ): Response { // $entity is processed by form and validated // Wizzaq\RestBundle\Exception\FormValidationException will be thrown if validation failed $em->persist($entity); $em->flush(); return new JsonResponse(['id' => $entity->getId()]); }
mapEntity
:null
|bool
(默认:null
)如果不为空,则严格通知
ProcessFormResolver
是否使用EntityValueResolver
在表单处理之前解析值。
如果为false
,则将null
用作创建表单的初始值。
如果为null
,则ProcessFormResolver
将尝试猜测。如果动作同时支持创建和编辑,则可能很有用。⚠️ 我们建议您始终明确指定此选项。
#[Route('/my-entity/{id}', methods: 'PUT')] public function edit( #[ProcessForm(form: MyEntityType::class, mapEntity: true)] MyEntity $entity, EntityManagerInterface $em ): Response { // $entity is processed by form and validated // Wizzaq\RestBundle\Exception\FormValidationException will be thrown if validation failed $em->persist($entity); $em->flush(); return new JsonResponse(['id' => $entity->getId()]); }
submit
:bool
(默认false
)如果为真,则使用
$form->submit($data, false)
(是的,带有$clearMissing
=false
) 而不是默认的$form->handleRequest($request)
#[Route('/my-entity', methods: 'GET')] public function list( #[ProcessForm(MyEntityFilterType::class, submit: true)] ?MyEntityFilter $filter = null, MyEntityRepository $repository ): Response { return $this->json(['list' => $repository->list($filter ?? new MyEntityFilter())]); }
throwOnNotValid
:bool
(默认true
)默认情况下,如果验证失败,则
ProcessFormResolver
将引发FormValidationException
。将
throwOnNotValid
设置为false
,如果您想在动作内部处理无效表单。处理后的表单可用
\Wizzaq\RestBundle\Config\RestConfig::processedForm($request)... use Wizzaq\RestBundle\Config\RestConfig; ... #[Route('/my-entity/{id}', methods: ['GET', 'POST'])] public function edit( #[ProcessForm(form: MyEntityType::class, throwOnNotValid: false)] ?MyEntity $entity = null, Request $request, RestConfig $restConfig, EntityManagerInterface $em ): Response { if (null === $entity) { $this->addFlash( 'error', 'Entity not found!' ); return new RedirectResponse('/my-entity'); } $form = $restConfig->processedForm($request); if ($form->isSubmitted() && $form->isValid()) { // do some stuff here $em->persist($entity); $em->flush(); return new RedirectResponse('/my-entity'); } return $this->render('my_entity/edit.html.twig', [ 'form' => $form, ]); }继承自 MapEntity
id
如果配置了
id
选项并且与路由参数匹配,则解析器将通过主键找到它
映射
配置用于
findOneBy()
方法的属性和值:键是路由占位符名称,值是Doctrine属性名称
排除
通过 排除 一个或多个属性来配置应用于
findOneBy()
方法的属性,以便不使用 所有 属性
stripNull
如果为真,则当使用
findOneBy()
时,任何值为null
的值都将不会用于查询。
objectManager
默认情况下,
EntityValueResolver
使用默认对象管理器,但您可以配置此设置
evictCache
如果为真,将强制Doctrine始终从数据库而不是缓存中获取实体。
disabled
如果为真,则
ProcessFormResolver
将不会尝试替换参数。
resolver
默认情况下,将使用
ProcessFormResolver
来解析参数,但您可以配置此设置
它如何工作。
ProcessForm
参数扩展 MapEntity 并使用 EntityValueResolver
查找现有实体,正如它们的文档中所描述的那样。
在 ProcessFormResolver
使用从 EntityValueResolver
返回的对象(或 null
)创建表单后
- 作为
$data
- 如果方法不是
GET
,则表单的当前method
将替换为$request
中的实际方法
并根据定义的选项进行处理。
如果表单有效,则返回表单中的处理数据。
如果没有,则抛出 FormValidationException
或者在 throwOnNotValid
为 false
时返回找到的对象
协议
在开发API时,我们都有一个主要监听器来从JSON解码负载并将其放回请求以进行处理,例如
public function onRequest(RequestEvent $event): void { $request = $event->getRequest(); if ('' === $request->getContent()) { return; } if ('json' !== $request->getContentTypeFormat()) { return; } try { $request->request->replace($request->toArray()); } catch (JsonException $e) { throw new BadRequestHttpException('Unable to parse request.', $e); } }
或者在操作中每次都解码它 🤔
现在您可以使用 WizzaqRestBundle
通过两种方式来自动化这个过程
⚠️ 默认配置启用协议处理
-
简单地在
config/routes.yaml
中将路由默认选项添加_rest: true
controllers: resource: ../src/Controller/Api/ type: attribute prefix: /api defaults: { _rest: true }
-
使用
Rest
参数。可以应用于类或方法<?php namespace App\Controller; use App\Form\Filter\MyEntityFilterType; use App\Form\Dto\Filter\MyEntityFilter; use App\Repository\MyEntityRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; use Wizzaq\RestBundle\Attribute\ProcessForm; use Wizzaq\RestBundle\Attribute\Rest; use Wizzaq\RestBundle\Exception\FormValidationException; class MyEntityController extends AbstractController { #[Route('/my-entity', methods: 'GET')] #[Rest(responseSerializationGroups: 'default')] public function list( #[ProcessForm(form: MyEntityFilterType::class, throwOnNotValid: false)] ?MyEntityFilter $filter = null, MyEntityRepository $repository ): array { return ['list' => $repository->list($filter ?? new MyEntityFilter())]; } #[Route('/my-entity/{id}', name: 'app.my_entity.get_or_update', methods: ['GET', 'POST'])] #[Route('/api/my-entity/{id}', name: 'api.my_entity.get_or_update', methods: ['GET', 'POST'])] #[Rest(routes: 'api.my_entity.get_or_update', responseSection: 'entity', responseSerializationGroups: ['with_related', 'default'])] #[Template('my_entity/edit.html.twig')] public function edit( #[ProcessForm(MyEntityType::class, throwOnNotValid: false)] MyEntity $entity, Request $request, RestConfig $restConfig, EntityManagerInterface $em ): array { $form = $restConfig->processedForm($request); if ($form->isSubmitted() && $form->isValid()) { // do some stuff here $em->persist($entity); $em->flush(); return $restConfig->isRest($request) ? ['entity' => $entity] : new RedirectResponse('/my-entity'); } elseif ($form->isSubmitted() && !$form->isValid() && $restConfig->isRest($request)) { throw new FormValidationException($form); } return [ 'form' => $form, 'entity' => $entity, ] } }
配置选项
完整的配置选项
# config/packages/wizzaq_rest.yaml wizzaq_rest: use_protocols: true # set to false to completely disable `ProtocolListener` default_protocol: null # uses first defined protocol by default protocols: # available protocols rest: true # only one simple protocol defined by bundle right now ^_^ default_response_section: null # if not null, then only defined section of returned result will be used as response for rest route serializer: null # serializer service id (autoselect by default from jms_serializer/serializer)
Rest参数选项
routes
:null
|string
|array
仅应用于定义的路由
protocol
:?string
要使用的协议名称
responseSection
:?string
仅使用返回结果的定义部分作为响应
responseSerializationGroups
:null
|string
|array
在通过协议序列化响应时传递定义的序列化组到序列化器
创建自己的协议
如果您想创建自己的协议,只需实现 Wizzaq\RestBundle\Protocol\NamedProtocolInterface
并通过自动配置将其标记为协议
namespace App\Protocol; use Wizzaq\RestBundle\Config\RestConfig; use Wizzaq\RestBundle\Protocol\NamedProtocolInterface; use Wizzaq\RestBundle\Protocol\RestProtocol; class MyProtocol extends RestProtocol implements NamedProtocolInterface { public function __construct(RestConfig $restConfig, bool $debug = false, $serializer = null) { parent::__construct($restConfig, $debug, $serializer); } public function getProtocolName(): string { return 'my_rest'; } public function processResponse($response, Request $request): Response { return parent::processResponse(['success' => true, 'data' => $response], $request); } }
或者实现 Wizzaq\RestBundle\Protocol\ProtocolInterface
并使用 alias
属性手动标记服务
# config/services.yaml ... services: ... App\Protocol\MyProtocol: tags: - { name: 'wizzaq_rest.protocol', alias: 'my_rest' }
然后您可以禁用不必要的默认协议
# config/packages/wizzaq_rest.yaml wizzaq_rest: default_protocol: 'my_rest' # not nessesary if you have only one protocol protocols: rest: false
附加功能
循环引用处理器
为 Symfony 序列化器 提供丢失的 CircularReferenceHandler。
# config/packages/framework.yaml framework: ... serializer: circular_reference_handler: 'wizzaq_rest.serializer.circular_reference_handler'