Commit 143b1c5ea5d for woocommerce

commit 143b1c5ea5d1c2ff63a90e943783ffe815f8a04d
Author: Néstor Soriano <konamiman@konamiman.com>
Date:   Mon Jul 6 17:03:47 2026 +0200

    Add woocommerce_format_phone_number filter, decouple from is_phone (#66122)

diff --git a/plugins/woocommerce/changelog/add-woocommerce-format-phone-filter b/plugins/woocommerce/changelog/add-woocommerce-format-phone-filter
new file mode 100644
index 00000000000..8f7a75e762e
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-woocommerce-format-phone-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a woocommerce_format_phone_number filter and a WC_Validation::is_phone_format() method.
diff --git a/plugins/woocommerce/includes/class-wc-validation.php b/plugins/woocommerce/includes/class-wc-validation.php
index c5cad488180..9ab923e124e 100644
--- a/plugins/woocommerce/includes/class-wc-validation.php
+++ b/plugins/woocommerce/includes/class-wc-validation.php
@@ -23,6 +23,25 @@ class WC_Validation {
 		return is_email( $email );
 	}

+	/**
+	 * Checks whether a string has the basic shape of a phone number, i.e. it contains
+	 * only digits and characters commonly used in phone numbers (whitespace and the
+	 * "# _ - + / ( ) ." characters).
+	 *
+	 * Unlike `is_phone`, this method doesn't apply the `woocommerce_validate_phone` filter,
+	 * so its result always reflects the default validation rules regardless of any
+	 * merchant-defined validation policy. It's intended for contexts that need a
+	 * country-agnostic sanity check, such as phone number formatting.
+	 *
+	 * @since 11.0.0
+	 *
+	 * @param  string $phone Phone number to check.
+	 * @return bool
+	 */
+	public static function is_phone_format( $phone ): bool {
+		return '' === trim( preg_replace( '/[\s\#0-9_\-\+\/\(\)\.]/', '', (string) $phone ) );
+	}
+
 	/**
 	 * Validates a phone number using a regular expression.
 	 *
@@ -31,7 +50,7 @@ class WC_Validation {
 	 * @return bool
 	 */
 	public static function is_phone( $phone, $country = null ) {
-		$valid = 0 === strlen( trim( preg_replace( '/[\s\#0-9_\-\+\/\(\)\.]/', '', $phone ) ) );
+		$valid = self::is_phone_format( $phone );

 		/**
 		 * Filters whether a phone number is considered valid.
diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php
index 710c184f53b..7488ae85731 100644
--- a/plugins/woocommerce/includes/wc-formatting-functions.php
+++ b/plugins/woocommerce/includes/wc-formatting-functions.php
@@ -1082,12 +1082,23 @@ function wc_normalize_postcode( $postcode ) {
  * @return string
  */
 function wc_format_phone_number( $phone ) {
-	$phone = $phone ?? '';
+	$original = $phone ?? '';

-	if ( ! WC_Validation::is_phone( $phone ) ) {
-		return '';
-	}
-	return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) );
+	$is_valid  = WC_Validation::is_phone_format( $original );
+	$formatted = $is_valid
+		? (string) preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $original ) )
+		: '';
+
+	/**
+	 * Filters the formatted phone number.
+	 *
+	 * @since 11.0.0
+	 *
+	 * @param string $formatted The formatted phone number, or an empty string if $original isn't a valid phone number.
+	 * @param string $original  The phone number passed to the function.
+	 * @param bool   $is_valid  Whether $original passed the default phone number validation.
+	 */
+	return apply_filters( 'woocommerce_format_phone_number', $formatted, $original, $is_valid );
 }

 /**
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 808f2739efb..af373fb9e2f 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -34377,12 +34377,6 @@ parameters:
 			count: 1
 			path: includes/wc-formatting-functions.php

-		-
-			message: '#^Function wc_format_phone_number\(\) should return string but returns string\|null\.$#'
-			identifier: return.type
-			count: 1
-			path: includes/wc-formatting-functions.php
-
 		-
 			message: '#^Function wc_get_filename_from_url\(\) should return string but return statement is missing\.$#'
 			identifier: return.missing
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php
index 1593ff747a1..bd46c90cdf9 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php
@@ -855,6 +855,49 @@ class WC_Tests_Formatting_Functions extends WC_Unit_Test_Case {
 		$this->assertEquals( '', wc_format_phone_number( '1-800-not a phone number' ) );
 	}

+	/**
+	 * Test that wc_format_phone_number() isn't affected by the woocommerce_validate_phone filter.
+	 *
+	 * The filter is country-aware and used for validation; formatting has no country context,
+	 * so it must not let the filter blank otherwise-formattable numbers.
+	 */
+	public function test_wc_format_phone_number_is_not_affected_by_validate_phone_filter() {
+		add_filter( 'woocommerce_validate_phone', '__return_false' );
+
+		try {
+			$this->assertEquals( '1-610-385-0000', wc_format_phone_number( '1.610.385.0000' ) );
+		} finally {
+			remove_filter( 'woocommerce_validate_phone', '__return_false' );
+		}
+	}
+
+	/**
+	 * Test that wc_format_phone_number() applies the woocommerce_format_phone_number filter.
+	 */
+	public function test_wc_format_phone_number_applies_format_filter() {
+		$received = array();
+
+		$callback = function ( $formatted, $original, $is_valid ) use ( &$received ) {
+			$received = array(
+				'formatted' => $formatted,
+				'original'  => $original,
+				'is_valid'  => $is_valid,
+			);
+			return 'filtered';
+		};
+
+		add_filter( 'woocommerce_format_phone_number', $callback, 10, 3 );
+
+		try {
+			$this->assertEquals( 'filtered', wc_format_phone_number( '1.610.385.0000' ) );
+			$this->assertEquals( '1-610-385-0000', $received['formatted'] );
+			$this->assertEquals( '1.610.385.0000', $received['original'] );
+			$this->assertTrue( $received['is_valid'] );
+		} finally {
+			remove_filter( 'woocommerce_format_phone_number', $callback, 10 );
+		}
+	}
+
 	/**
 	 * Test wc_sanitize_phone_number().
 	 *
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/util/validation.php b/plugins/woocommerce/tests/legacy/unit-tests/util/validation.php
index f7592d60580..2e066528b97 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/util/validation.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/util/validation.php
@@ -230,4 +230,27 @@ class WC_Tests_Validation extends WC_Unit_Test_Case {
 		$this->assertEquals( '+00-000-00-00-000', WC_Validation::format_phone( '+00.000.00.00.000' ) );
 		$this->assertEquals( '+00 000 00 00 000', WC_Validation::format_phone( '+00 000 00 00 000' ) );
 	}
+
+	/**
+	 * Test is_phone_format().
+	 */
+	public function test_is_phone_format() {
+		$this->assertTrue( WC_Validation::is_phone_format( '+00 000 00 00 000' ) );
+		$this->assertTrue( WC_Validation::is_phone_format( '(000) 00 00 000' ) );
+		$this->assertFalse( WC_Validation::is_phone_format( '+00 aaa dd ee fff' ) );
+	}
+
+	/**
+	 * Test that is_phone_format() ignores the woocommerce_validate_phone filter while is_phone() honors it.
+	 */
+	public function test_is_phone_format_ignores_validate_phone_filter() {
+		add_filter( 'woocommerce_validate_phone', '__return_false' );
+
+		try {
+			$this->assertTrue( WC_Validation::is_phone_format( '+00 000 00 00 000' ) );
+			$this->assertFalse( WC_Validation::is_phone( '+00 000 00 00 000' ) );
+		} finally {
+			remove_filter( 'woocommerce_validate_phone', '__return_false' );
+		}
+	}
 }