Files
wc-composable-product/CLAUDE.md
magdev a7d6a57f01
Some checks failed
Create Release Package / PHP Lint (push) Successful in 47s
Create Release Package / test (push) Failing after 53s
Create Release Package / build-release (push) Has been skipped
Add PHPUnit test suite, PSR-4 refactor, lint+test CI jobs (v1.3.1)
- 57 unit tests covering ProductType, StockManager, CartHandler, Plugin,
  Admin/ProductData, Admin/Settings using Brain Monkey + Mockery
- WooCommerce class stubs for testing without WP installation
- PHP lint and test jobs in release workflow (test gate blocks release)
- PSR-4 namespace change: WC_Composable_Product -> Magdev\WcComposableProduct
- PascalCase filenames for all classes under includes/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:08:22 +01:00

10 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
  • Testing: PHPUnit 9.6 + Brain Monkey 2.7 + Mockery 1.6
  • 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)
├── tests/
│   ├── bootstrap.php          # Test environment setup (constants, stubs)
│   ├── TestCase.php           # Base test case with Brain Monkey
│   ├── stubs/                 # Minimal WooCommerce class stubs
│   └── Unit/                  # PHPUnit unit tests
├── templates/
│   └── product-selector.twig  # Frontend selection interface
├── vendor/                    # Composer dependencies (gitignored, included in releases)
├── composer.json
├── phpunit.xml.dist           # PHPUnit configuration
└── wc-composable-product.php  # Main plugin file

Architecture

Core Classes

  1. 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_pages filter (not in includes()) to avoid "Class WC_Settings_Page not found" errors
  2. ProductType.php — Custom WooCommerce product type (composable)

    • Extends WC_Product
    • Queries available products via get_available_products() using WP_Query
    • Critical: Uses tax_query with product_type taxonomy to exclude composable products (NOT meta_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 (not is_in_stock() — stock is shown visually and validated at add-to-cart)
  3. CartHandler.php — Cart integration

    • Validates selections, stores selected products in cart meta, calculates pricing
    • Uses woocommerce_is_purchasable filter to hide default add-to-cart button for composable products
    • Price recalculation uses a static $already_calculated flag per request (no persistent session flags — set_price() is in-memory only)
  4. ProductSelector.php — Frontend renderer

    • Renders Twig template with product data, stock info, and pre-formatted price HTML via wc_price()
  5. 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
  6. Admin/Settings.php — Global settings (extends WC_Settings_Page)

    • Default selection limit, pricing mode, display preferences
  7. 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 selections
  • woocommerce_add_cart_item_data — store selections
  • woocommerce_before_calculate_totals — update prices
  • woocommerce_get_item_data — display in cart
  • woocommerce_order_status_completed/processing — deduct stock
  • woocommerce_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.

Testing

Run unit tests: vendor/bin/phpunit --testdox

Tests use Brain Monkey to mock WordPress/WooCommerce functions without a full WP installation. WooCommerce classes (WC_Product, WC_Settings_Page, etc.) are provided as minimal stubs in tests/stubs/ so PHP can parse extends declarations. The release workflow runs tests before building — a failing test blocks the release.

Release Workflow

Automated (Gitea CI/CD)

Push an annotated tag (v*) to trigger the workflow. It first runs the PHPUnit test suite, then installs PHP 8.3, production Composer deps, compiles translations, verifies version matches tag, creates ZIP with checksums, and publishes a Gitea release. Tests must pass before the release package is built.

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 dev branch, merge to main for releases
  • Tags: annotated, format vX.X.X (e.g., v1.2.0)
  • Commit messages include Co-Authored-By: Claude attribution

Critical Lessons Learned

  1. WooCommerce stores product types as taxonomy terms (product_type taxonomy), NOT as postmeta. Using meta_query on _product_type silently returns zero results because the meta key doesn't exist.

  2. 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.

  3. Settings.php must be lazy-loadedrequire_once in Plugin::includes() causes "Class WC_Settings_Page not found" because WooCommerce hasn't loaded that class yet. Load it inside the woocommerce_get_settings_pages filter callback instead.

  4. 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.

  5. HPOS compatibility declaration is required — without it, WooCommerce shows incompatibility warnings.

  6. WordPress i18n requires compiled .mo files — .po files are source only; WordPress cannot use them directly.

  7. 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:

  1. Read this CLAUDE.md first
  2. Check git log for recent changes
  3. Verify you're on the dev branch before making changes
  4. Run composer install if vendor/ is missing
  5. Test changes before committing
  6. Follow commit message format with Claude Code attribution
  7. Always use tax_query (not meta_query) for product type filtering