Commit c20d406e103 for woocommerce

commit c20d406e10389693e9375a2aa916fe4e91bc8cd7
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Fri Apr 10 19:31:10 2026 +0300

    Consolidate get_shipping_providers to always return AbstractShippingProvider instances (#64095)

    * Consolidate get_shipping_providers to always return AbstractShippingProvider instances

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Fix PHPStan baseline and address CodeRabbit review feedback

    Remove stale baseline entry for is_array() in FulfillmentUtils.php,
    guard nullable tracking URLs with ?? '', and add assertNotEmpty to
    shipping providers test.

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/64095-wooplug-6533-consolidate-get_shipping_providers-and b/plugins/woocommerce/changelog/64095-wooplug-6533-consolidate-get_shipping_providers-and
new file mode 100644
index 00000000000..c4936a0d80b
--- /dev/null
+++ b/plugins/woocommerce/changelog/64095-wooplug-6533-consolidate-get_shipping_providers-and
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Consolidate get_shipping_providers and get_shipping_providers_object into a single method that always returns AbstractShippingProvider instances.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 2df165ee07c..f605d3d65f2 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -3872,13 +3872,8 @@ class WC_AJAX {
 		$all_providers = \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils::get_shipping_providers();
 		$built_in_keys = array();
 		foreach ( $all_providers as $provider ) {
-			if ( is_string( $provider ) && class_exists( $provider ) && is_subclass_of( $provider, \Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider::class ) ) {
-				try {
-					$instance        = wc_get_container()->get( $provider );
-					$built_in_keys[] = $instance->get_key();
-				} catch ( \Throwable $e ) {
-					continue;
-				}
+			if ( ! $provider instanceof \Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\CustomShippingProvider ) {
+				$built_in_keys[] = $provider->get_key();
 			}
 		}
 		$reserved_slug_error = '';
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index a7314dd9b87..7c252a05ebf 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -46587,12 +46587,6 @@ parameters:
 			count: 1
 			path: src/Admin/Features/Fulfillments/FulfillmentUtils.php

-		-
-			message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#'
-			identifier: function.alreadyNarrowedType
-			count: 1
-			path: src/Admin/Features/Fulfillments/FulfillmentUtils.php
-
 		-
 			message: '#^Cannot access offset int on array\|false\.$#'
 			identifier: offsetAccess.nonOffsetAccessible
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php
index a88afb2225b..bc034724254 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php
@@ -409,7 +409,10 @@ class FulfillmentUtils {
 	 * This method retrieves the shipping providers registered in the WooCommerce Fulfillments system.
 	 * It can be filtered using the `woocommerce_fulfillment_shipping_providers` filter.
 	 *
-	 * @return array An associative array of shipping providers with their details.
+	 * Any class name strings in the filter result are resolved into AbstractShippingProvider instances
+	 * via the DI container. Invalid entries are silently skipped.
+	 *
+	 * @return AbstractShippingProvider[] An associative array of shipping provider instances keyed by provider key.
 	 */
 	public static function get_shipping_providers(): array {
 		/**
@@ -421,52 +424,33 @@ class FulfillmentUtils {
 		 *
 		 * @param array $shipping_providers The default list of shipping providers.
 		 */
-		return apply_filters(
+		$raw_providers = apply_filters(
 			'woocommerce_fulfillment_shipping_providers',
 			array()
 		);
-	}

-	/**
-	 * Get the shipping providers as an array of JS objects, for use in the fulfillment UI.
-	 *
-	 * @return array An associative array of shipping providers with their details.
-	 */
-	public static function get_shipping_providers_object(): array {
-		$shipping_providers = self::get_shipping_providers();
-		if ( ! is_array( $shipping_providers ) ) {
+		if ( ! is_array( $raw_providers ) ) {
 			return array();
 		}
-		$shipping_providers_object = array();
-		foreach ( $shipping_providers as $shipping_provider ) {
-			if ( is_string( $shipping_provider )
-			&& class_exists( $shipping_provider )
-			&& is_subclass_of( $shipping_provider, AbstractShippingProvider::class )
+
+		$resolved = array();
+		foreach ( $raw_providers as $provider ) {
+			if ( $provider instanceof AbstractShippingProvider ) {
+				$resolved[ $provider->get_key() ] = $provider;
+			} elseif ( is_string( $provider )
+				&& class_exists( $provider )
+				&& is_subclass_of( $provider, AbstractShippingProvider::class )
 			) {
 				try {
-					// Instantiate the shipping provider class.
-					$shipping_provider_instance = wc_get_container()->get( $shipping_provider );
+					$instance = wc_get_container()->get( $provider );
 				} catch ( \Throwable $e ) {
-					continue; // Skip if instantiation fails.
+					continue;
 				}
-				$shipping_providers_object[ $shipping_provider_instance->get_key() ] = array(
-					'label' => $shipping_provider_instance->get_name(),
-					'icon'  => $shipping_provider_instance->get_icon(),
-					'value' => $shipping_provider_instance->get_key(),
-					'url'   => $shipping_provider_instance->get_tracking_url( '__PLACEHOLDER__' ),
-				);
-			}
-			if ( is_object( $shipping_provider ) && $shipping_provider instanceof AbstractShippingProvider ) {
-				$shipping_providers_object[ $shipping_provider->get_key() ] = array(
-					'label' => $shipping_provider->get_name(),
-					'icon'  => $shipping_provider->get_icon(),
-					'value' => $shipping_provider->get_key(),
-					'url'   => $shipping_provider->get_tracking_url( '__PLACEHOLDER__' ),
-				);
+				$resolved[ $instance->get_key() ] = $instance;
 			}
 		}

-		return $shipping_providers_object;
+		return $resolved;
 	}

 	/**
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php
index ee3caa3dfce..e93218c38b8 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php
@@ -507,33 +507,9 @@ class FulfillmentsManager {
 		$shipping_providers = FulfillmentUtils::get_shipping_providers();
 		$results            = array();
 		foreach ( $shipping_providers as $provider ) {
-			if ( is_string( $provider ) && class_exists( $provider ) && is_subclass_of( $provider, AbstractShippingProvider::class ) ) {
-				try {
-					/**
-					 * Instantiate the shipping provider class.
-					 *
-					 * @var AbstractShippingProvider $provider_instance
-					 */
-					$provider_instance = wc_get_container()->get( $provider );
-				} catch ( \Throwable $e ) {
-					$logger = wc_get_logger();
-					$logger->error(
-						sprintf(
-							'Error instantiating shipping provider class %s: %s',
-							$provider,
-							$e->getMessage()
-						),
-						array( 'source' => 'woocommerce-fulfillments' )
-					);
-					continue; // Skip if the provider class cannot be instantiated.
-				}
-			} else {
-				continue; // Skip if the provider class does not exist or is not a valid shipping provider.
-			}
-
-			$parsing_result = $provider_instance->try_parse_tracking_number( $tracking_number, $shipping_from, $shipping_to );
+			$parsing_result = $provider->try_parse_tracking_number( $tracking_number, $shipping_from, $shipping_to );
 			if ( ! is_null( $parsing_result ) ) {
-				$results[ $provider_instance->get_key() ] = $parsing_result;
+				$results[ $provider->get_key() ] = $parsing_result;
 			}
 		}

diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
index f8e6d232cd7..ef2f6cd95d0 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
@@ -194,10 +194,11 @@ class FulfillmentsRenderer {
 		} elseif ( 1 === count( $providers ) ) {
 			$provider_fulfillment   = reset( $providers );
 			$provider_slug          = $provider_fulfillment->get_shipment_provider();
-			$known_providers        = FulfillmentUtils::get_shipping_providers_object();
+			$known_providers        = FulfillmentUtils::get_shipping_providers();
 			$provider_name_meta     = $provider_fulfillment->get_meta( '_provider_name' );
-			$provider_display_label = $known_providers[ $provider_slug ]['label']
-				?? ( ! empty( $provider_name_meta ) ? $provider_name_meta : $provider_slug );
+			$provider_display_label = isset( $known_providers[ $provider_slug ] )
+				? $known_providers[ $provider_slug ]->get_name()
+				: ( ! empty( $provider_name_meta ) ? $provider_name_meta : $provider_slug );
 			echo '<span>' . esc_html( $provider_display_label ) . '</span>';
 		} else {
 			echo '<span>--</span>';
@@ -423,8 +424,18 @@ class FulfillmentsRenderer {
 	 * @return void
 	 */
 	protected function load_fulfillments_js_settings() {
+		$providers_for_js = array();
+		foreach ( FulfillmentUtils::get_shipping_providers() as $provider ) {
+			$providers_for_js[ $provider->get_key() ] = array(
+				'label' => $provider->get_name(),
+				'icon'  => $provider->get_icon(),
+				'value' => $provider->get_key(),
+				'url'   => $provider->get_tracking_url( '__PLACEHOLDER__' ) ?? '',
+			);
+		}
+
 		$fulfillment_settings = array(
-			'providers'                  => FulfillmentUtils::get_shipping_providers_object(),
+			'providers'                  => $providers_for_js,
 			'currency_symbols'           => get_woocommerce_currency_symbols(),
 			'fulfillment_statuses'       => FulfillmentUtils::get_fulfillment_statuses(),
 			'order_fulfillment_statuses' => FulfillmentUtils::get_order_fulfillment_statuses(),
@@ -552,7 +563,7 @@ class FulfillmentsRenderer {
 			return;
 		}

-		$providers = FulfillmentUtils::get_shipping_providers_object();
+		$providers = FulfillmentUtils::get_shipping_providers();

 		// This is a read-only filter on the admin orders table, so nonce verification is not required.
 		// phpcs:ignore WordPress.Security.NonceVerification
@@ -560,9 +571,9 @@ class FulfillmentsRenderer {
 		?>
 		<select id="shipping-provider-filter" name="shipping_provider">
 			<option value="" <?php selected( $selected_provider, '' ); ?>><?php esc_html_e( 'Filter by shipping provider', 'woocommerce' ); ?></option>
-			<?php foreach ( $providers as $key => $provider ) : ?>
-				<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $selected_provider, $key ); ?>>
-					<?php echo esc_html( $provider['label'] ?? '' ); ?>
+			<?php foreach ( $providers as $provider ) : ?>
+				<option value="<?php echo esc_attr( $provider->get_key() ); ?>" <?php selected( $selected_provider, $provider->get_key() ); ?>>
+					<?php echo esc_html( $provider->get_name() ); ?>
 				</option>
 			<?php endforeach; ?>
 			<option value="__other__" <?php selected( $selected_provider, '__other__' ); ?>><?php esc_html_e( 'Other', 'woocommerce' ); ?></option>
@@ -673,7 +684,7 @@ class FulfillmentsRenderer {

 		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
 		if ( '__other__' === $shipping_provider ) {
-			$known_providers = FulfillmentUtils::get_shipping_providers_object();
+			$known_providers = FulfillmentUtils::get_shipping_providers();
 			$known_keys      = array_keys( $known_providers );

 			if ( empty( $known_keys ) ) {
diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Fulfillments/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Fulfillments/Controller.php
index e5226806d16..9941157a922 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Fulfillments/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Fulfillments/Controller.php
@@ -436,7 +436,15 @@ class Controller extends AbstractController {
 	 * @return WP_REST_Response
 	 */
 	public function get_providers( WP_REST_Request $request ): WP_REST_Response {
-		$providers = \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils::get_shipping_providers_object();
+		$providers = array();
+		foreach ( \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils::get_shipping_providers() as $provider ) {
+			$providers[ $provider->get_key() ] = array(
+				'label' => $provider->get_name(),
+				'icon'  => $provider->get_icon(),
+				'value' => $provider->get_key(),
+				'url'   => $provider->get_tracking_url( '__PLACEHOLDER__' ) ?? '',
+			);
+		}

 		/**
 		 * Filters the shipping providers response before it is returned.
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php
index 4ddfbaff2e5..2d1d49814d4 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php
@@ -136,34 +136,26 @@ class FulfillmentsManagerTest extends \WC_Unit_Test_Case {
 	}

 	/**
-	 * Test that the initial shipping providers can be extended.
+	 * Test that the initial shipping providers can be extended with an AbstractShippingProvider instance.
 	 */
 	public function test_extend_initial_shipping_providers() {
-		// Extend the shipping providers.
-		add_filter(
-			'woocommerce_fulfillment_shipping_providers',
-			function ( $providers ) {
-				$providers['custom_provider'] = array(
-					'label' => __( 'Custom Provider', 'woocommerce' ),
-					'icon'  => 'custom-icon',
-					'value' => 'custom_provider',
-				);
-				return $providers;
-			}
-		);
+		$mock_provider = new ShippingProviderMock();

-		/**
-		 * Filter to get initial shipping providers.
-		 *
-		 * @since 10.1.0
-		 */
-		$shipping_providers = apply_filters( 'woocommerce_fulfillment_shipping_providers', array() );
+		// Extend the shipping providers with an AbstractShippingProvider instance.
+		$filter = function ( $providers ) use ( $mock_provider ) {
+			$providers[] = $mock_provider;
+			return $providers;
+		};
+		add_filter( 'woocommerce_fulfillment_shipping_providers', $filter );
+
+		$shipping_providers = \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils::get_shipping_providers();
+
+		remove_filter( 'woocommerce_fulfillment_shipping_providers', $filter );

-		// Check if the custom provider is included.
-		$this->assertArrayHasKey( 'custom_provider', $shipping_providers );
-		$this->assertIsArray( $shipping_providers['custom_provider'] );
-		$this->assertArrayHasKey( 'label', $shipping_providers['custom_provider'] );
-		$this->assertEquals( __( 'Custom Provider', 'woocommerce' ), $shipping_providers['custom_provider']['label'] );
+		// Check if the mock provider is included, keyed by its key.
+		$this->assertArrayHasKey( $mock_provider->get_key(), $shipping_providers );
+		$this->assertInstanceOf( ShippingProviderMock::class, $shipping_providers[ $mock_provider->get_key() ] );
+		$this->assertEquals( 'Mock Shipping Provider', $shipping_providers[ $mock_provider->get_key() ]->get_name() );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/ShippingProvidersTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/ShippingProvidersTest.php
index 7c98d0e641e..363646aa381 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/ShippingProvidersTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/ShippingProvidersTest.php
@@ -40,7 +40,7 @@ class ShippingProvidersTest extends \WP_UnitTestCase {
 	}

 	/**
-	 * Test that the shipping providers configuration returns the correct classes.
+	 * Test that the shipping providers configuration returns the correct instances.
 	 */
 	public function test_shipping_providers_configuration(): void {
 		update_option( 'woocommerce_feature_fulfillments_enabled', 'yes' );
@@ -49,27 +49,22 @@ class ShippingProvidersTest extends \WP_UnitTestCase {
 		$controller->initialize_fulfillments();

 		$shipping_providers = FulfillmentUtils::get_shipping_providers();
+		$this->assertNotEmpty( $shipping_providers, 'Expected at least one registered shipping provider.' );

-		foreach ( $shipping_providers as $key => $provider_class ) {
-			$this->assertTrue(
-				class_exists( $provider_class ),
-				sprintf( 'Shipping provider class %s does not exist.', $provider_class )
-			);
-
-			$provider_instance = new $provider_class();
+		foreach ( $shipping_providers as $key => $provider ) {
 			$this->assertInstanceOf(
 				ShippingProviders\AbstractShippingProvider::class,
-				$provider_instance,
+				$provider,
 				sprintf( 'Shipping provider %s is not an instance of AbstractShippingProvider.', $key )
 			);
 			$this->assertNotEmpty(
-				$provider_instance->get_key(),
+				$provider->get_key(),
 				sprintf( 'Shipping provider %s does not have a valid key.', $key )
 			);
 			$this->assertEquals(
 				$key,
-				$provider_instance->get_key(),
-				sprintf( 'Shipping provider key %s does not match the expected key %s.', $provider_instance->get_key(), $key )
+				$provider->get_key(),
+				sprintf( 'Shipping provider key %s does not match the expected key %s.', $provider->get_key(), $key )
 			);
 		}
 	}