Skip to content

Commit dff36ff

Browse files
committed
[StudioForm] refactor order creation to use studio-form
1 parent 9de0047 commit dff36ff

File tree

22 files changed

+784
-597
lines changed

22 files changed

+784
-597
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",

src/CoreShop/Bundle/CoreBundle/Form/Extension/CartCreationTypeExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use CoreShop\Bundle\CoreBundle\Form\Type\AddressChoiceType;
2121
use CoreShop\Bundle\OrderBundle\Form\Type\CartCreationType;
22+
use CoreShop\Bundle\PaymentBundle\Form\Type\PaymentProviderChoiceType;
2223
use CoreShop\Bundle\ShippingBundle\Form\Type\CarrierChoiceType;
2324
use Symfony\Component\Form\AbstractTypeExtension;
2425
use Symfony\Component\Form\FormBuilderInterface;
@@ -35,6 +36,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
3536
'customer' => $options['customer'],
3637
]);
3738
$builder->add('carrier', CarrierChoiceType::class);
39+
$builder->add('paymentProvider', PaymentProviderChoiceType::class, [
40+
'required' => false,
41+
]);
3842
}
3943

4044
public static function getExtendedTypes(): iterable
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* CoreShop
7+
*
8+
* This source file is available under the terms of the
9+
* CoreShop Commercial License (CCL)
10+
* Full copyright and license information is available in
11+
* LICENSE.md which is distributed with this source code.
12+
*
13+
* @copyright Copyright (c) CoreShop GmbH (https://www.coreshop.com)
14+
* @license CoreShop Commercial License (CCL)
15+
*
16+
*/
17+
18+
namespace CoreShop\Bundle\CoreBundle\Form\Schema;
19+
20+
use CoreShop\Bundle\OrderBundle\Form\Type\CartCreationType;
21+
use CoreShop\Bundle\StudioFormBundle\Form\Schema\FormSchema;
22+
use CoreShop\Bundle\StudioFormBundle\Form\Schema\FormSchemaEnricherInterface;
23+
use CoreShop\Bundle\StudioFormBundle\Form\Schema\SectionSchema;
24+
25+
final class CartCreationSchemaEnricher implements FormSchemaEnricherInterface
26+
{
27+
private const array FIELD_SECTIONS = [
28+
'shippingAddress' => 'address',
29+
'invoiceAddress' => 'address',
30+
'carrier' => 'shipping',
31+
'paymentProvider' => 'payment',
32+
];
33+
34+
public function supports(string $formTypeClass): bool
35+
{
36+
return $formTypeClass === CartCreationType::class;
37+
}
38+
39+
public function enrich(FormSchema $schema, string $formTypeClass): FormSchema
40+
{
41+
$schema->sections = array_merge($schema->sections, [
42+
new SectionSchema('address', 'coreshop_order_creation_address', 30),
43+
new SectionSchema('shipping', 'coreshop_order_creation_shipping', 40),
44+
new SectionSchema('payment', 'coreshop_order_creation_payment', 50),
45+
]);
46+
47+
foreach ($schema->fields as $field) {
48+
if (isset(self::FIELD_SECTIONS[$field->name])) {
49+
$field->section = self::FIELD_SECTIONS[$field->name];
50+
}
51+
}
52+
53+
return $schema;
54+
}
55+
}

src/CoreShop/Bundle/CoreBundle/Form/Type/AddressChoiceType.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ public function __construct(
3636

3737
public function configureOptions(OptionsResolver $resolver): void
3838
{
39-
$resolver->setRequired('customer');
4039
$resolver
4140
->setDefaults(
4241
[
42+
'customer' => null,
4343
'choices' => function (Options $options) {
44+
if ($options['customer'] === null) {
45+
return [];
46+
}
47+
4448
/**
4549
* @var CustomerInterface $customer
4650
*/

src/CoreShop/Bundle/CoreBundle/Resources/assets/pimcore-studio/src/main.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { IAbstractPlugin, container } from '@pimcore/studio-ui-bundle'
1515
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
1616
import type { WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
1717
import { DynamicTypeObjectDataRegistry } from '@pimcore/studio-ui-bundle/modules/element'
18+
import { widgetRegistryServiceId } from '@coreshop/studio-form'
19+
import type { WidgetRegistry as StudioFormWidgetRegistry } from '@coreshop/studio-form'
1820
import { CoreBundleIconModule } from './modules/icon-library'
1921
import { DynamicTypeObjectDataCoreShopStoreValues } from './dynamic-types'
2022
import { CoreBundleMenuModule } from './modules/menu'
@@ -28,6 +30,8 @@ import { ReportsModule } from './modules/reports'
2830
import { SettingsModule } from './modules/settings'
2931
import { AssignToNewCompanyPanel, AssignToExistingCompanyPanel } from './modules/customer-company-assignment'
3032
import { PimcoreRelationWidgetModule } from './modules/pimcore-relation-widget'
33+
import { CustomerAddressSelectWidget } from './modules/extension/order-creation/widgets/CustomerAddressSelectWidget'
34+
import { PreviewCarrierSelectWidget } from './modules/extension/order-creation/widgets/PreviewCarrierSelectWidget'
3135

3236
const plugin: IAbstractPlugin = {
3337
name: 'coreshop-core',
@@ -39,6 +43,17 @@ const plugin: IAbstractPlugin = {
3943
)
4044

4145
objectDataRegistry.registerDynamicType(new DynamicTypeObjectDataCoreShopStoreValues())
46+
47+
// Register custom widgets for order creation schema forms
48+
const formWidgetRegistry = container.get<StudioFormWidgetRegistry>(widgetRegistryServiceId)
49+
50+
formWidgetRegistry.register('coreshop_customer_address_choice', () => ({
51+
component: CustomerAddressSelectWidget,
52+
}))
53+
54+
formWidgetRegistry.register('coreshop_carrier_choice', () => ({
55+
component: PreviewCarrierSelectWidget,
56+
}))
4257
},
4358

4459
onStartup({ moduleSystem }) {

0 commit comments

Comments
 (0)