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' => '',
-			)
-		);
-	}
-}