Skip to content

Commit eec695a

Browse files
Merge pull request #5 from dpfaffenbauer/claude/pimcore-studio-form-bundle-wwDly
[StudioFormBundle] introduce new dynamic form rendering bundle
2 parents 2fc8cab + dff36ff commit eec695a

File tree

1,769 files changed

+10340
-79531
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,769 files changed

+10340
-79531
lines changed

CLAUDE.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,5 +1061,148 @@ BundleX/Resources/assets/pimcore-studio/src/
10611061
└── main.ts # Registration in onInit()
10621062
```
10631063
1064+
## StudioFormBundle - Schema-Driven Form System
1065+
1066+
StudioFormBundle generates React forms from Symfony form types. Instead of manually building React forms, you define a Symfony FormType and the bundle auto-generates a JSON schema that the frontend renders dynamically.
1067+
1068+
### Data Flow
1069+
1070+
```
1071+
Symfony FormTypeFormSchemaGeneratorJSON SchemaFormSchemaAdapterFormBuilderDynamicForm (React)
1072+
```
1073+
1074+
### Backend (PHP)
1075+
1076+
#### Key Classes
1077+
1078+
| Class | Location | Purpose |
1079+
|-------|----------|---------|
1080+
| `FormSchemaGenerator` | `StudioFormBundle/Form/Schema/` | Converts Symfony FormView → JSON schema |
1081+
| `FormSchemaEnricherInterface` | `StudioFormBundle/Form/Schema/` | Extension point: add tabs, sections, hide fields |
1082+
| `BlockPrefixFormTypeRegistry` | `StudioFormBundle/Form/Schema/` | Maps Symfony block prefixes → form type classes |
1083+
| `RuleFormSchemaCollector` | `StudioFormBundle/Form/Schema/` | Collects schemas for rule engine form types |
1084+
| `FormSchemaController` | `StudioFormBundle/Controller/` | `GET /pimcore-studio/api/coreshop-studio-form/schema/{blockPrefix}` |
1085+
1086+
#### Enricher Pattern
1087+
1088+
Enrichers customize the generated schema per bundle (tagged `coreshop_studio_form.enricher`):
1089+
1090+
```php
1091+
class CartCreationSchemaEnricher implements FormSchemaEnricherInterface
1092+
{
1093+
public function supports(string $formTypeClass): bool
1094+
{
1095+
return $formTypeClass === CartCreationType::class;
1096+
}
1097+
1098+
public function enrich(FormSchema $schema, string $formTypeClass): FormSchema
1099+
{
1100+
$schema->addSection('base', 'Base', 10);
1101+
$schema->setFieldSection('currency', 'base');
1102+
return $schema;
1103+
}
1104+
}
1105+
```
1106+
1107+
Multiple enrichers process the same schema sequentially (priority-ordered). OrderBundle adds base sections, CoreBundle adds address/shipping/payment sections.
1108+
1109+
#### Service Tags
1110+
1111+
- `coreshop.studio_form` — registers a FormType in BlockPrefixFormTypeRegistry
1112+
- `coreshop_studio_form.enricher` — registers a FormSchemaEnricher (supports `priority`)
1113+
1114+
### Frontend (React/TypeScript)
1115+
1116+
#### Key Components
1117+
1118+
| Component | Location | Purpose |
1119+
|-----------|----------|---------|
1120+
| `WidgetRegistry` | `schema-adapter/WidgetRegistry.ts` | Maps block prefixes → React components |
1121+
| `FormSchemaAdapter` | `schema-adapter/FormSchemaAdapter.ts` | Converts JSON schema → FormBuilderConfig |
1122+
| `FormBuilder` | `form-builder/FormBuilder.ts` | Decorator-based config builder |
1123+
| `DynamicForm` | `form-builder/components/DynamicForm.tsx` | Renders form from config (tabs, sections, fields) |
1124+
| `SchemaForm` | `schema-adapter/SchemaForm.tsx` | Convenience: `useFormSchema` + `DynamicForm` |
1125+
1126+
#### Widget Resolution
1127+
1128+
WidgetRegistry resolves block prefixes right-to-left (most specific first), matching Symfony's Twig block resolution:
1129+
```
1130+
blockPrefixes: ['form', 'text', 'email'] → tries emailtextform
1131+
```
1132+
1133+
#### Default Widgets
1134+
1135+
Standard Symfony types are pre-mapped: `text`→Input, `textarea`→Input.TextArea, `integer`→InputNumber, `checkbox`→Switch, `choice`→Select/Radio/Checkbox, `date`→DatePicker, `collection`→CollectionWidget, `grid_collection`→GridCollectionWidget.
1136+
1137+
Custom widgets are registered via `WidgetRegistry.register(blockPrefix, resolver)`.
1138+
1139+
#### Schema Caching (Frontend)
1140+
1141+
`fetchFormSchema()` uses module-level caching + request deduplication. `preSeedSchemaCache()` for bulk pre-loading rule schemas.
1142+
1143+
#### Standard Decorators
1144+
1145+
Available from `@coreshop/studio-form/src/form-builder/decorators/`:
1146+
- `addFieldDecorator`, `removeFieldDecorator`, `transformFieldDecorator`
1147+
- `addSectionDecorator`, `sectionSortingDecorator`, `sectionFilterDecorator`
1148+
- `hiddenFieldsDecorator`, `readonlyDecorator`
1149+
- `addValidationDecorator`, `requiredFieldDecorator`
1150+
- `conditionalFieldsDecorator`, `groupFieldsDecorator`
1151+
1152+
#### Usage Example
1153+
1154+
```typescript
1155+
import { SchemaForm } from '@coreshop/studio-form'
1156+
1157+
// Simple: auto-generates form from Symfony FormType
1158+
<SchemaForm blockPrefix="coreshop_cart_creation" data={data} onChange={onChange} />
1159+
1160+
// With decorators:
1161+
const { builder, loading } = useFormSchema('coreshop_country', [
1162+
{ name: 'hide-field', decorator: removeFieldDecorator('internalCode') },
1163+
{ name: 'add-custom', decorator: addFieldDecorator({ name: 'custom', label: 'Custom', component: Input }) }
1164+
])
1165+
```
1166+
1167+
### File Structure
1168+
1169+
```
1170+
StudioFormBundle/
1171+
├── Form/Schema/
1172+
│ ├── FormSchema.php, FieldSchema.php # DTOs
1173+
│ ├── FormSchemaGenerator.php # Core engine
1174+
│ ├── FormSchemaEnricherInterface.php # Extension point
1175+
│ ├── BlockPrefixFormTypeRegistry.php # Block prefix → class map
1176+
│ └── RuleFormSchemaCollector.php # Multi-schema for rules
1177+
├── Controller/FormSchemaController.php # API endpoint
1178+
├── DependencyInjection/Compiler/
1179+
│ ├── RegisterFormSchemaEnricherPass.php
1180+
│ └── RegisterStudioFormTypesPass.php
1181+
└── Resources/assets/pimcore-studio/src/
1182+
├── schema-adapter/ # Schema → FormBuilder
1183+
│ ├── WidgetRegistry.ts, FormSchemaAdapter.ts
1184+
│ ├── api.ts, useFormSchema.ts, SchemaForm.tsx
1185+
│ └── defaultWidgets.ts
1186+
└── form-builder/ # FormBuilder + DynamicForm
1187+
├── FormBuilder.ts
1188+
├── components/DynamicForm.tsx
1189+
└── decorators/index.ts
1190+
```
1191+
1192+
### Bundle Integration Example
1193+
1194+
```yaml
1195+
# OrderBundle/Resources/config/services/forms.yml
1196+
services:
1197+
CoreShop\Bundle\OrderBundle\Form\Type\CartCreationType:
1198+
tags:
1199+
- { name: form.type }
1200+
- { name: coreshop.studio_form }
1201+
1202+
CoreShop\Bundle\OrderBundle\Form\Schema\CartCreationSchemaEnricher:
1203+
tags:
1204+
- { name: coreshop_studio_form.enricher, priority: 10 }
1205+
```
1206+
10641207
## Knowledge Graph
10651208
Use the knowledge-graph-mcp before and after every task you do.

composer.json

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,63 +26,64 @@
2626
}
2727
],
2828
"replace": {
29-
"coreshop/resource": "self.version",
3029
"coreshop/address": "self.version",
31-
"coreshop/configuration": "self.version",
32-
"coreshop/currency": "self.version",
33-
"coreshop/customer": "self.version",
34-
"coreshop/index": "self.version",
35-
"coreshop/locale": "self.version",
36-
"coreshop/notification": "self.version",
37-
"coreshop/order": "self.version",
38-
"coreshop/payment": "self.version",
39-
"coreshop/product": "self.version",
40-
"coreshop/registry": "self.version",
41-
"coreshop/rule": "self.version",
42-
"coreshop/sequence": "self.version",
43-
"coreshop/shipping": "self.version",
44-
"coreshop/store": "self.version",
45-
"coreshop/taxation": "self.version",
46-
"coreshop/core": "self.version",
47-
"coreshop/resource-bundle": "self.version",
4830
"coreshop/address-bundle": "self.version",
4931
"coreshop/admin-bundle": "self.version",
32+
"coreshop/class-definition-patch-bundle": "self.version",
33+
"coreshop/configuration": "self.version",
5034
"coreshop/configuration-bundle": "self.version",
35+
"coreshop/core": "self.version",
36+
"coreshop/core-bundle": "self.version",
37+
"coreshop/currency": "self.version",
5138
"coreshop/currency-bundle": "self.version",
39+
"coreshop/customer": "self.version",
5240
"coreshop/customer-bundle": "self.version",
5341
"coreshop/frontend-bundle": "self.version",
42+
"coreshop/index": "self.version",
5443
"coreshop/index-bundle": "self.version",
44+
"coreshop/inventory": "self.version",
45+
"coreshop/inventory-bundle": "self.version",
46+
"coreshop/locale": "self.version",
5547
"coreshop/locale-bundle": "self.version",
48+
"coreshop/menu-bundle": "self.version",
49+
"coreshop/messenger-bundle": "self.version",
5650
"coreshop/money-bundle": "self.version",
51+
"coreshop/notification": "self.version",
5752
"coreshop/notification-bundle": "self.version",
53+
"coreshop/optimistic-entity-lock-bundle": "self.version",
54+
"coreshop/order": "self.version",
5855
"coreshop/order-bundle": "self.version",
56+
"coreshop/payment": "self.version",
5957
"coreshop/payment-bundle": "self.version",
6058
"coreshop/payum-bundle": "self.version",
59+
"coreshop/payum-payment": "self.version",
60+
"coreshop/payum-payment-bundle": "self.version",
61+
"coreshop/pimcore": "self.version",
62+
"coreshop/pimcore-bundle": "self.version",
63+
"coreshop/product": "self.version",
6164
"coreshop/product-bundle": "self.version",
65+
"coreshop/registry": "self.version",
66+
"coreshop/resource": "self.version",
67+
"coreshop/resource-bundle": "self.version",
68+
"coreshop/rule": "self.version",
6269
"coreshop/rule-bundle": "self.version",
70+
"coreshop/seo": "self.version",
71+
"coreshop/seo-bundle": "self.version",
72+
"coreshop/sequence": "self.version",
6373
"coreshop/sequence-bundle": "self.version",
74+
"coreshop/shipping": "self.version",
6475
"coreshop/shipping-bundle": "self.version",
76+
"coreshop/storage-list": "self.version",
77+
"coreshop/store": "self.version",
6578
"coreshop/store-bundle": "self.version",
79+
"coreshop/studio-form-bundle": "self.version",
80+
"coreshop/taxation": "self.version",
6681
"coreshop/taxation-bundle": "self.version",
67-
"coreshop/tracking-bundle": "self.version",
68-
"coreshop/core-bundle": "self.version",
69-
"coreshop/pimcore": "self.version",
70-
"coreshop/storage-list": "self.version",
71-
"coreshop/inventory": "self.version",
72-
"coreshop/inventory-bundle": "self.version",
73-
"coreshop/workflow-bundle": "self.version",
74-
"coreshop/seo": "self.version",
75-
"coreshop/seo-bundle": "self.version",
76-
"coreshop/pimcore-bundle": "self.version",
77-
"coreshop/tracking": "self.version",
82+
"coreshop/test-bundle": "self.version",
7883
"coreshop/theme-bundle": "self.version",
79-
"coreshop/menu-bundle": "self.version",
80-
"coreshop/payum-payment": "self.version",
81-
"coreshop/payum-payment-bundle": "self.version",
82-
"coreshop/optimistic-entity-lock-bundle": "self.version",
83-
"coreshop/messenger-bundle": "self.version",
84-
"coreshop/class-definition-patch-bundle": "self.version",
85-
"coreshop/test-bundle": "self.version"
84+
"coreshop/tracking": "self.version",
85+
"coreshop/tracking-bundle": "self.version",
86+
"coreshop/workflow-bundle": "self.version"
8687
},
8788
"require": {
8889
"php": "^8.3",
@@ -94,6 +95,7 @@
9495
"doctrine/orm": "^3.0",
9596
"fakerphp/faker": "^1.16",
9697
"gedmo/doctrine-extensions": "^3.11",
98+
"jms/serializer": "^3.32",
9799
"jms/serializer-bundle": "^5.5",
98100
"knplabs/knp-menu-bundle": "^3.7",
99101
"payum/payum": "1.7.x-dev",

docs/03_Development/01_Extending_Guide/04_Extending_Rule_Actions.md

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,17 @@ core_shop_product:
118118
119119
## Pimcore Studio (React)
120120
121-
For the new Pimcore Studio UI, you need to create a React component instead of ExtJS:
121+
### Schema-Driven (Recommended — No Custom JS Needed)
122+
123+
**The service registration above is all you need for Pimcore Studio.** The `form-type` attribute in the service tag automatically makes the configuration form available in Studio. No React/TypeScript code is required.
124+
125+
The StudioFormBundle renders the PHP FormType (`CustomActionType`) as a React form at runtime. The `get-config` endpoint returns a `actionSchemaByType` mapping, and `registerSchemaComponentsFromConfig()` auto-generates the React component from the schema.
126+
127+
For details, see [StudioFormBundle — Rule Engine Integration](../14_Studio/02_Base_Infrastructure/05_StudioFormBundle_Examples.md#example-13--rule-conditionaction-as-schema-form).
128+
129+
### Hand-Written React Component (Only for Special UIs)
130+
131+
If your action needs custom interactive behavior that cannot be expressed as a Symfony FormType (e.g., complex multi-step wizards, drag-and-drop), you can still create a hand-written React component:
122132

123133
```typescript
124134
// src/CoreShop/Bundle/YourBundle/Resources/assets/pimcore-studio/src/modules/product-price-rules/actions/CustomAction.tsx
@@ -160,9 +170,7 @@ export const CustomAction: React.FC<ActionComponentProps> = ({
160170
}
161171
```
162172

163-
### Registering the React Action
164-
165-
Register the action in your bundle's main plugin file:
173+
Register the hand-written action in your bundle's main plugin file. Hand-written components take priority over schema-generated ones:
166174

167175
```typescript
168176
// src/CoreShop/Bundle/YourBundle/Resources/assets/pimcore-studio/src/main.ts
@@ -188,53 +196,3 @@ const plugin: IAbstractPlugin = {
188196
189197
export default plugin
190198
```
191-
192-
### Action Without Configuration
193-
194-
If your action doesn't need any configuration UI, you can use the built-in `EmptyAction`:
195-
196-
```typescript
197-
import { EmptyAction } from '@coreshop/rule/src/rules'
198-
199-
actionRegistry.register('customActionWithoutConfig', EmptyAction)
200-
```
201-
202-
### Available Form Components
203-
204-
The Studio UI uses Ant Design components. Commonly used form components:
205-
206-
- `InputNumber` - Number input with precision
207-
- `Input` - Text input
208-
- `Select` - Dropdown selection
209-
- `Checkbox` - Boolean values
210-
- `DatePicker` - Date selection
211-
- `Switch` - Toggle switch
212-
213-
### Using Entity Selects
214-
215-
For selecting entities (e.g., products, categories), use the `useEntitySelect` hook:
216-
217-
```typescript
218-
import { useEntitySelect } from '@coreshop/resource'
219-
import { productApi } from '@coreshop/product/src/modules/products/api'
220-
221-
export const CustomAction: React.FC<ActionComponentProps> = ({ data, onChange }) => {
222-
const productIds = data.products || []
223-
const [options, value, handleSelectChange, loading] = useEntitySelect(productApi, productIds)
224-
225-
const handleChange = (selectedIds: number[]) => {
226-
handleSelectChange(selectedIds)
227-
onChange({ ...data, products: selectedIds })
228-
}
229-
230-
return (
231-
<Select
232-
mode="multiple"
233-
value={value}
234-
onChange={handleChange}
235-
options={options}
236-
loading={loading}
237-
/>
238-
)
239-
}
240-
```

0 commit comments

Comments
 (0)