refactor: split Reddit JSON Client into RSS parser class

Extract parse_atom_feed() and RSS-specific XML parsing logic into a new
PRAutoBlogger_Reddit_RSS_Parser class. This parser is a self-contained data
transformation (Atom XML → normalized post array) with minimal dependencies.

File sizes after split:
- class-reddit-json-client.php: 371 lines (was 468)
- class-reddit-rss-parser.php: 137 lines (new)

Both files now under the 300-line cap. The JSON client retains post/comment
fetching, availability checks, and HTTP infrastructure, while the parser
handles the XML-to-array transformation in isolation.

No callers need updating — PRAutoBlogger_Reddit_Provider uses
fetch_posts() which internally delegates to the parser.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-16 20:46:44 +07:00
parent e7d498574f
commit ebf4f87f98
2 changed files with 143 additions and 103 deletions

View File

@@ -10,10 +10,11 @@ declare(strict_types=1);
* for comment fetching (comments are not available in RSS feeds).
*
* Triggered by: PRAutoBlogger_Reddit_Provider::collect_data().
* Dependencies: wp_remote_get(), PRAutoBlogger_Logger.
* Dependencies: wp_remote_get(), PRAutoBlogger_Logger, PRAutoBlogger_Reddit_RSS_Parser.
*
* @see providers/class-reddit-provider.php — Instantiates and calls this class.
* @see ARCHITECTURE.md — External API integrations table.
* @see providers/class-reddit-provider.php — Instantiates and calls this class.
* @see providers/class-reddit-rss-parser.php — Parses Atom XML into post arrays.
* @see ARCHITECTURE.md — External API integrations table.
*/
class PRAutoBlogger_Reddit_JSON_Client {
@@ -129,7 +130,8 @@ class PRAutoBlogger_Reddit_JSON_Client {
}
$body = wp_remote_retrieve_body( $response );
return $this->parse_atom_feed( $body, $subreddit );
$parser = new PRAutoBlogger_Reddit_RSS_Parser();
return $parser->parse( $body, $subreddit );
}
/**
@@ -173,105 +175,6 @@ class PRAutoBlogger_Reddit_JSON_Client {
return $posts;
}
/**
* Parse a Reddit Atom feed into post-data arrays matching .json format.
*
* Maps Atom entry fields to the standard Reddit post fields so the rest
* of the pipeline can consume RSS data without changes. Fields unavailable
* in RSS (score, num_comments, upvote_ratio) are set to reasonable defaults.
*
* @param string $xml Raw Atom XML string.
* @param string $subreddit The subreddit name for context.
*
* @return array<int, array<string, mixed>> Parsed post data.
*/
private function parse_atom_feed( string $xml, string $subreddit ): array {
// Suppress XML errors for malformed feeds.
$prev_errors = libxml_use_internal_errors( true );
$feed = simplexml_load_string( $xml );
libxml_use_internal_errors( $prev_errors );
if ( false === $feed ) {
PRAutoBlogger_Logger::instance()->error( 'Failed to parse Reddit RSS XML.', 'reddit' );
return [];
}
// Register the Atom namespace.
$feed->registerXPathNamespace( 'atom', 'http://www.w3.org/2005/Atom' );
$entries = $feed->xpath( '//atom:entry' );
if ( empty( $entries ) ) {
return [];
}
$posts = [];
foreach ( $entries as $entry ) {
$title = (string) $entry->title;
$link = '';
$content = '';
// Get the HTML link (type="text/html").
foreach ( $entry->link as $link_el ) {
if ( 'text/html' === (string) $link_el['type'] || 'alternate' === (string) $link_el['rel'] ) {
$link = (string) $link_el['href'];
break;
}
}
// Content is in <content> tag as HTML.
if ( isset( $entry->content ) ) {
$content = wp_strip_all_tags( (string) $entry->content );
}
// Extract post ID from the entry id (format: /r/subreddit/comments/ID/...).
$entry_id = (string) $entry->id;
$post_id = '';
if ( preg_match( '#/comments/([a-z0-9]+)#', $entry_id, $m ) ) {
$post_id = $m[1];
}
// Extract author name.
$author = '[deleted]';
if ( isset( $entry->author->name ) ) {
$author = str_replace( '/u/', '', (string) $entry->author->name );
}
// Parse published date to Unix timestamp.
$published = (string) ( $entry->published ?? $entry->updated ?? '' );
$created_utc = '' !== $published ? (int) strtotime( $published ) : time();
// Extract permalink (relative path) from full URL.
$permalink = '';
if ( '' !== $link ) {
$parsed = wp_parse_url( $link );
$permalink = $parsed['path'] ?? '';
}
$posts[] = [
'id' => $post_id,
'title' => $title,
'selftext' => $content,
'author' => $author,
'score' => 1, // Not available in RSS.
'num_comments' => 0, // Not available in RSS.
'permalink' => $permalink,
'created_utc' => $created_utc,
'is_self' => true,
'link_flair_text' => null,
'upvote_ratio' => null,
'is_original_content' => false,
'data_source' => 'reddit_rss',
];
}
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Parsed %d posts from Reddit RSS for r/%s.', count( $posts ), $subreddit ),
'reddit'
);
return $posts;
}
/**
* Fetch top comments for a specific post via Reddit's .json endpoint.
*

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
/**
* Reddit RSS/Atom feed parser — parses Atom XML into normalized post arrays.
*
* Handles parsing of Reddit's Atom/RSS feeds and mapping them to the standard
* Reddit post data format (same shape as .json responses). This parser is
* self-contained: it takes raw XML and produces normalized arrays.
*
* Fields unavailable in RSS feeds (score, num_comments, upvote_ratio) are set
* to sensible defaults so the rest of the pipeline can treat RSS and .json data uniformly.
*
* Usage: Called by PRAutoBlogger_Reddit_JSON_Client::fetch_posts_via_rss()
* to parse the response body.
*
* Dependencies: wp_strip_all_tags(), PRAutoBlogger_Logger, built-in SimpleXML.
*
* @see providers/class-reddit-json-client.php — Main client that calls this parser.
* @see ARCHITECTURE.md — External API integrations table.
*/
class PRAutoBlogger_Reddit_RSS_Parser {
/**
* Parse a Reddit Atom feed into post-data arrays matching .json format.
*
* Maps Atom entry fields to the standard Reddit post fields so the rest
* of the pipeline can consume RSS data without changes. Fields unavailable
* in RSS (score, num_comments, upvote_ratio) are set to reasonable defaults.
*
* @param string $xml Raw Atom XML string.
* @param string $subreddit The subreddit name for context (logging only).
*
* @return array<int, array<string, mixed>> Parsed post data.
*/
public function parse( string $xml, string $subreddit ): array {
// Suppress XML errors for malformed feeds.
$prev_errors = libxml_use_internal_errors( true );
$feed = simplexml_load_string( $xml );
libxml_use_internal_errors( $prev_errors );
if ( false === $feed ) {
PRAutoBlogger_Logger::instance()->error( 'Failed to parse Reddit RSS XML.', 'reddit' );
return [];
}
// Register the Atom namespace.
$feed->registerXPathNamespace( 'atom', 'http://www.w3.org/2005/Atom' );
$entries = $feed->xpath( '//atom:entry' );
if ( empty( $entries ) ) {
return [];
}
$posts = [];
foreach ( $entries as $entry ) {
$post = $this->parse_entry( $entry, $subreddit );
if ( null !== $post ) {
$posts[] = $post;
}
}
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Parsed %d posts from Reddit RSS for r/%s.', count( $posts ), $subreddit ),
'reddit'
);
return $posts;
}
/**
* Parse a single Atom entry element into a normalized post array.
*
* @param \SimpleXMLElement $entry An Atom entry element.
* @param string $subreddit The subreddit name (for context/logging).
*
* @return array<string, mixed>|null The parsed post data, or null if parsing fails.
*/
private function parse_entry( $entry, string $subreddit ): ?array {
$title = (string) $entry->title;
$link = '';
$content = '';
// Get the HTML link (type="text/html").
foreach ( $entry->link as $link_el ) {
if ( 'text/html' === (string) $link_el['type'] || 'alternate' === (string) $link_el['rel'] ) {
$link = (string) $link_el['href'];
break;
}
}
// Content is in <content> tag as HTML.
if ( isset( $entry->content ) ) {
$content = wp_strip_all_tags( (string) $entry->content );
}
// Extract post ID from the entry id (format: /r/subreddit/comments/ID/...).
$entry_id = (string) $entry->id;
$post_id = '';
if ( preg_match( '#/comments/([a-z0-9]+)#', $entry_id, $m ) ) {
$post_id = $m[1];
}
// Extract author name.
$author = '[deleted]';
if ( isset( $entry->author->name ) ) {
$author = str_replace( '/u/', '', (string) $entry->author->name );
}
// Parse published date to Unix timestamp.
$published = (string) ( $entry->published ?? $entry->updated ?? '' );
$created_utc = '' !== $published ? (int) strtotime( $published ) : time();
// Extract permalink (relative path) from full URL.
$permalink = '';
if ( '' !== $link ) {
$parsed = wp_parse_url( $link );
$permalink = $parsed['path'] ?? '';
}
return [
'id' => $post_id,
'title' => $title,
'selftext' => $content,
'author' => $author,
'score' => 1, // Not available in RSS.
'num_comments' => 0, // Not available in RSS.
'permalink' => $permalink,
'created_utc' => $created_utc,
'is_self' => true,
'link_flair_text' => null,
'upvote_ratio' => null,
'is_original_content' => false,
'data_source' => 'reddit_rss',
];
}
}