File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Product.tar
AttributeMapping/AttributeMappingHelper.php 0000644 00000010013 15154245121 0015146 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Adult;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AgeGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Brand;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Color;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Condition;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Gender;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\GTIN;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\IsBundle;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Material;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\MPN;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Multipack;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Pattern;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Size;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\SizeSystem;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\SizeType;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\WithMappingInterface;
defined( 'ABSPATH' ) || exit;
/**
* Helper Class for Attribute Mapping
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping
*/
class AttributeMappingHelper implements Service {
private const ATTRIBUTES_AVAILABLE_FOR_MAPPING = [
Adult::class,
AgeGroup::class,
Brand::class,
Color::class,
Condition::class,
Gender::class,
GTIN::class,
IsBundle::class,
Material::class,
MPN::class,
Multipack::class,
Pattern::class,
Size::class,
SizeSystem::class,
SizeType::class,
];
public const CATEGORY_CONDITION_TYPE_ALL = 'ALL';
public const CATEGORY_CONDITION_TYPE_ONLY = 'ONLY';
public const CATEGORY_CONDITION_TYPE_EXCEPT = 'EXCEPT';
/**
* Gets all the available attributes for mapping
*
* @return array
*/
public function get_attributes(): array {
$destinations = [];
/**
* @var WithMappingInterface $attribute
*/
foreach ( self::ATTRIBUTES_AVAILABLE_FOR_MAPPING as $attribute ) {
array_push(
$destinations,
[
'id' => $attribute::get_id(),
'label' => $attribute::get_name(),
'enum' => $attribute::is_enum(),
]
);
}
return $destinations;
}
/**
* Get the attribute class based on attribute ID.
*
* @param string $attribute_id The attribute ID to get the class
* @return string|null The attribute class path or null if it's not found
*/
public static function get_attribute_by_id( string $attribute_id ): ?string {
foreach ( self::ATTRIBUTES_AVAILABLE_FOR_MAPPING as $class ) {
if ( $class::get_id() === $attribute_id ) {
return $class;
}
}
return null;
}
/**
* Get the sources for an attribute
*
* @param string $attribute_id The attribute ID to get the sources from.
* @return array The sources for the attribute
*/
public function get_sources_for_attribute( string $attribute_id ): array {
/**
* @var AttributeInterface $attribute
*/
$attribute = self::get_attribute_by_id( $attribute_id );
$attribute_sources = [];
if ( is_null( $attribute ) ) {
return $attribute_sources;
}
foreach ( $attribute::get_sources() as $key => $value ) {
array_push(
$attribute_sources,
[
'id' => $key,
'label' => $value,
]
);
}
return $attribute_sources;
}
/**
* Get the available conditions for the category.
*
* @return string[] The list of available category conditions
*/
public function get_category_condition_types(): array {
return [
self::CATEGORY_CONDITION_TYPE_ALL,
self::CATEGORY_CONDITION_TYPE_EXCEPT,
self::CATEGORY_CONDITION_TYPE_ONLY,
];
}
}
AttributeMapping/Traits/IsEnumTrait.php 0000644 00000001104 15154245121 0014202 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits;
defined( 'ABSPATH' ) || exit;
/**
* Trait for enums
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits
*/
trait IsEnumTrait {
/**
* Returns true for the is_enum property
*
* @return true
*/
public static function is_enum(): bool {
return true;
}
/**
* Returns the attribute sources
*
* @return array
*/
public static function get_sources(): array {
return self::get_value_options();
}
}
AttributeMapping/Traits/IsFieldTrait.php 0000644 00000007572 15154245121 0014340 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits;
defined( 'ABSPATH' ) || exit;
/**
* Trait for fields
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits
*/
trait IsFieldTrait {
/**
* Returns false for the is_enum property
*
* @return false
*/
public static function is_enum(): bool {
return false;
}
/**
* Returns the attribute sources
*
* @return array The available sources
*/
public static function get_sources(): array {
return apply_filters(
'woocommerce_gla_attribute_mapping_sources',
array_merge(
self::get_source_product_fields(),
self::get_source_taxonomies(),
self::get_source_custom_attributes()
),
self::get_id()
);
}
/**
* Gets the taxonomies and global attributes to render them as options in the frontend.
*
* @return array An array with the taxonomies and global attributes
*/
public static function get_source_taxonomies(): array {
$object_taxonomies = get_object_taxonomies( 'product', 'objects' );
$taxonomies = [];
$attributes = [];
$sources = [];
foreach ( $object_taxonomies as $taxonomy ) {
if ( taxonomy_is_product_attribute( $taxonomy->name ) ) {
$attributes[ 'taxonomy:' . $taxonomy->name ] = $taxonomy->label;
continue;
}
$taxonomies[ 'taxonomy:' . $taxonomy->name ] = $taxonomy->label;
}
asort( $taxonomies );
asort( $attributes );
$attributes = apply_filters( 'woocommerce_gla_attribute_mapping_sources_global_attributes', $attributes );
$taxonomies = apply_filters( 'woocommerce_gla_attribute_mapping_sources_taxonomies', $taxonomies );
if ( ! empty( $attributes ) ) {
$sources = array_merge(
[
'disabled:attributes' => __( '- Global attributes -', 'google-listings-and-ads' ),
],
$attributes
);
}
if ( ! empty( $taxonomies ) ) {
$sources = array_merge(
$sources,
[
'disabled:taxonomies' => __( '- Taxonomies -', 'google-listings-and-ads' ),
],
$taxonomies
);
}
return $sources;
}
/**
* Get a list of the available product sources.
*
* @return array An array with the available product sources.
*/
public static function get_source_product_fields(): array {
$fields = [
'product:backorders' => __( 'Allow backorders setting', 'google-listings-and-ads' ),
'product:title' => __( 'Product title', 'google-listings-and-ads' ),
'product:sku' => __( 'SKU', 'google-listings-and-ads' ),
'product:stock_quantity' => __( 'Stock Qty', 'google-listings-and-ads' ),
'product:stock_status' => __( 'Stock Status', 'google-listings-and-ads' ),
'product:tax_class' => __( 'Tax class', 'google-listings-and-ads' ),
'product:name' => __( 'Variation title', 'google-listings-and-ads' ),
'product:weight' => __( 'Weight (raw value, no units)', 'google-listings-and-ads' ),
'product:weight_with_unit' => __( 'Weight (with units)', 'google-listings-and-ads' ),
];
asort( $fields );
$fields = array_merge(
[
'disabled:product' => __( '- Product fields -', 'google-listings-and-ads' ),
],
$fields
);
return apply_filters( 'woocommerce_gla_attribute_mapping_sources_product_fields', $fields );
}
/**
* Allowing to register custom attributes by using a filter.
*
* @return array The custom attributes
*/
public static function get_source_custom_attributes(): array {
$attributes = [];
$attribute_keys = apply_filters( 'woocommerce_gla_attribute_mapping_sources_custom_attributes', [] );
foreach ( $attribute_keys as $key ) {
$attributes[ 'attribute:' . $key ] = $key;
}
if ( ! empty( $attributes ) ) {
$attributes = array_merge(
[
'disabled:attribute' => __( '- Custom Attributes -', 'google-listings-and-ads' ),
],
$attributes
);
}
return $attributes;
}
}
Attributes/AbstractAttribute.php 0000644 00000003562 15154245121 0013040 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
defined( 'ABSPATH' ) || exit;
/**
* Class AbstractAttribute
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
abstract class AbstractAttribute implements AttributeInterface {
/**
* @var mixed
*/
protected $value = null;
/**
* AbstractAttribute constructor.
*
* @param mixed $value
*/
public function __construct( $value = null ) {
$this->set_value( $value );
}
/**
* Return the attribute type. Must be a valid PHP type.
*
* @return string
*
* @link https://www.php.net/manual/en/function.settype.php
*/
public static function get_value_type(): string {
return 'string';
}
/**
* Returns the attribute value.
*
* @return mixed
*/
public function get_value() {
return $this->value;
}
/**
* @param mixed $value
*
* @return $this
*/
public function set_value( $value ): AbstractAttribute {
$this->value = $this->cast_value( $value );
return $this;
}
/**
* Casts the value to the attribute value type and returns the result.
*
* @param mixed $value
*
* @return mixed
*/
protected function cast_value( $value ) {
if ( is_string( $value ) ) {
$value = trim( $value );
if ( '' === $value ) {
return null;
}
}
$value_type = static::get_value_type();
if ( in_array( $value_type, [ 'bool', 'boolean' ], true ) ) {
$value = wc_string_to_bool( $value );
} else {
settype( $value, $value_type );
}
return $value;
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variable', 'variation' ];
}
/**
* @return string
*/
public function __toString() {
return (string) $this->get_value();
}
}
Attributes/Adult.php 0000644 00000003630 15154245121 0010456 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\AdultInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Adult
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Adult extends AbstractAttribute implements WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'adult';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variable', 'variation' ];
}
/**
* Return the attribute type. Must be a valid PHP type.
*
* @return string
*
* @link https://www.php.net/manual/en/function.settype.php
*/
public static function get_value_type(): string {
return 'boolean';
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return AdultInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Adult', 'google-listings-and-ads' );
}
/**
* Returns the attribute sources
*
* @return array
*/
public static function get_sources(): array {
return [
'yes' => __( 'Yes', 'google-listings-and-ads' ),
'no' => __( 'No', 'google-listings-and-ads' ),
];
}
}
Attributes/AgeGroup.php 0000644 00000003760 15154245121 0011122 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\AgeGroupInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class AgeGroup
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class AgeGroup extends AbstractAttribute implements WithValueOptionsInterface, WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'ageGroup';
}
/**
* Return an array of values available to choose for the attribute.
*
* Note: array key is used as the option key.
*
* @return array
*/
public static function get_value_options(): array {
return [
'newborn' => __( 'Newborn', 'google-listings-and-ads' ),
'infant' => __( 'Infant', 'google-listings-and-ads' ),
'toddler' => __( 'Toddler', 'google-listings-and-ads' ),
'kids' => __( 'Kids', 'google-listings-and-ads' ),
'adult' => __( 'Adult', 'google-listings-and-ads' ),
];
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return AgeGroupInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Age group', 'google-listings-and-ads' );
}
}
Attributes/AttributeInterface.php 0000644 00000002563 15154245121 0013175 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\AttributeInputInterface;
defined( 'ABSPATH' ) || exit;
/**
* Interface AttributeInterface
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
interface AttributeInterface {
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string;
/**
* Return the attribute's value type. Must be a valid PHP type.
*
* @return string
*
* @link https://www.php.net/manual/en/function.settype.php
*/
public static function get_value_type(): string;
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string;
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array;
/**
* Returns the attribute value.
*
* @return mixed
*/
public function get_value();
}
Attributes/AttributeManager.php 0000644 00000026252 15154245121 0012650 0 ustar 00 <?php
declare( strict_types=1 );
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;
/**
* Class AttributeManager
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class AttributeManager implements Service {
use PluginHelper;
use ValidateInterface;
protected const ATTRIBUTES = [
GTIN::class,
MPN::class,
Brand::class,
Condition::class,
Gender::class,
Size::class,
SizeSystem::class,
SizeType::class,
Color::class,
Material::class,
Pattern::class,
AgeGroup::class,
Multipack::class,
IsBundle::class,
AvailabilityDate::class,
Adult::class,
];
/**
* @var array Attribute types mapped to product types
*/
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
*
* @throws InvalidValue If the attribute is invalid for the given product.
*/
public function update( WC_Product $product, AttributeInterface $attribute ) {
$this->validate( $product, $attribute::get_id() );
if ( null === $attribute->get_value() || '' === $attribute->get_value() ) {
$this->delete( $product, $attribute::get_id() );
return;
}
$value = $attribute->get_value();
if ( in_array( $attribute::get_value_type(), [ 'bool', 'boolean' ], true ) ) {
$value = wc_bool_to_string( $value );
}
$product->update_meta_data( $this->prefix_meta_key( $attribute::get_id() ), $value );
$product->save_meta_data();
}
/**
* @param WC_Product $product
* @param string $attribute_id
*
* @return AttributeInterface|null
*
* @throws InvalidValue If the attribute ID is invalid for the given product.
*/
public function get( WC_Product $product, string $attribute_id ): ?AttributeInterface {
$this->validate( $product, $attribute_id );
$value = null;
if ( $this->exists( $product, $attribute_id ) ) {
$value = $product->get_meta( $this->prefix_meta_key( $attribute_id ), true );
}
if ( null === $value || '' === $value ) {
return null;
}
$attribute_class = $this->get_attribute_types_for_product( $product )[ $attribute_id ];
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 2.8.0
*
* @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.
*
* @param WC_Product $product
* @param string $attribute_id
*
* @return mixed|null
*/
public function get_value( WC_Product $product, string $attribute_id ) {
$attribute = $this->get( $product, $attribute_id );
return $attribute instanceof AttributeInterface ? $attribute->get_value() : null;
}
/**
* Return all attributes for the given product
*
* @param WC_Product $product
*
* @return AttributeInterface[]
*/
public function get_all( WC_Product $product ): array {
$all_attributes = [];
foreach ( array_keys( $this->get_attribute_types_for_product( $product ) ) as $attribute_id ) {
$attribute = $this->get( $product, $attribute_id );
if ( null !== $attribute ) {
$all_attributes[ $attribute_id ] = $attribute;
}
}
return $all_attributes;
}
/**
* Return all attribute values for the given product
*
* @param WC_Product $product
*
* @return array of attribute values
*/
public function get_all_values( WC_Product $product ): array {
$all_attributes = [];
foreach ( array_keys( $this->get_attribute_types_for_product( $product ) ) as $attribute_id ) {
$attribute = $this->get_value( $product, $attribute_id );
if ( null !== $attribute ) {
$all_attributes[ $attribute_id ] = $attribute;
}
}
return $all_attributes;
}
/**
* @param WC_Product $product
* @param string $attribute_id
*
* @throws InvalidValue If the attribute ID is invalid for the given product.
*/
public function delete( WC_Product $product, string $attribute_id ) {
$this->validate( $product, $attribute_id );
$product->delete_meta_data( $this->prefix_meta_key( $attribute_id ) );
$product->save_meta_data();
}
/**
* Whether the attribute exists and has been set for the product.
*
* @param WC_Product $product
* @param string $attribute_id
*
* @return bool
*
* @since 1.2.0
*/
public function exists( WC_Product $product, string $attribute_id ): bool {
return $product->meta_exists( $this->prefix_meta_key( $attribute_id ) );
}
/**
* Returns an array of attribute types for the given product
*
* @param WC_Product $product
*
* @return string[] of attribute classes mapped to attribute IDs
*/
public function get_attribute_types_for_product( WC_Product $product ): array {
return $this->get_attribute_types_for_product_types( [ $product->get_type() ] );
}
/**
* Returns an array of attribute types for the given product types
*
* @param string[] $product_types array of WooCommerce product types
*
* @return string[] of attribute classes mapped to attribute IDs
*/
public function get_attribute_types_for_product_types( array $product_types ): array {
// flip the product types array to have them as array keys
$product_types_keys = array_flip( $product_types );
// intersect the product types with our stored attributes map to get arrays of attributes matching the given product types
$match_attributes = array_intersect_key( $this->get_attribute_types_map(), $product_types_keys );
// re-index the attributes map array to avoid string ($product_type) array keys
$match_attributes = array_values( $match_attributes );
if ( empty( $match_attributes ) ) {
return [];
}
// merge all of the attribute arrays from the map (there might be duplicates) and return the results
return array_merge( ...$match_attributes );
}
/**
* Returns all available attribute IDs.
*
* @return array
*
* @since 1.3.0
*/
public static function get_available_attribute_ids(): array {
$attributes = [];
foreach ( self::get_available_attribute_types() as $attribute_type ) {
if ( method_exists( $attribute_type, 'get_id' ) ) {
$attribute_id = call_user_func( [ $attribute_type, 'get_id' ] );
$attributes[ $attribute_id ] = $attribute_id;
}
}
return $attributes;
}
/**
* Return an array of all available attribute class names.
*
* @return string[] Attribute class names
*
* @since 1.3.0
*/
public static function get_available_attribute_types(): array {
/**
* Filters the list of available product attributes.
*
* @param string[] $attributes Array of attribute class names (FQN)
*/
return apply_filters( 'woocommerce_gla_product_attribute_types', self::ATTRIBUTES );
}
/**
* Returns an array of attribute types for all product types
*
* @return string[][] of attribute classes mapped to product types
*/
protected function get_attribute_types_map(): array {
if ( ! isset( $this->attribute_types_map ) ) {
$this->map_attribute_types();
}
return $this->attribute_types_map;
}
/**
* @param WC_Product $product
* @param string $attribute_id
*
* @throws InvalidValue If the attribute type is invalid for the given product.
*/
protected function validate( WC_Product $product, string $attribute_id ) {
$attribute_types = $this->get_attribute_types_for_product( $product );
if ( ! isset( $attribute_types[ $attribute_id ] ) ) {
do_action(
'woocommerce_gla_error',
sprintf( 'Attribute "%s" is not supported for a "%s" product (ID: %s).', $attribute_id, $product->get_type(), $product->get_id() ),
__METHOD__
);
throw InvalidValue::not_in_allowed_list( 'attribute_id', array_keys( $attribute_types ) );
}
}
/**
* @throws InvalidClass If any of the given attribute classes do not implement the AttributeInterface.
*/
protected function map_attribute_types(): void {
$this->attribute_types_map = [];
foreach ( self::get_available_attribute_types() as $attribute_type ) {
$this->validate_interface( $attribute_type, AttributeInterface::class );
$attribute_id = call_user_func( [ $attribute_type, 'get_id' ] );
$applicable_types = call_user_func( [ $attribute_type, 'get_applicable_product_types' ] );
/**
* Filters the list of applicable product types for each attribute.
*
* @param string[] $applicable_types Array of WooCommerce product types
* @param string $attribute_type Attribute class name (FQN)
*/
$applicable_types = apply_filters( "woocommerce_gla_attribute_applicable_product_types_{$attribute_id}", $applicable_types, $attribute_type );
foreach ( $applicable_types as $product_type ) {
$this->attribute_types_map[ $product_type ] = $this->attribute_types_map[ $product_type ] ?? [];
$this->attribute_types_map[ $product_type ][ $attribute_id ] = $attribute_type;
}
}
}
}
Attributes/AvailabilityDate.php 0000644 00000003350 15154245121 0012614 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\AvailabilityDateInput;
defined( 'ABSPATH' ) || exit;
/**
* Class AvailabilityDate
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class AvailabilityDate extends AbstractAttribute {
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'availabilityDate';
}
/**
* Returns a name for the attribute. Used in attribute's input.
*
* @return string
*/
public static function get_name(): string {
return __( 'Availability Date', 'google-listings-and-ads' );
}
/**
* Returns a short description for the attribute. Used in attribute's input.
*
* @return string
*/
public static function get_description(): string {
return __( 'The date a preordered or backordered product becomes available for delivery. Required if product availability is preorder or backorder', 'google-listings-and-ads' );
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*/
public static function get_input_type(): string {
return AvailabilityDateInput::class;
}
}
Attributes/Brand.php 0000644 00000002673 15154245121 0010441 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\BrandInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Brand
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Brand extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'brand';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variable' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return BrandInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Brand', 'google-listings-and-ads' );
}
}
Attributes/Color.php 0000644 00000002674 15154245121 0010472 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\ColorInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Color
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Color extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'color';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return ColorInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Color', 'google-listings-and-ads' );
}
}
Attributes/Condition.php 0000644 00000003613 15154245121 0011334 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\ConditionInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Condition
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Condition extends AbstractAttribute implements WithValueOptionsInterface, WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'condition';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return an array of values available to choose for the attribute.
*
* Note: array key is used as the option key.
*
* @return array
*/
public static function get_value_options(): array {
return [
'new' => __( 'New', 'google-listings-and-ads' ),
'refurbished' => __( 'Refurbished', 'google-listings-and-ads' ),
'used' => __( 'Used', 'google-listings-and-ads' ),
];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return ConditionInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Condition', 'google-listings-and-ads' );
}
}
Attributes/GTIN.php 0000644 00000002666 15154245121 0010156 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\GTINInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class GTIN
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class GTIN extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'gtin';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return GTINInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'GTIN', 'google-listings-and-ads' );
}
}
Attributes/Gender.php 0000644 00000003550 15154245121 0010612 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\GenderInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Gender
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Gender extends AbstractAttribute implements WithValueOptionsInterface, WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'gender';
}
/**
* Return an array of values available to choose for the attribute.
*
* Note: array key is used as the option key.
*
* @return array
*/
public static function get_value_options(): array {
return [
'male' => __( 'Male', 'google-listings-and-ads' ),
'female' => __( 'Female', 'google-listings-and-ads' ),
'unisex' => __( 'Unisex', 'google-listings-and-ads' ),
];
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return GenderInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Gender', 'google-listings-and-ads' );
}
}
Attributes/IsBundle.php 0000644 00000003640 15154245121 0011113 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\IsBundleInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class IsBundle
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class IsBundle extends AbstractAttribute implements WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'isBundle';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute type. Must be a valid PHP type.
*
* @return string
*
* @link https://www.php.net/manual/en/function.settype.php
*/
public static function get_value_type(): string {
return 'boolean';
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return IsBundleInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Is Bundle', 'google-listings-and-ads' );
}
/**
* Returns the attribute sources
*
* @return array
*/
public static function get_sources(): array {
return [
'yes' => __( 'Yes', 'google-listings-and-ads' ),
'no' => __( 'No', 'google-listings-and-ads' ),
];
}
}
Attributes/MPN.php 0000644 00000002660 15154245121 0010041 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\MPNInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class MPN
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class MPN extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'mpn';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return MPNInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'MPN', 'google-listings-and-ads' );
}
}
Attributes/Material.php 0000644 00000002716 15154245121 0011147 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\MaterialInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Material
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Material extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'material';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return MaterialInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Material', 'google-listings-and-ads' );
}
}
Attributes/Multipack.php 0000644 00000003272 15154245121 0011340 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\MultipackInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Multipack
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Multipack extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'multipack';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute type. Must be a valid PHP type.
*
* @return string
*
* @link https://www.php.net/manual/en/function.settype.php
*/
public static function get_value_type(): string {
return 'integer';
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return MultipackInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Multipack', 'google-listings-and-ads' );
}
}
Attributes/Pattern.php 0000644 00000002710 15154245121 0011020 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\PatternInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Pattern
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Pattern extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'pattern';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return PatternInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Pattern', 'google-listings-and-ads' );
}
}
Attributes/Size.php 0000644 00000002666 15154245121 0010327 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\SizeInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsFieldTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class Size
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class Size extends AbstractAttribute implements WithMappingInterface {
use IsFieldTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'size';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return SizeInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Size', 'google-listings-and-ads' );
}
}
Attributes/SizeSystem.php 0000644 00000004407 15154245121 0011527 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\SizeSystemInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class SizeSystem
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class SizeSystem extends AbstractAttribute implements WithValueOptionsInterface, WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'sizeSystem';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return an array of values available to choose for the attribute.
*
* Note: array key is used as the option key.
*
* @return array
*/
public static function get_value_options(): array {
return [
'US' => __( 'US', 'google-listings-and-ads' ),
'EU' => __( 'EU', 'google-listings-and-ads' ),
'UK' => __( 'UK', 'google-listings-and-ads' ),
'DE' => __( 'DE', 'google-listings-and-ads' ),
'FR' => __( 'FR', 'google-listings-and-ads' ),
'IT' => __( 'IT', 'google-listings-and-ads' ),
'AU' => __( 'AU', 'google-listings-and-ads' ),
'BR' => __( 'BR', 'google-listings-and-ads' ),
'CN' => __( 'CN', 'google-listings-and-ads' ),
'JP' => __( 'JP', 'google-listings-and-ads' ),
'MEX' => __( 'MEX', 'google-listings-and-ads' ),
];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return SizeSystemInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Size System', 'google-listings-and-ads' );
}
}
Attributes/SizeType.php 0000644 00000004160 15154245121 0011160 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\SizeTypeInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\Traits\IsEnumTrait;
defined( 'ABSPATH' ) || exit;
/**
* Class SizeType
*
* @see https://support.google.com/merchants/answer/6324497
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
class SizeType extends AbstractAttribute implements WithValueOptionsInterface, WithMappingInterface {
use IsEnumTrait;
/**
* Returns the attribute ID.
*
* Must be the same as a Google product's property name to be set automatically.
*
* @return string
*
* @see \Google\Service\ShoppingContent\Product for the list of properties.
*/
public static function get_id(): string {
return 'sizeType';
}
/**
* Return an array of WooCommerce product types that this attribute can be applied to.
*
* @return array
*/
public static function get_applicable_product_types(): array {
return [ 'simple', 'variation' ];
}
/**
* Return an array of values available to choose for the attribute.
*
* Note: array key is used as the option key.
*
* @return array
*/
public static function get_value_options(): array {
return [
'regular' => __( 'Regular', 'google-listings-and-ads' ),
'petite' => __( 'Petite', 'google-listings-and-ads' ),
'plus' => __( 'Plus', 'google-listings-and-ads' ),
'tall' => __( 'Tall', 'google-listings-and-ads' ),
'big' => __( 'Big', 'google-listings-and-ads' ),
'maternity' => __( 'Maternity', 'google-listings-and-ads' ),
];
}
/**
* Return the attribute's input class. Must be an instance of `AttributeInputInterface`.
*
* @return string
*
* @see AttributeInputInterface
*
* @since 1.5.0
*/
public static function get_input_type(): string {
return SizeTypeInput::class;
}
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string {
return __( 'Size Type', 'google-listings-and-ads' );
}
}
Attributes/WithMappingInterface.php 0000644 00000001227 15154245121 0013455 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
defined( 'ABSPATH' ) || exit;
/**
* Interface with specific options for mapping
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
interface WithMappingInterface {
/**
* Returns the attribute name
*
* @return string
*/
public static function get_name(): string;
/**
* Returns true if the attribute is enum type
*
* @return boolean
*/
public static function is_enum(): bool;
/**
* Returns the available attribute sources
*
* @return array
*/
public static function get_sources(): array;
}
Attributes/WithValueOptionsInterface.php 0000644 00000000775 15154245121 0014521 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes;
defined( 'ABSPATH' ) || exit;
/**
* Interface WithValueOptionsInterface
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*/
interface WithValueOptionsInterface {
/**
* Return an array of values available to choose for the attribute.
*
* Note: array key is used as the option key.
*
* @return array
*/
public static function get_value_options(): array;
}
BatchProductHelper.php 0000644 00000023624 15154245121 0011006 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\AttributeMappingRulesQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\GoogleListingsAndAdsException;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ValidateInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductIDRequestEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchInvalidProductEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductRequestEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\TargetAudience;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Product as GoogleProduct;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use WC_Product;
use WC_Product_Variable;
defined( 'ABSPATH' ) || exit;
/**
* Class BatchProductHelper
*
* Contains helper methods for batch processing products.
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class BatchProductHelper implements Service {
use ValidateInterface;
/**
* @var ProductMetaHandler
*/
protected $meta_handler;
/**
* @var ProductHelper
*/
protected $product_helper;
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* @var ProductFactory
*/
protected $product_factory;
/**
* @var TargetAudience
*/
protected $target_audience;
/**
* @var AttributeMappingRulesQuery
*/
protected $attribute_mapping_rules_query;
/**
* BatchProductHelper constructor.
*
* @param ProductMetaHandler $meta_handler
* @param ProductHelper $product_helper
* @param ValidatorInterface $validator
* @param ProductFactory $product_factory
* @param TargetAudience $target_audience
* @param AttributeMappingRulesQuery $attribute_mapping_rules_query
*/
public function __construct(
ProductMetaHandler $meta_handler,
ProductHelper $product_helper,
ValidatorInterface $validator,
ProductFactory $product_factory,
TargetAudience $target_audience,
AttributeMappingRulesQuery $attribute_mapping_rules_query
) {
$this->meta_handler = $meta_handler;
$this->product_helper = $product_helper;
$this->validator = $validator;
$this->product_factory = $product_factory;
$this->target_audience = $target_audience;
$this->attribute_mapping_rules_query = $attribute_mapping_rules_query;
}
/**
* Filters and returns only the products already synced with Google Merchant Center.
*
* @param WC_Product[] $products
*
* @return WC_Product[] The synced products.
*/
public function filter_synced_products( array $products ): array {
return array_filter( $products, [ $this->product_helper, 'is_product_synced' ] );
}
/**
* @param BatchProductEntry $product_entry
*/
public function mark_as_synced( BatchProductEntry $product_entry ) {
$wc_product = $this->product_helper->get_wc_product( $product_entry->get_wc_product_id() );
$google_product = $product_entry->get_google_product();
$this->validate_instanceof( $google_product, GoogleProduct::class );
$this->product_helper->mark_as_synced( $wc_product, $google_product );
}
/**
* @param BatchProductEntry $product_entry
*/
public function mark_as_unsynced( BatchProductEntry $product_entry ) {
try {
$wc_product = $this->product_helper->get_wc_product( $product_entry->get_wc_product_id() );
} catch ( InvalidValue $exception ) {
return;
}
$this->product_helper->mark_as_unsynced( $wc_product );
}
/**
* Mark a batch of WooCommerce product IDs as unsynced.
* Invalid products will be skipped.
*
* @since 1.12.0
*
* @param array $product_ids
*/
public function mark_batch_as_unsynced( array $product_ids ) {
foreach ( $product_ids as $product_id ) {
try {
$product = $this->product_helper->get_wc_product( $product_id );
} catch ( InvalidValue $exception ) {
continue;
}
$this->product_helper->mark_as_unsynced( $product );
}
}
/**
* Marks a WooCommerce product as invalid and stores the errors in a meta data key.
*
* Note: If a product variation is invalid then the parent product is also marked as invalid.
*
* @param BatchInvalidProductEntry $product_entry
*/
public function mark_as_invalid( BatchInvalidProductEntry $product_entry ) {
$wc_product = $this->product_helper->get_wc_product( $product_entry->get_wc_product_id() );
$errors = $product_entry->get_errors();
$this->product_helper->mark_as_invalid( $wc_product, $errors );
}
/**
* Generates an array map containing the Google product IDs as key and the WooCommerce product IDs as values.
*
* @param WC_Product[] $products
*
* @return BatchProductIDRequestEntry[]
*/
public function generate_delete_request_entries( array $products ): array {
$request_entries = [];
foreach ( $products as $product ) {
$this->validate_instanceof( $product, WC_Product::class );
if ( $product instanceof WC_Product_Variable ) {
$request_entries = array_merge( $request_entries, $this->generate_delete_request_entries( $product->get_available_variations( 'objects' ) ) );
continue;
}
$google_ids = $this->product_helper->get_synced_google_product_ids( $product );
if ( empty( $google_ids ) ) {
continue;
}
foreach ( $google_ids as $google_id ) {
$request_entries[ $google_id ] = new BatchProductIDRequestEntry(
$product->get_id(),
$google_id
);
}
}
return $request_entries;
}
/**
* @param WC_Product[] $products
*
* @return BatchProductRequestEntry[]
*/
public function validate_and_generate_update_request_entries( array $products ): array {
$request_entries = [];
$mapping_rules = $this->attribute_mapping_rules_query->get_results();
foreach ( $products as $product ) {
$this->validate_instanceof( $product, WC_Product::class );
try {
if ( ! $this->product_helper->is_sync_ready( $product ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Skipping product (ID: %s) because it is not ready to be synced.', $product->get_id() ),
__METHOD__
);
continue;
}
if ( $product instanceof WC_Product_Variable ) {
$request_entries = array_merge( $request_entries, $this->validate_and_generate_update_request_entries( $product->get_available_variations( 'objects' ) ) );
continue;
}
$target_countries = $this->target_audience->get_target_countries();
$main_target_country = $this->target_audience->get_main_target_country();
// validate the product
$adapted_product = $this->product_factory->create( $product, $main_target_country, $mapping_rules );
$validation_result = $this->validate_product( $adapted_product );
if ( $validation_result instanceof BatchInvalidProductEntry ) {
$this->mark_as_invalid( $validation_result );
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Skipping product (ID: %s) because it does not pass validation: %s', $product->get_id(), wp_json_encode( $validation_result ) ),
__METHOD__
);
continue;
}
// add shipping for all selected target countries
array_walk( $target_countries, [ $adapted_product, 'add_shipping_country' ] );
$request_entries[] = new BatchProductRequestEntry(
$product->get_id(),
$adapted_product
);
} catch ( GoogleListingsAndAdsException $exception ) {
do_action(
'woocommerce_gla_error',
sprintf( 'Skipping product (ID: %s) due to exception: %s', $product->get_id(), $exception->getMessage() ),
__METHOD__
);
continue;
}
}
return $request_entries;
}
/**
* @param WCProductAdapter $product
*
* @return BatchInvalidProductEntry|true
*/
protected function validate_product( WCProductAdapter $product ) {
$violations = $this->validator->validate( $product );
if ( 0 !== count( $violations ) ) {
$invalid_product = new BatchInvalidProductEntry( $product->get_wc_product()->get_id() );
$invalid_product->map_validation_violations( $violations );
return $invalid_product;
}
return true;
}
/**
* Filters and returns an array of request entries for Google products that should no longer be submitted for the selected target audience.
*
* @param WC_Product[] $products
*
* @return BatchProductIDRequestEntry[]
*/
public function generate_stale_products_request_entries( array $products ): array {
$target_audience = $this->target_audience->get_target_countries();
$request_entries = [];
foreach ( $products as $product ) {
$google_ids = $this->meta_handler->get_google_ids( $product ) ?: [];
$stale_ids = array_diff_key( $google_ids, array_flip( $target_audience ) );
foreach ( $stale_ids as $stale_id ) {
$request_entries[ $stale_id ] = new BatchProductIDRequestEntry(
$product->get_id(),
$stale_id
);
}
}
return $request_entries;
}
/**
* Returns an array of request entries for Google products that should no
* longer be submitted for every target country.
*
* @since 1.1.0
*
* @param WC_Product[] $products
*
* @return BatchProductIDRequestEntry[]
*/
public function generate_stale_countries_request_entries( array $products ): array {
$main_target_country = $this->target_audience->get_main_target_country();
$request_entries = [];
foreach ( $products as $product ) {
$google_ids = $this->meta_handler->get_google_ids( $product ) ?: [];
$stale_ids = array_diff_key( $google_ids, array_flip( [ $main_target_country ] ) );
foreach ( $stale_ids as $stale_id ) {
$request_entries[ $stale_id ] = new BatchProductIDRequestEntry(
$product->get_id(),
$stale_id
);
}
}
return $request_entries;
}
}
FilteredProductList.php 0000644 00000003170 15154245121 0011211 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Countable;
use WC_Product;
defined( 'ABSPATH' ) || exit;
/**
* Class FilteredProductList
*
* A list of filtered products and their total count before filtering.
*
* @since 1.4.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class FilteredProductList implements Countable {
/**
* List of product objects or IDs.
*
* @var WC_Product[]
*/
protected $products = [];
/**
* Count before filtering.
*
* @var int
*/
protected $unfiltered_count;
/**
* FilteredProductList constructor.
*
* @param WC_Product[] $products List of filtered products.
* @param int $unfiltered_count Product count before filtering.
*/
public function __construct( array $products, int $unfiltered_count ) {
$this->products = $products;
$this->unfiltered_count = $unfiltered_count;
}
/**
* Get the list of products.
*
* @return WC_Product[]
*/
public function get(): array {
return $this->products;
}
/**
* Get product IDs.
*
* @return int[]
*/
public function get_product_ids(): array {
return array_map(
function ( $product ) {
if ( $product instanceof WC_Product ) {
return $product->get_id();
}
return $product;
},
$this->products
);
}
/**
* Get the unfiltered amount of results.
*
* @return int
*/
public function get_unfiltered_count(): int {
return $this->unfiltered_count;
}
/**
* Count products for Countable.
*
* @return int
*/
public function count(): int {
return count( $this->products );
}
}
ProductFactory.php 0000644 00000004417 15154245121 0010233 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ValidateInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use WC_Product;
use WC_Product_Variable;
use WC_Product_Variation;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductFactory
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class ProductFactory {
use ValidateInterface;
/**
* @var AttributeManager
*/
protected $attribute_manager;
/**
* @var WC
*/
protected $wc;
/**
* ProductFactory constructor.
*
* @param AttributeManager $attribute_manager
* @param WC $wc
*/
public function __construct( AttributeManager $attribute_manager, WC $wc ) {
$this->attribute_manager = $attribute_manager;
$this->wc = $wc;
}
/**
* @param WC_Product $product
* @param string $target_country
* @param array $mapping_rules The mapping rules setup by the user
*
* @return WCProductAdapter
*
* @throws InvalidValue When the product is a variation and its parent does not exist.
*/
public function create( WC_Product $product, string $target_country, array $mapping_rules ): WCProductAdapter {
// We do not support syncing the parent variable product. Each variation is synced individually instead.
$this->validate_not_instanceof( $product, WC_Product_Variable::class );
$attributes = $this->attribute_manager->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->attribute_manager->get_all_values( $parent_product );
$attributes = array_merge( $parent_attributes, $attributes );
}
return new WCProductAdapter(
[
'wc_product' => $product,
'parent_wc_product' => $parent_product,
'targetCountry' => $target_country,
'gla_attributes' => $attributes,
'mapping_rules' => $mapping_rules,
]
);
}
}
ProductFilter.php 0000644 00000004471 15154245121 0010051 0 ustar 00 <?php
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use WC_Product;
/**
* Class ProductFilter
*
* Filters a list of products retrieved from the repository.
*
* @since 1.4.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class ProductFilter implements Service {
/**
* @var ProductHelper
*/
protected $product_helper;
/**
* ProductFilter constructor.
*
* @param ProductHelper $product_helper
*/
public function __construct( ProductHelper $product_helper ) {
$this->product_helper = $product_helper;
}
/**
* Filters and returns a list of products that are ready to be submitted to Google Merchant Center.
*
* @param WC_Product[] $products
*
* @return FilteredProductList
*/
public function filter_sync_ready_products( array $products ): FilteredProductList {
$unfiltered_count = count( $products );
/**
* Filters the list of products ready to be synced (before applying filters to check failures and sync-ready status).
*
* @param WC_Product[] $products Sync-ready WooCommerce products
*/
$products = apply_filters( 'woocommerce_gla_get_sync_ready_products_pre_filter', $products );
$results = array_values(
array_filter(
$products,
function ( $product ) {
return $this->product_helper->is_sync_ready( $product ) && ! $this->product_helper->is_sync_failed_recently( $product );
}
)
);
/**
* Filters the list of products ready to be synced (after applying filters to check failures and sync-ready status).
*
* @param WC_Product[] $results Sync-ready WooCommerce products
*/
$results = apply_filters( 'woocommerce_gla_get_sync_ready_products_filter', $results );
return new FilteredProductList( $results, $unfiltered_count );
}
/**
* Filter and return a list of products that can be deleted.
*
* @since 1.12.0
*
* @param WC_Product[] $products
*
* @return FilteredProductList
*/
public function filter_products_for_delete( array $products ): FilteredProductList {
$results = array_values(
array_filter(
$products,
function ( $product ) {
return ! $this->product_helper->is_delete_failed_threshold_reached( $product );
}
)
);
return new FilteredProductList( $results, count( $products ) );
}
}
ProductHelper.php 0000644 00000061474 15154245121 0010051 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleProductService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\TargetAudience;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\MCStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\SyncStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Product as GoogleProduct;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\HelperNotificationInterface;
use WC_Product;
use WC_Product_Variation;
use WP_Post;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductHelper
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class ProductHelper implements Service, HelperNotificationInterface {
use PluginHelper;
/**
* @var ProductMetaHandler
*/
protected $meta_handler;
/**
* @var WC
*/
protected $wc;
/**
* @var TargetAudience
*/
protected $target_audience;
/**
* ProductHelper constructor.
*
* @param ProductMetaHandler $meta_handler
* @param WC $wc
* @param TargetAudience $target_audience
*/
public function __construct( ProductMetaHandler $meta_handler, WC $wc, TargetAudience $target_audience ) {
$this->meta_handler = $meta_handler;
$this->wc = $wc;
$this->target_audience = $target_audience;
}
/**
* Mark the item as notified.
*
* @param WC_Product $product
*
* @return void
*/
public function mark_as_notified( $product ): void {
$this->meta_handler->delete_failed_delete_attempts( $product );
$this->meta_handler->update_synced_at( $product, time() );
$this->meta_handler->update_sync_status( $product, SyncStatus::SYNCED );
$this->update_empty_visibility( $product );
// mark the parent product as synced if it's a variation
if ( $product instanceof WC_Product_Variation ) {
try {
$parent_product = $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
return;
}
$this->mark_as_notified( $parent_product );
}
}
/**
* Mark a product as synced in the local database.
* This function also handles the following cleanup tasks:
* - Remove any failed delete attempts
* - Update the visibility (if it was previously empty)
* - Remove any previous product errors (if it was synced for all target countries)
*
* @param WC_Product $product
* @param GoogleProduct $google_product
*/
public function mark_as_synced( WC_Product $product, GoogleProduct $google_product ) {
$this->meta_handler->delete_failed_delete_attempts( $product );
$this->meta_handler->update_synced_at( $product, time() );
$this->meta_handler->update_sync_status( $product, SyncStatus::SYNCED );
$this->update_empty_visibility( $product );
// merge and update all google product ids
$current_google_ids = $this->meta_handler->get_google_ids( $product );
$current_google_ids = ! empty( $current_google_ids ) ? $current_google_ids : [];
$google_ids = array_unique( array_merge( $current_google_ids, [ $google_product->getTargetCountry() => $google_product->getId() ] ) );
$this->meta_handler->update_google_ids( $product, $google_ids );
// check if product is synced for main target country and remove any previous errors if it is
$synced_countries = array_keys( $google_ids );
$target_countries = $this->target_audience->get_target_countries();
if ( empty( array_diff( $synced_countries, $target_countries ) ) ) {
$this->meta_handler->delete_errors( $product );
$this->meta_handler->delete_failed_sync_attempts( $product );
$this->meta_handler->delete_sync_failed_at( $product );
}
// mark the parent product as synced if it's a variation
if ( $product instanceof WC_Product_Variation ) {
try {
$parent_product = $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
return;
}
$this->mark_as_synced( $parent_product, $google_product );
}
}
/**
* @param WC_Product $product
*/
public function mark_as_unsynced( $product ): void {
$this->meta_handler->delete_synced_at( $product );
if ( ! $this->is_sync_ready( $product ) ) {
$this->meta_handler->delete_sync_status( $product );
} else {
$this->meta_handler->update_sync_status( $product, SyncStatus::NOT_SYNCED );
}
$this->meta_handler->delete_google_ids( $product );
$this->meta_handler->delete_errors( $product );
$this->meta_handler->delete_failed_sync_attempts( $product );
$this->meta_handler->delete_sync_failed_at( $product );
// mark the parent product as un-synced if it's a variation
if ( $product instanceof WC_Product_Variation ) {
try {
$parent_product = $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
return;
}
$this->mark_as_unsynced( $parent_product );
}
}
/**
* @param WC_Product $product
* @param string $google_id
*/
public function remove_google_id( WC_Product $product, string $google_id ) {
$google_ids = $this->meta_handler->get_google_ids( $product );
if ( empty( $google_ids ) ) {
return;
}
$idx = array_search( $google_id, $google_ids, true );
if ( false === $idx ) {
return;
}
unset( $google_ids[ $idx ] );
if ( ! empty( $google_ids ) ) {
$this->meta_handler->update_google_ids( $product, $google_ids );
} else {
// if there are no Google IDs left then this product is no longer considered "synced"
$this->mark_as_unsynced( $product );
}
}
/**
* Marks a WooCommerce product as invalid and stores the errors in a meta data key.
*
* Note: If a product variation is invalid then the parent product is also marked as invalid.
*
* @param WC_Product $product
* @param string[] $errors
*/
public function mark_as_invalid( WC_Product $product, array $errors ) {
// bail if no errors exist
if ( empty( $errors ) ) {
return;
}
$this->meta_handler->update_errors( $product, $errors );
$this->meta_handler->update_sync_status( $product, SyncStatus::HAS_ERRORS );
$this->update_empty_visibility( $product );
if ( ! empty( $errors[ GoogleProductService::INTERNAL_ERROR_REASON ] ) ) {
// update failed sync attempts count in case of internal errors
$failed_attempts = ! empty( $this->meta_handler->get_failed_sync_attempts( $product ) ) ?
$this->meta_handler->get_failed_sync_attempts( $product ) :
0;
$this->meta_handler->update_failed_sync_attempts( $product, $failed_attempts + 1 );
$this->meta_handler->update_sync_failed_at( $product, time() );
}
// mark the parent product as invalid if it's a variation
if ( $product instanceof WC_Product_Variation ) {
try {
$parent_product = $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
return;
}
$parent_errors = ! empty( $this->meta_handler->get_errors( $parent_product ) ) ?
$this->meta_handler->get_errors( $parent_product ) :
[];
$parent_errors[ $product->get_id() ] = $errors;
$this->mark_as_invalid( $parent_product, $parent_errors );
}
}
/**
* Marks a WooCommerce product as pending synchronization.
*
* Note: If a product variation is pending then the parent product is also marked as pending.
*
* @param WC_Product $product
*/
public function mark_as_pending( WC_Product $product ) {
$this->meta_handler->update_sync_status( $product, SyncStatus::PENDING );
// mark the parent product as pending if it's a variation
if ( $product instanceof WC_Product_Variation ) {
try {
$parent_product = $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
return;
}
$this->mark_as_pending( $parent_product );
}
}
/**
* Update empty (NOT EXIST) visibility meta values to SYNC_AND_SHOW.
*
* @param WC_Product $product
*/
protected function update_empty_visibility( WC_Product $product ): void {
try {
$product = $this->maybe_swap_for_parent( $product );
} catch ( InvalidValue $exception ) {
return;
}
$visibility = $this->meta_handler->get_visibility( $product );
if ( empty( $visibility ) ) {
$this->meta_handler->update_visibility( $product, ChannelVisibility::SYNC_AND_SHOW );
}
}
/**
* Update a product's channel visibility.
*
* @param WC_Product $product
* @param string $visibility
*/
public function update_channel_visibility( WC_Product $product, string $visibility ): void {
try {
$product = $this->maybe_swap_for_parent( $product );
} catch ( InvalidValue $exception ) {
// The error has been logged within the call of maybe_swap_for_parent
return;
}
try {
$visibility = ChannelVisibility::cast( $visibility )->get();
} catch ( InvalidValue $exception ) {
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
return;
}
$this->meta_handler->update_visibility( $product, $visibility );
}
/**
* @param WC_Product $product
*
* @return string[]|null An array of Google product IDs stored for each WooCommerce product
*/
public function get_synced_google_product_ids( WC_Product $product ): ?array {
return $this->meta_handler->get_google_ids( $product );
}
/**
* See: WCProductAdapter::map_wc_product_id()
*
* @param string $mc_product_id Simple product ID (`merchant_center_id`) or
* namespaced product ID (`online:en:GB:merchant_center_id`)
*
* @return int the ID for the WC product linked to the provided Google product ID (0 if not found)
*/
public function get_wc_product_id( string $mc_product_id ): int {
// Maybe remove everything before the last colon ':'
$mc_product_id_tokens = explode( ':', $mc_product_id );
$mc_product_id = end( $mc_product_id_tokens );
// Support a fully numeric ID both with and without the `gla_` prefix.
$wc_product_id = 0;
$pattern = '/^(' . preg_quote( $this->get_slug(), '/' ) . '_)?(\d+)$/';
if ( preg_match( $pattern, $mc_product_id, $matches ) ) {
$wc_product_id = (int) $matches[2];
}
/**
* Filters the WooCommerce product ID that was determined to be associated with the
* given Merchant Center product ID.
*
* @param string $wc_product_id The WooCommerce product ID as determined by default.
* @param string $mc_product_id Simple Merchant Center product ID (without any prefixes).
* @since 2.4.6
*
* @return string Merchant Center product ID as normally generated by the plugin (e.g., gla_1234).
*/
return (int) apply_filters( 'woocommerce_gla_get_wc_product_id', $wc_product_id, $mc_product_id );
}
/**
* Attempt to get the WooCommerce product title.
* The MC ID is converted to a WC ID before retrieving the product.
* If we can't retrieve the title we fallback to the original MC ID.
*
* @param string $mc_product_id Merchant Center product ID.
*
* @return string
*/
public function get_wc_product_title( string $mc_product_id ): string {
try {
$product = $this->get_wc_product( $this->get_wc_product_id( $mc_product_id ) );
} catch ( InvalidValue $e ) {
return $mc_product_id;
}
return $product->get_title();
}
/**
* Get WooCommerce product
*
* @param int $product_id
*
* @return WC_Product
*
* @throws InvalidValue If the given ID doesn't reference a valid product.
*/
public function get_wc_product( int $product_id ): WC_Product {
return $this->wc->get_product( $product_id );
}
/**
* Get WooCommerce product by WP get_post
*
* @param int $product_id
*
* @return WP_Post|null
*/
public function get_wc_product_by_wp_post( int $product_id ): ?WP_Post {
return get_post( $product_id );
}
/**
* @param WC_Product $product
*
* @return bool
*/
public function is_product_synced( WC_Product $product ): bool {
$synced_at = $this->meta_handler->get_synced_at( $product );
$google_ids = $this->meta_handler->get_google_ids( $product );
return ! empty( $synced_at ) && ! empty( $google_ids );
}
/**
* Indicates if a product is ready for sending Notifications.
* A product is ready to send notifications if DONT_SYNC_AND_SHOW is not enabled and the post status is publish.
*
* @param WC_Product $product
*
* @return bool
*/
public function is_ready_to_notify( WC_Product $product ): bool {
$is_ready = ChannelVisibility::DONT_SYNC_AND_SHOW !== $this->get_channel_visibility( $product ) &&
$product->get_status() === 'publish' &&
in_array( $product->get_type(), ProductSyncer::get_supported_product_types(), true );
if ( $is_ready && $product instanceof WC_Product_Variation ) {
$parent = $this->maybe_swap_for_parent( $product );
$is_ready = $this->is_ready_to_notify( $parent );
}
/**
* Allow users to filter if a product is ready to notify.
*
* @since 2.8.0
*
* @param bool $value The current filter value.
* @param WC_Product $product The product for the notification.
*/
return apply_filters( 'woocommerce_gla_product_is_ready_to_notify', $is_ready, $product );
}
/**
* Indicates if a product is ready for sending a create Notification.
* A product is ready to send create notifications if is ready to notify and has not sent create notification yet.
*
* @param WC_Product $product
*
* @return bool
*/
public function should_trigger_create_notification( $product ): bool {
return ! $product instanceof WC_Product_Variation && $this->is_ready_to_notify( $product ) && ! $this->has_notified_creation( $product );
}
/**
* Indicates if a product is ready for sending an update Notification.
* A product is ready to send update notifications if is ready to notify and has sent create notification already.
*
* @param WC_Product $product
*
* @return bool
*/
public function should_trigger_update_notification( $product ): bool {
return ! $product instanceof WC_Product_Variation && $this->is_ready_to_notify( $product ) && $this->has_notified_creation( $product );
}
/**
* Indicates if a product is ready for sending a delete Notification.
* A product is ready to send delete notifications if it is not ready to notify and has sent create notification already.
*
* @param WC_Product $product
*
* @return bool
*/
public function should_trigger_delete_notification( $product ): bool {
return ! $this->is_ready_to_notify( $product ) && $this->has_notified_creation( $product );
}
/**
* Indicates if a product was already notified about its creation.
* Notice we consider synced products in MC as notified for creation.
*
* @param WC_Product $product
*
* @return bool
*/
public function has_notified_creation( WC_Product $product ): bool {
if ( $product instanceof WC_Product_Variation ) {
return $this->has_notified_creation( $this->maybe_swap_for_parent( $product ) );
}
$valid_has_notified_creation_statuses = [
NotificationStatus::NOTIFICATION_CREATED,
NotificationStatus::NOTIFICATION_UPDATED,
NotificationStatus::NOTIFICATION_PENDING_UPDATE,
NotificationStatus::NOTIFICATION_PENDING_DELETE,
];
return in_array(
$this->meta_handler->get_notification_status( $product ),
$valid_has_notified_creation_statuses,
true
) || $this->is_product_synced( $product );
}
/**
* Set the notification status for a WooCommerce product.
*
* @param WC_Product $product
* @param string $status
*/
public function set_notification_status( $product, $status ): void {
$this->meta_handler->update_notification_status( $product, $status );
}
/**
* @param WC_Product $product
*
* @return bool
*/
public function is_sync_ready( WC_Product $product ): bool {
$product_visibility = $product->is_visible();
$product_status = $product->get_status();
if ( $product instanceof WC_Product_Variation ) {
// Check the post status of the parent product if it's a variation
try {
$parent = $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
do_action(
'woocommerce_gla_error',
sprintf( 'Cannot sync an orphaned variation (ID: %s).', $product->get_id() ),
__METHOD__
);
return false;
}
$product_status = $parent->get_status();
/**
* Optionally hide invisible variations (disabled variations and variations with empty price).
*
* @see WC_Product_Variable::get_available_variations for filter documentation
*/
if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $parent->get_id(), $product ) && ! $product->variation_is_visible() ) {
$product_visibility = false;
}
}
return ( ChannelVisibility::DONT_SYNC_AND_SHOW !== $this->get_channel_visibility( $product ) ) &&
( in_array( $product->get_type(), ProductSyncer::get_supported_product_types(), true ) ) &&
( 'publish' === $product_status ) &&
$product_visibility;
}
/**
* Whether the sync has failed repeatedly for the product within the given timeframe.
*
* @param WC_Product $product
*
* @return bool
*
* @see ProductSyncer::FAILURE_THRESHOLD The number of failed attempts allowed per timeframe
* @see ProductSyncer::FAILURE_THRESHOLD_WINDOW The specified timeframe
*/
public function is_sync_failed_recently( WC_Product $product ): bool {
$failed_attempts = $this->meta_handler->get_failed_sync_attempts( $product );
$failed_at = $this->meta_handler->get_sync_failed_at( $product );
// if it has failed more times than the specified threshold AND if syncing it has failed within the specified window
return $failed_attempts > ProductSyncer::FAILURE_THRESHOLD &&
$failed_at > strtotime( sprintf( '-%s', ProductSyncer::FAILURE_THRESHOLD_WINDOW ) );
}
/**
* Increment failed delete attempts.
*
* @since 1.12.0
*
* @param WC_Product $product
*/
public function increment_failed_delete_attempt( WC_Product $product ) {
$failed_attempts = $this->meta_handler->get_failed_delete_attempts( $product ) ?? 0;
$this->meta_handler->update_failed_delete_attempts( $product, $failed_attempts + 1 );
}
/**
* Whether deleting has failed more times than the specified threshold.
*
* @since 1.12.0
*
* @param WC_Product $product
*
* @return boolean
*/
public function is_delete_failed_threshold_reached( WC_Product $product ): bool {
$failed_attempts = $this->meta_handler->get_failed_delete_attempts( $product ) ?? 0;
return $failed_attempts >= ProductSyncer::FAILURE_THRESHOLD;
}
/**
* Increment failed delete attempts.
*
* @since 1.12.2
*
* @param WC_Product $product
*/
public function increment_failed_update_attempt( WC_Product $product ) {
$failed_attempts = $this->meta_handler->get_failed_sync_attempts( $product ) ?? 0;
$this->meta_handler->update_failed_sync_attempts( $product, $failed_attempts + 1 );
}
/**
* Whether deleting has failed more times than the specified threshold.
*
* @since 1.12.2
*
* @param WC_Product $product
*
* @return boolean
*/
public function is_update_failed_threshold_reached( WC_Product $product ): bool {
$failed_attempts = $this->meta_handler->get_failed_sync_attempts( $product ) ?? 0;
return $failed_attempts >= ProductSyncer::FAILURE_THRESHOLD;
}
/**
* @param WC_Product $wc_product
*
* @return string|null
*/
public function get_channel_visibility( WC_Product $wc_product ): ?string {
try {
// todo: we might need to define visibility per variation later.
return $this->meta_handler->get_visibility( $this->maybe_swap_for_parent( $wc_product ) );
} catch ( InvalidValue $exception ) {
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Channel visibility forced to "%s" for invalid product (ID: %s).', ChannelVisibility::DONT_SYNC_AND_SHOW, $wc_product->get_id() ),
__METHOD__
);
return ChannelVisibility::DONT_SYNC_AND_SHOW;
}
}
/**
* Return a string indicating sync status based on several factors.
*
* @param WC_Product $wc_product
*
* @return string|null
*/
public function get_sync_status( WC_Product $wc_product ): ?string {
return $this->meta_handler->get_sync_status( $wc_product );
}
/**
* Return the string indicating the product status as reported by the Merchant Center.
*
* @param WC_Product $wc_product
*
* @return string|null
*/
public function get_mc_status( WC_Product $wc_product ): ?string {
try {
// If the mc_status is not set, return NOT_SYNCED.
return $this->meta_handler->get_mc_status( $this->maybe_swap_for_parent( $wc_product ) ) ?: MCStatus::NOT_SYNCED;
} catch ( InvalidValue $exception ) {
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Product status returned null for invalid product (ID: %s).', $wc_product->get_id() ),
__METHOD__
);
return null;
}
}
/**
* If an item from the provided list of products has a parent, replace it with the parent ID.
*
* @param int[] $product_ids A list of WooCommerce product ID.
* @param bool $check_product_status (Optional) Check if the product status is publish.
* @param bool $ignore_product_on_error (Optional) Ignore the product when invalid value error occurs.
*
* @return int[] A list of parent ID or product ID if it doesn't have a parent.
*
* @throws InvalidValue If the given param ignore_product_on_error is false and any of a given ID doesn't reference a valid product.
* Or if a variation product does not have a valid parent ID (i.e. it's an orphan).
*
* @since 2.2.0
*/
public function maybe_swap_for_parent_ids( array $product_ids, bool $check_product_status = true, bool $ignore_product_on_error = true ) {
$new_product_ids = [];
foreach ( $product_ids as $index => $product_id ) {
try {
$product = $this->get_wc_product( $product_id );
$new_product = $this->maybe_swap_for_parent( $product );
if ( ! $check_product_status || 'publish' === $new_product->get_status() ) {
$new_product_ids[ $index ] = $new_product->get_id();
}
} catch ( InvalidValue $exception ) {
if ( ! $ignore_product_on_error ) {
throw $exception;
}
}
}
return array_unique( $new_product_ids );
}
/**
* If the provided product has a parent, return its ID. Otherwise, return the given (valid product) ID.
*
* @param int $product_id WooCommerce product ID.
*
* @return int The parent ID or product ID if it doesn't have a parent.
*
* @throws InvalidValue If a given ID doesn't reference a valid product. Or if a variation product does not have a
* valid parent ID (i.e. it's an orphan).
*/
public function maybe_swap_for_parent_id( int $product_id ): int {
$product = $this->get_wc_product( $product_id );
return $this->maybe_swap_for_parent( $product )->get_id();
}
/**
* If the provided product has a parent, return its parent object. Otherwise, return the given product.
*
* @param WC_Product $product WooCommerce product object.
*
* @return WC_Product The parent product object or the given product object if it doesn't have a parent.
*
* @throws InvalidValue If a variation product does not have a valid parent ID (i.e. it's an orphan).
*
* @since 1.3.0
*/
public function maybe_swap_for_parent( WC_Product $product ): WC_Product {
if ( $product instanceof WC_Product_Variation ) {
try {
return $this->get_wc_product( $product->get_parent_id() );
} catch ( InvalidValue $exception ) {
do_action(
'woocommerce_gla_error',
sprintf( 'An orphaned variation found (ID: %s). Please delete it via "WooCommerce > Status > Tools > Delete orphaned variations".', $product->get_id() ),
__METHOD__
);
throw $exception;
}
}
return $product;
}
/**
* Get validation errors for a specific product.
* Combines errors for variable products, which have a variation-indexed array of errors.
*
* @param WC_Product $product
*
* @return array
*/
public function get_validation_errors( WC_Product $product ): array {
$errors = $this->meta_handler->get_errors( $product ) ?: [];
$first_key = array_key_first( $errors );
if ( ! empty( $errors ) && is_numeric( $first_key ) && 0 !== $first_key ) {
$errors = array_unique( array_merge( ...$errors ) );
}
return $errors;
}
/**
* Get categories list for a specific product.
*
* @param WC_Product $product
*
* @return array
*/
public function get_categories( WC_Product $product ): array {
$terms = get_the_terms( $product->get_id(), 'product_cat' );
return ( empty( $terms ) || is_wp_error( $terms ) ) ? [] : wp_list_pluck( $terms, 'name' );
}
/**
* Get the offer id for a product
*
* @since 2.8.0
* @param int $product_id The product id to get the offer id.
*
* @return string The offer id
*/
public function get_offer_id( int $product_id ) {
return WCProductAdapter::get_google_product_offer_id( $this->get_slug(), $product_id );
}
}
ProductMetaHandler.php 0000644 00000023210 15154245121 0011000 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidMeta;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use BadMethodCallException;
use WC_Product;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductMetaHandler
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*
* @method update_synced_at( WC_Product $product, $value )
* @method delete_synced_at( WC_Product $product )
* @method get_synced_at( WC_Product $product ): int|null
* @method update_google_ids( WC_Product $product, array $value )
* @method delete_google_ids( WC_Product $product )
* @method get_google_ids( WC_Product $product ): array|null
* @method update_visibility( WC_Product $product, $value )
* @method delete_visibility( WC_Product $product )
* @method get_visibility( WC_Product $product ): string|null
* @method update_errors( WC_Product $product, array $value )
* @method delete_errors( WC_Product $product )
* @method get_errors( WC_Product $product ): array|null
* @method update_failed_delete_attempts( WC_Product $product, int $value )
* @method delete_failed_delete_attempts( WC_Product $product )
* @method get_failed_delete_attempts( WC_Product $product ): int|null
* @method update_failed_sync_attempts( WC_Product $product, int $value )
* @method delete_failed_sync_attempts( WC_Product $product )
* @method get_failed_sync_attempts( WC_Product $product ): int|null
* @method update_sync_failed_at( WC_Product $product, int $value )
* @method delete_sync_failed_at( WC_Product $product )
* @method get_sync_failed_at( WC_Product $product ): int|null
* @method update_sync_status( WC_Product $product, string $value )
* @method delete_sync_status( WC_Product $product )
* @method get_sync_status( WC_Product $product ): string|null
* @method update_mc_status( WC_Product $product, string $value )
* @method delete_mc_status( WC_Product $product )
* @method get_mc_status( WC_Product $product ): string|null
* @method update_notification_status( WC_Product $product, string $value )
* @method delete_notification_status( WC_Product $product )
* @method get_notification_status( WC_Product $product ): string|null
*/
class ProductMetaHandler implements Service, Registerable {
use PluginHelper;
public const KEY_SYNCED_AT = 'synced_at';
public const KEY_GOOGLE_IDS = 'google_ids';
public const KEY_VISIBILITY = 'visibility';
public const KEY_ERRORS = 'errors';
public const KEY_FAILED_DELETE_ATTEMPTS = 'failed_delete_attempts';
public const KEY_FAILED_SYNC_ATTEMPTS = 'failed_sync_attempts';
public const KEY_SYNC_FAILED_AT = 'sync_failed_at';
public const KEY_SYNC_STATUS = 'sync_status';
public const KEY_MC_STATUS = 'mc_status';
public const KEY_NOTIFICATION_STATUS = 'notification_status';
protected const TYPES = [
self::KEY_SYNCED_AT => 'int',
self::KEY_GOOGLE_IDS => 'array',
self::KEY_VISIBILITY => 'string',
self::KEY_ERRORS => 'array',
self::KEY_FAILED_DELETE_ATTEMPTS => 'int',
self::KEY_FAILED_SYNC_ATTEMPTS => 'int',
self::KEY_SYNC_FAILED_AT => 'int',
self::KEY_SYNC_STATUS => 'string',
self::KEY_MC_STATUS => 'string',
self::KEY_NOTIFICATION_STATUS => 'string',
];
/**
* @param string $name
* @param mixed $arguments
*
* @return mixed
*
* @throws BadMethodCallException If the method that's called doesn't exist.
* @throws InvalidMeta If the meta key is invalid.
*/
public function __call( string $name, $arguments ) {
$found_matches = preg_match( '/^([a-z]+)_([\w\d]+)$/i', $name, $matches );
if ( ! $found_matches ) {
throw new BadMethodCallException( sprintf( 'The method %s does not exist in class ProductMetaHandler', $name ) );
}
[ $function_name, $method, $key ] = $matches;
// validate the method
if ( ! in_array( $method, [ 'update', 'delete', 'get' ], true ) ) {
throw new BadMethodCallException( sprintf( 'The method %s does not exist in class ProductMetaHandler', $function_name ) );
}
// set the value as the third argument if method is `update`
if ( 'update' === $method ) {
$arguments[2] = $arguments[1];
}
// set the key as the second argument
$arguments[1] = $key;
return call_user_func_array( [ $this, $method ], $arguments );
}
/**
* @param WC_Product $product
* @param string $key
* @param mixed $value
*
* @throws InvalidMeta If the meta key is invalid.
*/
public function update( WC_Product $product, string $key, $value ) {
self::validate_meta_key( $key );
if ( isset( self::TYPES[ $key ] ) ) {
if ( in_array( self::TYPES[ $key ], [ 'bool', 'boolean' ], true ) ) {
$value = wc_bool_to_string( $value );
} else {
settype( $value, self::TYPES[ $key ] );
}
}
$product->update_meta_data( $this->prefix_meta_key( $key ), $value );
$product->save_meta_data();
}
/**
* @param WC_Product $product
* @param string $key
*
* @throws InvalidMeta If the meta key is invalid.
*/
public function delete( WC_Product $product, string $key ) {
self::validate_meta_key( $key );
$product->delete_meta_data( $this->prefix_meta_key( $key ) );
$product->save_meta_data();
}
/**
* @param WC_Product $product
* @param string $key
*
* @return mixed The value, or null if the meta key doesn't exist.
*
* @throws InvalidMeta If the meta key is invalid.
*/
public function get( WC_Product $product, string $key ) {
self::validate_meta_key( $key );
$value = null;
if ( $product->meta_exists( $this->prefix_meta_key( $key ) ) ) {
$value = $product->get_meta( $this->prefix_meta_key( $key ), true );
if ( isset( self::TYPES[ $key ] ) && in_array( self::TYPES[ $key ], [ 'bool', 'boolean' ], true ) ) {
$value = wc_string_to_bool( $value );
}
}
return $value;
}
/**
* @param string $key
*
* @throws InvalidMeta If the meta key is invalid.
*/
protected static function validate_meta_key( string $key ) {
if ( ! self::is_meta_key_valid( $key ) ) {
do_action(
'woocommerce_gla_error',
sprintf( 'Product meta key is invalid: %s', $key ),
__METHOD__
);
throw InvalidMeta::invalid_key( $key );
}
}
/**
* @param string $key
*
* @return bool Whether the meta key is valid.
*/
public static function is_meta_key_valid( string $key ): bool {
return isset( self::TYPES[ $key ] );
}
/**
* Register a service.
*/
public function register(): void {
add_filter(
'woocommerce_product_data_store_cpt_get_products_query',
function ( array $query, array $query_vars ) {
return $this->handle_query_vars( $query, $query_vars );
},
10,
2
);
}
/**
* Handle the WooCommerce product's meta data query vars.
*
* @hooked handle_query_vars
*
* @param array $query Args for WP_Query.
* @param array $query_vars Query vars from WC_Product_Query.
*
* @return array modified $query
*/
protected function handle_query_vars( array $query, array $query_vars ): array {
if ( ! empty( $query_vars['meta_query'] ) ) {
$meta_query = $this->sanitize_meta_query( $query_vars['meta_query'] );
if ( ! empty( $meta_query ) ) {
$query['meta_query'] = array_merge( $query['meta_query'], $meta_query );
}
}
return $query;
}
/**
* Ensure the 'meta_query' argument passed to self::handle_query_vars is well-formed.
*
* @param array $queries Array of meta query clauses.
*
* @return array Sanitized array of meta query clauses.
*/
protected function sanitize_meta_query( $queries ): array {
$prefixed_valid_keys = array_map( [ $this, 'prefix_meta_key' ], array_keys( self::TYPES ) );
$clean_queries = [];
if ( ! is_array( $queries ) ) {
return $clean_queries;
}
foreach ( $queries as $key => $meta_query ) {
if ( 'relation' !== $key && ! is_array( $meta_query ) ) {
continue;
}
if ( 'relation' === $key && is_string( $meta_query ) ) {
$clean_queries[ $key ] = $meta_query;
// First-order clause.
} elseif ( isset( $meta_query['key'] ) || isset( $meta_query['value'] ) ) {
if ( in_array( $meta_query['key'], $prefixed_valid_keys, true ) ) {
$clean_queries[ $key ] = $meta_query;
}
// Otherwise, it's a nested meta_query, so we recurse.
} else {
$cleaned_query = $this->sanitize_meta_query( $meta_query );
if ( ! empty( $cleaned_query ) ) {
$clean_queries[ $key ] = $cleaned_query;
}
}
}
return $clean_queries;
}
/**
* @param array $meta_queries
*
* @return array
*/
public function prefix_meta_query_keys( $meta_queries ): array {
$updated_queries = [];
if ( ! is_array( $meta_queries ) ) {
return $updated_queries;
}
foreach ( $meta_queries as $key => $meta_query ) {
// First-order clause.
if ( 'relation' === $key && is_string( $meta_query ) ) {
$updated_queries[ $key ] = $meta_query;
// First-order clause.
} elseif ( isset( $meta_query['key'] ) || isset( $meta_query['value'] ) ) {
if ( self::is_meta_key_valid( $meta_query['key'] ) ) {
$meta_query['key'] = $this->prefix_meta_key( $meta_query['key'] );
}
} else {
// Otherwise, it's a nested meta_query, so we recurse.
$meta_query = $this->prefix_meta_query_keys( $meta_query );
}
$updated_queries[ $key ] = $meta_query;
}
return $updated_queries;
}
/**
* Returns all available meta keys.
*
* @return array
*/
public static function get_all_meta_keys(): array {
return array_keys( self::TYPES );
}
}
ProductRepository.php 0000644 00000030522 15154245121 0010777 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use WC_Product;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductRepository
*
* Contains methods to find and retrieve products from database.
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class ProductRepository implements Service {
use PluginHelper;
/**
* @var ProductMetaHandler
*/
protected $meta_handler;
/**
* @var ProductFilter
*/
protected $product_filter;
/**
* ProductRepository constructor.
*
* @param ProductMetaHandler $meta_handler
* @param ProductFilter $product_filter
*/
public function __construct( ProductMetaHandler $meta_handler, ProductFilter $product_filter ) {
$this->meta_handler = $meta_handler;
$this->product_filter = $product_filter;
}
/**
* Find and return an array of WooCommerce product objects based on the provided arguments.
*
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @see execute_woocommerce_query For more information about the arguments.
*
* @return WC_Product[] Array of WooCommerce product objects
*/
public function find( array $args = [], int $limit = -1, int $offset = 0 ): array {
$args['return'] = 'objects';
return $this->execute_woocommerce_query( $args, $limit, $offset );
}
/**
* Find and return an array of WooCommerce product IDs based on the provided arguments.
*
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @see execute_woocommerce_query For more information about the arguments.
*
* @return int[] Array of WooCommerce product IDs
*/
public function find_ids( array $args = [], int $limit = -1, int $offset = 0 ): array {
$args['return'] = 'ids';
return $this->execute_woocommerce_query( $args, $limit, $offset );
}
/**
* Find and return an array of WooCommerce product objects based on the provided product IDs.
*
* @param int[] $ids Array of WooCommerce product IDs
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return WC_Product[] Array of WooCommerce product objects
*/
public function find_by_ids( array $ids, array $args = [], int $limit = -1, int $offset = 0 ): array {
// If no product IDs are supplied then return early to avoid querying and loading every product.
if ( empty( $ids ) ) {
return [];
}
$args['include'] = $ids;
return $this->find( $args, $limit, $offset );
}
/**
* Find and return an associative array of products with the product ID as the key.
*
* @param int[] $ids Array of WooCommerce product IDs
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return WC_Product[] Array of WooCommerce product objects
*/
public function find_by_ids_as_associative_array( array $ids, array $args = [], int $limit = -1, int $offset = 0 ): array {
$products = $this->find_by_ids( $ids, $args, $limit, $offset );
$map = [];
foreach ( $products as $product ) {
$map[ $product->get_id() ] = $product;
}
return $map;
}
/**
* Find and return an array of WooCommerce product objects already submitted to Google Merchant Center.
*
* @param array $args Array of WooCommerce args (except 'return' and 'meta_query').
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return WC_Product[] Array of WooCommerce product objects
*/
public function find_synced_products( array $args = [], int $limit = -1, int $offset = 0 ): array {
$args['meta_query'] = $this->get_synced_products_meta_query();
return $this->find( $args, $limit, $offset );
}
/**
* Find and return an array of WooCommerce product IDs already submitted to Google Merchant Center.
*
* Note: Includes product variations.
*
* @param array $args Array of WooCommerce args (except 'return' and 'meta_query').
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return int[] Array of WooCommerce product IDs
*/
public function find_synced_product_ids( array $args = [], int $limit = -1, int $offset = 0 ): array {
$args['meta_query'] = $this->get_synced_products_meta_query();
return $this->find_ids( $args, $limit, $offset );
}
/**
* @return array
*/
protected function get_synced_products_meta_query(): array {
return [
[
'key' => ProductMetaHandler::KEY_GOOGLE_IDS,
'compare' => 'EXISTS',
],
];
}
/**
* Find and return an array of WooCommerce product objects ready to be submitted to Google Merchant Center.
*
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return FilteredProductList List of WooCommerce product objects after filtering.
*/
public function find_sync_ready_products( array $args = [], int $limit = - 1, int $offset = 0 ): FilteredProductList {
$results = $this->find( $this->get_sync_ready_products_query_args( $args ), $limit, $offset );
return $this->product_filter->filter_sync_ready_products( $results );
}
/**
* Find and return an array of WooCommerce product ID's ready to be deleted from the Google Merchant Center.
*
* @since 1.12.0
*
* @param int[] $ids Array of WooCommerce product IDs
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return array
*/
public function find_delete_product_ids( array $ids, int $limit = - 1, int $offset = 0 ): array {
// Default status query args in WC_Product_Query plus status trash.
$args = [ 'status' => [ 'draft', 'pending', 'private', 'publish', 'trash' ] ];
$results = $this->find_by_ids( $ids, $args, $limit, $offset );
return $this->product_filter->filter_products_for_delete( $results )->get_product_ids();
}
/**
* @return array
*/
protected function get_sync_ready_products_meta_query(): array {
return [
'relation' => 'OR',
[
'key' => ProductMetaHandler::KEY_VISIBILITY,
'compare' => 'NOT EXISTS',
],
[
'key' => ProductMetaHandler::KEY_VISIBILITY,
'compare' => '!=',
'value' => ChannelVisibility::DONT_SYNC_AND_SHOW,
],
];
}
/**
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
*
* @return array
*/
protected function get_sync_ready_products_query_args( array $args = [] ): array {
$args['meta_query'] = $this->get_sync_ready_products_meta_query();
// don't include variable products in query
$args['type'] = array_diff( ProductSyncer::get_supported_product_types(), [ 'variable' ] );
// only include published products
if ( empty( $args['status'] ) ) {
$args['status'] = [ 'publish' ];
}
return $args;
}
/**
* @return array
*/
protected function get_valid_products_meta_query(): array {
return [
'relation' => 'OR',
[
'key' => ProductMetaHandler::KEY_ERRORS,
'compare' => 'NOT EXISTS',
],
[
'key' => ProductMetaHandler::KEY_ERRORS,
'compare' => '=',
'value' => '',
],
];
}
/**
* Find and return an array of WooCommerce product IDs nearly expired and ready to be re-submitted to Google Merchant Center.
*
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return int[] Array of WooCommerce product IDs
*/
public function find_expiring_product_ids( int $limit = - 1, int $offset = 0 ): array {
$args['meta_query'] = [
'relation' => 'AND',
$this->get_sync_ready_products_meta_query(),
$this->get_valid_products_meta_query(),
[
[
'key' => ProductMetaHandler::KEY_SYNCED_AT,
'compare' => '<',
'value' => strtotime( '-25 days' ),
],
],
];
return $this->find_ids( $args, $limit, $offset );
}
/**
* Find all simple and variable product IDs regardless of MC status or visibility.
*
* @since 2.6.4
*
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @return int[] Array of WooCommerce product IDs
*/
public function find_all_product_ids( int $limit = -1, int $offset = 0 ): array {
$args = [
'status' => 'publish',
'return' => 'ids',
'type' => 'any',
];
return $this->find_ids( $args, $limit, $offset );
}
/**
* Returns an array of Google Product IDs associated with all synced WooCommerce products.
* Note: excludes variable parent products as only the child variation products are actually synced
* to Merchant Center
*
* @since 1.1.0
*
* @return array Google Product IDS
*/
public function find_all_synced_google_ids(): array {
// Don't include variable parent products as they aren't actually synced to Merchant Center.
$args['type'] = array_diff( ProductSyncer::get_supported_product_types(), [ 'variable' ] );
$synced_product_ids = $this->find_synced_product_ids( $args );
$google_ids_meta_key = $this->prefix_meta_key( ProductMetaHandler::KEY_GOOGLE_IDS );
$synced_google_ids = [];
foreach ( $synced_product_ids as $product_id ) {
$meta_google_ids = get_post_meta( $product_id, $google_ids_meta_key, true );
if ( ! is_array( $meta_google_ids ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Invalid Google IDs retrieve for product %d', $product_id ),
__METHOD__
);
continue;
}
$synced_google_ids = array_merge( $synced_google_ids, array_values( $meta_google_ids ) );
}
return $synced_google_ids;
}
/**
* Find and return an array of WooCommerce products based on the provided arguments.
*
* @param array $args Array of WooCommerce args (see below), and product metadata.
* @param int $limit Maximum number of results to retrieve or -1 for unlimited.
* @param int $offset Amount to offset product results.
*
* @link https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query
* @see ProductMetaHandler::TYPES For the list of meta data that can be used as query arguments.
*
* @return WC_Product[]|int[] Array of WooCommerce product objects or IDs, depending on the 'return' argument.
*/
protected function execute_woocommerce_query( array $args = [], int $limit = -1, int $offset = 0 ): array {
$args['limit'] = $limit;
$args['offset'] = $offset;
return wc_get_products( $this->prepare_query_args( $args ) );
}
/**
* @param array $args Array of WooCommerce args (except 'return'), and product metadata.
*
* @see execute_woocommerce_query For more information about the arguments.
*
* @return array
*/
protected function prepare_query_args( array $args = [] ): array {
if ( empty( $args ) ) {
return [];
}
if ( ! empty( $args['meta_query'] ) ) {
$args['meta_query'] = $this->meta_handler->prefix_meta_query_keys( $args['meta_query'] );
}
// only include supported product types
if ( empty( $args['type'] ) ) {
$args['type'] = ProductSyncer::get_supported_product_types();
}
// It'll fetch all products with the post_type of 'product', excluding variations.
if ( $args['type'] === 'any' ) {
unset( $args['type'] );
}
// use no ordering unless specified in arguments. overrides the default WooCommerce query args
if ( empty( $args['orderby'] ) ) {
$args['orderby'] = 'none';
}
$args = apply_filters( 'woocommerce_gla_product_query_args', $args );
return $args;
}
}
ProductSyncer.php 0000644 00000031111 15154245121 0010056 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchInvalidProductEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductIDRequestEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductRequestEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductResponse;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleProductService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Exception;
use WC_Product;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductSyncer
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class ProductSyncer implements Service {
public const FAILURE_THRESHOLD = 5; // Number of failed attempts allowed per FAILURE_THRESHOLD_WINDOW
public const FAILURE_THRESHOLD_WINDOW = '3 hours'; // PHP supported Date and Time format: https://www.php.net/manual/en/datetime.formats.php
/**
* @var GoogleProductService
*/
protected $google_service;
/**
* @var BatchProductHelper
*/
protected $batch_helper;
/**
* @var ProductHelper
*/
protected $product_helper;
/**
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* @var WC
*/
protected $wc;
/**
* @var ProductRepository
*/
protected $product_repository;
/**
* ProductSyncer constructor.
*
* @param GoogleProductService $google_service
* @param BatchProductHelper $batch_helper
* @param ProductHelper $product_helper
* @param MerchantCenterService $merchant_center
* @param WC $wc
* @param ProductRepository $product_repository
*/
public function __construct(
GoogleProductService $google_service,
BatchProductHelper $batch_helper,
ProductHelper $product_helper,
MerchantCenterService $merchant_center,
WC $wc,
ProductRepository $product_repository
) {
$this->google_service = $google_service;
$this->batch_helper = $batch_helper;
$this->product_helper = $product_helper;
$this->merchant_center = $merchant_center;
$this->wc = $wc;
$this->product_repository = $product_repository;
}
/**
* Submits an array of WooCommerce products to Google Merchant Center.
*
* @param WC_Product[] $products
*
* @return BatchProductResponse Containing both the synced and invalid products.
*
* @throws ProductSyncerException If there are any errors while syncing products with Google Merchant Center.
*/
public function update( array $products ): BatchProductResponse {
$this->validate_merchant_center_setup();
// prepare and validate products
$product_entries = $this->batch_helper->validate_and_generate_update_request_entries( $products );
return $this->update_by_batch_requests( $product_entries );
}
/**
* Submits an array of WooCommerce products to Google Merchant Center.
*
* @param BatchProductRequestEntry[] $product_entries
*
* @return BatchProductResponse Containing both the synced and invalid products.
*
* @throws ProductSyncerException If there are any errors while syncing products with Google Merchant Center.
*/
public function update_by_batch_requests( array $product_entries ): BatchProductResponse {
$this->validate_merchant_center_setup();
// bail if no valid products provided
if ( empty( $product_entries ) ) {
return new BatchProductResponse( [], [] );
}
$updated_products = [];
$invalid_products = [];
foreach ( array_chunk( $product_entries, GoogleProductService::BATCH_SIZE ) as $batch_entries ) {
try {
$response = $this->google_service->insert_batch( $batch_entries );
$updated_products = array_merge( $updated_products, $response->get_products() );
$invalid_products = array_merge( $invalid_products, $response->get_errors() );
// update the meta data for the synced and invalid products
array_walk( $updated_products, [ $this->batch_helper, 'mark_as_synced' ] );
array_walk( $invalid_products, [ $this->batch_helper, 'mark_as_invalid' ] );
} catch ( Exception $exception ) {
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
throw new ProductSyncerException( sprintf( 'Error updating Google products: %s', $exception->getMessage() ), 0, $exception );
}
}
$this->handle_update_errors( $invalid_products );
do_action(
'woocommerce_gla_batch_updated_products',
$updated_products,
$invalid_products
);
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Submitted %s products:\n%s",
count( $updated_products ),
wp_json_encode( $updated_products )
),
__METHOD__
);
if ( ! empty( $invalid_products ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf(
"%s products failed to sync with Merchant Center:\n%s",
count( $invalid_products ),
wp_json_encode( $invalid_products )
),
__METHOD__
);
}
return new BatchProductResponse( $updated_products, $invalid_products );
}
/**
* Deletes an array of WooCommerce products from Google Merchant Center.
*
* @param WC_Product[] $products
*
* @return BatchProductResponse Containing both the deleted and invalid products.
*
* @throws ProductSyncerException If there are any errors while deleting products from Google Merchant Center.
*/
public function delete( array $products ): BatchProductResponse {
$this->validate_merchant_center_setup();
$synced_products = $this->batch_helper->filter_synced_products( $products );
$product_entries = $this->batch_helper->generate_delete_request_entries( $synced_products );
return $this->delete_by_batch_requests( $product_entries );
}
/**
* Deletes an array of WooCommerce products from Google Merchant Center.
*
* Note: This method does not automatically delete variations of a parent product. They each must be provided via the $product_entries argument.
*
* @param BatchProductIDRequestEntry[] $product_entries
*
* @return BatchProductResponse Containing both the deleted and invalid products (including their variation).
*
* @throws ProductSyncerException If there are any errors while deleting products from Google Merchant Center.
*/
public function delete_by_batch_requests( array $product_entries ): BatchProductResponse {
$this->validate_merchant_center_setup();
// return empty response if no synced product found
if ( empty( $product_entries ) ) {
return new BatchProductResponse( [], [] );
}
$deleted_products = [];
$invalid_products = [];
foreach ( array_chunk( $product_entries, GoogleProductService::BATCH_SIZE ) as $batch_entries ) {
try {
$response = $this->google_service->delete_batch( $batch_entries );
$deleted_products = array_merge( $deleted_products, $response->get_products() );
$invalid_products = array_merge( $invalid_products, $response->get_errors() );
array_walk( $deleted_products, [ $this->batch_helper, 'mark_as_unsynced' ] );
} catch ( Exception $exception ) {
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
throw new ProductSyncerException( sprintf( 'Error deleting Google products: %s', $exception->getMessage() ), 0, $exception );
}
}
$this->handle_delete_errors( $invalid_products );
do_action(
'woocommerce_gla_batch_deleted_products',
$deleted_products,
$invalid_products
);
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Deleted %s products:\n%s",
count( $deleted_products ),
wp_json_encode( $deleted_products ),
),
__METHOD__
);
if ( ! empty( $invalid_products ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Failed to delete %s products from Merchant Center:\n%s",
count( $invalid_products ),
wp_json_encode( $invalid_products )
),
__METHOD__
);
}
return new BatchProductResponse( $deleted_products, $invalid_products );
}
/**
* Return the list of supported product types.
*
* @return array
*/
public static function get_supported_product_types(): array {
return (array) apply_filters( 'woocommerce_gla_supported_product_types', [ 'simple', 'variable', 'variation' ] );
}
/**
* @param BatchInvalidProductEntry[] $invalid_products
*/
protected function handle_update_errors( array $invalid_products ) {
$error_products = [];
foreach ( $invalid_products as $invalid_product ) {
if ( $invalid_product->has_error( GoogleProductService::INTERNAL_ERROR_REASON ) ) {
$wc_product_id = $invalid_product->get_wc_product_id();
$wc_product = $this->wc->maybe_get_product( $wc_product_id );
// Only schedule for retry if the failure threshold has not been reached.
if (
$wc_product instanceof WC_Product &&
! $this->product_helper->is_update_failed_threshold_reached( $wc_product )
) {
$error_products[ $wc_product_id ] = $wc_product_id;
}
}
}
if ( ! empty( $error_products ) && apply_filters( 'woocommerce_gla_products_update_retry_on_failure', true, $invalid_products ) ) {
do_action( 'woocommerce_gla_batch_retry_update_products', $error_products );
do_action(
'woocommerce_gla_error',
sprintf( 'Internal API errors while submitting the following products: %s', join( ', ', $error_products ) ),
__METHOD__
);
}
}
/**
* @param BatchInvalidProductEntry[] $invalid_products
*/
protected function handle_delete_errors( array $invalid_products ) {
$internal_error_ids = [];
foreach ( $invalid_products as $invalid_product ) {
$google_product_id = $invalid_product->get_google_product_id();
$wc_product_id = $invalid_product->get_wc_product_id();
$wc_product = $this->wc->maybe_get_product( $wc_product_id );
if ( ! $wc_product instanceof WC_Product || empty( $google_product_id ) ) {
continue;
}
// not found
if ( $invalid_product->has_error( GoogleProductService::NOT_FOUND_ERROR_REASON ) ) {
do_action(
'woocommerce_gla_error',
sprintf(
'Attempted to delete product "%s" (WooCommerce Product ID: %s) but it did not exist in Google Merchant Center, removing the synced product ID from database.',
$google_product_id,
$wc_product_id
),
__METHOD__
);
$this->product_helper->remove_google_id( $wc_product, $google_product_id );
}
// internal error
if ( $invalid_product->has_error( GoogleProductService::INTERNAL_ERROR_REASON ) ) {
$this->product_helper->increment_failed_delete_attempt( $wc_product );
// Only schedule for retry if the failure threshold has not been reached.
if ( ! $this->product_helper->is_delete_failed_threshold_reached( $wc_product ) ) {
$internal_error_ids[ $google_product_id ] = $wc_product_id;
}
}
}
// Exclude any ID's which are not ready to delete or are not available in the DB.
$product_ids = array_values( $internal_error_ids );
$ready_ids = $this->product_repository->find_delete_product_ids( $product_ids );
$internal_error_ids = array_intersect( $internal_error_ids, $ready_ids );
// call an action to retry if any products with internal errors exist
if ( ! empty( $internal_error_ids ) && apply_filters( 'woocommerce_gla_products_delete_retry_on_failure', true, $invalid_products ) ) {
do_action( 'woocommerce_gla_batch_retry_delete_products', $internal_error_ids );
do_action(
'woocommerce_gla_error',
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
sprintf( 'Internal API errors while deleting the following products: %s', print_r( $internal_error_ids, true ) ),
__METHOD__
);
}
}
/**
* Validates whether Merchant Center is connected and ready for pushing data.
*
* @throws ProductSyncerException If the Google Merchant Center connection is not ready or cannot push data.
*/
protected function validate_merchant_center_setup(): void {
if ( ! $this->merchant_center->is_ready_for_syncing() ) {
do_action( 'woocommerce_gla_error', 'Cannot sync any products before setting up Google Merchant Center.', __METHOD__ );
throw new ProductSyncerException( __( 'Google Merchant Center has not been set up correctly. Please review your configuration.', 'google-listings-and-ads' ) );
}
if ( ! $this->merchant_center->should_push() ) {
do_action(
'woocommerce_gla_error',
'Cannot push any products because they are being fetched automatically.',
__METHOD__
);
throw new ProductSyncerException(
__(
'Pushing products will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings.',
'google-listings-and-ads'
)
);
}
}
}
ProductSyncerException.php 0000644 00000000661 15154245121 0011743 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\GoogleListingsAndAdsException;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductSyncerException
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class ProductSyncerException extends Exception implements GoogleListingsAndAdsException {
}
SyncerHooks.php 0000644 00000034471 15154245121 0007535 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductIDRequestEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\DeleteProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\ProductNotificationJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use WC_Product;
use WC_Product_Variable;
defined( 'ABSPATH' ) || exit;
/**
* Class SyncerHooks
*
* Hooks to various WooCommerce and WordPress actions to provide automatic product sync functionality.
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class SyncerHooks implements Service, Registerable {
use PluginHelper;
protected const SCHEDULE_TYPE_UPDATE = 'update';
protected const SCHEDULE_TYPE_DELETE = 'delete';
/**
* Array of strings mapped to product IDs indicating that they have been already
* scheduled for update or delete during current request. Used to avoid scheduling
* duplicate jobs.
*
* @var string[]
*/
protected $already_scheduled = [];
/**
* @var BatchProductIDRequestEntry[][]
*/
protected $delete_requests_map;
/**
* @var BatchProductHelper
*/
protected $batch_helper;
/**
* @var ProductHelper
*/
protected $product_helper;
/**
* @var JobRepository
*/
protected $job_repository;
/**
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* @var NotificationsService
*/
protected $notifications_service;
/**
* @var WC
*/
protected $wc;
/**
* SyncerHooks constructor.
*
* @param BatchProductHelper $batch_helper
* @param ProductHelper $product_helper
* @param JobRepository $job_repository
* @param MerchantCenterService $merchant_center
* @param NotificationsService $notifications_service
* @param WC $wc
*/
public function __construct(
BatchProductHelper $batch_helper,
ProductHelper $product_helper,
JobRepository $job_repository,
MerchantCenterService $merchant_center,
NotificationsService $notifications_service,
WC $wc
) {
$this->batch_helper = $batch_helper;
$this->product_helper = $product_helper;
$this->job_repository = $job_repository;
$this->merchant_center = $merchant_center;
$this->notifications_service = $notifications_service;
$this->wc = $wc;
}
/**
* Register a service.
*/
public function register(): void {
// only register the hooks if Merchant Center is connected correctly.
if ( ! $this->merchant_center->is_ready_for_syncing() ) {
return;
}
// when a product is added / updated, schedule an "update" job.
add_action( 'woocommerce_new_product', [ $this, 'update_by_id' ], 90 );
add_action( 'woocommerce_new_product_variation', [ $this, 'update_by_id' ], 90 );
add_action( 'woocommerce_update_product', [ $this, 'update_by_object' ], 90, 2 );
add_action( 'woocommerce_update_product_variation', [ $this, 'update_by_object' ], 90, 2 );
// if we don't attach to these we miss product gallery updates.
add_action( 'woocommerce_process_product_meta', [ $this, 'update_by_id' ], 90 );
// when a product is trashed or removed, schedule a "delete" job.
add_action( 'wp_trash_post', [ $this, 'pre_delete' ], 90 );
add_action( 'before_delete_post', [ $this, 'pre_delete' ], 90 );
add_action( 'woocommerce_before_delete_product_variation', [ $this, 'pre_delete' ], 90 );
add_action( 'trashed_post', [ $this, 'delete' ], 90 );
add_action( 'deleted_post', [ $this, 'delete' ], 90 );
// when a product is restored from the trash, schedule an "update" job.
add_action( 'untrashed_post', [ $this, 'update_by_id' ], 90 );
// exclude the sync metadata when duplicating the product
add_filter(
'woocommerce_duplicate_product_exclude_meta',
[ $this, 'duplicate_product_exclude_meta' ],
90
);
}
/**
* Update a Product by WC_Product
*
* @param int $product_id
* @param WC_Product $product
*/
public function update_by_object( int $product_id, WC_Product $product ) {
$this->handle_update_products( [ $product ] );
}
/**
* Update a Product by the ID
*
* @param int $product_id
*/
public function update_by_id( int $product_id ) {
$product = $this->wc->maybe_get_product( $product_id );
$this->handle_update_products( [ $product ] );
}
/**
* Pre delete a Product by the ID
*
* @param int $product_id
*/
public function pre_delete( int $product_id ) {
$this->handle_pre_delete_product( $product_id );
}
/**
* Delete a Product by the ID
*
* @param int $product_id
*/
public function delete( int $product_id ) {
$this->handle_delete_product( $product_id );
}
/**
* Filters woocommerce_duplicate_product_exclude_meta adding some custom prefix
*
* @param array $exclude_meta
* @return array
*/
public function duplicate_product_exclude_meta( array $exclude_meta ): array {
return $this->get_duplicated_product_excluded_meta( $exclude_meta );
}
/**
* Handle updating of a product.
*
* @param WC_Product[] $products The products being saved.
* @param bool $notify If true. It will try to handle notifications.
*
* @return void
*/
protected function handle_update_products( array $products, $notify = true ) {
$products_to_update = [];
$products_to_delete = [];
foreach ( $products as $product ) {
if ( ! $product instanceof WC_Product ) {
continue;
}
$product_id = $product->get_id();
// Avoid to handle variations directly. We handle them from the parent.
if ( $this->notifications_service->is_ready() && $notify ) {
$this->handle_update_product_notification( $product );
}
// Bail if an event is already scheduled for this product in the current request
if ( $this->is_already_scheduled_to_update( $product_id ) ) {
continue;
}
// If it's a variable product we handle each variation separately
if ( $product instanceof WC_Product_Variable ) {
// This is only for MC Push mechanism. We don't handle notifications here.
$this->handle_update_products( $product->get_available_variations( 'objects' ), false );
continue;
}
// Schedule an update job if product sync is enabled.
if ( $this->product_helper->is_sync_ready( $product ) ) {
$this->product_helper->mark_as_pending( $product );
$products_to_update[] = $product->get_id();
$this->set_already_scheduled_to_update( $product_id );
} elseif ( $this->product_helper->is_product_synced( $product ) ) {
// Delete the product from Google Merchant Center if it's already synced BUT it is not sync ready after the edit.
$products_to_delete[] = $product;
$this->set_already_scheduled_to_delete( $product_id );
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Deleting product (ID: %s) from Google Merchant Center because it is not ready to be synced.', $product->get_id() ),
__METHOD__
);
} else {
$this->product_helper->mark_as_unsynced( $product );
}
}
if ( ! empty( $products_to_update ) ) {
$this->job_repository->get( UpdateProducts::class )->schedule( [ $products_to_update ] );
}
if ( ! empty( $products_to_delete ) ) {
$request_entries = $this->batch_helper->generate_delete_request_entries( $products_to_delete );
$this->job_repository->get( DeleteProducts::class )->schedule( [ BatchProductIDRequestEntry::convert_to_id_map( $request_entries )->get() ] );
}
}
/**
* Schedules notifications for an updated product
*
* @param WC_Product $product
*/
protected function handle_update_product_notification( WC_Product $product ) {
if ( $this->product_helper->should_trigger_create_notification( $product ) ) {
$this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_PENDING_CREATE );
$this->job_repository->get( ProductNotificationJob::class )->schedule(
[
'item_id' => $product->get_id(),
'topic' => NotificationsService::TOPIC_PRODUCT_CREATED,
]
);
} elseif ( $this->product_helper->should_trigger_update_notification( $product ) ) {
$this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_PENDING_UPDATE );
$this->job_repository->get( ProductNotificationJob::class )->schedule(
[
'item_id' => $product->get_id(),
'topic' => NotificationsService::TOPIC_PRODUCT_UPDATED,
]
);
} elseif ( $this->product_helper->should_trigger_delete_notification( $product ) ) {
$this->schedule_delete_notification( $product );
// Schedule variation deletion when the parent is deleted.
if ( $product instanceof WC_Product_Variable ) {
foreach ( $product->get_available_variations( 'objects' ) as $variation ) {
$this->handle_update_product_notification( $variation );
}
}
}
}
/**
* Handle deleting of a product.
*
* @param int $product_id
*/
protected function handle_delete_product( int $product_id ) {
if ( isset( $this->delete_requests_map[ $product_id ] ) ) {
$product_id_map = BatchProductIDRequestEntry::convert_to_id_map( $this->delete_requests_map[ $product_id ] )->get();
if ( ! empty( $product_id_map ) && ! $this->is_already_scheduled_to_delete( $product_id ) ) {
$this->job_repository->get( DeleteProducts::class )->schedule( [ $product_id_map ] );
$this->set_already_scheduled_to_delete( $product_id );
}
}
}
/**
* Maybe send the product deletion notification
* and mark the product as un-synced after.
*
* @since 2.8.0
* @param int $product_id
*/
protected function maybe_send_delete_notification( int $product_id ) {
$product = $this->wc->maybe_get_product( $product_id );
if ( $product instanceof WC_Product && $this->product_helper->has_notified_creation( $product ) ) {
$result = $this->notifications_service->notify( NotificationsService::TOPIC_PRODUCT_DELETED, $product_id, [ 'offer_id' => $this->product_helper->get_offer_id( $product_id ) ] );
if ( $result ) {
$this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_DELETED );
$this->product_helper->mark_as_unsynced( $product );
}
}
}
/**
* Schedules a job to send the product deletion notification
*
* @since 2.8.0
* @param WC_Product $product
*/
protected function schedule_delete_notification( $product ) {
$this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_PENDING_DELETE );
$this->job_repository->get( ProductNotificationJob::class )->schedule(
[
'item_id' => $product->get_id(),
'topic' => NotificationsService::TOPIC_PRODUCT_DELETED,
]
);
}
/**
* Create request entries for the product (containing its Google ID) so that we can schedule a delete job when the
* product is actually trashed / deleted.
*
* @param int $product_id
*/
protected function handle_pre_delete_product( int $product_id ) {
if ( $this->notifications_service->is_ready() ) {
/**
* For deletions, we do send directly the notification instead of scheduling it.
* This is because we want to avoid that the product is not in the database anymore when the scheduled action runs.
*/
$this->maybe_send_delete_notification( $product_id );
}
$product = $this->wc->maybe_get_product( $product_id );
// each variation is passed to this method separately so we don't need to delete the variable product
if ( $product instanceof WC_Product && ! $product instanceof WC_Product_Variable && $this->product_helper->is_product_synced( $product ) ) {
$this->delete_requests_map[ $product_id ] = $this->batch_helper->generate_delete_request_entries( [ $product ] );
}
}
/**
* Return the list of metadata keys to be excluded when duplicating a product.
*
* @param array $exclude_meta The keys to exclude from the duplicate.
*
* @return array
*/
protected function get_duplicated_product_excluded_meta( array $exclude_meta ): array {
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_SYNCED_AT );
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_GOOGLE_IDS );
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_ERRORS );
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_FAILED_SYNC_ATTEMPTS );
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_SYNC_FAILED_AT );
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_SYNC_STATUS );
$exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_MC_STATUS );
return $exclude_meta;
}
/**
* @param int $product_id
* @param string $schedule_type
*
* @return bool
*/
protected function is_already_scheduled( int $product_id, string $schedule_type ): bool {
return isset( $this->already_scheduled[ $product_id ] ) && $this->already_scheduled[ $product_id ] === $schedule_type;
}
/**
* @param int $product_id
*
* @return bool
*/
protected function is_already_scheduled_to_update( int $product_id ): bool {
return $this->is_already_scheduled( $product_id, self::SCHEDULE_TYPE_UPDATE );
}
/**
* @param int $product_id
*
* @return bool
*/
protected function is_already_scheduled_to_delete( int $product_id ): bool {
return $this->is_already_scheduled( $product_id, self::SCHEDULE_TYPE_DELETE );
}
/**
* @param int $product_id
* @param string $schedule_type
*
* @return void
*/
protected function set_already_scheduled( int $product_id, string $schedule_type ): void {
$this->already_scheduled[ $product_id ] = $schedule_type;
}
/**
* @param int $product_id
*
* @return void
*/
protected function set_already_scheduled_to_update( int $product_id ): void {
$this->set_already_scheduled( $product_id, self::SCHEDULE_TYPE_UPDATE );
}
/**
* @param int $product_id
*
* @return void
*/
protected function set_already_scheduled_to_delete( int $product_id ): void {
$this->set_already_scheduled( $product_id, self::SCHEDULE_TYPE_DELETE );
}
}
WCProductAdapter.php 0000644 00000113010 15154245121 0010424 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\AttributeMapping\AttributeMappingHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\Condition;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\SizeSystem;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\SizeType;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AgeGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\Validator\GooglePriceConstraint;
use Automattic\WooCommerce\GoogleListingsAndAds\Validator\ImageUrlConstraint;
use Automattic\WooCommerce\GoogleListingsAndAds\Validator\Validatable;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Price as GooglePrice;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Product as GoogleProduct;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\ProductShipping as GoogleProductShipping;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\ProductShippingDimension as GoogleProductShippingDimension;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\ProductShippingWeight as GoogleProductShippingWeight;
use DateInterval;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use WC_DateTime;
use WC_Product;
use WC_Product_Variable;
use WC_Product_Variation;
defined( 'ABSPATH' ) || exit;
/**
* Class WCProductAdapter
*
* This class adapts the WooCommerce Product class to the Google's Product class by mapping their attributes.
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*/
class WCProductAdapter extends GoogleProduct implements Validatable {
use PluginHelper;
public const AVAILABILITY_IN_STOCK = 'in_stock';
public const AVAILABILITY_OUT_OF_STOCK = 'out_of_stock';
public const AVAILABILITY_BACKORDER = 'backorder';
public const AVAILABILITY_PREORDER = 'preorder';
public const IMAGE_SIZE_FULL = 'full';
public const CHANNEL_ONLINE = 'online';
/**
* @var WC_Product WooCommerce product object
*/
protected $wc_product;
/**
* @var WC_Product WooCommerce parent product object if $wc_product is a variation
*/
protected $parent_wc_product;
/**
* @var bool Whether tax is excluded from product price
*/
protected $tax_excluded;
/**
* @var array Product category ids
*/
protected $product_category_ids;
/**
* Initialize this object's properties from an array.
*
* @param array $properties Used to seed this object's properties.
*
* @return void
*
* @throws InvalidValue When a WooCommerce product is not provided or it is invalid.
*/
public function mapTypes( $properties ) {
if ( empty( $properties['wc_product'] ) || ! $properties['wc_product'] instanceof WC_Product ) {
throw InvalidValue::not_instance_of( WC_Product::class, 'wc_product' );
}
// throw an exception if the parent product isn't provided and this is a variation
if ( $properties['wc_product'] instanceof WC_Product_Variation &&
( empty( $properties['parent_wc_product'] ) || ! $properties['parent_wc_product'] instanceof WC_Product_Variable )
) {
throw InvalidValue::not_instance_of( WC_Product_Variable::class, 'parent_wc_product' );
}
if ( empty( $properties['targetCountry'] ) ) {
throw InvalidValue::is_empty( 'targetCountry' );
}
$this->wc_product = $properties['wc_product'];
$this->parent_wc_product = $properties['parent_wc_product'] ?? null;
$mapping_rules = $properties['mapping_rules'] ?? [];
$gla_attributes = $properties['gla_attributes'] ?? [];
// Google doesn't expect extra fields, so it's best to remove them
unset( $properties['wc_product'] );
unset( $properties['parent_wc_product'] );
unset( $properties['gla_attributes'] );
unset( $properties['mapping_rules'] );
parent::mapTypes( $properties );
$this->map_woocommerce_product();
$this->map_attribute_mapping_rules( $mapping_rules );
$this->map_gla_attributes( $gla_attributes );
$this->map_gtin();
// Allow users to override the product's attributes using a WordPress filter.
$this->override_attributes();
}
/**
* Map the WooCommerce product attributes to the current class.
*
* @return void
*/
protected function map_woocommerce_product() {
$this->setChannel( self::CHANNEL_ONLINE );
$content_language = empty( get_locale() ) ? 'en' : strtolower( substr( get_locale(), 0, 2 ) ); // ISO 639-1.
$this->setContentLanguage( $content_language );
$this->map_wc_product_id()
->map_wc_general_attributes()
->map_product_categories()
->map_wc_product_image( self::IMAGE_SIZE_FULL )
->map_wc_availability()
->map_wc_product_shipping()
->map_wc_prices();
}
/**
* Overrides the product attributes by applying a filter and setting the provided values.
*
* @since 1.4.0
*/
protected function override_attributes() {
/**
* Filters the list of overridden attributes to set for this product.
*
* Note: This filter takes precedence over any other filter that modify products attributes. Including
* `woocommerce_gla_product_attribute_value_{$attribute_id}` defined in self::map_gla_attributes.
*
* @param array $attributes An array of values for the product properties. All properties of the
* `\Google\Service\ShoppingContent\Product` class can be set by providing
* the property name as key and its value as array item.
* For example:
* [ 'imageLink' => 'https://example.com/image.jpg' ] overrides the product's
* main image.
*
* @param WC_Product $wc_product The WooCommerce product object.
* @param WCProductAdapter $this The Adapted Google product object. All WooCommerce product properties
* are already mapped to this object.
*
* @see \Google\Service\ShoppingContent\Product for the list of product properties that can be overriden.
* @see WCProductAdapter::map_gla_attributes for the docuementation of `woocommerce_gla_product_attribute_value_{$attribute_id}`
* filter, which allows modifying some attributes such as GTIN, MPN, etc.
*
* @since 1.4.0
*/
$attributes = apply_filters( 'woocommerce_gla_product_attribute_values', [], $this->wc_product, $this );
if ( ! empty( $attributes ) ) {
parent::mapTypes( $attributes );
}
}
/**
* Map the general WooCommerce product attributes.
*
* @return $this
*/
protected function map_wc_general_attributes() {
$this->setTitle( $this->wc_product->get_title() );
$this->setDescription( $this->get_wc_product_description() );
$this->setLink( $this->wc_product->get_permalink() );
// set item group id for variations
if ( $this->is_variation() ) {
$this->setItemGroupId( $this->parent_wc_product->get_id() );
}
return $this;
}
/**
* Map WooCommerce product categories to Google product types.
*
* @return $this
*/
protected function map_product_categories() {
// set product type using merchants defined product categories
$base_product_id = $this->is_variation() ? $this->parent_wc_product->get_id() : $this->wc_product->get_id();
// Fetch only selected term ids without parents.
$this->product_category_ids = wc_get_product_term_ids( $base_product_id, 'product_cat' );
if ( ! empty( $this->product_category_ids ) ) {
$google_product_types = self::convert_product_types( $this->product_category_ids );
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Product category (ID: %s): %s.',
$base_product_id,
wp_json_encode( $google_product_types )
),
__METHOD__
);
$google_product_types = array_slice( $google_product_types, 0, 10 );
$this->setProductTypes( $google_product_types );
}
return $this;
}
/**
* Covert WooCommerce product categories to product_type, which follows Google requirements:
* https://support.google.com/merchants/answer/6324406?hl=en#
*
* @param int[] $category_ids
*
* @return array
*/
public static function convert_product_types( $category_ids ): array {
$product_types = [];
foreach ( array_unique( $category_ids ) as $category_id ) {
if ( ! is_int( $category_id ) ) {
continue;
}
$product_type = self::get_product_type_by_id( $category_id );
array_push( $product_types, $product_type );
}
return $product_types;
}
/**
* Return category names including ancestors, separated by ">"
*
* @param int $category_id
*
* @return string
*/
protected static function get_product_type_by_id( int $category_id ): string {
$category_names = [];
do {
$term = get_term_by( 'id', $category_id, 'product_cat', 'ARRAY_A' );
array_push( $category_names, $term['name'] );
$category_id = $term['parent'];
} while ( ! empty( $term['parent'] ) );
return implode( ' > ', array_reverse( $category_names ) );
}
/**
* Map the WooCommerce product ID.
*
* @return $this
*/
protected function map_wc_product_id(): WCProductAdapter {
$this->setOfferId( self::get_google_product_offer_id( $this->get_slug(), $this->wc_product->get_id() ) );
return $this;
}
/**
*
* @param string $slug
* @param int $product_id
* @return string
*/
public static function get_google_product_offer_id( string $slug, int $product_id ): string {
/**
* Filters a WooCommerce product ID to be used as the Merchant Center product ID.
*
* @param string $mc_product_id Default generated Merchant Center product ID.
* @param int $product_id WooCommerce product ID.
* @since 2.4.6
*
* @return string Merchant Center product ID corresponding to the given WooCommerce product ID.
*/
return apply_filters( 'woocommerce_gla_get_google_product_offer_id', "{$slug}_{$product_id}", $product_id );
}
/**
* Get the description for the WooCommerce product.
*
* @return string
*/
protected function get_wc_product_description(): string {
/**
* Filters whether the short product description should be used for the synced product.
*
* @param bool $use_short_description
*/
$use_short_description = apply_filters( 'woocommerce_gla_use_short_description', false );
$description = ! empty( $this->wc_product->get_description() ) && ! $use_short_description ?
$this->wc_product->get_description() :
$this->wc_product->get_short_description();
// prepend the parent product description to the variation product
if ( $this->is_variation() ) {
$parent_description = ! empty( $this->parent_wc_product->get_description() ) && ! $use_short_description ?
$this->parent_wc_product->get_description() :
$this->parent_wc_product->get_short_description();
$new_line = ! empty( $description ) && ! empty( $parent_description ) ? PHP_EOL : '';
$description = $parent_description . $new_line . $description;
}
/**
* Filters whether the shortcodes should be applied for product descriptions when syncing a product or be stripped out.
*
* @since 1.4.0
*
* @param bool $apply_shortcodes Shortcodes are applied if set to `true` and stripped out if set to `false`.
* @param WC_Product $wc_product WooCommerce product object.
*/
$apply_shortcodes = apply_filters( 'woocommerce_gla_product_description_apply_shortcodes', false, $this->wc_product );
if ( $apply_shortcodes ) {
// Apply active shortcodes
$description = do_shortcode( $description );
} else {
// Strip out active shortcodes
$description = strip_shortcodes( $description );
}
// Strip out invalid unicode.
$description = mb_convert_encoding( $description, 'UTF-8', 'UTF-8' );
$description = preg_replace(
'/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u',
'',
$description
);
// Strip out invalid HTML tags (e.g. script, style, canvas, etc.) along with attributes of all tags.
$valid_html_tags = array_keys( wp_kses_allowed_html( 'post' ) );
$kses_allowed_tags = array_fill_keys( $valid_html_tags, [] );
$description = wp_kses( $description, $kses_allowed_tags );
// Trim the description if it's more than 5000 characters.
$description = mb_substr( $description, 0, 5000, 'utf-8' );
/**
* Filters the product's description.
*
* @param string $description Product description.
* @param WC_Product $wc_product WooCommerce product object.
*/
return apply_filters( 'woocommerce_gla_product_attribute_value_description', $description, $this->wc_product );
}
/**
* Map the WooCommerce product images.
*
* @param string $image_size
*
* @return $this
*/
protected function map_wc_product_image( string $image_size ) {
$image_id = $this->wc_product->get_image_id();
$gallery_image_ids = $this->wc_product->get_gallery_image_ids() ?: [];
// check if we can use the parent product image if it's a variation
if ( $this->is_variation() ) {
$image_id = $image_id ?? $this->parent_wc_product->get_image_id();
$parent_gallery_images = $this->parent_wc_product->get_gallery_image_ids() ?: [];
$gallery_image_ids = ! empty( $gallery_image_ids ) ? $gallery_image_ids : $parent_gallery_images;
}
// use a gallery image as the main product image if no main image is available
if ( empty( $image_id ) && ! empty( $gallery_image_ids[0] ) ) {
$image_id = $gallery_image_ids[0];
// remove the recently set main image from the list of gallery images
unset( $gallery_image_ids[0] );
}
// set main image
$image_link = wp_get_attachment_image_url( $image_id, $image_size, false );
$this->setImageLink( $image_link );
// set additional images
$gallery_image_links = array_map(
function ( $gallery_image_id ) use ( $image_size ) {
return wp_get_attachment_image_url( $gallery_image_id, $image_size, false );
},
$gallery_image_ids
);
// Uniquify the set of additional images
$gallery_image_links = array_unique( $gallery_image_links, SORT_REGULAR );
// Limit additional image links up to 10
$gallery_image_links = array_slice( $gallery_image_links, 0, 10 );
$this->setAdditionalImageLinks( $gallery_image_links );
return $this;
}
/**
* Map the general WooCommerce product attributes.
*
* @return $this
*/
protected function map_wc_availability() {
if ( ! $this->wc_product->is_in_stock() ) {
$availability = self::AVAILABILITY_OUT_OF_STOCK;
} elseif ( $this->wc_product->is_on_backorder( 1 ) ) {
$availability = self::AVAILABILITY_BACKORDER;
} else {
$availability = self::AVAILABILITY_IN_STOCK;
}
$this->setAvailability( $availability );
return $this;
}
/**
* Map the shipping information for WooCommerce product.
*
* @return $this
*/
protected function map_wc_product_shipping(): WCProductAdapter {
$this->add_shipping_country( $this->getTargetCountry() );
if ( ! $this->is_virtual() ) {
$dimension_unit = apply_filters( 'woocommerce_gla_dimension_unit', get_option( 'woocommerce_dimension_unit' ) );
$weight_unit = apply_filters( 'woocommerce_gla_weight_unit', get_option( 'woocommerce_weight_unit' ) );
$this->map_wc_shipping_dimensions( $dimension_unit )
->map_wc_shipping_weight( $weight_unit );
}
// Set the product's shipping class slug as the shipping label.
$shipping_class = $this->wc_product->get_shipping_class();
if ( ! empty( $shipping_class ) ) {
$this->setShippingLabel( $shipping_class );
}
return $this;
}
/**
* Add a shipping country for the product.
*
* @param string $country
*/
public function add_shipping_country( string $country ): void {
$product_shipping = [
'country' => $country,
];
// Virtual products should override any country shipping cost.
if ( $this->is_virtual() ) {
$product_shipping['price'] = [
'currency' => get_woocommerce_currency(),
'value' => 0,
];
}
$new_shipping = [
new GoogleProductShipping( $product_shipping ),
];
if ( ! $this->shipping_country_exists( $country ) ) {
$current_shipping = $this->getShipping() ?? [];
$this->setShipping( array_merge( $current_shipping, $new_shipping ) );
}
}
/**
* Remove a shipping country from the product.
*
* @param string $country
*
* @since 1.2.0
*/
public function remove_shipping_country( string $country ): void {
$product_shippings = $this->getShipping() ?? [];
foreach ( $product_shippings as $index => $shipping ) {
if ( $country === $shipping->getCountry() ) {
unset( $product_shippings[ $index ] );
}
}
$this->setShipping( $product_shippings );
}
/**
* @param string $country
*
* @return bool
*/
protected function shipping_country_exists( string $country ): bool {
$current_shipping = $this->getShipping() ?? [];
foreach ( $current_shipping as $shipping ) {
if ( $country === $shipping->getCountry() ) {
return true;
}
}
return false;
}
/**
* Map the measurements for the WooCommerce product.
*
* @param string $unit
*
* @return $this
*/
protected function map_wc_shipping_dimensions( string $unit = 'cm' ): WCProductAdapter {
$length = $this->wc_product->get_length();
$width = $this->wc_product->get_width();
$height = $this->wc_product->get_height();
// Use cm if the unit isn't supported.
if ( ! in_array( $unit, [ 'in', 'cm' ], true ) ) {
$unit = 'cm';
}
$length = wc_get_dimension( (float) $length, $unit );
$width = wc_get_dimension( (float) $width, $unit );
$height = wc_get_dimension( (float) $height, $unit );
if ( $length > 0 && $width > 0 && $height > 0 ) {
$this->setShippingLength(
new GoogleProductShippingDimension(
[
'unit' => $unit,
'value' => $length,
]
)
);
$this->setShippingWidth(
new GoogleProductShippingDimension(
[
'unit' => $unit,
'value' => $width,
]
)
);
$this->setShippingHeight(
new GoogleProductShippingDimension(
[
'unit' => $unit,
'value' => $height,
]
)
);
}
return $this;
}
/**
* Map the weight for the WooCommerce product.
*
* @param string $unit
*
* @return $this
*/
protected function map_wc_shipping_weight( string $unit = 'g' ): WCProductAdapter {
// Use g if the unit isn't supported.
if ( ! in_array( $unit, [ 'g', 'lbs', 'oz' ], true ) ) {
$unit = 'g';
}
$weight = wc_get_weight( $this->wc_product->get_weight(), $unit );
// Use lb if the unit is lbs, since GMC uses lb.
if ( 'lbs' === $unit ) {
$unit = 'lb';
}
$this->setShippingWeight(
new GoogleProductShippingWeight(
[
'unit' => $unit,
'value' => $weight,
]
)
);
return $this;
}
/**
* Sets whether tax is excluded from product price.
*
* @return $this
*/
protected function map_tax_excluded(): WCProductAdapter {
// tax is excluded from price in US and CA
$this->tax_excluded = in_array( $this->getTargetCountry(), [ 'US', 'CA' ], true );
$this->tax_excluded = boolval( apply_filters( 'woocommerce_gla_tax_excluded', $this->tax_excluded ) );
return $this;
}
/**
* Map the prices (base and sale price) for the product.
*
* @return $this
*/
protected function map_wc_prices(): WCProductAdapter {
$this->map_tax_excluded();
$this->map_wc_product_price( $this->wc_product );
return $this;
}
/**
* Map the prices (base and sale price) for a given WooCommerce product.
*
* @param WC_Product $product
*
* @return $this
*/
protected function map_wc_product_price( WC_Product $product ): WCProductAdapter {
// set regular price
$regular_price = $product->get_regular_price();
if ( '' !== $regular_price ) {
$price = $this->tax_excluded ?
wc_get_price_excluding_tax( $product, [ 'price' => $regular_price ] ) :
wc_get_price_including_tax( $product, [ 'price' => $regular_price ] );
/**
* Filters the calculated product price.
*
* @param float $price Calculated price of the product
* @param WC_Product $product WooCommerce product
* @param bool $tax_excluded Whether tax is excluded from product price
*/
$price = apply_filters( 'woocommerce_gla_product_attribute_value_price', $price, $product, $this->tax_excluded );
$this->setPrice(
new GooglePrice(
[
'currency' => get_woocommerce_currency(),
'value' => $price,
]
)
);
}
// set sale price
$this->map_wc_product_sale_price( $product );
return $this;
}
/**
* Map the sale price and sale effective date for a given WooCommerce product.
*
* @param WC_Product $product
*
* @return $this
*/
protected function map_wc_product_sale_price( WC_Product $product ): WCProductAdapter {
// Grab the sale price of the base product. Some plugins (Dynamic
// pricing as an example) filter the active price, but not the sale
// price. If the active price < the regular price treat it as a sale
// price.
$regular_price = $product->get_regular_price();
$sale_price = $product->get_sale_price();
$active_price = $product->get_price();
if (
( empty( $sale_price ) && $active_price < $regular_price ) ||
( ! empty( $sale_price ) && $active_price < $sale_price )
) {
$sale_price = $active_price;
}
// set sale price and sale effective date if any
if ( '' !== $sale_price ) {
$sale_price = $this->tax_excluded ?
wc_get_price_excluding_tax( $product, [ 'price' => $sale_price ] ) :
wc_get_price_including_tax( $product, [ 'price' => $sale_price ] );
/**
* Filters the calculated product sale price.
*
* @param float $sale_price Calculated sale price of the product
* @param WC_Product $product WooCommerce product
* @param bool $tax_excluded Whether tax is excluded from product price
*/
$sale_price = apply_filters( 'woocommerce_gla_product_attribute_value_sale_price', $sale_price, $product, $this->tax_excluded );
// If the sale price dates no longer apply, make sure we don't include a sale price.
$now = new WC_DateTime();
$sale_price_end_date = $product->get_date_on_sale_to();
if ( empty( $sale_price_end_date ) || $sale_price_end_date >= $now ) {
$this->setSalePrice(
new GooglePrice(
[
'currency' => get_woocommerce_currency(),
'value' => $sale_price,
]
)
);
$this->setSalePriceEffectiveDate( $this->get_wc_product_sale_price_effective_date( $product ) );
}
}
return $this;
}
/**
* Return the sale effective dates for the WooCommerce product.
*
* @param WC_Product $product
*
* @return string|null
*/
protected function get_wc_product_sale_price_effective_date( WC_Product $product ): ?string {
$start_date = $product->get_date_on_sale_from();
$end_date = $product->get_date_on_sale_to();
$now = new WC_DateTime();
// if we have a sale end date in the future, but no start date, set the start date to now()
if (
! empty( $end_date ) &&
$end_date > $now &&
empty( $start_date )
) {
$start_date = $now;
}
// if we have a sale start date in the past, but no end date, do not include the start date.
if (
! empty( $start_date ) &&
$start_date < $now &&
empty( $end_date )
) {
$start_date = null;
}
// if we have a start date in the future, but no end date, assume a one-day sale.
if (
! empty( $start_date ) &&
$start_date > $now &&
empty( $end_date )
) {
$end_date = clone $start_date;
$end_date->add( new DateInterval( 'P1D' ) );
}
if ( empty( $start_date ) && empty( $end_date ) ) {
return null;
}
return sprintf( '%s/%s', (string) $start_date, (string) $end_date );
}
/**
* Return whether the WooCommerce product is a variation.
*
* @return bool
*/
public function is_variation(): bool {
return $this->wc_product instanceof WC_Product_Variation;
}
/**
* Return whether the WooCommerce product is virtual.
*
* @return bool
*/
public function is_virtual(): bool {
$is_virtual = $this->wc_product->is_virtual();
/**
* Filters the virtual property value of a product.
*
* @param bool $is_virtual Whether a product is virtual
* @param WC_Product $product WooCommerce product
*/
$is_virtual = apply_filters( 'woocommerce_gla_product_property_value_is_virtual', $is_virtual, $this->wc_product );
return false !== $is_virtual;
}
/**
* @param ClassMetadata $metadata
*/
public static function load_validator_metadata( ClassMetadata $metadata ) {
$metadata->addPropertyConstraint( 'offerId', new Assert\NotBlank() );
$metadata->addPropertyConstraint( 'title', new Assert\NotBlank() );
$metadata->addPropertyConstraint( 'description', new Assert\NotBlank() );
$metadata->addPropertyConstraint( 'link', new Assert\NotBlank() );
$metadata->addPropertyConstraint( 'link', new Assert\Url() );
$metadata->addPropertyConstraint( 'imageLink', new Assert\NotBlank() );
$metadata->addPropertyConstraint( 'imageLink', new ImageUrlConstraint() );
$metadata->addPropertyConstraint(
'additionalImageLinks',
new Assert\All(
[
'constraints' => [ new ImageUrlConstraint() ],
]
)
);
$metadata->addGetterConstraint( 'price', new Assert\NotNull() );
$metadata->addGetterConstraint( 'price', new GooglePriceConstraint() );
$metadata->addGetterConstraint( 'salePrice', new GooglePriceConstraint() );
$metadata->addConstraint( new Assert\Callback( 'validate_item_group_id' ) );
$metadata->addConstraint( new Assert\Callback( 'validate_availability' ) );
$metadata->addPropertyConstraint( 'gtin', new Assert\Regex( '/^\d{8}(?:\d{4,6})?$/' ) );
$metadata->addPropertyConstraint( 'mpn', new Assert\Type( 'string' ) );
$metadata->addPropertyConstraint( 'mpn', new Assert\Length( null, 0, 70 ) ); // maximum 70 characters
$metadata->addPropertyConstraint(
'sizes',
new Assert\All(
[
'constraints' => [
new Assert\Type( 'string' ),
new Assert\Length( null, 0, 100 ), // maximum 100 characters
],
]
)
);
$metadata->addPropertyConstraint( 'sizeSystem', new Assert\Choice( array_keys( SizeSystem::get_value_options() ) ) );
$metadata->addPropertyConstraint( 'sizeType', new Assert\Choice( array_keys( SizeType::get_value_options() ) ) );
$metadata->addPropertyConstraint( 'color', new Assert\Length( null, 0, 100 ) ); // maximum 100 characters
$metadata->addPropertyConstraint( 'material', new Assert\Length( null, 0, 200 ) ); // maximum 200 characters
$metadata->addPropertyConstraint( 'pattern', new Assert\Length( null, 0, 100 ) ); // maximum 200 characters
$metadata->addPropertyConstraint( 'ageGroup', new Assert\Choice( array_keys( AgeGroup::get_value_options() ) ) );
$metadata->addPropertyConstraint( 'adult', new Assert\Type( 'boolean' ) );
$metadata->addPropertyConstraint( 'condition', new Assert\Choice( array_keys( Condition::get_value_options() ) ) );
$metadata->addPropertyConstraint( 'multipack', new Assert\Type( 'integer' ) );
$metadata->addPropertyConstraint( 'multipack', new Assert\PositiveOrZero() );
$metadata->addPropertyConstraint( 'isBundle', new Assert\Type( 'boolean' ) );
}
/**
* Used by the validator to check if the variation product has an itemGroupId
*
* @param ExecutionContextInterface $context
*/
public function validate_item_group_id( ExecutionContextInterface $context ) {
if ( $this->is_variation() && empty( $this->getItemGroupId() ) ) {
$context->buildViolation( 'ItemGroupId needs to be set for variable products.' )
->atPath( 'itemGroupId' )
->addViolation();
}
}
/**
* Used by the validator to check if the availability date is set for product available as `backorder` or
* `preorder`.
*
* @param ExecutionContextInterface $context
*/
public function validate_availability( ExecutionContextInterface $context ) {
if (
( self::AVAILABILITY_BACKORDER === $this->getAvailability() || self::AVAILABILITY_PREORDER === $this->getAvailability() ) &&
empty( $this->getAvailabilityDate() )
) {
$context->buildViolation( 'Availability date is required if you set the product\'s availability to backorder or pre-order.' )
->atPath( 'availabilityDate' )
->addViolation();
}
}
/**
* @return WC_Product
*/
public function get_wc_product(): WC_Product {
return $this->wc_product;
}
/**
* @param array $attributes Attribute values
*
* @return $this
*/
protected function map_gla_attributes( array $attributes ): WCProductAdapter {
$gla_attributes = [];
foreach ( $attributes as $attribute_id => $attribute_value ) {
if ( property_exists( $this, $attribute_id ) ) {
/**
* Filters a product attribute's value.
*
* This only applies to the extra attributes defined in `AttributeManager::ATTRIBUTES`
* like GTIN, MPN, Brand, Size, etc. and it cannot modify other product attributes.
*
* This filter also cannot add or set a new attribute or modify one that isn't currently
* set for the product through WooCommerce's edit product page
*
* In order to override all product attributes and/or set new ones for the product use the
* `woocommerce_gla_product_attribute_values` filter.
*
* Note that the `woocommerce_gla_product_attribute_values` filter takes precedence over
* this filter, and it can be used to override any values defined here.
*
* @param mixed $attribute_value The attribute's current value
* @param WC_Product $wc_product The WooCommerce product object.
*
* @see AttributeManager::ATTRIBUTES for the list of attributes that their values can be modified using this filter.
* @see WCProductAdapter::override_attributes for the documentation of the `woocommerce_gla_product_attribute_values` filter.
*/
$gla_attributes[ $attribute_id ] = apply_filters( "woocommerce_gla_product_attribute_value_{$attribute_id}", $attribute_value, $this->get_wc_product() );
}
}
parent::mapTypes( $gla_attributes );
// Size
if ( ! empty( $attributes['size'] ) ) {
$this->setSizes( [ $attributes['size'] ] );
}
return $this;
}
/**
* Map the WooCommerce core global unique ID (GTIN) value if it's available.
*
* @since 2.9.0
*
* @return $this
*/
protected function map_gtin(): WCProductAdapter {
// compatibility-code "WC < 9.2" -- Core global unique ID field was added in 9.2
if ( ! method_exists( $this->wc_product, 'get_global_unique_id' ) ) {
return $this;
}
// avoid dashes and other unsupported format
$global_unique_id = preg_replace( '/[^0-9]/', '', $this->wc_product->get_global_unique_id() );
if ( ! empty( $global_unique_id ) ) {
$this->setGtin( $global_unique_id );
}
return $this;
}
/**
* @param string $targetCountry
*
* phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
*/
public function setTargetCountry( $targetCountry ) {
// remove shipping for current target country
$this->remove_shipping_country( $this->getTargetCountry() );
// set the new target country
parent::setTargetCountry( $targetCountry );
// we need to reset the prices because tax is based on the country
$this->map_wc_prices();
// product shipping information is also country based
$this->map_wc_product_shipping();
}
/**
* Performs the attribute mapping.
* This function applies rules setting values for the different attributes in the product.
*
* @param array $mapping_rules The set of rules to apply
*/
protected function map_attribute_mapping_rules( array $mapping_rules ) {
$attributes = [];
if ( empty( $mapping_rules ) ) {
return $this;
}
foreach ( $mapping_rules as $mapping_rule ) {
if ( $this->rule_match_conditions( $mapping_rule ) ) {
$attribute_id = $mapping_rule['attribute'];
$attributes[ $attribute_id ] = $this->format_attribute(
apply_filters(
"woocommerce_gla_product_attribute_value_{$attribute_id}",
$this->get_source( $mapping_rule['source'] ),
$this->get_wc_product()
),
$attribute_id
);
}
}
parent::mapTypes( $attributes );
// Size
if ( ! empty( $attributes['size'] ) ) {
$this->setSizes( [ $attributes['size'] ] );
}
return $this;
}
/**
* Get a source value for attribute mapping
*
* @param string $source The source to get the value
* @return string The source value for this product
*/
protected function get_source( string $source ) {
$source_type = null;
$type_separator = strpos( $source, ':' );
if ( $type_separator ) {
$source_type = substr( $source, 0, $type_separator );
$source_value = substr( $source, $type_separator + 1 );
}
// Detect if the source_type is kind of product, taxonomy or attribute. Otherwise, we take it the full source as a static value.
switch ( $source_type ) {
case 'product':
return $this->get_product_field( $source_value );
case 'taxonomy':
return $this->get_product_taxonomy( $source_value );
case 'attribute':
return $this->get_custom_attribute( $source_value );
default:
return $source;
}
}
/**
* Check if the current product match the conditions for applying the Attribute mapping rule.
* For now the conditions are just matching with the product category conditions.
*
* @param array $rule The attribute mapping rule
* @return bool True if the rule is applicable
*/
protected function rule_match_conditions( array $rule ): bool {
$attribute = $rule['attribute'];
$category_condition_type = $rule['category_condition_type'];
if ( $category_condition_type === AttributeMappingHelper::CATEGORY_CONDITION_TYPE_ALL ) {
return true;
}
// size is not the real attribute, the real attribute is sizes
if ( ! property_exists( $this, $attribute ) && $attribute !== 'size' ) {
return false;
}
$categories = explode( ',', $rule['categories'] );
$contains_rules_categories = ! empty( array_intersect( $categories, $this->product_category_ids ) );
if ( $category_condition_type === AttributeMappingHelper::CATEGORY_CONDITION_TYPE_ONLY ) {
return $contains_rules_categories;
}
return ! $contains_rules_categories;
}
/**
* Get taxonomy source type for attribute mapping
*
* @param string $taxonomy The taxonomy to get
* @return string The taxonomy value
*/
protected function get_product_taxonomy( $taxonomy ) {
$product = $this->get_wc_product();
if ( $product->is_type( 'variation' ) ) {
$values = $product->get_attribute( $taxonomy );
if ( ! $values ) { // if taxonomy is not a global attribute (ie product_tag), attempt to get is with wc_get_product_terms
$values = $this->get_taxonomy_term_names( $product->get_id(), $taxonomy );
}
if ( ! $values ) { // if the value is still not available at this point, we try to get it from the parent
$parent = wc_get_product( $product->get_parent_id() );
$values = $parent->get_attribute( $taxonomy );
if ( ! $values ) {
$values = $this->get_taxonomy_term_names( $parent->get_id(), $taxonomy );
}
}
if ( is_string( $values ) ) {
$values = explode( ', ', $values );
}
} else {
$values = $this->get_taxonomy_term_names( $product->get_id(), $taxonomy );
}
if ( empty( $values ) || is_wp_error( $values ) ) {
return '';
}
return $values[0];
}
/**
* Get product source type for attribute mapping.
* Those are fields belonging to the product core data. Like title, weight, SKU...
*
* @param string $field The field to get
* @return string|null The field value (null if data is not available)
*/
protected function get_product_field( $field ) {
$product = $this->get_wc_product();
if ( 'weight_with_unit' === $field ) {
$weight = $product->get_weight();
return $weight ? $weight . ' ' . get_option( 'woocommerce_weight_unit' ) : null;
}
if ( is_callable( [ $product, 'get_' . $field ] ) ) {
$getter = 'get_' . $field;
return $product->$getter();
}
return '';
}
/**
*
* Formats the attribute for sending it via Google API
*
* @param string $value The value to format
* @param string $attribute_id The attribute ID for which this value belongs
* @return string|bool|int The attribute formatted based on theit attribute type
*/
protected function format_attribute( $value, $attribute_id ) {
$attribute = AttributeMappingHelper::get_attribute_by_id( $attribute_id );
if ( in_array( $attribute::get_value_type(), [ 'bool', 'boolean' ], true ) ) {
return wc_string_to_bool( $value );
}
if ( in_array( $attribute::get_value_type(), [ 'int', 'integer' ], true ) ) {
return (int) $value;
}
return $value;
}
/**
* Gets a custom attribute from a product
*
* @param string $attribute_name - The attribute name to get.
* @return string|null The attribute value or null if no value is found
*/
protected function get_custom_attribute( $attribute_name ) {
$product = $this->get_wc_product();
$attribute_value = $product->get_attribute( $attribute_name );
if ( ! $attribute_value ) {
$attribute_value = $product->get_meta( $attribute_name );
}
// We only support scalar values.
if ( ! is_scalar( $attribute_value ) ) {
return '';
}
$values = explode( WC_DELIMITER, (string) $attribute_value );
$values = array_filter( array_map( 'trim', $values ) );
return empty( $values ) ? '' : $values[0];
}
/**
* Get a taxonomy term names from a product using
*
* @param int $product_id - The product ID to get the taxonomy term
* @param string $taxonomy - The taxonomy to get.
* @return string[] An array of term names.
*/
protected function get_taxonomy_term_names( $product_id, $taxonomy ) {
$values = wc_get_product_terms( $product_id, $taxonomy );
return wp_list_pluck( $values, 'name' );
}
}
Attributes/AttributesForm.php 0000644 00000017455 15154653110 0012372 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Form;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\FormException;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\InputInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Select;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\SelectWithTextInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input\GTINInput;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ValidateInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\WithValueOptionsInterface;
defined( 'ABSPATH' ) || exit;
/**
* Class AttributesForm
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes
*/
class AttributesForm extends Form {
use ValidateInterface;
/**
* @var string[]
*/
protected $attribute_types = [];
/**
* AttributesForm constructor.
*
* @param string[] $attribute_types The names of the attribute classes extending AttributeInterface.
* @param array $data
*/
public function __construct( array $attribute_types, array $data = [] ) {
foreach ( $attribute_types as $attribute_type ) {
$this->add_attribute( $attribute_type );
}
parent::__construct( $data );
}
/**
* Return the data used for the input's view.
*
* @return array
*/
public function get_view_data(): array {
$view_data = parent::get_view_data();
// add classes to hide/display attributes based on product type
foreach ( $view_data['children'] as $index => $input ) {
if ( ! isset( $this->attribute_types[ $index ] ) ) {
continue;
}
$attribute_type = $this->attribute_types[ $index ];
$attribute_product_types = self::get_attribute_product_types( $attribute_type );
$hidden_types = $attribute_product_types['hidden'];
$visible_types = $attribute_product_types['visible'];
$input['gla_wrapper_class'] = $input['gla_wrapper_class'] ?? '';
if ( ! empty( $visible_types ) ) {
$input['gla_wrapper_class'] .= ' show_if_' . join( ' show_if_', $visible_types );
}
if ( ! empty( $hidden_types ) ) {
$input['gla_wrapper_class'] .= ' hide_if_' . join( ' hide_if_', $hidden_types );
}
$view_data['children'][ $index ] = $input;
}
return $view_data;
}
/**
* Get the hidden and visible types of an attribute's applicable product types.
*
* @param string $attribute_type The name of an attribute class extending AttributeInterface.
*
* @return array
*/
public static function get_attribute_product_types( string $attribute_type ): array {
$attribute_id = call_user_func( [ $attribute_type, 'get_id' ] );
$applicable_product_types = call_user_func( [ $attribute_type, 'get_applicable_product_types' ] );
/**
* This filter is documented in AttributeManager::map_attribute_types
*
* @see AttributeManager::map_attribute_types
*/
$applicable_product_types = apply_filters( "woocommerce_gla_attribute_applicable_product_types_{$attribute_id}", $applicable_product_types, $attribute_type );
/**
* Filters the list of product types to hide the attribute for.
*/
$hidden_product_types = apply_filters( "woocommerce_gla_attribute_hidden_product_types_{$attribute_id}", [] );
$visible_product_types = array_diff( $applicable_product_types, $hidden_product_types );
return [
'hidden' => $hidden_product_types,
'visible' => $visible_product_types,
];
}
/**
* @param InputInterface $input
* @param AttributeInterface $attribute
*
* @return InputInterface
*/
public static function init_input( InputInterface $input, AttributeInterface $attribute ) {
$input->set_id( $attribute::get_id() )
->set_name( $attribute::get_id() );
$value_options = [];
if ( $attribute instanceof WithValueOptionsInterface ) {
$value_options = $attribute::get_value_options();
}
$value_options = apply_filters( "woocommerce_gla_product_attribute_value_options_{$attribute::get_id()}", $value_options );
if ( ! empty( $value_options ) ) {
if ( ! $input instanceof Select && ! $input instanceof SelectWithTextInput ) {
$new_input = new SelectWithTextInput();
$new_input->set_label( $input->get_label() )
->set_description( $input->get_description() );
// When GTIN uses the SelectWithTextInput field, copy the readonly/hidden attributes from the GTINInput field.
if ( $input->name === 'gtin' ) {
$gtin_input = new GTINInput();
$new_input->set_hidden( $gtin_input->is_hidden() );
$new_input->set_readonly( $gtin_input->is_readonly() );
}
return self::init_input( $new_input, $attribute );
}
// add a 'default' value option
$value_options = [ '' => __( 'Default', 'google-listings-and-ads' ) ] + $value_options;
$input->set_options( $value_options );
}
return $input;
}
/**
* Add an attribute to the form
*
* @param string $attribute_type The name of an attribute class extending AttributeInterface.
* @param string|null $input_type The name of an input class extending InputInterface to use for attribute input.
*
* @return AttributesForm
*
* @throws InvalidValue If the attribute type is invalid or an invalid input type is specified for the attribute.
* @throws FormException If form is already submitted.
*/
public function add_attribute( string $attribute_type, ?string $input_type = null ): AttributesForm {
$this->validate_interface( $attribute_type, AttributeInterface::class );
// use the attribute's default input type if none provided.
if ( empty( $input_type ) ) {
$input_type = call_user_func( [ $attribute_type, 'get_input_type' ] );
}
$this->validate_interface( $input_type, InputInterface::class );
$attribute_input = self::init_input( new $input_type(), new $attribute_type() );
if ( ! $attribute_input->is_hidden() ) {
$this->add( $attribute_input );
$attribute_id = call_user_func( [ $attribute_type, 'get_id' ] );
$this->attribute_types[ $attribute_id ] = $attribute_type;
}
return $this;
}
/**
* Remove an attribute from the form
*
* @param string $attribute_type The name of an attribute class extending AttributeInterface.
*
* @return AttributesForm
*
* @throws InvalidValue If the attribute type is invalid or an invalid input type is specified for the attribute.
* @throws FormException If form is already submitted.
*/
public function remove_attribute( string $attribute_type ): AttributesForm {
$this->validate_interface( $attribute_type, AttributeInterface::class );
$attribute_id = call_user_func( [ $attribute_type, 'get_id' ] );
unset( $this->attribute_types[ $attribute_id ] );
$this->remove( $attribute_id );
return $this;
}
/**
* Sets the input type for the given attribute.
*
* @param string $attribute_type The name of an attribute class extending AttributeInterface.
* @param string $input_type The name of an input class extending InputInterface to use for attribute input.
*
* @return $this
*
* @throws FormException If form is already submitted.
*/
public function set_attribute_input( string $attribute_type, string $input_type ): AttributesForm {
if ( $this->is_submitted ) {
throw FormException::cannot_modify_submitted();
}
$this->validate_interface( $attribute_type, AttributeInterface::class );
$this->validate_interface( $input_type, InputInterface::class );
$attribute_id = call_user_func( [ $attribute_type, 'get_id' ] );
if ( $this->has( $attribute_id ) ) {
$this->children[ $attribute_id ] = self::init_input( new $input_type(), new $attribute_type() );
}
return $this;
}
}
Attributes/AttributesTab.php 0000644 00000011704 15154653110 0012164 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Admin;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\AdminConditional;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Conditional;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncer;
use WC_Product;
defined( 'ABSPATH' ) || exit;
/**
* Class AttributesTab
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes
*/
class AttributesTab implements Service, Registerable, Conditional {
use AdminConditional;
use AttributesTrait;
/**
* @var Admin
*/
protected $admin;
/**
* @var AttributeManager
*/
protected $attribute_manager;
/**
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* AttributesTab constructor.
*
* @param Admin $admin
* @param AttributeManager $attribute_manager
* @param MerchantCenterService $merchant_center
*/
public function __construct( Admin $admin, AttributeManager $attribute_manager, MerchantCenterService $merchant_center ) {
$this->admin = $admin;
$this->attribute_manager = $attribute_manager;
$this->merchant_center = $merchant_center;
}
/**
* Register a service.
*/
public function register(): void {
// Register the hooks only if Merchant Center is set up.
if ( ! $this->merchant_center->is_setup_complete() ) {
return;
}
add_action(
'woocommerce_new_product',
function ( int $product_id, WC_Product $product ) {
$this->handle_update_product( $product );
},
10,
2
);
add_action(
'woocommerce_update_product',
function ( int $product_id, WC_Product $product ) {
$this->handle_update_product( $product );
},
10,
2
);
add_action(
'woocommerce_product_data_tabs',
function ( array $tabs ) {
return $this->add_tab( $tabs );
}
);
add_action(
'woocommerce_product_data_panels',
function () {
$this->render_panel();
}
);
}
/**
* Adds the Google for WooCommerce tab to the WooCommerce product data box.
*
* @param array $tabs The current product data tabs.
*
* @return array An array with product tabs with the Yoast SEO tab added.
*/
private function add_tab( array $tabs ): array {
$tabs['gla_attributes'] = [
'label' => 'Google for WooCommerce',
'class' => 'gla',
'target' => 'gla_attributes',
];
return $tabs;
}
/**
* Render the product attributes tab.
*/
private function render_panel() {
$product = wc_get_product( get_the_ID() );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->admin->get_view( 'attributes/tab-panel', [ 'form' => $this->get_form( $product )->get_view_data() ] );
}
/**
* Handle form submission and update the product attributes.
*
* @param WC_Product $product
*/
private function handle_update_product( WC_Product $product ) {
/**
* Array of `true` values for each product IDs already handled by this method. Used to prevent double submission.
*
* @var bool[] $already_updated
*/
static $already_updated = [];
if ( isset( $already_updated[ $product->get_id() ] ) ) {
return;
}
$form = $this->get_form( $product );
$form_view_data = $form->get_view_data();
// phpcs:disable WordPress.Security.NonceVerification
if ( empty( $_POST[ $form_view_data['name'] ] ) ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$submitted_data = (array) wc_clean( wp_unslash( $_POST[ $form_view_data['name'] ] ) );
// phpcs:enable WordPress.Security.NonceVerification
$form->submit( $submitted_data );
$this->update_data( $product, $form->get_data() );
$already_updated[ $product->get_id() ] = true;
}
/**
* @param WC_Product $product
*
* @return AttributesForm
*/
protected function get_form( WC_Product $product ): AttributesForm {
$attribute_types = $this->attribute_manager->get_attribute_types_for_product_types( $this->get_applicable_product_types() );
$form = new AttributesForm( $attribute_types, $this->attribute_manager->get_all_values( $product ) );
$form->set_name( 'attributes' );
return $form;
}
/**
* @param WC_Product $product
* @param array $data
*
* @return void
*/
protected function update_data( WC_Product $product, array $data ): void {
foreach ( $this->attribute_manager->get_attribute_types_for_product( $product ) as $attribute_id => $attribute_type ) {
if ( isset( $data[ $attribute_id ] ) ) {
$this->attribute_manager->update( $product, new $attribute_type( $data[ $attribute_id ] ) );
}
}
}
}
Attributes/AttributesTrait.php 0000644 00000001172 15154653110 0012537 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes;
/**
* Trait AttributesTrait
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes
*/
trait AttributesTrait {
/**
* Return an array of WooCommerce product types that the Google for WooCommerce tab can be displayed for.
*
* @return array of WooCommerce product types (e.g. 'simple', 'variable', etc.)
*/
protected function get_applicable_product_types(): array {
return apply_filters( 'woocommerce_gla_attributes_tab_applicable_product_types', [ 'simple', 'variable' ] );
}
}
Attributes/Input/AdultInput.php 0000644 00000001267 15154653110 0012602 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\BooleanSelect;
defined( 'ABSPATH' ) || exit;
/**
* Class Adult
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class AdultInput extends BooleanSelect {
/**
* AdultInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Adult content', 'google-listings-and-ads' ) );
$this->set_description( __( 'Whether the product contains nudity or sexually suggestive content', 'google-listings-and-ads' ) );
}
}
Attributes/Input/AgeGroupInput.php 0000644 00000001211 15154653110 0013227 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Select;
defined( 'ABSPATH' ) || exit;
/**
* Class AgeGroup
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class AgeGroupInput extends Select {
/**
* AgeGroupInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Age Group', 'google-listings-and-ads' ) );
$this->set_description( __( 'Target age group of the item.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/AttributeInputInterface.php 0000644 00000001433 15154653110 0015310 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
defined( 'ABSPATH' ) || exit;
/**
* Class AttributeInputInterface
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input
*
* @since 1.5.0
*/
interface AttributeInputInterface {
/**
* Returns a name for the attribute input.
*
* @return string
*/
public static function get_name(): string;
/**
* Returns a short description for the attribute input.
*
* @return string
*/
public static function get_description(): string;
/**
* Returns the input class used for the attribute input.
*
* Must be an instance of `InputInterface`.
*
* @return string
*/
public static function get_input_type(): string;
}
Attributes/Input/AvailabilityDateInput.php 0000644 00000001426 15154653110 0014736 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\DateTime;
defined( 'ABSPATH' ) || exit;
/**
* Class AvailabilityDate
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class AvailabilityDateInput extends DateTime {
/**
* AvailabilityDateInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Availability Date', 'google-listings-and-ads' ) );
$this->set_description( __( 'The date a preordered or backordered product becomes available for delivery. Required if product availability is preorder or backorder', 'google-listings-and-ads' ) );
}
}
Attributes/Input/BrandInput.php 0000644 00000001160 15154653110 0012547 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
defined( 'ABSPATH' ) || exit;
/**
* Class Brand
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class BrandInput extends Text {
/**
* BrandInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Brand', 'google-listings-and-ads' ) );
$this->set_description( __( 'Brand of the product.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/ColorInput.php 0000644 00000001160 15154653110 0012577 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
defined( 'ABSPATH' ) || exit;
/**
* Class Color
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class ColorInput extends Text {
/**
* ColorInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Color', 'google-listings-and-ads' ) );
$this->set_description( __( 'Color of the product.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/ConditionInput.php 0000644 00000001216 15154653110 0013451 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Select;
defined( 'ABSPATH' ) || exit;
/**
* Class Condition
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class ConditionInput extends Select {
/**
* ConditionInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Condition', 'google-listings-and-ads' ) );
$this->set_description( __( 'Condition or state of the item.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/GTINInput.php 0000644 00000003125 15154653110 0012265 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
use Automattic\WooCommerce\GoogleListingsAndAds\HelperTraits\GTINMigrationUtilities;
defined( 'ABSPATH' ) || exit;
/**
* Class GTIN
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class GTINInput extends Text {
use GTINMigrationUtilities;
/**
* GTINInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Global Trade Item Number (GTIN)', 'google-listings-and-ads' ) );
$this->set_description( __( 'Global Trade Item Number (GTIN) for your item. These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books)', 'google-listings-and-ads' ) );
$this->set_field_visibility();
}
/**
* Controls the inputs visibility based on the WooCommerce version and the
* initial version of Google for WooCommerce at the time of installation.
*
* @since 2.9.0
* @return void
*/
public function set_field_visibility(): void {
if ( $this->is_gtin_available_in_core() ) {
// For versions after the GTIN changes are published. Hide the GTIN field from G4W tab. Otherwise, set as readonly.
if ( $this->should_hide_gtin() ) {
$this->set_hidden( true );
} else {
$this->set_readonly( true );
$this->set_description( __( 'The Global Trade Item Number (GTIN) for your item can now be entered on the "Inventory" tab', 'google-listings-and-ads' ) );
}
}
}
}
Attributes/Input/GenderInput.php 0000644 00000001221 15154653110 0012723 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Select;
defined( 'ABSPATH' ) || exit;
/**
* Class Gender
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class GenderInput extends Select {
/**
* GenderInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Gender', 'google-listings-and-ads' ) );
$this->set_description( __( 'The gender for which your product is intended.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/IsBundleInput.php 0000644 00000001377 15154653110 0013240 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\BooleanSelect;
defined( 'ABSPATH' ) || exit;
/**
* Class IsBundle
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class IsBundleInput extends BooleanSelect {
/**
* IsBundleInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Is Bundle?', 'google-listings-and-ads' ) );
$this->set_description( __( 'Whether the item is a bundle of products. A bundle is a custom grouping of different products sold by a merchant for a single price.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/MPNInput.php 0000644 00000001254 15154653110 0012157 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
defined( 'ABSPATH' ) || exit;
/**
* Class MPN
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class MPNInput extends Text {
/**
* MPNInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Manufacturer Part Number (MPN)', 'google-listings-and-ads' ) );
$this->set_description( __( 'This code uniquely identifies the product to its manufacturer.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/MaterialInput.php 0000644 00000001216 15154653110 0013261 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
defined( 'ABSPATH' ) || exit;
/**
* Class Material
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class MaterialInput extends Text {
/**
* MaterialInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Material', 'google-listings-and-ads' ) );
$this->set_description( __( 'The material of which the item is made.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/MultipackInput.php 0000644 00000001500 15154653110 0013450 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Integer;
defined( 'ABSPATH' ) || exit;
/**
* Class Multipack
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class MultipackInput extends Integer {
/**
* MultipackInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Multipack', 'google-listings-and-ads' ) );
$this->set_description( __( 'The number of identical products in a multipack. Use this attribute to indicate that you\'ve grouped multiple identical products for sale as one item.', 'google-listings-and-ads' ) );
$this->set_block_attribute( 'min', [ 'value' => 0 ] );
}
}
Attributes/Input/PatternInput.php 0000644 00000001211 15154653110 0013133 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
defined( 'ABSPATH' ) || exit;
/**
* Class Pattern
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class PatternInput extends Text {
/**
* PatternInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Pattern', 'google-listings-and-ads' ) );
$this->set_description( __( 'The item\'s pattern (e.g. polka dots).', 'google-listings-and-ads' ) );
}
}
Attributes/Input/SizeInput.php 0000644 00000001153 15154653110 0012435 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Text;
defined( 'ABSPATH' ) || exit;
/**
* Class Size
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class SizeInput extends Text {
/**
* SizeInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Size', 'google-listings-and-ads' ) );
$this->set_description( __( 'Size of the product.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/SizeSystemInput.php 0000644 00000001271 15154653110 0013643 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Select;
defined( 'ABSPATH' ) || exit;
/**
* Class SizeSystem
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class SizeSystemInput extends Select {
/**
* SizeSystemInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Size system', 'google-listings-and-ads' ) );
$this->set_description( __( 'System in which the size is specified. Recommended for apparel items.', 'google-listings-and-ads' ) );
}
}
Attributes/Input/SizeTypeInput.php 0000644 00000001237 15154653110 0013302 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes\Input;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Select;
defined( 'ABSPATH' ) || exit;
/**
* Class SizeType
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes
*
* @since 1.5.0
*/
class SizeTypeInput extends Select {
/**
* SizeTypeInput constructor.
*/
public function __construct() {
parent::__construct();
$this->set_label( __( 'Size type', 'google-listings-and-ads' ) );
$this->set_description( __( 'The cut of the item. Recommended for apparel items.', 'google-listings-and-ads' ) );
}
}
Attributes/VariationsAttributes.php 0000644 00000012114 15154653110 0013571 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Admin;
use Automattic\WooCommerce\GoogleListingsAndAds\Admin\Input\Form;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\AdminConditional;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Conditional;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use WC_Product_Variation;
use WP_Post;
defined( 'ABSPATH' ) || exit;
/**
* Class VariationsAttributes
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product\Attributes
*/
class VariationsAttributes implements Service, Registerable, Conditional {
use AdminConditional;
/**
* @var Admin
*/
protected $admin;
/**
* @var AttributeManager
*/
protected $attribute_manager;
/**
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* VariationsAttributes constructor.
*
* @param Admin $admin
* @param AttributeManager $attribute_manager
* @param MerchantCenterService $merchant_center
*/
public function __construct( Admin $admin, AttributeManager $attribute_manager, MerchantCenterService $merchant_center ) {
$this->admin = $admin;
$this->attribute_manager = $attribute_manager;
$this->merchant_center = $merchant_center;
}
/**
* Register a service.
*/
public function register(): void {
// Register the hooks only if Merchant Center is set up.
if ( ! $this->merchant_center->is_setup_complete() ) {
return;
}
add_action(
'woocommerce_product_after_variable_attributes',
function ( int $variation_index, array $variation_data, WP_Post $variation ) {
$this->render_attributes_form( $variation_index, $variation );
},
90,
3
);
add_action(
'woocommerce_save_product_variation',
function ( int $variation_id, int $variation_index ) {
$this->handle_save_variation( $variation_id, $variation_index );
},
10,
2
);
}
/**
* Render the attributes form for variations.
*
* @param int $variation_index Position in the loop.
* @param WP_Post $variation Post data.
*/
private function render_attributes_form( int $variation_index, WP_Post $variation ) {
/**
* @var WC_Product_Variation $product
*/
$product = wc_get_product( $variation->ID );
$data = $this->get_form( $product, $variation_index )->get_view_data();
// Do not render the form if it doesn't contain any child attributes.
$attributes = reset( $data['children'] );
if ( empty( $data['children'] ) || empty( $attributes['children'] ) ) {
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->admin->get_view( 'attributes/variations-form', $data );
}
/**
* Handle form submission and update the product attributes.
*
* @param int $variation_id
* @param int $variation_index
*/
private function handle_save_variation( int $variation_id, int $variation_index ) {
/**
* @var WC_Product_Variation $variation
*/
$variation = wc_get_product( $variation_id );
$form = $this->get_form( $variation, $variation_index );
$form_view_data = $form->get_view_data();
// phpcs:disable WordPress.Security.NonceVerification
if ( empty( $_POST[ $form_view_data['name'] ] ) ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$submitted_data = (array) wc_clean( wp_unslash( $_POST[ $form_view_data['name'] ] ) );
// phpcs:enable WordPress.Security.NonceVerification
$form->submit( $submitted_data );
$form_data = $form->get_data();
if ( ! empty( $form_data[ $variation_index ] ) ) {
$this->update_data( $variation, $form_data[ $variation_index ] );
}
}
/**
* @param WC_Product_Variation $variation
* @param int $variation_index
*
* @return Form
*/
protected function get_form( WC_Product_Variation $variation, int $variation_index ): Form {
$attribute_types = $this->attribute_manager->get_attribute_types_for_product( $variation );
$attribute_form = new AttributesForm( $attribute_types );
$attribute_form->set_name( (string) $variation_index );
$form = new Form();
$form->set_name( 'variation_attributes' )
->add( $attribute_form )
->set_data( [ (string) $variation_index => $this->attribute_manager->get_all_values( $variation ) ] );
return $form;
}
/**
* @param WC_Product_Variation $variation
* @param array $data
*
* @return void
*/
protected function update_data( WC_Product_Variation $variation, array $data ): void {
foreach ( $this->attribute_manager->get_attribute_types_for_product( $variation ) as $attribute_id => $attribute_type ) {
if ( isset( $data[ $attribute_id ] ) ) {
$this->attribute_manager->update( $variation, new $attribute_type( $data[ $attribute_id ] ) );
}
}
}
}
ChannelVisibilityBlock.php 0000644 00000012043 15154653110 0011651 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncer;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\SyncStatus;
use WC_Data;
use WC_Product;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class ChannelVisibilityBlock
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Admin\Product
*/
class ChannelVisibilityBlock implements Service, Registerable {
public const PROPERTY = 'google_listings_and_ads__channel_visibility';
/**
* @var ProductHelper
*/
protected $product_helper;
/**
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* ChannelVisibilityBlock constructor.
*
* @param ProductHelper $product_helper
* @param MerchantCenterService $merchant_center
*/
public function __construct( ProductHelper $product_helper, MerchantCenterService $merchant_center ) {
$this->product_helper = $product_helper;
$this->merchant_center = $merchant_center;
}
/**
* Register hooks for querying and updating product via REST APIs.
*/
public function register(): void {
if ( ! $this->merchant_center->is_setup_complete() ) {
return;
}
// https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php#L182-L192
add_filter( 'woocommerce_rest_prepare_product_object', [ $this, 'prepare_data' ], 10, 2 );
// https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php#L200-L207
// https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php#L247-L254
add_action( 'woocommerce_rest_insert_product_object', [ $this, 'update_data' ], 10, 2 );
}
/**
* Get channel visibility data from the given product and add it to the given response.
*
* @param Response $response Response to be added channel visibility data.
* @param WC_Product|WC_Data $product WooCommerce product to get data.
*
* @return Response
*/
public function prepare_data( Response $response, WC_Data $product ): Response {
if ( ! $product instanceof WC_Product ) {
return $response;
}
$response->data[ self::PROPERTY ] = [
'is_visible' => $product->is_visible(),
'channel_visibility' => $this->product_helper->get_channel_visibility( $product ),
'sync_status' => $this->product_helper->get_sync_status( $product ),
'issues' => $this->product_helper->get_validation_errors( $product ),
];
return $response;
}
/**
* Get channel visibility data from the given request and update it to the given product.
*
* @param WC_Product|WC_Data $product WooCommerce product to be updated.
* @param Request $request Response to get the channel visibility data.
*/
public function update_data( WC_Data $product, Request $request ): void {
if ( ! $product instanceof WC_Product || ! in_array( $product->get_type(), $this->get_visible_product_types(), true ) ) {
return;
}
$params = $request->get_params();
if ( ! isset( $params[ self::PROPERTY ] ) ) {
return;
}
$channel_visibility = $params[ self::PROPERTY ]['channel_visibility'];
if ( $channel_visibility !== $this->product_helper->get_channel_visibility( $product ) ) {
$this->product_helper->update_channel_visibility( $product, $channel_visibility );
}
}
/**
* Return the visible product types to control the hidden condition of the channel visibility block
* in the Product Block Editor.
*
* @return array
*/
public function get_visible_product_types(): array {
return array_diff( ProductSyncer::get_supported_product_types(), [ 'variation' ] );
}
/**
* Return the config used for the input's block within the Product Block Editor.
*
* @return array
*/
public function get_block_config(): array {
$options = [];
foreach ( ChannelVisibility::get_value_options() as $key => $value ) {
$options[] = [
'label' => $value,
'value' => $key,
];
}
$attributes = [
'property' => self::PROPERTY,
'options' => $options,
'valueOfSync' => ChannelVisibility::SYNC_AND_SHOW,
'valueOfDontSync' => ChannelVisibility::DONT_SYNC_AND_SHOW,
'statusOfSynced' => SyncStatus::SYNCED,
'statusOfHasErrors' => SyncStatus::HAS_ERRORS,
];
return [
'id' => 'google-listings-and-ads-product-channel-visibility',
'blockName' => 'google-listings-and-ads/product-channel-visibility',
'attributes' => $attributes,
];
}
}