Add WooCommerce-to-Twig rendering bridge

Intercept WooCommerce's PHP template loading via
woocommerce_before_template_part / woocommerce_after_template_part hooks
to render Bootstrap 5 Twig templates instead. This makes all 99 child
theme templates functional in a standard WooCommerce environment.

- Create WooCommerceExtension (Twig AbstractExtension) with ~50 functions
  and 7 filters covering WC API, WordPress hooks, escaping, and forms
- Rewrite TemplateOverride to use hook-based interception with stack-based
  output buffering for nested template support
- Wire bridge initialization at init priority 20 in functions.php
- Fix invalid {% do return() %} in two order templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 11:15:59 +01:00
parent 49b1d52701
commit 46eb7f0a22
5 changed files with 538 additions and 136 deletions

View File

@@ -18,46 +18,44 @@
# @since 0.1.0
#}
{% if not apply_filters('woocommerce_order_item_visible', true, item) %}
{% do return() %}
{% endif %}
{% if apply_filters('woocommerce_order_item_visible', true, item) %}
{% set is_visible = product and product.is_visible() %}
{% set product_permalink = apply_filters('woocommerce_order_item_permalink', is_visible ? product.get_permalink(item) : '', item, order) %}
{% set is_visible = product and product.is_visible() %}
{% set product_permalink = apply_filters('woocommerce_order_item_permalink', is_visible ? product.get_permalink(item) : '', item, order) %}
<tr class="{{ apply_filters('woocommerce_order_item_class', 'woocommerce-table__line-item order_item', item, order) }}">
<td class="product-name">
{% if product_permalink %}
<a href="{{ product_permalink|esc_url }}">{{ item.get_name()|esc_html }}</a>
{% else %}
{{ item.get_name()|esc_html }}
{% endif %}
{% set qty = item.get_quantity() %}
{% set refunded_qty = order.get_qty_refunded_for_item(item_id) %}
<strong class="product-quantity">
{% if refunded_qty %}
&times;&nbsp;<del>{{ qty }}</del> <ins>{{ qty - (refunded_qty * -1) }}</ins>
<tr class="{{ apply_filters('woocommerce_order_item_class', 'woocommerce-table__line-item order_item', item, order) }}">
<td class="product-name">
{% if product_permalink %}
<a href="{{ product_permalink|esc_url }}">{{ item.get_name()|esc_html }}</a>
{% else %}
&times;&nbsp;{{ qty }}
{{ item.get_name()|esc_html }}
{% endif %}
</strong>
{{ do_action('woocommerce_order_item_meta_start', item_id, item, order, false) }}
{{ wc_display_item_meta(item) }}
{{ do_action('woocommerce_order_item_meta_end', item_id, item, order, false) }}
</td>
{% set qty = item.get_quantity() %}
{% set refunded_qty = order.get_qty_refunded_for_item(item_id) %}
<td class="product-total text-end">
{{ order.get_formatted_line_subtotal(item)|raw }}
</td>
</tr>
<strong class="product-quantity">
{% if refunded_qty %}
&times;&nbsp;<del>{{ qty }}</del> <ins>{{ qty - (refunded_qty * -1) }}</ins>
{% else %}
&times;&nbsp;{{ qty }}
{% endif %}
</strong>
{% if show_purchase_note and purchase_note %}
<tr class="product-purchase-note">
<td colspan="2">
{{ purchase_note|wp_kses_post|wpautop }}
{{ do_action('woocommerce_order_item_meta_start', item_id, item, order, false) }}
{{ wc_display_item_meta(item) }}
{{ do_action('woocommerce_order_item_meta_end', item_id, item, order, false) }}
</td>
<td class="product-total text-end">
{{ order.get_formatted_line_subtotal(item)|raw }}
</td>
</tr>
{% if show_purchase_note and purchase_note %}
<tr class="product-purchase-note">
<td colspan="2">
{{ purchase_note|wp_kses_post|wpautop }}
</td>
</tr>
{% endif %}
{% endif %}

View File

