You've already forked wc-tier-and-package-prices
Document v1.2.9 learnings in CLAUDE.md
Added three new critical sections documenting lessons learned during v1.2.9 development:
1. WordPress Translation Functions with printf
- Correct pattern: printf(esc_html__('Text (%s)', 'domain'), value)
- Wrong pattern: printf(__('Text (%s)', 'domain'), value) - missing text domain
- Explains why text domain must be in translation function
- Shows proper output escaping with esc_html
2. Twig Translation Filters and HTML Entity Encoding
- Explains why translation filters encode special characters in concatenated strings
- Correct pattern: {{ 'text ' ~ currency_symbol }} (no translation filter)
- Wrong pattern: {{ ('text ' ~ currency_symbol)|__() }} causes HTML entity encoding
- Rule: Only translate static text, not concatenated dynamic values
3. Defensive Programming for POST Data Processing
- Compares v1.2.8 (branching) vs v1.2.9 (defensive) patterns
- Shows why single decision point is better than multiple branches
- Key principles: initialize early, minimize branching, guaranteed execution
- Pattern: initialize → conditionally populate → unconditionally act
Also corrected v1.2.8 examples that had wrong patterns, noting they were fixed in v1.2.9.
These patterns prevent future bugs and ensure consistent, secure implementation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
137
CLAUDE.md
137
CLAUDE.md
@@ -821,15 +821,15 @@ WooCommerce has different hooks for different product types in the admin product
|
||||
|
||||
**Lesson:** When adding admin UI for variable product parents, use `woocommerce_product_options_general_product_data` and check `$product->is_type('variable')` to conditionally display. Using `woocommerce_product_options_pricing` will cause forms to never appear for variable products (as discovered in v1.2.6 → v1.2.7 fix).
|
||||
|
||||
#### CRITICAL: Currency Symbol Display (Learned in v1.2.8)
|
||||
#### CRITICAL: Currency Symbol Display (Learned in v1.2.8, Corrected in v1.2.9)
|
||||
|
||||
When displaying currency symbols in admin interface table headers and input placeholders:
|
||||
|
||||
**Table Headers:**
|
||||
|
||||
```php
|
||||
// ✅ Correct - Use printf with translation and WooCommerce currency function
|
||||
<th><?php printf(__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ?></th>
|
||||
// ✅ Correct - Use printf with esc_html__ for translation (CORRECTED in v1.2.9)
|
||||
<th><?php printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ?></th>
|
||||
|
||||
// ❌ Wrong - Hard-coded or missing currency
|
||||
<th><?php _e('Price', 'wc-tier-package-prices'); ?></th>
|
||||
@@ -839,8 +839,8 @@ When displaying currency symbols in admin interface table headers and input plac
|
||||
**Twig Template Placeholders:**
|
||||
|
||||
```twig
|
||||
{# ✅ Correct - Pass currency_symbol from PHP and concatenate in template #}
|
||||
placeholder="{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}"
|
||||
{# ✅ Correct - Pass currency_symbol from PHP and concatenate (CORRECTED in v1.2.9 - no translation filter) #}
|
||||
placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}"
|
||||
|
||||
{# ❌ Wrong - Hard-coded or missing currency #}
|
||||
placeholder="{{ 'e.g., 9.99'|__('wc-tier-package-prices') }}"
|
||||
@@ -917,6 +917,133 @@ if (isset($_POST['_wc_tpp_tiers'])) {
|
||||
|
||||
**Rule:** Always check `if (!empty($array))` before calling `update_post_meta()` for array data. If empty, call `delete_post_meta()` instead.
|
||||
|
||||
#### CRITICAL: WordPress Translation Functions with printf (Learned in v1.2.9)
|
||||
|
||||
When using `printf()` with WordPress translation functions, the text domain must be passed to the translation function, NOT to printf:
|
||||
|
||||
**Wrong Pattern:**
|
||||
|
||||
```php
|
||||
// ❌ WRONG - Text domain not passed to translation function
|
||||
printf(__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol());
|
||||
```
|
||||
|
||||
**Problem:** The `__()` function receives the text domain as a second parameter, but in this pattern it's missing. This causes the string "Price (%s)" to not be found in translation files, so it won't be translated.
|
||||
|
||||
**Correct Pattern:**
|
||||
|
||||
```php
|
||||
// ✅ CORRECT - Text domain in translation function, with esc_html for output escaping
|
||||
printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol());
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
|
||||
- `esc_html__('Price (%s)', 'wc-tier-package-prices')` translates the string and returns it
|
||||
- `printf()` then substitutes the `%s` placeholder with the currency symbol
|
||||
- The translated string is used in the final output
|
||||
- `esc_html` ensures proper output escaping
|
||||
|
||||
**Applied in v1.2.9:** All 6 table headers in `includes/class-wc-tpp-product-meta.php`
|
||||
|
||||
#### CRITICAL: Twig Translation Filters and HTML Entity Encoding (Learned in v1.2.9)
|
||||
|
||||
When concatenating dynamic values in Twig templates, applying the translation filter can cause HTML entity encoding issues:
|
||||
|
||||
**Wrong Pattern:**
|
||||
|
||||
```twig
|
||||
{# ❌ WRONG - Translation filter encodes special characters in concatenated string #}
|
||||
placeholder="{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}"
|
||||
```
|
||||
|
||||
**Problem:** When `currency_symbol` contains special characters (€, £, ¥, etc.), the concatenated string is passed through the translation function which treats it as a translatable string and encodes special characters as HTML entities (`€`, `£`, etc.).
|
||||
|
||||
**Correct Pattern:**
|
||||
|
||||
```twig
|
||||
{# ✅ CORRECT - No translation filter, just concatenation #}
|
||||
placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}"
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
|
||||
- Placeholder examples don't need translation - they're illustrative values
|
||||
- Direct concatenation preserves special characters
|
||||
- Currency symbol displays correctly (€ instead of €)
|
||||
|
||||
**Rule:** Only apply translation filters to static text that needs translation, not to concatenated strings with dynamic values that contain special characters.
|
||||
|
||||
**Applied in v1.2.9:**
|
||||
|
||||
- `templates/admin/tier-row.twig` - Price input placeholder
|
||||
- `templates/admin/package-row.twig` - Price input placeholder
|
||||
|
||||
#### CRITICAL: Defensive Programming for POST Data Processing (Learned in v1.2.9)
|
||||
|
||||
The v1.2.8 fix for variation pricing deletion had the right logic but used a branching structure that could miss edge cases. The v1.2.9 refactor demonstrates a more defensive pattern:
|
||||
|
||||
**Less Defensive Pattern (v1.2.8):**
|
||||
|
||||
```php
|
||||
// ❌ BRITTLE - Multiple branches, easy to miss edge cases
|
||||
if (isset($_POST['wc_tpp_tiers'][$loop])) {
|
||||
$tiers = array();
|
||||
foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) {
|
||||
// ... populate tiers ...
|
||||
}
|
||||
if (!empty($tiers)) {
|
||||
update_post_meta($variation_id, '_wc_tpp_tiers', $tiers);
|
||||
} else {
|
||||
delete_post_meta($variation_id, '_wc_tpp_tiers');
|
||||
}
|
||||
} else {
|
||||
delete_post_meta($variation_id, '_wc_tpp_tiers');
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Two separate code paths to `delete_post_meta()`. If logic changes, easy to update one path but forget the other.
|
||||
|
||||
**More Defensive Pattern (v1.2.9):**
|
||||
|
||||
```php
|
||||
// ✅ DEFENSIVE - Single decision point, guaranteed cleanup
|
||||
$tiers = array();
|
||||
if (isset($_POST['wc_tpp_tiers'][$loop]) && is_array($_POST['wc_tpp_tiers'][$loop])) {
|
||||
foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) {
|
||||
// ... populate tiers ...
|
||||
}
|
||||
}
|
||||
// Always execute one of these based on final state
|
||||
if (!empty($tiers)) {
|
||||
update_post_meta($variation_id, '_wc_tpp_tiers', $tiers);
|
||||
} else {
|
||||
delete_post_meta($variation_id, '_wc_tpp_tiers');
|
||||
}
|
||||
```
|
||||
|
||||
**Why This Is Better:**
|
||||
|
||||
- Initialize array at the start (guaranteed initial state)
|
||||
- Single conditional for populating (with extra `is_array()` safety check)
|
||||
- Single decision point for save/delete (one place to maintain)
|
||||
- Impossible to have a code path that doesn't call either update or delete
|
||||
- Much easier to reason about and modify
|
||||
|
||||
**Key Principles:**
|
||||
|
||||
1. **Initialize variables early** - Establish known initial state
|
||||
2. **Minimize branching** - Fewer code paths = fewer bugs
|
||||
3. **Single decision point** - One place determines final action
|
||||
4. **Add safety checks** - Validate assumptions (`is_array()`)
|
||||
5. **Guaranteed execution** - Always perform one of update/delete, never neither
|
||||
|
||||
**Applied in v1.2.9:**
|
||||
|
||||
- `save_variation_pricing_fields()` - Both tier and package pricing logic refactored
|
||||
|
||||
**Rule:** When processing user input to decide between update and delete, prefer the pattern: initialize → conditionally populate → unconditionally act based on final state.
|
||||
|
||||
### When Adding Features
|
||||
|
||||
- Follow the existing pattern: add setting → add UI → add logic → add template
|
||||
|
||||
Reference in New Issue
Block a user