w3r-one / json-schema-bundle
将 Symfony 表单序列化为 JSON 模式
Requires
- php: >=7.4
- doctrine/inflector: >=1.4
- symfony/config: >=4.4
- symfony/dependency-injection: >=4.4
- symfony/finder: >=4.4
- symfony/form: >=4.4
- symfony/http-foundation: >=4.4
- symfony/http-kernel: >=4.4
- symfony/intl: >=4.4
- symfony/security-csrf: >=4.4
- symfony/translation: >=4.4
Requires (Dev)
- phpunit/phpunit: ^8.0
Suggests
- symfony/security-core: For hashing users passwords.
- symfony/serializer: To serialize your initial data.
- symfony/validator: For form validation.
README
一个用于将 Symfony 表单 序列化为 JSON 模式(RFC 2020-12
)的包。
安装
$ composer require w3r-one/json-schema-bundle
如果你没有使用 Symfony Flex,你需要手动注册这个包
// config/bundles.php return [ // ... W3rOne\JsonSchemaBundle\W3rOneJsonSchemaBundle::class => ['all' => true], ];
使用
namespace App\Controller; use App\Entity\Partner; use App\Form\PartnerType; use W3rOne\JsonSchemaBundle\JsonSchema; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; class FormController extends AbstractController { public function partnerAdd(JsonSchema $jsonSchema): Response { $form = $this->createForm(PartnerType::class, new Partner(), ['validation_groups' => ['Default', 'Form-Partner']]); return new JsonResponse($jsonSchema($form)); }
查看生成的 JSON 模式
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https:///schemas/partner.json", "type": "object", "title": "partner", "properties": { "_token": { "type": "string", "title": "", "writeOnly": true, "default": "1996112795cc2bfa7d399fb.1rqGabut308UPJvtLSqXwgrrIXMqdei_M0T3DH53B50.tdzwLf-Atgdddf-qZF3dl127SABsENrMfiCdOAwRXvqBz_4Dz5SMfWMF6A", "options": { "widget": "hidden", "layout": "default" } }, "name": { "type": "string", "title": "Nom", "options": { "widget": "text", "layout": "default", } }, "types": { "type": "array", "title": "Types", "options": { "widget": "choice", "layout": "default", "attr": { "readonly": true }, "choice": { "expanded": true, "multiple": true, "filterable": true, "enumTitles": ["Client", "Fabricant", "Sous-traitant", "Installateur", "Fournisseur", "Concurrent", "Gestionnaire"] } }, "items": { "type": "string", "enum": ["customer", "manufacturer", "subcontractor", "installer", "supplier", "rival", "administrator"] } "uniqueItems": true }, "address": { "type": "object", "title": "Adresse", "options": { "widget": "address", "layout": "default" }, "properties": { "raw": { "type": "string", "title": "", "writeOnly": true, "options": { "widget": "text", "layout": "default", "attr": { "maxlength": 255, "placeholder": "Tapez une adresse" } } }, "formatted": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "coords": { "type": "object", "title": "", "options": { "widget": "coords", "layout": "default" }, "properties": { "lat": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "lng": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } } } }, "nb": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "street": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "zipcode": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "state": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "city": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } }, "country": { "type": "string", "title": "", "options": { "widget": "hidden", "layout": "default" } } } }, "url": { "type": "string", "title": "Site web", "options": { "widget": "url", "layout": "default" } }, "email": { "type": "string", "title": "Adresse email", "options": { "widget": "email", "layout": "default" } }, }, "required": [], "options": { "widget": "partner", "layout": "default", "form": { "method": "POST", "action": "https:///partner/json_schema", "async": true } } }
目的
这个包背后的目标是基于这样一个事实:对于现代前端应用来说,维护一个未直接映射到 Symfony FormType 的表单组件是比较复杂的。
大多数时候,前端组件以静态方式定义表单的属性,如果后端想要更新表单,我们需要做两次工作,这很容易出错,并且几乎不能扩展。
主要思想是让后端主导,提供动态的 JSON 模式,这将详细说明完整的组件及其相关文档;前端“只需”在其端显示和处理表单。
如果表单在变化,或者即使它是基于某些角色/范围等动态的,前端开发者也无需更改。
这也允许在 Twig 和 JavaScript 环境中直接处理表单。
业务规则不重复,且仅由后端处理。
这个包不提供任何前端组件,请根据您的需求选择合适的堆栈来构建自己的 JavaScript 表单。
逻辑
- 后端
- 像往常一样创建你的 FormType(它可以包含动态字段、ACL、业务规则、FormEvents 等)
- 如有必要,通过
w3r_one_json_schema
或你自己的转换器扩展它 - 可选:将其发送到视图并在 Twig 中直接显示/测试
- 将表单序列化为 JSON 模式并发送到视图
- 可选:也将初始数据序列化以初始化表单数据
- 前端
- 创建主表单(
options.form.method
+options.form.action
) - 递归遍历所有子属性以创建完整的表单。
- 通过
options.widget
将每个子映射到正确的 JS 组件(如果需要,还可以使用options.layout
) - 可选:使用初始数据初始化每个字段的值
- 处理 XHR 提交(通过
options.form.async
使用自定义 HTTP 标头X-Requested-With: XMLHttpRequest
)或正常处理 - 如果有错误,显示错误 - 否则显示闪存消息/重定向用户
- 创建主表单(
具体示例
这个示例允许直接在 Twig 中处理表单(无需 XHR)并且具有异步 JavaScript,你可以完全删除 twig/not async 部分。
<?php namespace App\Controller; use App\Entity\Partner; use App\Form\PartnerType; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use W3rOne\JsonSchemaBundle\JsonSchema; use W3rOne\JsonSchemaBundle\Utils; class PartnerController extends AppAbstractController { /** * @Entity("partner", expr="repository.findOne(partnerId)") */ public function edit(Partner $partner, JsonSchema $jsonSchema, Request $request): Response { $form = $this->createForm(PartnerType::class, $partner, ['validation_groups' => ['Default', 'Form-Partner'], 'scope' => 'edit'])->handleRequest($request); if ($form->isSubmitted()) { if ($form->isValid()) { $this->em->flush(); if ($request->isXmlHttpRequest()) { return new JsonResponse([ 'message' => 'The partner was successfully updated.', 'redirect_url' => $this->generateUrl('app_partner_show', ['partnerId' => $partner->getId()]), ], Response::HTTP_OK); } else { $this->addFlash('success', 'The partner was successfully updated.'); return $this->redirectToRoute('app_partner_show', ['partnerId' => $partner->getId()]); } } else { if ($request->isXmlHttpRequest()) { return new JsonResponse([ 'message' => 'There are errors in the form, please check.', 'errors' => Utils::getErrors($form), ], Response::HTTP_BAD_REQUEST); } else { $this->addFlash('error', 'There are errors in the form, please check.'); } } } return $this->render('pages/partner/edit.html.twig', [ 'form' => $form->createView(), 'partner' => $partner, 'pageProps' => \json_encode([ 'form' => $jsonSchema($form), 'errors' => Utils::getErrors($form), 'partner' => \json_decode($this->apiSerializer->serialize($partner, ['default', 'partner'])), ]), ]); } }
$this->em
是对 EntityManagerInterface
的简单引用。
$this->apiSerializer
是基于 Symfony Serializer 的一个简单服务。
查看服务
<?php namespace App\Serializer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; class ApiSerializer { private $serializer; public function __construct(SerializerInterface $serializer) { $this->serializer = $serializer; } public function serialize($data, array $groups = ['default'], array $attributes = [], array $callbacks = []): string { $context = [ AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true, AbstractNormalizer::GROUPS => $groups, ]; if (!empty($attributes)) { $context[AbstractNormalizer::ATTRIBUTES] = $attributes; } if (!empty($callbacks)) { $context[AbstractNormalizer::CALLBACKS] = $callbacks; } return $this->serializer->serialize($data, 'json', $context); } }
架构
解析器 resolver 将遍历表单,猜测每个属性的适当转换器,并根据以下模式应用递归转换。
图例
- 转换器
- 黑色:基本转换器
- 黄色:JSON Schema 原生转换器
- 橙色:中间转换器
- 白:Symfony 表单类型转换器
- 关系
- 红:直接继承
- 蓝:根据特定表单类型的选项间接继承,例如
widget
用于日期类型multiple
用于选择类型input
用于NumberType
fractional
用于PercentType
您可以添加自己的转换器,或者通过自己覆盖/扩展您选择的转换器,请参阅本readme的专用部分。
如果需要,可以添加自定义属性的 表单扩展,将数据传递到您的json规范中。
翻译器
此包依赖于Symfony TranslatorInterface
进行翻译
label
(带有label_translation_parameters
)help
(带有help_translation_parameters
)- 命名枚举(来自
ChoiceType
) attr.title
(带有attr_translation_parameters
)attr.placeholder
(带有attr_translation_parameters
)- 错误信息
翻译域从 translation_domain
选项动态检索
- 如果
false === translation_domain
,则禁用 - 如果
null !== translation_domain
,则属性作用域 - 如果找到任何
translation_domain
,则父级作用域
CSRF
如果您已安装 symfony/security-csrf
并在您的 FormType 上启用了 crsf_protection
,则该包将自动在 hidden
小部件中添加正确的 csrf 属性(默认为 _token
)以及默认生成的值(归功于 TokenGeneratorInterface
)。
内置表单类型
所有 6.2 版本的 Symfony FormTypes 都受支持。
查看完整列表
- TextType
- TextareaType
- EmailType
- PasswordType
- SearchType
- UrlType
- TelType
- ColorType
- FileType
- RadioType
- UuidType
- UlidType
- HiddenType
- IntegerType
- MoneyType
- NumberType
- PercentType
- RangeType
- ChoiceType
- EnumType
- EntityType
- CountryType
- LanguageType
- LocaleType
- TimezoneType
- CurrencyType
- DateType
- DateTimeType
- TimeType
- WeekType
- BirthdayType
- DateIntervalType
- CollectionType
- CheckboxType
- ButtonType
- ResetType
- SubmitType
- RepeatedType
支持的 JSON Schema 规范
- $schema (
https://json-schema.fullstack.org.cn/draft/2020-12/schema
) - $id (
{host}/schemas/{formType}.json
) - type (
object
|array
|string
|number
|integer
|bool
) - title (父级别 FormType 名称,子级别 FormType 的
label
选项或空字符串,如果label
设置为false
) - description (FormType 的
help_message
选项) - properties (子属性)
- enum (常量值)
- readOnly(如果
disabled
FormType 的选项设置为true
) - writeOnly(如果
mapped
FormType 的选项设置为false
) - 默认值(如果定义了FormType的data选项)
- uniqueItems(对于数组为true)
不支持的JSON Schema规范(目前不支持)
- required(需要从
Doctrine
类型+潜在的Validator
断言中猜测) - minItems|maxItems(需要从断言
Count
中猜测) - exclusiveMinimum|minimum|exclusiveMaximum|maximum(需要从断言
GreaterThanOrEqual
、GreaterThan
、LowerThanOrEqual
和LowerThan
中猜测) - minLength|maxLength(需要从
Doctrine
类型+断言Length
中猜测) - pattern(需要从断言
Regex
中猜测) - schema组合
- 格式(?)
额外的JSON Schema规范
所有非标准属性都包含在options
属性中。
它包括
- 用于识别组件后端小部件的
widget
属性- 它是snake_case形式的FormType名称,基本上
CustomCollectionType
将提供小部件custom_collection
- 唯一的例外是所有日期的FormType,我们通过
widget
FormType的选项在javascript小部件后附加后缀。它可以提供非常不同的组件DateType
date_choice
date_text
date_single_text
DateTimeType
date_time_choice
date_time_text
date_time_single_text
TimeType
time_choice
time_text
time_single_text
WeekType
week_choice
week_text
week_single_text
BirthdayType
birthday_choice
birthday_text
birthday_single_text
DateIntervalType
date_interval_choice
date_interval_text
date_interval_integer
date_interval_single_text
- 它是snake_case形式的FormType名称,基本上
- 用于将特定布局应用到组件上的
layout
属性(默认default
,可由w3r_one_json_schema.layout
覆盖) attr
FormType中定义的所有HTML属性- 在父级,一个包含以下内容的
form
属性options.form.method
(字符串:表单的方法 - 默认POST
)options.form.action
(字符串:表单背后的操作 - 默认当前URI)options.form.async
(布尔值:如果您希望表单在XMLHttpRequest中 - 默认true,可由w3r_one_json_schema.xmlHttpRequest
覆盖)
- 与特定FormType相关的所有其他设置
CheckboxType
options.checkbox.value
ChoiceType
options.choice.expanded
options.choice.multiple
options.choice.filterable
options.choice.placeholder
options.choice.preferredChoices
options.choice.enumTitles
CollectionType
options.collection.allowAdd
options.collection.allowDelete
CountryType
options.choice.alpha3
DateType/DateTimeType/TimeType/WeekType/BirthdayType/DateIntervalType
options.date_time.format
options.date_time.input
options.date_time.inputFormat
options.date_time.modelTimezone
options.date_time.placeholder
IntegerType
options.integer.roundingMode
LanguageType
options.choice.alpha3
MoneyType
options.money.currency
options.money.divisor
options.money.roundingMode
options.money.scale
NumberType
options.number.roundingMode
options.number.scale
PasswordType
options.password.alwaysEmpty
PercentType
options.percent.symbol
options.percent.type
options.percent.roundingMode
options.percent.scale
RadioType
options.radio.value
options.radio.falseValues
如果您想将其他特定属性传递给您的组件,请随意将它们包含在w3r_one_json_schema
属性中。
例如
$builder ->add('name', TextType::class, [ 'label' => 'Name', 'w3r_one_json_schema' => [ 'foo' => 'bar', ], ]);
{ "name": { "type": "string", "title": "Name", "options": { "widget": "text", "layout": "default", "foo": "bar" } }, }
覆盖/扩展
您可以完全覆盖或扩展此捆绑包的任何转换器/JSON规范。
小部件/布局解析
在您的FormTypes中,您可以通过w3r_one_json_schema
选项覆盖您选择的任何widget
/ layout
。
例如
$builder ->add('address', TextType::class, [ 'label' => 'Address', 'w3r_one_json_schema' => [ 'widget' => 'google_autocomplete', 'layout' => 'two-cols', ], ]);
{ "address": { "type": "string", "title": "address", "options": { "widget": "google_autocomplete", "layout": "two-cols", } }, }
如果需要,您还可以全局覆盖默认的layout
# config/packages/w3r_one_json_schema.yaml w3r_one_json_schema: default_layout: 'fluid'
FormType转换器
您可以注册自己的转换器。
使用名称 w3r_one_json_schema.transformer
标记它们,并定义您要转换的 form_type
。
# config/services.yaml services: App\JsonSchema\DateIntervalTypeTransformer: parent: W3rOne\JsonSchemaBundle\Transformer\AbstractTransformer tags: - { name: w3r_one_json_schema.transformer, form_type: 'date_interval'}
您的转换器将在我们之前解析,因此如果您覆盖了现有的转换器,它将替换内置的bundle转换器执行。
转换器 必须 实现 TransformerInterface。
正确的做法是扩展我们之一抽象或具体的转换器,重新定义方法 transform
,调用父函数,并在返回之前扩展/覆盖json模式。
您也可以直接实现该接口,但在此情况下您必须自己管理一切。
示例 1
您正在使用 VichUploaderBundle,并希望序列化此bundle的特定选项。
只需扩展 ObjectTransformer,调用父函数,嵌入您的json属性即可!
<?php namespace App\JsonSchema; use Symfony\Component\Form\FormInterface; use W3rOne\JsonSchemaBundle\Transformer\ObjectTransformer; use W3rOne\JsonSchemaBundle\Utils; class VichFileTypeTransformer extends ObjectTransformer { public function transform(FormInterface $form): array { $schema = parent::transform($form); $schema['options']['vichFile'] = [ 'allowDelete' => $form->getConfig()->getOption('allow_delete'), 'downloadLink' => $form->getConfig()->getOption('download_link'), 'downloadUri' => $form->getConfig()->getOption('download_uri'), 'downloadLabel' => $this->translator->trans($form->getConfig()->getOption('download_label'), [], Utils::getTranslationDomain($form)), 'deleteLabel' => $this->translator->trans($form->getConfig()->getOption('delete_label'), [], Utils::getTranslationDomain($form)), ]; return $schema; } }
示例 2
您想要添加一个 PositionType
作为 integer
。
这里我们只是扩展了正确的基 IntegerTransformer
。
<?php namespace App\JsonSchema; use W3rOne\JsonSchemaBundle\Transformer\IntegerTransformer; class PositionTypeTransformer extends IntegerTransformer { }
示例 3
您想要覆盖 TextareaType
以替换为富文本/wysiwyg编辑器。
<?php namespace App\JsonSchema; use Symfony\Component\Form\FormInterface; use W3rOne\JsonSchemaBundle\Transformer\Type\TextareaTypeTransformer as BaseTextAreaTypeTransformer; class TextareaTypeTransformer extends BaseTextAreaTypeTransformer { public function transform(FormInterface $form): array { $schema = parent::transform($form); $schema['options']['widget'] = 'wysiwyg'; $schema['options']['wysiwyg'] = [ 'config' => [ // ... ], ]; return $schema; } }
请注意,更好的方法可能是使用 WysiwygType
并创建一个特定的 WysiwygTypeTransformer
。