@@ -16,76 +16,74 @@
{% set order = wc_get_order(order_id) %}
{% if not order %}
{% do return() %}
{% endif %}
{% if order %}
{% set order_items = order.get_items(apply_filters('woocommerce_purchase_order_item_types', 'line_item')) %}
{% set show_purchase_note = order.has_status(apply_filters('woocommerce_purchase_note_order_statuses', ['completed', 'processing'])) %}
{% set show_customer_details = order.get_user_id() == get_current_user_id() %}
{% set order_items = order.get_items(apply_filters('woocommerce_purchase_order_item_types', 'line_item')) %}
{% set show_purchase_note = order.has_status(apply_filters('woocommerce_purchase_note_order_statuses', ['completed', 'processing'])) %}
{% set show_customer_details = order.get_user_id() == get_current_user_id() %}
{% if show_downloads is defined and show_downloads %}
{% set downloads = order.get_downloadable_items() %}
{% if downloads is not empty %}
{{ wc_get_template('order/order-downloads.php', { downloads: downloads, show_title: true }) }}
{% endif %}
{% endif %}
{% if show_downloads is defined and show_downloads %}
{% set downloads = order.get_downloadable_items() %}
{% if downloads is not empty %}
{{ wc_get_template('order/order-downloads.php', { downloads: downloads, show_title: true }) }}
<section class="woocommerce-order-details">
{{ do_action('woocommerce_order_details_before_order_table', order) }}
<h2 class="h5 mb-3">{{ __('Order details') }}</h2>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{{ __('Product') }}</th>
<th class="text-end">{{ __('Total') }}</th>
</tr>
</thead>
<tbody>
{{ do_action('woocommerce_order_details_before_order_table_items', order) }}
{% for item_id, item in order_items %}
{% set product = item.get_product() %}
{% include 'order/order-details-item.html.twig' with {
order: order,
item_id: item_id,
item: item,
show_purchase_note: show_purchase_note,
purchase_note: product ? product.get_purchase_note() : '',
product: product
} %}
{% endfor %}
{{ do_action('woocommerce_order_details_after_order_table_items', order) }}
</tbody>
<tfoot>
{% for key, total in order.get_order_item_totals() %}
<tr>
<th scope="row">{{ total.label|esc_html }}</th>
<td class="text-end">{{ total.value|wp_kses_post }}</td>
</tr>
{% endfor %}
{% if order.get_customer_note() %}
<tr>
<th>{{ __('Note:') }}</th>
<td class="text-end">{{ order.get_customer_note()|nl2br|esc_html }}</td>
</tr>
{% endif %}
</tfoot>
</table>
</div>
{{ do_action('woocommerce_order_details_after_order_table', order) }}
</section>
{{ do_action('woocommerce_after_order_details', order) }}
{% if show_customer_details %}
{% include 'order/order-details-customer.html.twig' with { order: order } %}
{% endif %}
{% endif %}
<section class="woocommerce-order-details">
{{ do_action('woocommerce_order_details_before_order_table', order) }}
<h2 class="h5 mb-3">{{ __('Order details') }}</h2>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{{ __('Product') }}</th>
<th class="text-end">{{ __('Total') }}</th>
</tr>
</thead>
<tbody>
{{ do_action('woocommerce_order_details_before_order_table_items', order) }}
{% for item_id, item in order_items %}
{% set product = item.get_product() %}
{% include 'order/order-details-item.html.twig' with {
order: order,
item_id: item_id,
item: item,
show_purchase_note: show_purchase_note,
purchase_note: product ? product.get_purchase_note() : '',
product: product
} %}
{% endfor %}
{{ do_action('woocommerce_order_details_after_order_table_items', order) }}
</tbody>
<tfoot>
{% for key, total in order.get_order_item_totals() %}
<tr>
<th scope="row">{{ total.label|esc_html }}</th>
<td class="text-end">{{ total.value|wp_kses_post }}</td>
</tr>
{% endfor %}
{% if order.get_customer_note() %}
<tr>
<th>{{ __('Note:') }}</th>
<td class="text-end">{{ order.get_customer_note()|nl2br|esc_html }}</td>
</tr>
{% endif %}
</tfoot>
</table>
</div>
{{ do_action('woocommerce_order_details_after_order_table', order) }}
</section>
{{ do_action('woocommerce_after_order_details', order) }}
{% if show_customer_details %}
{% include 'order/order-details-customer.html.twig' with { order: order } %}
{% endif %}