w3r-one/json-schema-bundle

将 Symfony 表单序列化为 JSON 模式

安装次数: 1,581

依赖项: 0

建议者: 0

安全性: 0

星标: 3

关注者: 3

分支: 0

开放问题: 6

类型:symfony-bundle

1.1.0 2024-01-02 12:44 UTC

This package is auto-updated.

Last update: 2024-09-10 09:33:58 UTC


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 将遍历表单,猜测每个属性的适当转换器,并根据以下模式应用递归转换。

Architecture

图例

  • 转换器
    • 黑色:基本转换器
    • 黄色: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 都受支持。

查看完整列表

支持的 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(需要从断言GreaterThanOrEqualGreaterThanLowerThanOrEqualLowerThan中猜测)
  • 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
  • 用于将特定布局应用到组件上的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