Commit 4bbe06204ec for woocommerce
commit 4bbe06204ec5bd24c6758cf80c131bb683d4ce12
Author: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
Date: Wed Sep 16 13:59:36 2026 +0300
Revert composable filters for My Account order columns (#68766)
This reverts PR #67510, which added the dynamic
woocommerce_account_orders_column_content_{$column_id} filter to the
myaccount/orders.php template.
My Account is planned to move to blocks, so a new extension point in the
legacy template would carry maintenance cost we do not want to take on.
The filter is unreleased, so its changelog entry goes with it.
diff --git a/plugins/woocommerce/changelog/fix-35635-account-orders-column-filter b/plugins/woocommerce/changelog/fix-35635-account-orders-column-filter
deleted file mode 100644
index aa446dc0052..00000000000
--- a/plugins/woocommerce/changelog/fix-35635-account-orders-column-filter
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: add
-
-Add composable woocommerce_account_orders_column_content_{$column_id} filters for the My Account orders table columns and bump the myaccount/orders.php template version to 11.1.0.
diff --git a/plugins/woocommerce/templates/myaccount/orders.php b/plugins/woocommerce/templates/myaccount/orders.php
index ba867e0938b..5a54c6561d7 100644
--- a/plugins/woocommerce/templates/myaccount/orders.php
+++ b/plugins/woocommerce/templates/myaccount/orders.php
@@ -14,7 +14,7 @@
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 11.1.0
+ * @version 9.5.0
*/
defined( 'ABSPATH' ) || exit;
@@ -35,10 +35,7 @@ do_action( 'woocommerce_before_account_orders', $has_orders ); ?>
<tbody>
<?php
foreach ( $customer_orders->orders as $customer_order ) {
- $order = wc_get_order( $customer_order ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
- if ( ! $order instanceof WC_Order ) {
- continue;
- }
+ $order = wc_get_order( $customer_order ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$item_count = $order->get_item_count() - $order->get_item_count_refunded();
?>
<tr class="woocommerce-orders-table__row woocommerce-orders-table__row--status-<?php echo esc_attr( $order->get_status() ); ?> order">
@@ -51,8 +48,6 @@ do_action( 'woocommerce_before_account_orders', $has_orders ); ?>
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-<?php echo esc_attr( $column_id ); ?>" data-title="<?php echo esc_attr( $column_name ); ?>">
<?php endif; ?>
- <?php ob_start(); ?>
-
<?php if ( has_action( 'woocommerce_my_account_my_orders_column_' . $column_id ) ) : ?>
<?php do_action( 'woocommerce_my_account_my_orders_column_' . $column_id, $order ); ?>
@@ -94,54 +89,6 @@ do_action( 'woocommerce_before_account_orders', $has_orders ); ?>
?>
<?php endif; ?>
- <?php
- $column_content = trim( (string) ob_get_clean() );
-
- /**
- * Filters the content of a My Account orders table column.
- *
- * The dynamic portion of the hook name, `$column_id`, refers to the
- * order table column ID. The filter receives the full cell content:
- * the default column HTML or, when callbacks are registered on the
- * `woocommerce_my_account_my_orders_column_{$column_id}` action, the
- * output of those callbacks. Default content is escaped before this
- * filter runs, and callbacks should return safe, escaped HTML.
- *
- * Callbacks that replace the content rather than append to it should
- * carry over the default markup's accessibility affordances: the order
- * number link's `aria-label`, the order action `aria-label`s, and the
- * order date `<time datetime>` attribute.
- *
- * This filter runs from the `myaccount/orders.php` template, so it is
- * not available on sites where a theme overrides the template with a
- * copy predating version 11.1.0. Registering the legacy action as well
- * is not a workaround for that: the action still suppresses the default
- * content and this filter then runs over the action's output, so
- * callbacks on both hooks emitting the same markup render it twice.
- *
- * @param string $column_content Current column cell HTML.
- * @param WC_Order $order Current order object.
- * @param string $column_id Current column ID.
- *
- * @since 11.1.0
- */
- $filtered_column_content = apply_filters( 'woocommerce_account_orders_column_content_' . $column_id, $column_content, $order, $column_id );
-
- if ( is_string( $filtered_column_content ) ) {
- $column_content = $filtered_column_content;
- } elseif ( is_int( $filtered_column_content ) || is_float( $filtered_column_content ) || ( is_object( $filtered_column_content ) && method_exists( $filtered_column_content, '__toString' ) ) ) {
- $column_content = (string) $filtered_column_content;
- } else {
- wc_doing_it_wrong(
- 'woocommerce_account_orders_column_content_' . $column_id,
- __( 'Filter callbacks must return a string (or stringable value) of safe, escaped HTML. Return an empty string to render an empty cell. The unfiltered column content was used instead.', 'woocommerce' ),
- '11.1.0'
- );
- }
-
- echo $column_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Contains escaped default content or action hook output; filter callbacks must return safe HTML.
- ?>
-
<?php if ( $is_order_number ) : ?>
</th>
<?php else : ?>
diff --git a/plugins/woocommerce/tests/php/includes/templates/class-wc-my-account-orders-template-test.php b/plugins/woocommerce/tests/php/includes/templates/class-wc-my-account-orders-template-test.php
deleted file mode 100644
index 711b596a9ad..00000000000
--- a/plugins/woocommerce/tests/php/includes/templates/class-wc-my-account-orders-template-test.php
+++ /dev/null
@@ -1,374 +0,0 @@
-<?php
-/**
- * Tests for the My Account orders template.
- */
-
-declare( strict_types = 1 );
-
-/**
- * My Account orders template test.
- */
-class WC_My_Account_Orders_Template_Test extends WC_Unit_Test_Case {
-
- /**
- * Hooks registered by the current test.
- *
- * @var array<int, array{type: 'filter'|'action', hook_name: string, callback: callable, priority: int}>
- */
- private array $registered_hooks = array();
-
- /**
- * Tear down the test.
- */
- public function tearDown(): void {
- foreach ( $this->registered_hooks as $hook ) {
- if ( 'filter' === $hook['type'] ) {
- remove_filter( $hook['hook_name'], $hook['callback'], $hook['priority'] );
- } else {
- remove_action( $hook['hook_name'], $hook['callback'], $hook['priority'] );
- }
- }
-
- $this->registered_hooks = array();
-
- parent::tearDown();
- }
-
- /**
- * @testdox Default order status output can be changed by composable filters.
- */
- public function test_order_status_column_filters_compose_with_default_output(): void {
- $order = $this->create_order_with_status( 'processing' );
-
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-status',
- static function ( string $column_content ): string {
- return $column_content . '<span class="first-filter">First filter</span>';
- },
- 10,
- 1
- );
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-status',
- static function ( string $column_content ): string {
- return $column_content . '<span class="second-filter">Second filter</span>';
- },
- 20,
- 1
- );
-
- $html = $this->render_orders_template( $order );
-
- $this->assertStringContainsString( 'Processing', $html, 'Default order status output should remain available to filters.' );
- $this->assertStringContainsString( 'First filter', $html, 'First filter output should render.' );
- $this->assertStringContainsString( 'Second filter', $html, 'Second filter output should render.' );
- $this->assertSame( 1, substr_count( $html, 'Processing' ), 'Default order status should not be duplicated when filters compose.' );
- }
-
- /**
- * @testdox Non-stringable filtered column content falls back to default output with a doing-it-wrong notice.
- */
- public function test_non_string_filtered_column_content_falls_back_to_default_output(): void {
- $this->setExpectedIncorrectUsage( 'woocommerce_account_orders_column_content_order-status' );
-
- $order = $this->create_order_with_status( 'processing' );
- $filtered_content = null;
-
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-status',
- static function () use ( &$filtered_content ) {
- return $filtered_content;
- }
- );
-
- foreach ( array( array( 'invalid' ), new WP_Error( 'invalid' ), null, false ) as $filtered_content ) {
- $html = $this->render_orders_template( $order );
-
- $this->assertStringContainsString( 'Processing', $html, 'Invalid filtered content should fall back to default output.' );
- $this->assertSame( 1, substr_count( $html, 'Processing' ), 'Default output should render once after fallback.' );
- }
- }
-
- /**
- * @testdox An empty filtered column content clears the default output instead of falling back to it.
- */
- public function test_empty_filtered_column_content_clears_default_output(): void {
- $order = $this->create_order_with_status( 'processing' );
-
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-status',
- static function (): string {
- return '';
- }
- );
-
- $html = $this->render_orders_template( $order );
-
- $this->assertStringNotContainsString( 'Processing', $html, 'An empty string is valid content and should not fall back to the default output.' );
- $this->assertStringContainsString( 'woocommerce-orders-table__cell-order-status', $html, 'The emptied column should still render its cell.' );
- }
-
- /**
- * @testdox Numeric and stringable filtered column content is coerced to a string.
- */
- public function test_numeric_and_stringable_filtered_column_content_is_coerced(): void {
- $order = $this->create_order_with_status( 'processing' );
- $stringable = new class() {
- /**
- * Render as string.
- *
- * @return string
- */
- public function __toString(): string {
- return '<span class="stringable-status">Stringable status</span>';
- }
- };
-
- $filtered_content = null;
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-status',
- static function () use ( &$filtered_content ) {
- return $filtered_content;
- }
- );
-
- foreach ( array( 42, 4.5, $stringable ) as $filtered_content ) {
- $html = $this->render_orders_template( $order );
-
- $expected = is_object( $filtered_content ) ? 'Stringable status' : (string) $filtered_content;
- $this->assertStringContainsString( $expected, $html, 'Stringable filtered content should be coerced and rendered.' );
- $this->assertStringNotContainsString( 'Processing', $html, 'Coerced filtered content should replace the default output.' );
- }
- }
-
- /**
- * @testdox Content filters compose with legacy column action output.
- */
- public function test_content_filters_compose_with_legacy_action_output(): void {
- $order = $this->create_order_with_status( 'processing' );
- $filter_calls = 0;
- $received_content = null;
-
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-status',
- static function ( string $column_content ) use ( &$filter_calls, &$received_content ): string {
- ++$filter_calls;
- $received_content = $column_content;
- return $column_content . '<span class="filtered-status">Filtered status</span>';
- }
- );
- $this->add_test_action(
- 'woocommerce_my_account_my_orders_column_order-status',
- static function (): void {
- echo '<span class="legacy-status">Legacy status</span>';
- }
- );
-
- $html = $this->render_orders_template( $order );
-
- $this->assertStringContainsString( 'Legacy status', $html, 'Existing action callbacks should still render.' );
- $this->assertStringNotContainsString( 'Processing', $html, 'Existing action callbacks should still suppress default output.' );
- $this->assertSame( 1, $filter_calls, 'Content filters should run when a legacy action replaces the default content.' );
- $this->assertStringContainsString( 'legacy-status', (string) $received_content, 'Content filters should receive the legacy action output.' );
- $this->assertStringContainsString( 'Filtered status', $html, 'Content filters should compose over legacy action output.' );
- }
-
- /**
- * @testdox Built-in order columns expose their default content and context to filters.
- * @dataProvider default_column_provider
- *
- * @param string $column_id Column ID.
- * @param string $default_content Semantic content expected from the default renderer.
- */
- public function test_default_order_columns_are_filterable( string $column_id, string $default_content ): void {
- $order = $this->create_order_with_status( 'processing' );
- $filtered_content = null;
- $filtered_order = null;
- $filtered_column_id = null;
-
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_' . $column_id,
- static function ( string $content, WC_Order $current_order, string $current_column_id ) use ( &$filtered_content, &$filtered_order, &$filtered_column_id ): string {
- $filtered_content = $content;
- $filtered_order = $current_order;
- $filtered_column_id = $current_column_id;
- return $content . '<span class="column-filter-marker">Filtered column</span>';
- },
- 10,
- 3
- );
-
- $html = $this->render_orders_template( $order );
-
- $this->assertIsString( $filtered_content, 'The column content filter should run.' );
- $this->assertStringContainsString( $default_content, $filtered_content, 'The filter should receive the existing default column HTML.' );
- $this->assertSame( trim( $filtered_content ), $filtered_content, 'The filter should receive content without surrounding template whitespace.' );
- $this->assertInstanceOf( WC_Order::class, $filtered_order, 'The filter should receive an order object.' );
- $this->assertSame( $order->get_id(), $filtered_order instanceof WC_Order ? $filtered_order->get_id() : null, 'The filter should receive the current order.' );
- $this->assertSame( $column_id, $filtered_column_id, 'The filter should receive the current column ID.' );
- $this->assertStringContainsString( 'Filtered column', $html, 'The filtered column HTML should render.' );
- }
-
- /**
- * @testdox Existing account order column actions do not collide with content filters.
- */
- public function test_existing_account_order_column_actions_do_not_collide_with_content_filters(): void {
- $order = $this->create_order_with_status( 'processing' );
- $legacy_action_calls = 0;
-
- $this->add_test_filter(
- 'woocommerce_account_orders_columns',
- static function ( array $columns ): array {
- $columns['order-type'] = 'Order type';
- return $columns;
- }
- );
- $this->add_test_action(
- 'woocommerce_account_orders_column_order-type',
- static function () use ( &$legacy_action_calls ): void {
- ++$legacy_action_calls;
- }
- );
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-type',
- static function (): string {
- return '<span class="filtered-order-type">Filtered order type</span>';
- }
- );
-
- $html = $this->render_orders_template( $order );
-
- $this->assertSame( 0, $legacy_action_calls, 'A content filter should not invoke callbacks registered on the existing action-style hook name.' );
- $this->assertStringContainsString( 'Filtered order type', $html, 'A custom column should render content from its filter.' );
- }
-
- /**
- * @testdox A custom column without default content exposes an empty string to filters.
- */
- public function test_custom_column_content_filter_receives_empty_string(): void {
- $order = $this->create_order_with_status( 'processing' );
- $received_content = null;
-
- $this->add_test_filter(
- 'woocommerce_account_orders_columns',
- static function ( array $columns ): array {
- $columns['order-custom'] = 'Custom';
- return $columns;
- }
- );
- $this->add_test_filter(
- 'woocommerce_account_orders_column_content_order-custom',
- static function ( string $column_content ) use ( &$received_content ): string {
- $received_content = $column_content;
- return $column_content;
- }
- );
-
- $this->render_orders_template( $order );
-
- $this->assertSame( '', $received_content, 'A custom column with no default renderer should expose an empty string, not template whitespace.' );
- }
-
- /**
- * @testdox Rows whose order cannot be resolved are skipped while valid rows render.
- */
- public function test_rows_with_unresolvable_orders_are_skipped(): void {
- $order = $this->create_order_with_status( 'processing' );
-
- $html = $this->render_orders_template( $order, array( PHP_INT_MAX, $order->get_id() ) );
-
- $this->assertStringContainsString( 'View order number ' . $order->get_order_number(), $html, 'The valid order row should render.' );
- $this->assertSame( 1, substr_count( $html, '<tr class="woocommerce-orders-table__row' ), 'The unresolvable order row should be skipped.' );
- }
-
- /**
- * Built-in columns and representative default content.
- *
- * @return array<string, array{string, string}>
- */
- public function default_column_provider(): array {
- return array(
- 'order number' => array( 'order-number', 'View order number' ),
- 'order date' => array( 'order-date', '<time datetime=' ),
- 'order status' => array( 'order-status', 'Processing' ),
- 'order total' => array( 'order-total', 'woocommerce-Price-amount' ),
- 'order actions' => array( 'order-actions', 'woocommerce-button' ),
- );
- }
-
- /**
- * Create an order with a specific status.
- *
- * @param string $status Order status without wc- prefix.
- * @return WC_Order
- */
- private function create_order_with_status( string $status ): WC_Order {
- $order = wc_create_order( array( 'status' => $status ) );
- if ( is_wp_error( $order ) ) {
- throw new RuntimeException( 'Could not create an order for the template test.' );
- }
-
- return $order;
- }
-
- /**
- * Register a filter that will be removed during tear down.
- *
- * @param string $hook_name Filter name.
- * @param callable $callback Filter callback.
- * @param int $priority Filter priority.
- * @param int $accepted_args Number of accepted arguments.
- */
- private function add_test_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): void {
- add_filter( $hook_name, $callback, $priority, $accepted_args );
- $this->registered_hooks[] = array(
- 'type' => 'filter',
- 'hook_name' => $hook_name,
- 'callback' => $callback,
- 'priority' => $priority,
- );
- }
-
- /**
- * Register an action that will be removed during tear down.
- *
- * @param string $hook_name Action name.
- * @param callable $callback Action callback.
- * @param int $priority Action priority.
- * @param int $accepted_args Number of accepted arguments.
- */
- private function add_test_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): void {
- add_action( $hook_name, $callback, $priority, $accepted_args );
- $this->registered_hooks[] = array(
- 'type' => 'action',
- 'hook_name' => $hook_name,
- 'callback' => $callback,
- 'priority' => $priority,
- );
- }
-
- /**
- * Render the active My Account orders template.
- *
- * @param WC_Order $order Order object.
- * @param array<int>|null $order_ids Order IDs to render. Defaults to the given order's ID.
- * @return string
- */
- private function render_orders_template( WC_Order $order, ?array $order_ids = null ): string {
- $order_ids = $order_ids ?? array( $order->get_id() );
-
- return wc_get_template_html(
- 'myaccount/orders.php',
- array(
- 'current_page' => 1,
- 'customer_orders' => (object) array(
- 'orders' => $order_ids,
- 'total' => count( $order_ids ),
- 'max_num_pages' => 1,
- ),
- 'has_orders' => true,
- 'wp_button_class' => '',
- )
- );
- }
-}