PageSourceSearch

https://docs.coreshop.com/assets/js/be7620e0.9bccf590.js

js coreshop.com collected 2026-10-10 00:35:48 UTC 28,155 bytes, 1 lines download raw bytes

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.