1"use strict";(self.webpackChunkcoreshop_docs=self.webpackChunkcoreshop_docs||[]).push([[3666],{13315:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>c,default:()=>h,frontMatter:()=>s,metadata:()=>t,toc:()=>l});const t=JSON.parse('{"id":"CoreShop/Development/Studio/Base_Infrastructure/RuleBundle","title":"RuleBundle","description":"The RuleBundle provides the infrastructure for all rule-based features in CoreShop (Cart Price Rules, Product Price Rules, Shipping Rules, etc.). It includes the RuleApi, Registry system, and reusable UI components.","source":"@site/versioned_docs/version-5.1/01_CoreShop/03_Development/14_Studio/02_Base_Infrastructure/02_RuleBundle.md","sourceDirName":"01_CoreShop/03_Development/14_Studio/02_Base_Infrastructure","slug":"/CoreShop/Development/Studio/Base_Infrastructure/RuleBundle","permalink":"/5.1/CoreShop/Development/Studio/Base_Infrastructure/RuleBundle","draft":false,"unlisted":false,"tags":[],"version":"5.1","sidebarPosition":2,"frontMatter":{},"sidebar":"defaultSidebar","previous":{"title":"ResourceBundle","permalink":"/5.1/CoreShop/Development/Studio/Base_Infrastructure/ResourceBundle"},"next":{"title":"FormBuilder Pattern","permalink":"/5.1/CoreShop/Development/Studio/Base_Infrastructure/FormBuilder"}}');var r=i(74848),o=i(28453);const s={},c="RuleBundle",a={},l=[{value:"Schema-Driven Rule Forms (Default)",id:"schema-driven-rule-forms-default",level:2},{value:"How It Works",id:"how-it-works",level:3},{value:"What Bundles Still Do in JS",id:"what-bundles-still-do-in-js",level:3},{value:"RuleApi Base Class",id:"ruleapi-base-class",level:2},{value:"Basic Usage",id:"basic-usage",level:3},{value:"Additional Methods",id:"additional-methods",level:3},{value:"<code>getConfig()</code> - Get Rule Configuration",id:"getconfig---get-rule-configuration",level:4},{value:"Registry Pattern",id:"registry-pattern",level:2},{value:"ConditionRegistry",id:"conditionregistry",level:3},{value:"ActionRegistry",id:"actionregistry",level:3},{value:"Multiple Registries",id:"multiple-registries",level:3},{value:"Schema Registration Functions",id:"schema-registration-functions",level:2},{value:"<code>registerSchemaComponentsFromConfig()</code>",id:"registerschemacomponentsfromconfig",level:3},{value:"<code>registerSchemaComponentsFromMaps()</code>",id:"registerschemacomponentsfrommaps",level:3},{value:"<code>createSchemaCondition()</code> / <code>createSchemaAction()</code>",id:"createschemacondition--createschemaaction",level:3},{value:"Rule Components",id:"rule-components",level:2},{value:"RuleManager",id:"rulemanager",level:3},{value:"Recommended Pattern: EntityTabbedManager + RuleForm",id:"recommended-pattern-entitytabbedmanager--ruleform",level:3},{value:"RuleForm",id:"ruleform",level:3},{value:"ConditionsPanel / ActionsPanel",id:"conditionspanel--actionspanel",level:3},{value:"Components That Still Need Custom JS",id:"components-that-still-need-custom-js",level:2},{value:"Adding a New Condition/Action (Preferred Approach)",id:"adding-a-new-conditionaction-preferred-approach",level:2},{value:"Step 1: Create the PHP FormType",id:"step-1-create-the-php-formtype",level:3},{value:"Step 2: Register the Service with <code>form-type</code>",id:"step-2-register-the-service-with-form-type",level:3},{value:"Step 3: Done",id:"step-3-done",level:3},{value:"When You Need Custom JS (Rare)",id:"when-you-need-custom-js-rare",level:3},{value:"Best Practices",id:"best-practices",level:2},{value:"1. Prefer Schema-Driven Components",id:"1-prefer-schema-driven-components",level:3},{value:"2. Separate Registries Per Rule Type",id:"2-separate-registries-per-rule-type",level:3},{value:"3. Bind Registries in onInit",id:"3-bind-registries-in-oninit",level:3},{value:"4. Schema Cache is Automatic",id:"4-schema-cache-is-automatic",level:3},{value:"Real-World Examples",id:"real-world-examples",level:2}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"rulebundle",children:"RuleBundle"})}),"\n",(0,r.jsx)(n.p,{children:"The RuleBundle provides the infrastru
1cture for all rule-based features in CoreShop (Cart Price Rules, Product Price Rules, Shipping Rules, etc.). It includes the RuleApi, Registry system, and reusable UI components."}),"\n",(0,r.jsx)(n.h2,{id:"schema-driven-rule-forms-default",children:"Schema-Driven Rule Forms (Default)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Most rule conditions and actions no longer require custom JavaScript/React code."})," The StudioFormBundle automatically renders configuration forms from PHP FormTypes. When you register a condition or action with a ",(0,r.jsx)(n.code,{children:"form-type"})," attribute in its service tag, the frontend auto-generates the React component from the backend schema."]}),"\n",(0,r.jsx)(n.p,{children:"This means:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Adding a new condition/action"})," = PHP FormType + service tag. No React code needed."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The frontend calls ",(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromConfig()"})]})," which creates React components dynamically from the ",(0,r.jsx)(n.code,{children:"conditionSchemaByType"})," / ",(0,r.jsx)(n.code,{children:"actionSchemaByType"})," maps returned by the ",(0,r.jsx)(n.code,{children:"get-config"})," endpoint."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Only special components"})," that need custom UI behavior (e.g., ",(0,r.jsx)(n.code,{children:"NestedCondition"}),", ",(0,r.jsx)(n.code,{children:"TimespanCondition"}),") still require hand-written React components."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["For the full pattern with examples, see ",(0,r.jsx)(n.a,{href:"/5.1/CoreShop/Development/Studio/Base_Infrastructure/StudioFormBundle#rule-form-integrations",children:"StudioFormBundle Examples \u2014 Rule Engine Integration"})," and ",(0,r.jsx)(n.a,{href:"/5.1/CoreShop/Development/Studio/Base_Infrastructure/StudioFormBundle_Examples#example-13--rule-conditionaction-as-schema-form",children:"Example 13"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"how-it-works",children:"How It Works"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"PHP FormType defines the condition/action configuration fields"}),"\n",(0,r.jsxs)(n.li,{children:["Service tag includes the ",(0,r.jsx)(n.code,{children:"form-type"})," attribute pointing to the FormType class"]}),"\n",(0,r.jsxs)(n.li,{children:["A ",(0,r.jsx)(n.code,{children:"RegisterFormTypesFromTagsPass"})," compiler pass collects the form types and registers them in the block prefix registry"]}),"\n",(0,r.jsxs)(n.li,{children:["The ",(0,r.jsx)(n.code,{children:"get-config"})," endpoint returns ",(0,r.jsx)(n.code,{children:"conditionSchemaByType"})," / ",(0,r.jsx)(n.code,{children:"actionSchemaByType"})," maps plus embedded ",(0,r.jsx)(n.code,{children:"schemas"})]}),"\n",(0,r.jsxs)(n.li,{children:["The frontend calls ",(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromConfig()"})," which uses ",(0,r.jsx)(n.code,{children:"createSchemaCondition()"})," / ",(0,r.jsx)(n.code,{children:"createSchemaAction()"})," to generate React wrappers around ",(0,r.jsx)(n.code,{children:"SchemaForm"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"preSeedSchemaCache()"})," is called automatically inside ",(0,r.jsx)(n.code,{children:"RuleApi.getConfig()"})," with the embedded schemas"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"what-bundles-still-do-in-js",children:"What Bundles Still Do in JS"}),"\n",(0,r.jsxs)(n.p,{children:["Each bundle's ",(0,r.jsx)(n.code,{children:"main.ts"})," still:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Creates and binds registries"})," (ConditionRegistry, ActionRegistry)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Registers special components"})," that cannot be schema-driven (nested, timespan, etc.)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Hides rule collection block prefixes"})," in the widget registry (so they don't render inside generic entity forms)"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Schema-based components are registered at runtime when ",(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromConfig()"})," is called, not at plugin init time."]}),"\n",(0,r.jsx)(n.h2,{id:"ruleapi-base-class",children:"RuleApi Base Class"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"RuleApi"})," class extends ",(0,r.jsx)(n.code,{children:"EntityApi"})," and adds rule-specific functionality."]}),"\n",(0,r.jsx)(n.h3,{id:"basic-usage",children:"Basic Usage"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { RuleApi } from '@coreshop/rule/src/rules'\nimport type { CartPriceRule }
1 from './types'\n\nexport class CartPriceRuleApi extends RuleApi<CartPriceRule> {}\n\nexport const cartPriceRuleApi = new CartPriceRuleApi({\n basePath: '/pimcore-studio/api',\n resourcePath: '/coreshop/cart_price_rules'\n})\n"})}),"\n",(0,r.jsx)(n.h3,{id:"additional-methods",children:"Additional Methods"}),"\n",(0,r.jsx)(n.p,{children:"In addition to all EntityApi methods (list, get, add, save, delete), RuleApi provides:"}),"\n",(0,r.jsxs)(n.h4,{id:"getconfig---get-rule-configuration",children:[(0,r.jsx)(n.code,{children:"getConfig()"})," - Get Rule Configuration"]}),"\n",(0,r.jsxs)(n.p,{children:["Fetches available conditions, actions, and their schema mappings. ",(0,r.jsxs)(n.strong,{children:["Automatically calls ",(0,r.jsx)(n.code,{children:"preSeedSchemaCache()"})]})," with the embedded schemas."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"const config = await cartPriceRuleApi.getConfig()\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Response Format:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"interface RuleConfig {\n conditions: string[] // Available condition type keys\n actions: string[] // Available action type keys\n conditionSchemaByType: Record<string, string> // type -> block prefix mapping\n actionSchemaByType: Record<string, string> // type -> block prefix mapping\n schemas: Record<string, FormSchema> // Embedded schemas (auto-seeded)\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"registry-pattern",children:"Registry Pattern"}),"\n",(0,r.jsx)(n.p,{children:"The Registry Pattern allows bundles to register their rule conditions and actions dynamically."}),"\n",(0,r.jsx)(n.h3,{id:"conditionregistry",children:"ConditionRegistry"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { ConditionRegistry } from '@coreshop/rule/src/rules/registry'\nimport { container } from '@pimcore/studio-ui-bundle'\n\n// Bind registry as singleton\ncontainer.bind(serviceIds.conditionRegistry)\n .to(ConditionRegistry)\n .inSingletonScope()\n\n// Get registry\nconst conditionRegistry = container.get<ConditionRegistry>(\n serviceIds.conditionRegistry\n)\n\n// Register a hand-written condition (only for special components)\nconditionRegistry.register('nested', NestedCondition)\n\n// Schema-based conditions are registered automatically via registerSchemaComponentsFromConfig()\n"})}),"\n",(0,r.jsx)(n.h3,{id:"actionregistry",children:"ActionRegistry"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { ActionRegistry } from '@coreshop/rule/src/rules/registry'\n\n// Bind registry as singleton\ncontainer.bind(serviceIds.actionRegistry)\n .to(ActionRegistry)\n .inSingletonScope()\n\n// Get registry\nconst actionRegistry = container.get<ActionRegistry>(\n serviceIds.actionRegistry\n)\n\n// Schema-based actions are registered automatically via registerSchemaComponentsFromConfig()\n"})}),"\n",(0,r.jsx)(n.h3,{id:"multiple-registries",children:"Multiple Registries"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Important:"})," Each rule type has its own registries!"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"// Product Price Rules\nconst productConditionRegistry = container.get<ConditionRegistry>(\n coreshopProductServiceIds.productPriceRuleConditionRegistry\n)\n\n// Cart Price Rules\nconst cartConditionRegistry = container.get<ConditionRegistry>(\n coreshopOrderServiceIds.cartPriceRuleConditionRegistry\n)\n"})}),"\n",(0,r.jsx)(n.h2,{id:"schema-registration-functions",children:"Schema Registration Functions"}),"\n",(0,r.jsx)(n.h3,{id:"registerschemacomponentsfromconfig",children:(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromConfig()"})}),"\n",(0,r.jsxs)(n.p,{children:["Auto-registers schema-driven components from a ",(0,r.jsx)(n.code,{children:"RuleConfig"})," response:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { registerSchemaComponentsFromConfig } from '@coreshop/rule/src/rules/registry'\n\nconst config = await ruleApi.getConfig()\nregisterSchemaComponentsFromConfig(conditionRegistry, actionRegistry, config)\n"})}),"\n",(0,r.jsxs)(n.p,{children:["This iterates over ",(0,r.jsx)(n.code,{children:"conditionSchemaByType"})," and ",(0,r.jsx)(n.code,{children:"actionSchemaByType"}),", creating ",(0,r.jsx)(n.code,{children:"SchemaCondition"})," / ",(0,r.jsx)(n.code,{children:"SchemaAction"})," wrappers. It ",(0,r.jsx)(n.strong,{children:"does not overwrite"})," hand-written components already registered in the registry."]}),"\n",(0,r.jsx)(n.h3,{id:"registerschemacomponentsfrommaps",children:(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromMaps()"})}),"\n",(0,r.jsx)(n.p,{children:"Lower-level function if you need direct control:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { registerSchemaComponentsFromMaps } from '@coreshop/rule/src/rules/registry'\n\nregisterSchemaComponentsFromMaps(\n conditionRegistry,\n actionRegistry,\n conditionSchemaByType,\n actionSchemaByType,\n schemas,\n { over
1writeExisting: false }\n)\n"})}),"\n",(0,r.jsxs)(n.h3,{id:"createschemacondition--createschemaaction",children:[(0,r.jsx)(n.code,{children:"createSchemaCondition()"})," / ",(0,r.jsx)(n.code,{children:"createSchemaAction()"})]}),"\n",(0,r.jsxs)(n.p,{children:["Factory functions that create a React component wrapping ",(0,r.jsx)(n.code,{children:"SchemaForm"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { createSchemaCondition, createSchemaAction } from '@coreshop/rule/src/rules/components'\n\n// Manual registration (usually not needed \u2014 registerSchemaComponentsFromConfig does this)\nconditionRegistry.register('amount', createSchemaCondition('coreshop_cart_price_rule_condition_amount'))\nactionRegistry.register('surchargePercent', createSchemaAction('coreshop_cart_price_rule_action_surcharge_percent'))\n"})}),"\n",(0,r.jsx)(n.h2,{id:"rule-components",children:"Rule Components"}),"\n",(0,r.jsx)(n.h3,{id:"rulemanager",children:"RuleManager"}),"\n",(0,r.jsxs)(n.p,{children:["A simple list+detail component built on ",(0,r.jsx)(n.code,{children:"EntitySplitManager"}),". Loads config automatically."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"interface RuleManagerProps<T extends Rule> {\n api: RuleApi<T>\n renderForm: (rule: T, config: RuleConfig, onSave: (rule: T) => Promise<void>, onChange: (rule: T) => void) => React.ReactNode\n createEmptyRule: () => T\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Note:"})," ",(0,r.jsx)(n.code,{children:"RuleManager"})," does ",(0,r.jsx)(n.strong,{children:"not"})," auto-register schema components. You must call ",(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromConfig()"})," yourself in the ",(0,r.jsx)(n.code,{children:"renderForm"})," callback or in a ",(0,r.jsx)(n.code,{children:"useEffect"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"recommended-pattern-entitytabbedmanager--ruleform",children:"Recommended Pattern: EntityTabbedManager + RuleForm"}),"\n",(0,r.jsxs)(n.p,{children:["In practice, most rule managers use ",(0,r.jsx)(n.code,{children:"EntityTabbedManager"})," from ResourceBundle (for tabbed detail views with dirty tracking) rather than ",(0,r.jsx)(n.code,{children:"RuleManager"}),". Here's the actual pattern used:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { EntityTabbedManager } from '@coreshop/resource'\nimport { RuleForm, registerSchemaComponentsFromConfig } from '@coreshop/rule/src/rules'\nimport type { RuleConfig } from '@coreshop/rule/src/rules'\n\nexport const ShippingRuleManager: React.FC = () => {\n const { t } = useTranslation()\n const messageApi = useMessage()\n const modal = useFormModal()\n const [config, setConfig] = React.useState<RuleConfig>({ conditions: [], actions: [] })\n\n // Load config and register schema components on mount\n React.useEffect(() => {\n shippingRuleApi.getConfig()\n .then((cfg) => {\n const conditionRegistry = container.get<ConditionRegistry>(\n coreshopShippingServiceIds.shippingRuleConditionRegistry\n )\n const actionRegistry = container.get<ActionRegistry>(\n coreshopShippingServiceIds.shippingRuleActionRegistry\n )\n registerSchemaComponentsFromConfig(conditionRegistry, actionRegistry, cfg)\n setConfig(cfg)\n })\n .catch(err => {\n void messageApi.error(getErrorMessage(err, 'Failed to load config'))\n })\n }, [])\n\n return (\n <EntityTabbedManager<ShippingRuleDetail>\n api={shippingRuleApi}\n dragType='coreshop:shipping_rule'\n leftRootTitle={t('coreshop_carriers_shipping_rule')}\n getTitle={(li, data) => data?.name ?? li?.name ?? `#${li?.id ?? ''}`}\n buildSavePayload={(data) => data}\n onAdd={async () => await new Promise<number>((resolve) => {\n modal.input({\n title: t('coreshop_carriers_shipping_rule'),\n label: t('coreshop_name'),\n rule: { required: true, message: t('coreshop_name_required') },\n onOk: async (nameValue: string) => {\n const res = await shippingRuleApi.add({ name: nameValue })\n resolve(res.data.id!)\n }\n })\n })}\n renderDetail={(data, setData, ctx) =>
1 {\n if (!data) return <div>Select a shipping rule...</div>\n\n return (\n <RuleForm\n rule={data}\n config={config}\n conditionRegistryId={coreshopShippingServiceIds.shippingRuleConditionRegistry}\n actionRegistryId={coreshopShippingServiceIds.shippingRuleActionRegistry}\n currentLocale={ctx?.currentLocale ?? 'en'}\n locales={ctx?.locales}\n settingsComponent={\n <SettingsForm\n rule={data}\n onChange={setData}\n currentLocale={ctx?.currentLocale ?? 'en'}\n locales={ctx?.locales}\n />\n }\n onChange={setData}\n hideToolbar={true}\n />\n )\n }}\n />\n )\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"ruleform",children:"RuleForm"}),"\n",(0,r.jsx)(n.p,{children:"Form component with Settings, Conditions, and Actions tabs:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"interface RuleFormProps {\n rule: Rule\n config: RuleConfig\n settingsComponent: React.ReactNode\n conditionRegistryId: symbol | string\n actionRegistryId: symbol | string\n currentLocale?: string\n locales?: string[]\n additionalTabs?: RuleFormTab[]\n onSave?: (rule: Rule) => Promise<void>\n onChange: (rule: Rule) => void\n hideToolbar?: boolean // Hide save button (when managed by EntityTabbedManager)\n}\n\n// Additional tabs (e.g., Voucher Codes for Cart Price Rules)\ninterface RuleFormTab {\n key: string\n label: string\n component: React.ReactNode\n disabled?: boolean\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"conditionspanel--actionspanel",children:"ConditionsPanel / ActionsPanel"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { ConditionsPanel, ActionsPanel } from '@coreshop/rule/src/rules/components'\n\n<ConditionsPanel\n conditions={rule.conditions}\n availableTypes={config.conditions}\n onChange={handleConditionsChange}\n registryId={serviceIds.conditionRegistry}\n currentLocale={currentLocale}\n locales={locales}\n/>\n\n<ActionsPanel\n actions={rule.actions}\n availableTypes={config.actions}\n onChange={handleActionsChange}\n registryId={serviceIds.actionRegistry}\n currentLocale={currentLocale}\n locales={locales}\n/>\n"})}),"\n",(0,r.jsx)(n.h2,{id:"components-that-still-need-custom-js",children:"Components That Still Need Custom JS"}),"\n",(0,r.jsx)(n.p,{children:"Only a few special components still require hand-written React code:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Component"}),(0,r.jsx)(n.th,{children:"Reason"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"NestedCondition"})}),(0,r.jsx)(n.td,{children:"Recursively renders sub-conditions with AND/OR logic; cannot be expressed as a flat form"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"TimespanCondition"})}),(0,r.jsx)(n.td,{children:"Uses custom date/time picker composition"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CartItemAction"})}),(0,r.jsx)(n.td,{children:"Renders nested cart-item condition/action panels"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"All other conditions and actions (countries, stores, currencies, categories, products, customers, amount, weight, etc.) are rendered automatically from their PHP FormTypes."}),"\n",(0,r.jsx)(n.h2,{id:"adding-a-new-conditionaction-preferred-approach",children:"Adding a New Condition/Action (Preferred Approach)"}),"\n",(0,r.jsx)(n.h3,{id:"step-1-create-the-php-formtype",children:"Step 1: Create the PHP FormType"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-php",children:"<?php\n\nnamespace App\\Form\\Type\\Rule\\Condition;\n\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nfinal class MinimumWeightConfigurationType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options): void\n {\n $builder\n ->add('minWeight', IntegerType::class, [\n 'label' => 'app_condition_min_weight',\n ])\n ;\n }\n\n public function getBlockPrefix(): string\n {\n return 'app_shipping_rule_condition_minimum_weight';\n }\n}\n"})}),"\n",(0,r.jsxs)(n.h3,{id:"step-2-register-the-service-with-form-type",children:["Step 2: Register the Service with ",(0,r.jsx)(n.code,{children:"form-type"})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"services:\n app.shipping_rule.condition.minimum_weight:\n class: App\\Rule\\Condition\\MinimumWeightChecker\n tags:\n - name: coreshop.shipping_rule.condition\n type: minimumWeight\n form-type: App\\Form\\Type\\Rule\\Condition\\MinimumWeightConfigurationType\n"})}),"\n",(0,r.jsx)(n.h3,{id:"step-3-done",children:"Step 3: Done"}),"\n",(0,r.jsx)(n.p,{children:"No React/TypeScript code needed. The condition form is rendered automatically in Studio."}),"\n",(0,r.jsx)(n.h3,{id:"when-you-need-custom-js-rare",children:"When You Need Custom JS (Rare)"}),"\n",(0,r.jsx)(n.p,{children:"If your condition/action requires custom UI behavior that cannot be expressed as a Symfony FormType (e.g., recursive nesting, complex interactive widgets), you can still register a hand-written React component:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"// In your bundle's main.ts\nconst conditionRegistry = container.get<ConditionRegistry>(serviceIds.conditionRegistry)\nconditionRegistry.register('myCustomCondition', MyCustomCondition)\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Hand-written components take priority over schema-generated ones \u2014 ",(0,r.jsx)(n.code,{children:"registerSchemaComponentsFromConfig()"})," does not overwrite existing entries."]}),"\n",(0,r.jsx)(n.h2,{id:"best-practices",children:"Best Practices"}),"\n",(0,r.jsx)(n.h3,{id:"1-prefer-schema-driven-components",children:"1. Prefer Schema-Driven Components"}),"\n",(0,r.jsx)(n.p,{children:"Use PHP FormTypes instead of writing React components. This:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Eliminates frontend/backend duplication"}),"\n",(0,r.jsx)(n.li,{children:"Automatically supports all StudioFormBundle widgets (choices, autocomplete, collections, etc.)"}),"\n",(0,r.jsx)(n.li,{children:"Requires zero JS build steps for new conditions/actions"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"2-separate-registries-per-rule-type",children:"2. Separate Registries Per Rule Type"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Each rule type MUST have its own registries!"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"// Separate registries\nconst productC
1onditionRegistry = container.get(productServiceIds.conditionRegistry)\nconst cartConditionRegistry = container.get(cartServiceIds.conditionRegistry)\n"})}),"\n",(0,r.jsx)(n.h3,{id:"3-bind-registries-in-oninit",children:"3. Bind Registries in onInit"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"onInit() {\n container.bind(serviceIds.registry).to(ConditionRegistry).inSingletonScope()\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"4-schema-cache-is-automatic",children:"4. Schema Cache is Automatic"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"RuleApi.getConfig()"})," automatically calls ",(0,r.jsx)(n.code,{children:"preSeedSchemaCache()"})," with the embedded schemas. No manual call needed."]}),"\n",(0,r.jsx)(n.h2,{id:"real-world-examples",children:"Real-World Examples"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/5.1/CoreShop/Development/Studio/Base_Infrastructure/StudioFormBundle_Examples#example-13--rule-conditionaction-as-schema-form",children:"StudioFormBundle Examples \u2014 Example 13"})," \u2014 Full end-to-end schema-driven rule component"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/5.1/CoreShop/Development/Studio/Base_Infrastructure/StudioFormBundle_Examples#example-14--cross-bundle-extension-formtypeextension",children:"StudioFormBundle Examples \u2014 Example 14"})," \u2014 Cross-bundle form extension"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/5.1/CoreShop/Development/Studio/Extending_Shipping_Rules",children:"Extending Shipping Rules"})," \u2014 Shipping rule extensions"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},28453:(e,n,i)=>{i.d(n,{R:()=>s,x:()=>c});var t=i(96540);const r={},o=t.createContext(r);function s(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.