You've already forked wc-composable-product
- 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>
200 lines
10 KiB
Markdown
200 lines
10 KiB
Markdown
# 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
|
|
|
|
```txt
|
|
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
|
|
|
|
```php
|
|
$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
|
|
|
|
```bash
|
|
# 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-loaded** — `require_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
|