Skip to content

Conversation

@peterwilsoncc
Copy link
Contributor

Handles passing an array to fetch_feed() by requesting each feed individually and then calling SimplePie\SimplePie::merge_items(); on the result before passing the data back in to a SimplePie object.

I don't love this so would love some suggestions of anything I am missing.

Trac ticket: https://core.trac.wordpress.org/ticket/64136


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Dec 15, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props peterwilsoncc, wildworks, tobiasbg, swissspidy.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@peterwilsoncc peterwilsoncc force-pushed the fix/64136-fetch-feed-deprecation branch from b73e186 to 3ec52a0 Compare December 15, 2025 03:35
@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@peterwilsoncc peterwilsoncc force-pushed the fix/64136-fetch-feed-deprecation branch from 3ec52a0 to 1e227d2 Compare December 15, 2025 23:06
@t-hamano
Copy link
Contributor

t-hamano commented Jan 4, 2026

If we simply want to handle multiple feeds, the following approach might work:

diff --git a/src/wp-includes/feed.php b/src/wp-includes/feed.php
index 421bee7173..74f0ee77cd 100644
--- a/src/wp-includes/feed.php
+++ b/src/wp-includes/feed.php
@@ -832,7 +832,20 @@ function fetch_feed( $url ) {
 
        $feed->get_registry()->register( SimplePie\File::class, 'WP_SimplePie_File', true );
 
-       $feed->set_feed_url( $url );
+       if ( is_array( $url ) && count( $url ) > 1 ) {
+               $feed->multifeed_url = array();
+               foreach ( $url as $value ) {
+                       $feed->multifeed_url[] = $feed->get_registry()->call(
+                               SimplePie\Misc::class, 'fix_protocol',
+                               array( $value, 1 )
+                       );
+               }
+       } elseif ( is_array( $url ) && count( $url ) === 1 ) {
+               $feed->set_feed_url( array_shift( $url ) );
+       } else {
+               $feed->set_feed_url( $url );
+       }
+
        /** This filter is documented in wp-includes/class-wp-feed-cache-transien
t.php */
        $feed->set_cache_duration( apply_filters( 'wp_feed_cache_transient_lifeti
me', 12 * HOUR_IN_SECONDS, $url ) );

However, this may not be the recommended approach.

@peterwilsoncc
Copy link
Contributor Author

@t-hamano The hook will still run for each individual URL but it won't run with all of them.

IE fetch_feed( [ 'one.example.com', 'two.example.com' ] ) will fire the hook twice, once for each URL. At the moment it fires once with both URLs as the second parameter.

I'll take a look at your suggested diff. I don't know their plans but I wouldn't be surprised if $feed->multifeed_url[] is removed if/when SimplePie hard deprecate an array of URLs.

@peterwilsoncc peterwilsoncc force-pushed the fix/64136-fetch-feed-deprecation branch from 3300255 to c8f11ac Compare January 12, 2026 00:22
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );

if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an error occurs in one feed, the other feeds will be ignored. As I understand it, SimplePie will continue processing even if an error occurs and merge successful feeds:

https://github.com/t-hamano/wordpress-develop/blob/b47e1c7c5985fff02153cd89cbba912aa85b8a02/src/wp-includes/SimplePie/src/SimplePie.php#L1704-L1713

I haven't tested it, but how about an approach like this:

foreach ( (array) $url as $feed_url ) {
	$simplepie_instance = clone $feed;
	$simplepie_instance->set_feed_url( $feed_url );
	$simplepie_instance->init();
	$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );

	if ( ! $simplepie_instance->error() ) {
		$feeds[] = $simplepie_instance;
	} else {
		$errors[] = $simplepie_instance->error();
	}
}

if ( empty( $feeds ) ) {
	return new WP_Error( 'simplepie-error', $errors );
}

We may also need unit tests to ensure that if an error occurs, the rest of the feed still gets processed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the 6.8 behavior for when a feed encounters an error? We should try to replicate that

@t-hamano
Copy link
Contributor

@swissspidy @TobiasBg This is my first time looking at SimplePie code, so I may have missed something, and I'd appreciate any feedback if there is anything.

@TobiasBg
Copy link

This is my first time looking at SimplePie code

Sorry, I'm not familiar with this specific part either :-(

@swissspidy swissspidy requested a review from Copilot January 12, 2026 10:45
@swissspidy
Copy link
Member

Same here

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for passing an array of feed URLs to fetch_feed() to handle multiple feeds. When multiple URLs are provided, the function fetches each feed individually, merges their items using SimplePie's merge_items() method, and returns a single SimplePie object with the combined items.

Changes:

  • Modified fetch_feed() to detect and handle array URLs by fetching each feed individually and merging items
  • Moved set_feed_url() call to after array processing to avoid setting URL prematurely
  • Added test coverage to verify multiple feeds are supported without triggering deprecation warnings

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
src/wp-includes/feed.php Implements multi-feed support by cloning SimplePie instance for each URL, fetching items, and merging them via SimplePie::merge_items()
tests/phpunit/tests/feed/fetchFeed.php Adds test to verify fetch_feed() accepts array of URLs and doesn't trigger SimplePie deprecation warning

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

$url = array_shift( $url );
} elseif ( is_array( $url ) ) {
$feeds = array();
$simplepie_instance = clone $feed;
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code doesn't preserve or apply the feed options (cache duration, sanitization settings, etc.) from the original $feed object to the cloned $simplepie_instance that processes individual feeds. This means each individual feed in the array might not respect the options set via wp_feed_options action or other configuration methods.

The cloned instance should inherit all the configuration from the parent feed object to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +858 to +860
if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When one feed in the array fails to fetch or has an error, the function returns immediately with a WP_Error, abandoning the processing of remaining feeds. This means if you request 5 feeds and the 2nd one fails, you won't get any data from feeds 1, 3, 4, or 5.

Consider whether this is the desired behavior or if it would be better to continue processing remaining feeds and either aggregate errors or return partial results with a warning about failed feeds.

Copilot uses AI. Check for mistakes.
Comment on lines +852 to +863
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
$simplepie_instance->set_feed_url( $feed_url );
$simplepie_instance->init();
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );

