w3r-one/json-schema-bundle
Composer 安装命令:
composer require w3r-one/json-schema-bundle
包简介
serialize Symfony Forms into JSON schema
关键字:
README 文档
README
A bundle to serialize a Symfony Form into a JSON Schema (RFC 2020-12).
Installation
$ composer require w3r-one/json-schema-bundle
If you're not using Symfony Flex, you've to register the bundle manually:
// config/bundles.php return [ // ... W3rOne\JsonSchemaBundle\W3rOneJsonSchemaBundle::class => ['all' => true], ];
Usage
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)); }
View the generated JSON Schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://localhost/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": "http://localhost/partner/json_schema", "async": true } } }
Purpose
The goal behind this bundle is based on the fact that it is complicated for a modern front-end application to maintain a form component that is not mapped directly on a Symfony FormType.
Most of the time, the front-end component is defining form's props in a static way and if the back-end wants to update the form, we need to work twice, it's error prone and it's not extensible at all.
The main idea is to give the lead to the back-end, provide a JSON schema dynamically that will detail the full component and its related documentation ; the front-end "just" have to display and handle the form on his side.
If the Form is changing or even if it's dynamic based on some roles / scopes / etc., the front-end developer have nothing to change.
It's also allow working with forms directly in Twig and in the same time in a Javascript context.
The business rules are not duplicated and are only handled by the back-end.
This bundle doesn't provide any Front-End component, feel free to choose the stack that feet your needs to build your own Javascript Form.
Logic
- Back-End
- Create your FormType as usual (it can include dynamic fiels, ACL, business rules, FormEvents, etc.)
- Extend it if needed (through
w3r_one_json_schemaor your own Transformers) - Optional: sending it to the view and display/test it directly in Twig
- Serialize the Form into a JSON Schema and send it to the view
- Optional: serialize the initial data as well to hydrate your form data
- Front-End
- Create the main form (
options.form.method+options.form.action) - Iterate recursively on all sub properties to create the complete form.
- Map each child with the correct JS component thanks to
options.widget(+options.layoutif needed) - Optional: hydrate each field value with initial data
- Handle submit on XHR (custom HTTP header
X-Requested-With: XMLHttpRequestthanks tooptions.form.async) or normally - Display errors if any - else a flash message / redirect the user
- Create the main form (
Concrete exemple
This example allow to handle a form directly in Twig without XHR AND with async Javascript, feel free to drop completely the twig/not async part.
<?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 is a simple reference to the EntityManagerInterface.
$this->apiSerializer is a simple service based on the Symfony Serializer.
View the service
<?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); } }
Architecture
The resolver will traverse the form, guess the right Transformer for each property and apply recursive transformations based on the following schema.
Legend
- Transformers
- Black: Base transformer
- Yellow: Json Schema native transformers
- Orange: Intermediate transformers
- White: Symfony Form Type' transformers
- Relations:
- Red: direct inheritance
- Blue: indirect inheritance depending on a specific FormType's option, e.g.
widgetfor the Date typesmultiplefor the Choice typeinputforNumberTypefractionalforPercentType
You can add your own transformers, or override/extend the transformers of your choice by yourself, see the dedicated section of this readme.
If needed, the form extension allow you to add custom props in w3r_one_json_schema to pass to your json specs.
Translator
This bundle relies on Symfony TranslatorInterface to translate:
label(withlabel_translation_parameters)help(withhelp_translation_parameters)- named enums (from
ChoiceType) attr.title(withattr_translation_parameters)attr.placeholder(withattr_translation_parameters)- error messages
The translation domain is dynamically retrieved from translation_domain option:
- disabled if
false === translation_domain - property scope if
null !== translation_domain - parent scope if any
translation_domainis found recursively
CSRF
If you've installed symfony/security-csrf and enabled crsf_protection on you FormType, the bundle will automatically add the correct csrf property (_token by default) with the default generated value (thanks to the TokenGeneratorInterface) in a hidden widget.
Built-in FormType
All Symfony FormTypes as 6.2 version are supported.
View the complete list
- 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
Supported JSON Schema specs
- $schema (
https://json-schema.org/draft/2020-12/schema) - $id (
{host}/schemas/{formType}.json) - type (
object|array|string|number|integer|bool) - title (FormType name at parent level,
labelFormType's option at child level or empty string if thelabelis set tofalse) - description (FormType
help_messageFormType's option) - properties (children properties)
- enum (constant values)
- readOnly (if
disabledFormType's option is set totrue) - writeOnly (if
mappedFormType's option is set tofalse) - default (if
dataFormType's option is defined) - uniqueItems (
truefor an array)
Unsupported JSON Schema specs (for now)
- required (need to be guessed from
Doctrinetype + potentialValidatorasserts) - minItems|maxItems (need to be guessed from assert
Count) - exclusiveMinimum|minimum|exclusiveMaximum|maximum (need to be guessed from assert
GreaterThanOrEqual,GreaterThan,LowerThanOrEqual&LowerThan) - minLength|maxLength (need to be guessed from
Doctrinetype + assertLength) - pattern (need to be guessed from assert
Regex) - schema composition
- format (?)
Additional JSON Schema specs
All non standards properties are wrapped into options property.
It includes:
widgetproperty to identify the widget behind the component- it's the FormType name in snake_case, basically
CustomCollectionTypewill give you the widgetcustom_collection - the only exception is related to all date's FormTypes where we suffix the javascript widget by the
widgetFormType's option. It can give you very different components:DateTypedate_choicedate_textdate_single_text
DateTimeTypedate_time_choicedate_time_textdate_time_single_text
TimeTypetime_choicetime_texttime_single_text
WeekTypeweek_choiceweek_textweek_single_text
BirthdayTypebirthday_choicebirthday_textbirthday_single_text
DateIntervalTypedate_interval_choicedate_interval_textdate_interval_integerdate_interval_single_text
- it's the FormType name in snake_case, basically
layoutproperty to apply a specific layout to the component (defaultdefault, can be overridden byw3r_one_json_schema.layout)attrall the HTML attributes defined in the FormType- at parent level, a
formproperty with:options.form.method(string: the method of your form - defaultPOST)options.form.action(string: the action behind your form - default current URI)options.form.async(bool: if you want your form in XMLHttpRequest - default true, can be overridden byw3r_one_json_schema.xmlHttpRequest)
- all others settings related to a specific FormType:
CheckboxTypeoptions.checkbox.value
ChoiceTypeoptions.choice.expandedoptions.choice.multipleoptions.choice.filterableoptions.choice.placeholderoptions.choice.preferredChoicesoptions.choice.enumTitles
CollectionTypeoptions.collection.allowAddoptions.collection.allowDelete
CountryTypeoptions.choice.alpha3
DateType/DateTimeType/TimeType/WeekType/BirthdayType/DateIntervalTypeoptions.date_time.formatoptions.date_time.inputoptions.date_time.inputFormatoptions.date_time.modelTimezoneoptions.date_time.placeholder
IntegerTypeoptions.integer.roundingMode
LanguageTypeoptions.choice.alpha3
MoneyTypeoptions.money.currencyoptions.money.divisoroptions.money.roundingModeoptions.money.scale
NumberTypeoptions.number.roundingModeoptions.number.scale
PasswordTypeoptions.password.alwaysEmpty
PercentTypeoptions.percent.symboloptions.percent.typeoptions.percent.roundingModeoptions.percent.scale
RadioTypeoptions.radio.valueoptions.radio.falseValues
If you want to pass other specific properties to your component, feel free to wrap them into w3r_one_json_schema property.
For example:
$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" } }, }
Override / Extend
You can totally override or extend any transformer / json specs of this bundle.
Widget / Layout resolving
In your FormTypes, you can override any widget / layout of your choice thanks to the w3r_one_json_schema option.
For example:
$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", } }, }
You can also override the default layout globally if needed:
# config/packages/w3r_one_json_schema.yaml w3r_one_json_schema: default_layout: 'fluid'
FormType Transformers
You can register your own transformers.
Tag them with the name w3r_one_json_schema.transformer and define the form_type you want to transform.
# config/services.yaml services: App\JsonSchema\DateIntervalTypeTransformer: parent: W3rOne\JsonSchemaBundle\Transformer\AbstractTransformer tags: - { name: w3r_one_json_schema.transformer, form_type: 'date_interval'}
Your transformers are resolved before ours, so if you override an existing transformer, it'll be executed in place of the bundle built-in ones.
Transformers must implement the TransformerInterface.
The proper approach is to extend one of ours abstract or specific transformers, redefine method transform, call the parent function and extending/overwriting the json schema before returning it.
You can also implement directly the interface, but you've to manage everything by yourself in this case.
Example 1
You're using VichUploaderBundle and you want to serialize specific options of this bundle.
Just extend the ObjectTransformer, call the parent function, embed your json props and voila!
<?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; } }
Example 2
You want to add a PositionType as an integer.
Here we just extend the correct base IntegerTransformer.
<?php namespace App\JsonSchema; use W3rOne\JsonSchemaBundle\Transformer\IntegerTransformer; class PositionTypeTransformer extends IntegerTransformer { }
Example 3
You want to override the TextareaType to replace it by a rich-text / wysiwyg editor.
<?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; } }
Note that a better approach would have been to use a WysiwygType and to create a specific WysiwygTypeTransformer.
w3r-one/json-schema-bundle 适用场景与选型建议
w3r-one/json-schema-bundle 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 9.23k 次下载、GitHub Stars 达 3, 最近一次更新时间为 2023 年 01 月 31 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「symfony」 「form」 「JSON Schema」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 w3r-one/json-schema-bundle 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 w3r-one/json-schema-bundle 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 w3r-one/json-schema-bundle 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Kinikit - PHP Application development framework MVC component
The bundle for easy using json-rpc api on your project
Diese Contao 4 Erweiterung stellt Google reCAPTCHA V2 in Form eines neuen Formularfeldes im Formulargenerator bereit. This extension provides Google reCAPTCHA V2 in the form of a new form field in the form generator of Contao Open Source CMS.
ext-json wrapper with sane defaults
A package to cast json fields, each sub-keys is castable
A Laravel Filament Forms slug field.
统计信息
- 总下载量: 9.23k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 3
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2023-01-31