Skip to content

Commit

Permalink
Merge pull request #2366 from woocommerce/update/expose-attribute-map…
Browse files Browse the repository at this point in the history
…ping-rule-values

Expose attribute mapping rule values using WCProductAdapter
  • Loading branch information
jorgemd24 authored Apr 15, 2024
2 parents 68c0de3 + d2d8b32 commit 689273c
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 30 deletions.
67 changes: 50 additions & 17 deletions src/Integration/WPCOMProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use WC_Product;
use WP_REST_Response;
use WP_REST_Request;

Expand All @@ -35,13 +37,30 @@ class WPCOMProxy implements Service, Registerable, OptionsAwareInterface {
*/
protected $shipping_time_query;

/**
* The AttributeManager object.
*
* @var AttributeManager
*/
protected $attribute_manager;

/**
* The protected resources. Only items with visibility set to sync-and-show will be returned.
*/
protected const PROTECTED_RESOURCES = [
'products',
'coupons',
];

/**
* WPCOMProxy constructor.
*
* @param ShippingTimeQuery $shipping_time_query The ShippingTimeQuery object.
* @param AttributeManager $attribute_manager The AttributeManager object.
*/
public function __construct( ShippingTimeQuery $shipping_time_query ) {
public function __construct( ShippingTimeQuery $shipping_time_query, AttributeManager $attribute_manager ) {
$this->shipping_time_query = $shipping_time_query;
$this->attribute_manager = $attribute_manager;
}

/**
Expand Down Expand Up @@ -113,13 +132,13 @@ protected function register_object_types_filter( string $object_type ): void {
add_filter(
'woocommerce_rest_prepare_' . $object_type . '_object',
[ $this, 'filter_response_by_syncable_item' ],
9,
1000, // Run this filter last to override any other response.
3
);

add_filter(
'woocommerce_rest_prepare_' . $object_type . '_object',
[ $this, 'filter_metadata' ],
[ $this, 'prepare_response' ],
10,
3
);
Expand Down Expand Up @@ -186,6 +205,21 @@ protected function is_gla_request( WP_REST_Request $request ): bool {
return $request->get_param( 'gla_syncable' ) === '1';
}

/**
* Get route pieces: resource and id, if present.
*
* @param WP_REST_Request $request The request object.
*
* @return array The route pieces.
*/
protected function get_route_pieces( WP_REST_Request $request ): array {
$route = $request->get_route();
$pattern = '/(?P<resource>[\w]+)(?:\/(?P<id>[\d]+))?$/';
preg_match( $pattern, $route, $matches );

return $matches;
}

/**
* Filter response by syncable item.
*
Expand All @@ -200,15 +234,9 @@ public function filter_response_by_syncable_item( $response, $item, WP_REST_Requ
return $response;
}

$route = $request->get_route();
$pattern = '/(?P<resource>[\w]+)\/(?P<id>[\d]+$)/';
$protected_resources = [
'products',
'coupons',
];
preg_match( $pattern, $route, $matches );
$pieces = $this->get_route_pieces( $request );

if ( ! isset( $matches['id'] ) || ! isset( $matches['resource'] ) || ! in_array( $matches['resource'], $protected_resources, true ) ) {
if ( ! isset( $pieces['id'] ) || ! isset( $pieces['resource'] ) || ! in_array( $pieces['resource'], self::PROTECTED_RESOURCES, true ) ) {
return $response;
}

Expand Down Expand Up @@ -258,26 +286,31 @@ public function filter_by_metaquery( array $args, WP_REST_Request $request ): ar
}

/**
* Filter the response metadata returning all public metadata and those prefixed with _wc_gla
* Prepares the response when the request is coming from the WPCOM proxy:
*
* Filter all the private metadata and returns only the public metadata and those prefixed with _wc_gla
* For WooCommerce products, it will add the attribute mapping values.
*
* @param WP_REST_Response $response The response object.
* @param mixed $item The item.
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response object updated.
*/
public function filter_metadata( WP_REST_Response $response, $item, WP_REST_Request $request ): WP_REST_Response {
public function prepare_response( WP_REST_Response $response, $item, WP_REST_Request $request ): WP_REST_Response {
if ( ! $this->is_gla_request( $request ) ) {
return $response;
}

$data = $response->get_data();
$data = $response->get_data();
$resource = $this->get_route_pieces( $request )['resource'] ?? null;

if ( ! isset( $data['meta_data'] ) ) {
return $response;
if ( $item instanceof WC_Product && ( $resource === 'products' || $resource === 'variations' ) ) {
$attr = $this->attribute_manager->get_all_aggregated_values( $item );
$data['gla_attributes'] = $attr;
}

foreach ( $data['meta_data'] as $key => $meta ) {
foreach ( $data['meta_data'] ?? [] as $key => $meta ) {
if ( str_starts_with( $meta->key, '_' ) && ! str_starts_with( $meta->key, '_wc_gla' ) ) {
unset( $data['meta_data'][ $key ] );
}
Expand Down
2 changes: 1 addition & 1 deletion src/Internal/DependencyManagement/CoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ function ( ...$arguments ) {
$this->share_with_tags( NoteInitializer::class, ActionScheduler::class );

// Product attributes
$this->conditionally_share_with_tags( AttributeManager::class );
$this->conditionally_share_with_tags( AttributeManager::class, AttributeMappingRulesQuery::class, WC::class );
$this->conditionally_share_with_tags( AttributesTab::class, Admin::class, AttributeManager::class, MerchantCenterService::class );
$this->conditionally_share_with_tags( VariationsAttributes::class, Admin::class, AttributeManager::class, MerchantCenterService::class );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function register(): void {
$this->share_with_tags( WooCommerceProductBundles::class, AttributeManager::class );
$this->share_with_tags( WooCommercePreOrders::class, ProductHelper::class );
$this->conditionally_share_with_tags( JetpackWPCOM::class );
$this->share_with_tags( WPCOMProxy::class, ShippingTimeQuery::class );
$this->share_with_tags( WPCOMProxy::class, ShippingTimeQuery::class, AttributeManager::class );

$this->share_with_tags(
IntegrationInitializer::class,
Expand Down
73 changes: 73 additions & 0 deletions src/Product/Attributes/AttributeManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;

use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidClass;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ValidateInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\WCProductAdapter;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\AttributeMappingRulesQuery;
use WC_Product;
use WC_Product_Variation;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -46,6 +50,27 @@ class AttributeManager implements Service {
*/
protected $attribute_types_map;

/**
* @var AttributeMappingRulesQuery
*/
protected $attribute_mapping_rules_query;

/**
* @var WC
*/
protected $wc;

/**
* AttributeManager constructor.
*
* @param AttributeMappingRulesQuery $attribute_mapping_rules_query
* @param WC $wc
*/
public function __construct( AttributeMappingRulesQuery $attribute_mapping_rules_query, WC $wc ) {
$this->attribute_mapping_rules_query = $attribute_mapping_rules_query;
$this->wc = $wc;
}

/**
* @param WC_Product $product
* @param AttributeInterface $attribute
Expand Down Expand Up @@ -93,6 +118,54 @@ public function get( WC_Product $product, string $attribute_id ): ?AttributeInte
return new $attribute_class( $value );
}

/**
* Return all attribute values for the given product, after the mapping rules, GLA attributes, and filters have been applied.
* GLA Attributes has priority over the product attributes.
*
* @since x.x.x
*
* @param WC_Product $product
*
* @return array of attribute values
* @throws InvalidValue When the product does not exist.
*/
public function get_all_aggregated_values( WC_Product $product ) {
$attributes = $this->get_all_values( $product );

$parent_product = null;
// merge with parent's attributes if it's a variation product
if ( $product instanceof WC_Product_Variation ) {
$parent_product = $this->wc->get_product( $product->get_parent_id() );
$parent_attributes = $this->get_all_values( $parent_product );
$attributes = array_merge( $parent_attributes, $attributes );
}

$mapping_rules = $this->attribute_mapping_rules_query->get_results();

$adapted_product = new WCProductAdapter(
[
'wc_product' => $product,
'parent_wc_product' => $parent_product,
'targetCountry' => 'US', // targetCountry is required to create a new WCProductAdapter instance, but it's not used in the attributes context.
'gla_attributes' => $attributes,
'mapping_rules' => $mapping_rules,
]
);

foreach ( self::ATTRIBUTES as $attribute_class ) {
$attribute_id = $attribute_class::get_id();
if ( $attribute_id === 'size' ) {
$attribute_id = 'sizes';
}

if ( isset( $adapted_product->$attribute_id ) ) {
$attributes[ $attribute_id ] = $adapted_product->$attribute_id;
}
}

return $attributes;
}

/**
* Return attribute value.
*
Expand Down
61 changes: 54 additions & 7 deletions tests/Unit/Integration/WPCOMProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\RESTControllerUnitTest;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Integration\WPCOMProxy;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\AttributeMappingRulesTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingTimeTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerInterface;
use WC_Meta_Data;
use WP_REST_Response;

Expand All @@ -18,8 +21,17 @@
*/
class WPCOMProxyTest extends RESTControllerUnitTest {

/**
* @var ContainerInterface
*/
protected $container;

public function setUp(): void {
parent::setUp();
$this->container = woogle_get_container();
// Since the shipping time tables and attributeMappingRules aren't set up in the test environment, we install them to prevent warnings.
$this->container->get( AttributeMappingRulesTable::class )->install();
$this->container->get( ShippingTimeTable::class )->install();
do_action( 'rest_api_init' );
}

Expand Down Expand Up @@ -110,6 +122,7 @@ public function test_get_products() {

$this->assertEquals( $product_1->get_id(), $response->get_data()[0]['id'] );
$this->assertEquals( $expected_metadata, $this->format_metadata( $response->get_data()[0]['meta_data'] ) );
$this->assertArrayHasKey( 'gla_attributes', $response->get_data()[0] );
}

public function test_get_products_with_gla_syncable_false() {
Expand All @@ -131,6 +144,7 @@ public function test_get_products_with_gla_syncable_false() {

$this->assertEquals( $this->get_test_metadata(), $this->format_metadata( $response_mapped[ $product_1->get_id() ]['meta_data'] ) );
$this->assertEquals( $this->get_test_metadata( ChannelVisibility::DONT_SYNC_AND_SHOW ), $this->format_metadata( $response_mapped[ $product_2->get_id() ]['meta_data'] ) );
$this->assertArrayNotHasKey( 'gla_attributes', $response->get_data()[0] );
}

public function test_get_products_without_gla_visibility_metadata() {
Expand All @@ -152,6 +166,7 @@ public function test_get_products_without_gla_visibility_metadata() {

$this->assertEquals( $product_2->get_id(), $response->get_data()[0]['id'] );
$this->assertEquals( $expected_metadata, $this->format_metadata( $response->get_data()[0]['meta_data'] ) );
$this->assertArrayHasKey( 'gla_attributes', $response->get_data()[0] );
}

public function test_get_product_without_gla_visibility_metadata() {
Expand Down Expand Up @@ -232,6 +247,7 @@ public function test_get_variations() {
foreach ( $variations as $variation ) {
$this->assertArrayHasKey( $variation['variation_id'], $response_mapped );
$this->assertEquals( $expected_metadata, $this->format_metadata( $response_mapped[ $variation['variation_id'] ]['meta_data'] ) );
$this->assertArrayHasKey( 'gla_attributes', $response->get_data()[0] );
}
}

Expand Down Expand Up @@ -259,9 +275,47 @@ public function test_get_variations_without_gla_syncable_param() {
foreach ( $variations as $variation ) {
$this->assertArrayHasKey( $variation['variation_id'], $response_mapped );
$this->assertEquals( $expected_metadata, $this->format_metadata( $response_mapped[ $variation['variation_id'] ]['meta_data'] ) );
$this->assertArrayNotHasKey( 'gla_attributes', $response->get_data()[0] );
}
}

public function test_get_specific_variation_with_gla_syncable() {
$product = ProductHelper::create_variation_product();
$variation = $product->get_available_variations()[0];

$this->add_metadata( $variation['variation_id'], $this->get_test_metadata( null ) );

$response = $this->do_request( '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation['variation_id'], 'GET', [ 'gla_syncable' => '1' ] );

$this->assertEquals( 200, $response->get_status() );

$expected_metadata = [
'public_meta' => 'public',
];

$this->assertEquals( $expected_metadata, $this->format_metadata( $response->get_data()['meta_data'] ) );
$this->assertArrayHasKey( 'gla_attributes', $response->get_data() );
}

public function test_get_specific_variation_without_gla_syncable() {
$product = ProductHelper::create_variation_product();
$variation = $product->get_available_variations()[0];

$this->add_metadata( $variation['variation_id'], $this->get_test_metadata( null ) );

$response = $this->do_request( '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation['variation_id'], 'GET', [ 'gla_syncable' => '0' ] );

$this->assertEquals( 200, $response->get_status() );

$expected_metadata = [
'public_meta' => 'public',
'_private_meta' => 'private',
];

$this->assertEquals( $expected_metadata, $this->format_metadata( $response->get_data()['meta_data'] ) );
$this->assertArrayNotHasKey( 'gla_attributes', $response->get_data() );
}

public function test_get_coupons() {
$coupon_1 = CouponHelper::create_coupon( 'dummycoupon-1', 'publish', $this->get_test_metadata() );
$coupon_2 = CouponHelper::create_coupon( 'dummycoupon-2', 'publish', $this->get_test_metadata( ChannelVisibility::DONT_SYNC_AND_SHOW ) );
Expand Down Expand Up @@ -388,15 +442,8 @@ public function test_get_settings_without_gla_syncable_param() {
}

public function test_get_settings_with_gla_syncable_param() {
global $wpdb;

// As the shipping time tables are not created in the test environment, we need to suppress the errors.
$wpdb->suppress_errors = true;

$response = $this->do_request( '/wc/v3/settings/general', 'GET', [ 'gla_syncable' => '1' ] );

$wpdb->suppress_errors = false;

$this->assertEquals( 200, $response->get_status() );

$response_mapped = $this->maps_the_response_with_the_item_id( $response );
Expand Down
Loading

0 comments on commit 689273c

Please sign in to comment.