Wrangle MarketingRecommendations as RemoteSpecsEngine (#44828)

* Fix potential recursion error, add MarketingRecommendations to extend RemoteSpecsEngine, add error handling, add tests

* Changelog

* Added spec evaluation and return default when empty

* Update plugins/woocommerce/src/Admin/Features/MarketingRecommendations/Init.php

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

---------

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
This commit is contained in:
Ilyas Foo 2024-02-21 23:22:47 +08:00 committed by GitHub
parent 34a71f3963
commit c601a0b197
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 150 additions and 9 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Wrangle MarketingRecommendations as RemoteSpecsEngine

View File

@ -2,13 +2,15 @@
namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations; namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations;
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
/** /**
* Marketing Recommendations engine. * Marketing Recommendations engine.
* This goes through the specs and gets marketing recommendations. * This goes through the specs and gets marketing recommendations.
*/ */
class Init { class Init extends RemoteSpecsEngine {
/** /**
* Slug of the category specifying marketing extensions on the Woo.com store. * Slug of the category specifying marketing extensions on the Woo.com store.
* *
@ -54,6 +56,29 @@ class Init {
return $specs; return $specs;
} }
/**
* Process specs.
*
* @param array|null $specs Marketing recommendations spec array.
* @return array
*/
protected static function evaluate_specs( array $specs = null ) {
$suggestions = array();
$errors = array();
foreach ( $specs as $spec ) {
try {
$suggestions[] = self::object_to_array( $spec );
} catch ( \Throwable $e ) {
$errors[] = $e;
}
}
return array(
'suggestions' => $suggestions,
'errors' => $errors,
);
}
/** /**
* Load recommended plugins from Woo.com * Load recommended plugins from Woo.com
@ -62,13 +87,29 @@ class Init {
*/ */
public static function get_recommended_plugins(): array { public static function get_recommended_plugins(): array {
$specs = self::get_specs(); $specs = self::get_specs();
$result = array(); $results = self::evaluate_specs( $specs );
foreach ( $specs as $spec ) { $specs_to_return = $results['suggestions'];
$result[] = self::object_to_array( $spec ); $specs_to_save = null;
if ( empty( $specs_to_return ) ) {
// When suggestions is empty, replace it with defaults and save for 3 hours.
$specs_to_save = DefaultMarketingRecommendations::get_all();
$specs_to_return = self::evaluate_specs( $specs_to_save )['suggestions'];
} elseif ( count( $results['errors'] ) > 0 ) {
// When suggestions is not empty but has errors, save it for 3 hours.
$specs_to_save = $specs;
} }
return $result; if ( $specs_to_save ) {
MarketingRecommendationsDataSourcePoller::get_instance()->set_specs_transient( $specs_to_save, 3 * HOUR_IN_SECONDS );
}
$errors = $results['errors'];
if ( ! empty( $errors ) ) {
self::log_errors( $errors );
}
return $specs_to_return;
} }
/** /**
@ -139,16 +180,22 @@ class Init {
* This is used to convert the specs to an array so that they can be returned by the API. * This is used to convert the specs to an array so that they can be returned by the API.
* *
* @param mixed $obj Object to convert. * @param mixed $obj Object to convert.
* @param array &$visited Reference to an array keeping track of all seen objects to detect circular references.
* @return array * @return array
*/ */
protected static function object_to_array( $obj ) { public static function object_to_array( $obj, &$visited = array() ) {
if ( is_object( $obj ) ) { if ( is_object( $obj ) ) {
if ( in_array( $obj, $visited, true ) ) {
// Circular reference detected.
return null;
}
$visited[] = $obj;
$obj = (array) $obj; $obj = (array) $obj;
} }
if ( is_array( $obj ) ) { if ( is_array( $obj ) ) {
$new = array(); $new = array();
foreach ( $obj as $key => $val ) { foreach ( $obj as $key => $val ) {
$new[ $key ] = self::object_to_array( $val ); $new[ $key ] = self::object_to_array( $val, $visited );
} }
} else { } else {
$new = $obj; $new = $obj;

View File

@ -0,0 +1,90 @@
<?php
namespace Automattic\WooCommerce\Tests\Internal\Admin\ShippingPartnerSuggestions;
use Automattic\WooCommerce\Admin\DataSourcePoller;
use Automattic\WooCommerce\Admin\Features\MarketingRecommendations\Init;
use Automattic\WooCommerce\Admin\Features\MarketingRecommendations\DefaultMarketingRecommendations;
use Automattic\WooCommerce\Admin\Features\MarketingRecommendations\MarketingRecommendationsDataSourcePoller;
use WC_Unit_Test_Case;
/**
* class WC_Admin_Tests_MarketingRecommendations_Init
*
* @covers \Automattic\WooCommerce\Admin\Features\MarketingRecommendations\Init
*/
class InitTest extends WC_Unit_Test_Case {
/**
* Set up.
*/
public function setUp(): void {
parent::setUp();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
delete_option( 'woocommerce_show_marketplace_suggestions' );
}
/**
* Tear down.
*/
public function tearDown(): void {
parent::tearDown();
MarketingRecommendationsDataSourcePoller::get_instance()->delete_specs_transient();
remove_all_filters( 'transient_woocommerce_admin_' . MarketingRecommendationsDataSourcePoller::ID . '_specs' );
}
/**
* Test that default specs are provided when remote sources don't exist.
*/
public function test_get_default_specs() {
remove_all_filters( 'transient_woocommerce_admin_' . MarketingRecommendationsDataSourcePoller::ID . '_specs' );
add_filter(
DataSourcePoller::FILTER_NAME,
function() {
return array();
}
);
$specs = Init::get_specs();
$defaults = DefaultMarketingRecommendations::get_all();
remove_all_filters( DataSourcePoller::FILTER_NAME );
$this->assertEquals( $defaults, $specs );
}
/**
* Test that specs are read from cache when they exist.
*/
public function test_specs_transient() {
set_transient(
'woocommerce_admin_' . MarketingRecommendationsDataSourcePoller::ID . '_specs',
array(
'en_US' => array(
array(
'id' => 'mock1',
),
array(
'id' => 'mock2',
),
),
)
);
$specs = Init::get_specs();
$this->assertCount( 2, $specs );
}
/**
* Test that recursive objects does not cause an error and returns null for the recursive child.
*/
public function test_matching_suggestions() {
$node1 = new \StdClass();
$node2 = new \StdClass();
$node1->child = $node2;
$node2->child = $node1;
$result = Init::object_to_array( $node1 );
$this->assertEquals( array( 'child' => array( 'child' => null ) ), $result );
}
}