Summary: Introducing Custom Product Types the Right Way
Summary: Introducing Custom Product Types the Right Way
~5 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Block 9 ends the way block 6 did with chapter 54, block 7 with chapter 61, and block 8 with chapter 70: a collected look back - this time across the entire path from the decision question in chapter 71 to the redeemable, discount-eligible order in chapter 77.
All files from this block at a glance
New and changed files from block 9
app/code/Mironsoft/Loyalty/
├── Model/
│ └── Product/
│ └── Type/
│ ├── PointsPackage.php (chapter 73)
│ └── PointsPackage/
│ └── Price.php (chapter 73)
├── Observer/
│ └── CreditPurchasedPointsPackageOnOrderPlaced.php (chapter 76)
├── Setup/
│ └── Patch/
│ └── Data/
│ ├── InstallPointsPackageAttributeSet.php (chapter 74)
│ ├── InstallPointsPackageAmountAttribute.php (chapter 74)
│ └── InstallSalesLoyaltyPointsPackageCreditedAttribute.php (chapter 76)
├── etc/
│ ├── product_types.xml (chapter 72)
│ └── events.xml (chapter 76, extended)
└── view/
├── adminhtml/
│ └── ui_component/
│ └── product_form.xml (chapter 74, optional)
└── frontend/
├── layout/
│ └── catalog_product_view_type_loyalty_points_package.xml (chapter 75)
└── templates/
└── product/
└── view/
└── type/
└── points-package.phtml (chapter 75)Checklist: introducing custom product types
- Justify the decision (chapter 71): does an attribute on an existing type suffice, or does the concept need its own
apply_toboundary? - Register it (chapter 72):
etc/product_types.xmlwithmodelInstance,priceModel,indexPriceModel- flush the cache, and runsetup:di:compilein production. - Implement the type and price models (chapter 73): pick the right parent class (here
Virtual), override only what genuinely differs. - Prepare the admin form (chapter 74): a dedicated attribute set, required fields scoped with
apply_to, communicate attribute set AND type together. - Display and sell it in the frontend (chapter 75): use the automatic layout handle, generic
additional_optionsinstead of new cart/order templates. - Wire up order processing (chapter 76): observer instead of plugin, a dedicated idempotency attribute, reuse existing block 1 repository interfaces instead of reinventing them.
- Test compatibility (chapter 77): a full reindex after introduction, click through price rules and third-party campaigns end-to-end instead of assuming they work.
Tipp: The thread running through all eight chapters of this block: a new product type is almost never "new" in the sense of "written entirely from scratch" - PointsPackage inherits from Virtual, the price model inherits from the core Price class, order processing keeps using PointsLedgerRepositoryInterface and CustomerRepositoryInterface from block 1 unchanged. What actually makes this type new is exclusively the distinct type code itself and the handful of spots where the business concept - a purchasable bundle of loyalty points - genuinely differs from everything already in place.
Block 10 turns to APIs next: REST and GraphQL endpoints that expose the same points balance and the same repositories - including the points-package logic this block just wired in - to external clients.