- Rename files to PascalCase: Product_Type → ProductType, Cart_Handler → CartHandler, Product_Selector → ProductSelector, Stock_Manager → StockManager, Admin/Product_Data → Admin/ProductData - Switch namespace from WC_Composable_Product to Magdev\WcComposableProduct - Update all cross-references in PHP, CSS, JS, translations, and docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.2 KiB
WooCommerce Composable Products - AI Context Document
Author: Marco Graetsch
Project Overview
This plugin implements a custom WooCommerce product type where customers select a limited number of products from a configurable set of simple or variable products. The limit is configurable globally and per-product. The selectable products are defined by category, tag, or SKU. Pricing is either fixed or the sum of selected products. Think of a sticker pack where each package contains N stickers chosen by the customer.
This project is 100% AI-generated ("vibe-coded") using Claude.AI.
Technical Stack
- Language: PHP 8.3+
- Framework: WordPress Plugin API
- E-commerce: WooCommerce 10.0+
- Template Engine: Twig 3.0 (via Composer)
- Frontend: Vanilla JavaScript + jQuery
- Styling: Custom CSS
- Dependencies: Composer
- i18n: WordPress i18n (.pot/.po/.mo), text domain:
wc-composable-product - CI/CD: Gitea Actions (
.gitea/workflows/release.yml)
Project Structure
wc-composable-product/
├── .gitea/workflows/
│ └── release.yml # CI/CD release workflow
├── assets/
│ ├── css/
│ │ ├── admin.css # Admin panel styling
│ │ └── frontend.css # Customer-facing styles (with stock indicators)
│ └── js/
│ ├── admin.js # Product edit interface logic
│ └── frontend.js # AJAX cart & selection UI
├── cache/ # Twig template cache (writable, gitignored)
├── includes/
│ ├── Admin/
│ │ ├── ProductData.php # Product data tab & meta boxes
│ │ └── Settings.php # WooCommerce settings integration
│ ├── CartHandler.php # Add-to-cart & cart display logic (with stock validation)
│ ├── Plugin.php # Main plugin class (Singleton)
│ ├── ProductSelector.php # Frontend product selector renderer (with stock info)
│ ├── ProductType.php # Custom WC_Product extension
│ └── StockManager.php # Stock management & inventory tracking
├── languages/ # Translation files (.pot, .po, .mo)
├── releases/ # Release packages (gitignored)
├── templates/
│ └── product-selector.twig # Frontend selection interface
├── vendor/ # Composer dependencies (gitignored, included in releases)
├── composer.json
└── wc-composable-product.php # Main plugin file
Architecture
Core Classes
-
Plugin.php — Main singleton class
- Initializes Twig with WordPress functions registered as both Twig functions AND filters
- Registers hooks, manages asset enqueuing, provides template rendering API
- Settings.php is lazy-loaded via
woocommerce_get_settings_pagesfilter (not inincludes()) to avoid "Class WC_Settings_Page not found" errors
-
ProductType.php — Custom WooCommerce product type (
composable)- Extends
WC_Product - Queries available products via
get_available_products()usingWP_Query - Critical: Uses
tax_querywithproduct_typetaxonomy to exclude composable products (NOTmeta_query— WooCommerce stores product types as taxonomy terms) - Handles variable products by expanding them into individual variations via
get_children() - Products are filtered by
is_purchasable()only (notis_in_stock()— stock is shown visually and validated at add-to-cart)
- Extends
-
CartHandler.php — Cart integration
- Validates selections, stores selected products in cart meta, calculates pricing
- Uses
woocommerce_is_purchasablefilter to hide default add-to-cart button for composable products - Price recalculation uses a static
$already_calculatedflag per request (no persistent session flags —set_price()is in-memory only)
-
ProductSelector.php — Frontend renderer
- Renders Twig template with product data, stock info, and pre-formatted price HTML via
wc_price()
- Renders Twig template with product data, stock info, and pre-formatted price HTML via
-
Admin/ProductData.php — Product edit interface
- Adds "Composable Options" tab with category/tag/SKU selection fields
- Saved meta:
_composable_selection_limit,_composable_pricing_mode,_composable_criteria_type,_composable_categories,_composable_tags,_composable_skus
-
Admin/Settings.php — Global settings (extends
WC_Settings_Page)- Default selection limit, pricing mode, display preferences
-
StockManager.php — Inventory management
- Stock validation, automatic deduction on order completion, restoration on cancellation
- Prevents WooCommerce double-deduction via
woocommerce_can_reduce_order_stock
Data Flow
Product Creation: Admin selects "Composable product" type → configures criteria/limits/pricing → metadata saved as _composable_* fields
Frontend Display: CartHandler::render_product_selector() → ProductType::get_available_products() queries products via taxonomy/SKU → ProductSelector::render() passes data to Twig template → JavaScript handles selection UI
Add to Cart: Customer selects products → JS validates limit → AJAX request with composable_products[] → server-side validation (selection + stock) → selections stored in cart item data → price calculated per pricing mode
Order Processing: Order completed → StockManager deducts inventory → order notes added for audit → on cancellation/refund: stock restored
Key Hooks
woocommerce_add_to_cart_validation— validate selectionswoocommerce_add_cart_item_data— store selectionswoocommerce_before_calculate_totals— update priceswoocommerce_get_item_data— display in cartwoocommerce_order_status_completed/processing— deduct stockwoocommerce_order_status_cancelled/refunded— restore stock
Security
- Input:
absint()for IDs/limits,sanitize_text_field()for modes,sanitize_textarea_field()for SKUs - Output:
esc_html(),esc_attr(),esc_url()(registered as both Twig functions and filters) - Nonce verification via WooCommerce
Developer API
$product = wc_get_product($product_id);
if ($product->get_type() === 'composable') {
$products = $product->get_available_products();
$limit = $product->get_selection_limit();
$price = $product->calculate_composed_price($selected_ids);
}
Translations
All strings use text domain wc-composable-product. Available locales:
en_US(base),de_DE,de_DE_informal,de_CH,de_CH_informal,fr_CH,it_CH
Compile .po to .mo: for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
WordPress requires compiled .mo files — .po files alone are insufficient.
Release Workflow
Automated (Gitea CI/CD)
Push an annotated tag (v*) to trigger the workflow. It installs PHP 8.3, production Composer deps, compiles translations, verifies version matches tag, creates ZIP with checksums, and publishes a Gitea release.
Manual
# From project root
zip -r releases/wc-composable-product-vX.X.X.zip . \
-x "*.git*" "*.vscode*" "*.claude*" "CLAUDE.md" \
"wp-core/*" "wp-plugins/*" "*.log" "composer.lock" \
"cache/*" "releases/*" "*.zip" "logs/*"
The vendor/ directory MUST be included in releases (Twig dependency required at runtime).
Git Workflow
- Develop on
devbranch, merge tomainfor releases - Tags: annotated, format
vX.X.X(e.g.,v1.2.0) - Commit messages include
Co-Authored-By: Claudeattribution
Critical Lessons Learned
-
WooCommerce stores product types as taxonomy terms (
product_typetaxonomy), NOT as postmeta. Usingmeta_queryon_product_typesilently returns zero results because the meta key doesn't exist. -
WC_Product::set_price()is in-memory only — changes are lost between HTTP requests. Never persist a "price already calculated" flag to cart session; use a static per-request flag instead. -
Settings.php must be lazy-loaded —
require_onceinPlugin::includes()causes "Class WC_Settings_Page not found" because WooCommerce hasn't loaded that class yet. Load it inside thewoocommerce_get_settings_pagesfilter callback instead. -
Register WordPress functions as both Twig functions AND filters — other plugins may bundle their own Twig instance that parses our templates. Both
{{ esc_attr(value) }}and{{ value|esc_attr }}syntax must work. -
HPOS compatibility declaration is required — without it, WooCommerce shows incompatibility warnings.
-
WordPress i18n requires compiled .mo files — .po files are source only; WordPress cannot use them directly.
-
Don't filter by
is_in_stock()during product retrieval — it's too strict (excludes backorder-enabled products, products without stock management). Show all purchasable products; let the frontend display stock status and validate at add-to-cart time.
For AI Assistants
When starting a new session:
- Read this CLAUDE.md first
- Check git log for recent changes
- Verify you're on the
devbranch before making changes - Run
composer installif vendor/ is missing - Test changes before committing
- Follow commit message format with Claude Code attribution
- Always use
tax_query(notmeta_query) for product type filtering