if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}

$feeds[] = $simplepie_instance;
}
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same $simplepie_instance object is being reused across multiple iterations without proper reinitialization. This means that after the first feed is processed, the instance retains state from the previous feed which could lead to incorrect data or unexpected behavior. Each feed URL should either get a fresh SimplePie instance, or the instance should be properly reset between iterations.

Consider creating a new SimplePie instance for each feed URL in the loop instead of cloning once and reusing.

Copilot uses AI. Check for mistakes.
Comment on lines +850 to +867
} elseif ( is_array( $url ) ) {
$feeds = array();
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
$simplepie_instance->set_feed_url( $feed_url );
$simplepie_instance->init();
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );

if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}

$feeds[] = $simplepie_instance;
}
$items = SimplePie\SimplePie::merge_items( $feeds );
$feed->init();
$feed->data['items'] = $items;
return $feed;
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly assigning to $feed->data['items'] bypasses SimplePie's internal state management and could cause issues with other SimplePie methods that depend on proper initialization. The SimplePie object wasn't initialized with init() before this assignment, and calling init() on line 865 after data assignment may not work as expected since the object wasn't properly set up with a feed URL.

Instead of manually manipulating the data property, consider using SimplePie's merge_items() method to create a properly merged feed or ensure the feed object is properly initialized with the merged data.

Suggested change
} elseif ( is_array( $url ) ) {
$feeds = array();
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
$simplepie_instance->set_feed_url( $feed_url );
$simplepie_instance->init();
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );
if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}
$feeds[] = $simplepie_instance;
}
$items = SimplePie\SimplePie::merge_items( $feeds );
$feed->init();
$feed->data['items'] = $items;
return $feed;

Copilot uses AI. Check for mistakes.
Comment on lines +848 to +867
if ( is_array( $url ) && count( $url ) <= 1 ) {
$url = array_shift( $url );
} elseif ( is_array( $url ) ) {
$feeds = array();
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
$simplepie_instance->set_feed_url( $feed_url );
$simplepie_instance->init();
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );

