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:
2025-12-30 05:08:09 +01:00
parent b2efb89d59
commit 63c8137f4e

137
CLAUDE.md
View File

@@ -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). **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: When displaying currency symbols in admin interface table headers and input placeholders:
**Table Headers:** **Table Headers:**
```php ```php
// ✅ Correct - Use printf with translation and WooCommerce currency function // ✅ Correct - Use printf with esc_html__ for translation (CORRECTED in v1.2.9)
<th><?php printf(__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ?></th> <th><?php printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ?></th>
// ❌ Wrong - Hard-coded or missing currency // ❌ Wrong - Hard-coded or missing currency
<th><?php _e('Price', 'wc-tier-package-prices'); ?></th> <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 Template Placeholders:**
```twig ```twig
{# ✅ Correct - Pass currency_symbol from PHP and concatenate in template #} {# ✅ Correct - Pass currency_symbol from PHP and concatenate (CORRECTED in v1.2.9 - no translation filter) #}
placeholder="{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}"
{# ❌ Wrong - Hard-coded or missing currency #} {# ❌ Wrong - Hard-coded or missing currency #}
placeholder="{{ 'e.g., 9.99'|__('wc-tier-package-prices') }}" 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. **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 (`&euro;`, `&pound;`, 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 &euro;)
**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 ### When Adding Features
- Follow the existing pattern: add setting → add UI → add logic → add template - Follow the existing pattern: add setting → add UI → add logic → add template