Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to `WP Curate` will be documented in this file.

## Unreleased

Nothing yet.
- Bug Fix: Restore trending content queries with Parse.ly 3.20.4 and later.

## 3.3.0 - 2026-10-12

Expand Down
63 changes: 57 additions & 6 deletions src/features/class-parsely-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
namespace Alley\WP\WP_Curate\Features;

use Alley\WP\Types\Feature;
use Parsely\RemoteAPI\Analytics_Posts_API;
use Parsely\Parsely;

/**
* Add support for Parsely, if the plugin is installed.
*/
final class Parsely_Support implements Feature {
/**
* The minimum supported Parse.ly version.
*/
private const MINIMUM_PARSELY_VERSION = '3.20.4';

/**
* Set up.
*/
Expand All @@ -24,7 +28,11 @@ public function __construct() {}
* Boot the feature.
*/
public function boot(): void {
if ( ! class_exists( 'Parsely\Parsely' ) ) {
if (
! class_exists( 'Parsely\Parsely' ) ||
! defined( 'Parsely\PARSELY_VERSION' ) ||
! self::is_supported_version( (string) constant( 'Parsely\PARSELY_VERSION' ) )
) {
return;
}
// Elsewhere in the plugin, we'll use $GLOBALS['parsely'], but it is not available here.
Expand Down Expand Up @@ -68,11 +76,15 @@ public function get_trending_posts( array $args ): array {
if ( ! $parsely->api_secret_is_set() ) {
return [];
}
if ( ! class_exists( '\Parsely\RemoteAPI\Analytics_Posts_API' ) ) {
if (
! defined( 'Parsely\PARSELY_VERSION' ) ||
! self::is_supported_version( (string) constant( 'Parsely\PARSELY_VERSION' ) ) ||
! method_exists( $parsely, 'get_content_api' )
) {
return [];
}

$parsely_options = $GLOBALS['parsely']->get_options();
$parsely_options = $parsely->get_options();
/**
* Filter the period start for the Parsely API.
*
Expand Down Expand Up @@ -111,8 +123,8 @@ public function get_trending_posts( array $args ): array {
$cache_key = 'parsely_trending_posts_' . md5( wp_json_encode( $parsely_args ) ); // @phpstan-ignore-line - wp_Json_encode not likely to return false.
$ids = wp_cache_get( $cache_key );
if ( false === $ids || ! is_array( $ids ) ) {
$api = new Analytics_Posts_API( $GLOBALS['parsely'] );
$posts = $api->get_posts_analytics( $parsely_args );
$posts = $parsely->get_content_api()->get_posts( $this->get_content_api_args( $parsely_args ) );

if ( \is_wp_error( $posts ) || ! \is_array( $posts ) ) {
return [];
}
Expand Down Expand Up @@ -156,6 +168,45 @@ function ( $post ) {
return $ids;
}

/**
* Check whether a Parse.ly version is supported.
*
* @param string $version The Parse.ly version.
* @return bool Whether the version is supported.
*/
private static function is_supported_version( string $version ): bool {
return version_compare( $version, self::MINIMUM_PARSELY_VERSION, '>=' );
}

/**
* Convert legacy Analytics API arguments for the Content API service.
*
* Parse.ly 3.20.4 and later requires tag filters to be arrays.
*
* @param array<string, mixed> $args The legacy API arguments.
* @return array<string, mixed> The Content API arguments.
*/
private function get_content_api_args( array $args ): array {
if ( isset( $args['tag'] ) ) {
$args['tag'] = $this->get_api_argument_values( $args['tag'] );
}

return $args;
}

/**
* Convert a comma-separated API argument into individual values.
*
* @param mixed $value The API argument value.
* @return array<string> The individual non-empty values.
*/
private function get_api_argument_values( mixed $value ): array {
$values = is_array( $value ) ? $value : explode( ',', (string) $value );
$values = array_map( static fn ( mixed $item ): string => trim( (string) $item ), $values );

return array_values( array_filter( $values, static fn ( string $item ): bool => '' !== $item ) );
}

/**
* Get slugs from term IDs.
*
Expand Down
100 changes: 100 additions & 0 deletions tests/Unit/ParselySupportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
/**
* WP Curate Tests: Parse.ly Support
*
* @package wp-curate
*/

namespace Alley\WP\WP_Curate\Tests\Unit;

use Alley\WP\WP_Curate\Features\Parsely_Support;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use ReflectionMethod;

/**
* Tests the Parse.ly compatibility layer.
*/
class ParselySupportTest extends TestCase {
/**
* Test support for Parse.ly versions.
*
* @param string $version The Parse.ly version.
* @param bool $expected Whether the version is supported.
*/
#[DataProvider( 'supported_version_provider' )]
public function test_is_supported_version( string $version, bool $expected ): void {
try {
$method = new ReflectionMethod( Parsely_Support::class, 'is_supported_version' );
} catch ( ReflectionException ) {
self::fail( 'Parse.ly version support must be checked before enabling the feature.' );
}

$actual = $method->invoke( null, $version );

self::assertSame( $expected, $actual );
}

/**
* Provide Parse.ly version support cases.
*
* @return iterable<string, array{string, bool}>
*/
public static function supported_version_provider(): iterable {
yield 'Parse.ly 3.20.3 is not supported' => [ '3.20.3', false ];
yield 'Parse.ly 3.20.4 is supported' => [ '3.20.4', true ];
yield 'newer Parse.ly versions are supported' => [ '3.24.1', true ];
}

/**
* Test conversion of legacy arguments for the supported Content API.
*
* @param array<string, mixed> $args The legacy API arguments.
* @param array<string, mixed> $expected The expected Content API arguments.
*/
#[DataProvider( 'content_api_args_provider' )]
public function test_get_content_api_args( array $args, array $expected ): void {
$method = new ReflectionMethod( Parsely_Support::class, 'get_content_api_args' );
$actual = $method->invoke( new Parsely_Support(), $args );

self::assertSame( $expected, $actual );
}

/**
* Provide supported Content API argument cases.
*
* @return iterable<string, array{array<string, mixed>, array<string, mixed>}>
*/
public static function content_api_args_provider(): iterable {
$base_args = [
'limit' => 5,
'sort' => 'views',
'period_start' => '1d',
'period_end' => 'now',
'tag' => 'featured, breaking-news',
'section' => 'news, politics',
];

yield 'tags are converted to individual values while section remains scalar' => [
$base_args,
array_merge(
$base_args,
[
'tag' => [ 'featured', 'breaking-news' ],
]
),
];

yield 'empty list values are removed' => [
[
'tag' => 'featured, , breaking-news,',
'section' => 'news, politics',
],
[
'tag' => [ 'featured', 'breaking-news' ],
'section' => 'news, politics',
],
];
}
}
Loading