if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}

$feeds[] = $simplepie_instance;
}
$items = SimplePie\SimplePie::merge_items( $feeds );
$feed->init();
$feed->data['items'] = $items;
return $feed;
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition count( $url ) <= 1 will cause arrays with exactly one URL to be processed differently (extracting the single URL), but arrays with 0 URLs will fall through to the multi-feed handling logic. This could lead to attempting to process an empty array of feeds, which may not be the intended behavior.

Consider also checking for empty arrays and handling them appropriately, or adding validation earlier in the function to ensure the URL parameter is valid.

Suggested change
if ( is_array( $url ) && count( $url ) <= 1 ) {
$url = array_shift( $url );
} elseif ( is_array( $url ) ) {
$feeds = array();
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
$simplepie_instance->set_feed_url( $feed_url );
$simplepie_instance->init();
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );
if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}
$feeds[] = $simplepie_instance;
}
$items = SimplePie\SimplePie::merge_items( $feeds );
$feed->init();
$feed->data['items'] = $items;
return $feed;
if ( is_array( $url ) ) {
if ( 1 === count( $url ) ) {
$url = array_shift( $url );
} elseif ( 0 === count( $url ) ) {
return new WP_Error( 'simplepie-error', 'Feed URL array must not be empty.' );
} else {
$feeds = array();
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
$simplepie_instance->set_feed_url( $feed_url );
$simplepie_instance->init();
$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );
if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
}
$feeds[] = $simplepie_instance;
}
$items = SimplePie\SimplePie::merge_items( $feeds );
$feed->init();
$feed->data['items'] = $items;
return $feed;
}

Copilot uses AI. Check for mistakes.
* @param SimplePie\SimplePie $feed SimplePie feed object (passed by reference).
* @param string|string[] $url URL of feed or array of URLs of feeds to retrieve.
*/
do_action_ref_array( 'wp_feed_options', array( &$feed, $url ) );
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wp_feed_options action is called with the original $feed object and the $url parameter before the URL array is processed. However, for multiple feeds, this hook is only called once with the array of URLs, not once per feed. This means plugins or themes that hook into this to configure feed options won't have a chance to configure each individual feed instance that's created in the loop.

Consider whether the hook should be called for each individual feed URL when processing multiple feeds, or clearly document this behavior change.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +55
$feed = fetch_feed( array( 'https://wordpress.org/news/feed/', 'https://wordpress.org/news/feed/' ) );
$content = array();

foreach ( $feed->get_items( 0, 2 ) as $item ) {
$content[] = $item->get_content();
}

$this->assertEqualHTML( $content[0], $content[1], null, 'The contents of the first two items should be identical.' );
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test validates that the content of the first two items is identical by passing the same feed URL twice. However, this doesn't verify that both feed URLs were actually processed independently. If there's a bug in the implementation that only processes the first feed and ignores the rest, this test would still pass.

Consider using two different feed URLs (or mock responses) to verify that items from both feeds are actually included in the merged result, and that the feeds are processed independently.

Copilot uses AI. Check for mistakes.
} elseif ( is_array( $url ) ) {
$feeds = array();
$simplepie_instance = clone $feed;
foreach ( (array) $url as $feed_url ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: At this point, this is already known to be an array.

Suggested change
foreach ( (array) $url as $feed_url ) {
foreach ( $url as $feed_url ) {

* @ticket 64136
*/
public function test_fetch_feed_supports_multiple_feeds() {
$feed = fetch_feed( array( 'https://wordpress.org/news/feed/', 'https://wordpress.org/news/feed/' ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second call should be returned cached. I think it might make sense to use a different url (maybe just the atom url?) since this response is mocked anyways.

$simplepie_instance->set_output_encoding( get_bloginfo( 'charset' ) );

if ( $simplepie_instance->error() ) {
return new WP_Error( 'simplepie-error', $simplepie_instance->error() );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the 6.8 behavior for when a feed encounters an error? We should try to replicate that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants