diff --git a/includes/class-core-schema-filters.php b/includes/class-core-schema-filters.php index 09b9124f..c8bf861a 100644 --- a/includes/class-core-schema-filters.php +++ b/includes/class-core-schema-filters.php @@ -384,4 +384,35 @@ public static function inject_type_resolver( $type, $value ) { return $type; } + + /** + * Resolves GraphQL type for provided product model. + * + * @param \WPGraphQL\WooCommerce\Model\Product $value Product model. + * + * @throws \GraphQL\Error\UserError Invalid product type requested. + * + * @return mixed + */ + public static function resolve_product_type( $value ) { + $type_registry = \WPGraphQL::get_type_registry(); + $possible_types = WooGraphQL::get_enabled_product_types(); + $product_type = $value->get_type(); + if ( isset( $possible_types[ $product_type ] ) ) { + return $type_registry->get_type( $possible_types[ $product_type ] ); + } elseif ( str_ends_with( $product_type, 'variation' ) ) { + return $type_registry->get_type( 'ProductVariation' ); + } elseif ( 'on' === woographql_setting( 'enable_unsupported_product_type', 'off' ) ) { + $unsupported_type = WooGraphQL::get_supported_product_type(); + return $type_registry->get_type( $unsupported_type ); + } + + throw new UserError( + sprintf( + /* translators: %s: Product type */ + __( 'The "%s" product type is not supported by the core WPGraphQL WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ), + $value->type + ) + ); + } } diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index 212177e1..8d6e12a9 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -71,6 +71,11 @@ public function init() { Type\WPInterface\Payment_Token::register_interface(); Type\WPInterface\Product_Union::register_interface(); Type\WPInterface\Cart_Item::register_interface(); + Type\WPInterface\Downloadable_Products::register_interface(); + Type\WPInterface\Inventoried_Products::register_interface(); + Type\WPInterface\Products_With_Dimensions::register_interface(); + Type\WPInterface\Products_With_Pricing::register_interface(); + Type\WPInterface\Products_With_Variations::register_interface(); /** * Objects. diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index fed600aa..fe62b041 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -245,6 +245,11 @@ private function includes() { require $include_directory_path . 'type/interface/class-payment-token.php'; require $include_directory_path . 'type/interface/class-product-union.php'; require $include_directory_path . 'type/interface/class-cart-item.php'; + require $include_directory_path . 'type/interface/class-downloadable-products.php'; + require $include_directory_path . 'type/interface/class-inventoried-products.php'; + require $include_directory_path . 'type/interface/class-products-with-dimensions.php'; + require $include_directory_path . 'type/interface/class-products-with-pricing.php'; + require $include_directory_path . 'type/interface/class-products-with-variations.php'; // Include object type class files. require $include_directory_path . 'type/object/class-cart-error-types.php'; diff --git a/includes/connection/class-products.php b/includes/connection/class-products.php index b3bcbeb7..a34dca14 100644 --- a/includes/connection/class-products.php +++ b/includes/connection/class-products.php @@ -146,26 +146,6 @@ static function () { ) ); - // From VariableProduct to ProductVariation. - register_graphql_connection( - self::get_connection_config( - [ - 'fromType' => 'VariableProduct', - 'toType' => 'ProductVariation', - 'fromFieldName' => 'variations', - 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { - $resolver = new Product_Connection_Resolver( $source, $args, $context, $info ); - - $resolver->set_query_arg( 'post_parent', $source->ID ); - $resolver->set_query_arg( 'post_type', 'product_variation' ); - $resolver->set_query_arg( 'post__in', $source->variation_ids ); - - return $resolver->get_connection(); - }, - ] - ) - ); - register_graphql_connection( [ 'fromType' => 'ProductVariation', diff --git a/includes/type/interface/class-downloadable-products.php b/includes/type/interface/class-downloadable-products.php new file mode 100644 index 00000000..0ef8ef24 --- /dev/null +++ b/includes/type/interface/class-downloadable-products.php @@ -0,0 +1,72 @@ + __( 'Downloadable products.', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => self::get_fields(), + 'resolveType' => [ Core::class, 'resolve_product_type' ], + ] + ); + } + + /** + * Defines "DownloadableProducts" fields. + * + * @return array + */ + public static function get_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), + ], + 'databaseId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), + ], + 'virtual' => [ + 'type' => 'Boolean', + 'description' => __( 'Is product virtual?', 'wp-graphql-woocommerce' ), + ], + 'downloadExpiry' => [ + 'type' => 'Int', + 'description' => __( 'Download expiry', 'wp-graphql-woocommerce' ), + ], + 'downloadable' => [ + 'type' => 'Boolean', + 'description' => __( 'Is downloadable?', 'wp-graphql-woocommerce' ), + ], + 'downloadLimit' => [ + 'type' => 'Int', + 'description' => __( 'Download limit', 'wp-graphql-woocommerce' ), + ], + 'downloads' => [ + 'type' => [ 'list_of' => 'ProductDownload' ], + 'description' => __( 'Product downloads', 'wp-graphql-woocommerce' ), + ], + ]; + } +} diff --git a/includes/type/interface/class-inventoried-products.php b/includes/type/interface/class-inventoried-products.php new file mode 100644 index 00000000..0c45281c --- /dev/null +++ b/includes/type/interface/class-inventoried-products.php @@ -0,0 +1,76 @@ + __( 'Products with stock information.', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => self::get_fields(), + 'resolveType' => [ Core::class, 'resolve_product_type' ], + ] + ); + } + + /** + * Defines "InventoriedProducts" fields. + * + * @return array + */ + public static function get_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), + ], + 'databaseId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), + ], + 'manageStock' => [ + 'type' => 'Boolean', + 'description' => __( 'If product manage stock', 'wp-graphql-woocommerce' ), + ], + 'stockQuantity' => [ + 'type' => 'Int', + 'description' => __( 'Number of items available for sale', 'wp-graphql-woocommerce' ), + ], + 'backorders' => [ + 'type' => 'BackordersEnum', + 'description' => __( 'Product backorders status', 'wp-graphql-woocommerce' ), + ], + 'soldIndividually' => [ + 'type' => 'Boolean', + 'description' => __( 'If should be sold individually', 'wp-graphql-woocommerce' ), + ], + 'backordersAllowed' => [ + 'type' => 'Boolean', + 'description' => __( 'Can product be backordered?', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => 'StockStatusEnum', + 'description' => __( 'Product stock status', 'wp-graphql-woocommerce' ), + ], + ]; + } +} diff --git a/includes/type/interface/class-product-union.php b/includes/type/interface/class-product-union.php index d36ae886..1fcdfd11 100644 --- a/includes/type/interface/class-product-union.php +++ b/includes/type/interface/class-product-union.php @@ -8,9 +8,7 @@ namespace WPGraphQL\WooCommerce\Type\WPInterface; -use GraphQL\Error\UserError; -use WPGraphQL; -use WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce as WooGraphQL; +use WPGraphQL\WooCommerce\Core_Schema_Filters as Core; /** * Class Product_Union @@ -29,27 +27,7 @@ public static function register_interface(): void { 'description' => __( 'Union between the product and product variation types', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], 'fields' => self::get_fields(), - 'resolveType' => static function ( $value ) { - $type_registry = WPGraphQL::get_type_registry(); - $possible_types = WooGraphQL::get_enabled_product_types(); - $product_type = $value->get_type(); - if ( isset( $possible_types[ $product_type ] ) ) { - return $type_registry->get_type( $possible_types[ $product_type ] ); - } elseif ( str_ends_with( $product_type, 'variation' ) ) { - return $type_registry->get_type( 'ProductVariation' ); - } elseif ( 'on' === woographql_setting( 'enable_unsupported_product_type', 'off' ) ) { - $unsupported_type = WooGraphQL::get_supported_product_type(); - return $type_registry->get_type( $unsupported_type ); - } - - throw new UserError( - sprintf( - /* translators: %s: Product type */ - __( 'The "%s" product type is not supported by the core WPGraphQL WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ), - $value->type - ) - ); - }, + 'resolveType' => [ Core::class, 'resolve_product_type' ], ] ); } diff --git a/includes/type/interface/class-products-with-dimensions.php b/includes/type/interface/class-products-with-dimensions.php new file mode 100644 index 00000000..42d063ee --- /dev/null +++ b/includes/type/interface/class-products-with-dimensions.php @@ -0,0 +1,80 @@ + __( 'Physical products.', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => self::get_fields(), + 'resolveType' => [ Core::class, 'resolve_product_type' ], + ] + ); + } + + /** + * Defines "ProductsWithDimensions" fields. + * + * @return array + */ + public static function get_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), + ], + 'databaseId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), + ], + 'weight' => [ + 'type' => 'String', + 'description' => __( 'Product\'s weight', 'wp-graphql-woocommerce' ), + ], + 'length' => [ + 'type' => 'String', + 'description' => __( 'Product\'s length', 'wp-graphql-woocommerce' ), + ], + 'width' => [ + 'type' => 'String', + 'description' => __( 'Product\'s width', 'wp-graphql-woocommerce' ), + ], + 'height' => [ + 'type' => 'String', + 'description' => __( 'Product\'s height', 'wp-graphql-woocommerce' ), + ], + 'shippingClassId' => [ + 'type' => 'Int', + 'description' => __( 'shipping class ID', 'wp-graphql-woocommerce' ), + ], + 'shippingRequired' => [ + 'type' => 'Boolean', + 'description' => __( 'Does product need to be shipped?', 'wp-graphql-woocommerce' ), + ], + 'shippingTaxable' => [ + 'type' => 'Boolean', + 'description' => __( 'Is product shipping taxable?', 'wp-graphql-woocommerce' ), + ], + ]; + } +} diff --git a/includes/type/interface/class-products-with-pricing.php b/includes/type/interface/class-products-with-pricing.php new file mode 100644 index 00000000..a44511ad --- /dev/null +++ b/includes/type/interface/class-products-with-pricing.php @@ -0,0 +1,116 @@ + __( 'Products with pricing.', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => self::get_fields(), + 'resolveType' => [ Core::class, 'resolve_product_type' ], + ] + ); + } + + /** + * Defines "ProductsWithPricing" fields. + * + * @return array + */ + public static function get_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), + ], + 'databaseId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), + ], + 'price' => [ + 'type' => 'String', + 'description' => __( 'Product\'s active price', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PricingFieldFormatEnum', + 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, $args ) { + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + // @codingStandardsIgnoreLine. + return $source->priceRaw; + } else { + return $source->price; + } + }, + ], + 'regularPrice' => [ + 'type' => 'String', + 'description' => __( 'Product\'s regular price', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PricingFieldFormatEnum', + 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, $args ) { + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + // @codingStandardsIgnoreLine. + return $source->regularPriceRaw; + } else { + // @codingStandardsIgnoreLine. + return $source->regularPrice; + } + }, + ], + 'salePrice' => [ + 'type' => 'String', + 'description' => __( 'Product\'s sale price', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PricingFieldFormatEnum', + 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, $args ) { + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + // @codingStandardsIgnoreLine. + return $source->salePriceRaw; + } else { + // @codingStandardsIgnoreLine. + return $source->salePrice; + } + }, + ], + 'taxStatus' => [ + 'type' => 'TaxStatusEnum', + 'description' => __( 'Tax status', 'wp-graphql-woocommerce' ), + ], + 'taxClass' => [ + 'type' => 'TaxClassEnum', + 'description' => __( 'Tax class', 'wp-graphql-woocommerce' ), + ], + ]; + } +} diff --git a/includes/type/interface/class-products-with-variations.php b/includes/type/interface/class-products-with-variations.php new file mode 100644 index 00000000..760332e8 --- /dev/null +++ b/includes/type/interface/class-products-with-variations.php @@ -0,0 +1,80 @@ + __( 'Products with variations.', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => self::get_fields(), + 'connections' => self::get_connections(), + 'resolveType' => [ Core::class, 'resolve_product_type' ], + ] + ); + } + + /** + * Defines "ProductsWithVariations" fields. + * + * @return array + */ + public static function get_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), + ], + 'databaseId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines "ProductsWithVariations" connections. + * + * @return array + */ + public static function get_connections() { + return [ + 'variations' => [ + 'toType' => 'ProductVariation', + 'connectionArgs' => Products::get_connection_args(), + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Product_Connection_Resolver( $source, $args, $context, $info ); + + $resolver->set_query_arg( 'post_parent', $source->ID ); + $resolver->set_query_arg( 'post_type', 'product_variation' ); + $resolver->set_query_arg( 'post__in', $source->variation_ids ); + + return $resolver->get_connection(); + }, + ], + ]; + } +} diff --git a/includes/type/object/class-product-types.php b/includes/type/object/class-product-types.php index 4e95783a..148e2d12 100644 --- a/includes/type/object/class-product-types.php +++ b/includes/type/object/class-product-types.php @@ -11,7 +11,6 @@ namespace WPGraphQL\WooCommerce\Type\WPObject; use WPGraphQL\WooCommerce\Model\Product as Model; -use WPGraphQL\WooCommerce\Type\WPInterface\Product; use WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce as WooGraphQL; @@ -53,202 +52,6 @@ public static function get_product_interfaces() { ]; } - /** - * Defines fields related to product inventory. - * - * @param array $fields Fields array for overwriting any of the inventory fields. - * - * @return array - */ - public static function get_inventory_fields( $fields = [] ) { - return array_merge( - [ - 'manageStock' => [ - 'type' => 'Boolean', - 'description' => __( 'If product manage stock', 'wp-graphql-woocommerce' ), - ], - 'stockQuantity' => [ - 'type' => 'Int', - 'description' => __( 'Number of items available for sale', 'wp-graphql-woocommerce' ), - ], - 'backorders' => [ - 'type' => 'BackordersEnum', - 'description' => __( 'Product backorders status', 'wp-graphql-woocommerce' ), - ], - 'soldIndividually' => [ - 'type' => 'Boolean', - 'description' => __( 'If should be sold individually', 'wp-graphql-woocommerce' ), - ], - 'backordersAllowed' => [ - 'type' => 'Boolean', - 'description' => __( 'Can product be backordered?', 'wp-graphql-woocommerce' ), - ], - 'stockStatus' => [ - 'type' => 'StockStatusEnum', - 'description' => __( 'Product stock status', 'wp-graphql-woocommerce' ), - ], - ], - $fields - ); - } - - /** - * Defines fields related to product shipping. - * - * @param array $fields Fields array for overwriting any of shipping fields. - * - * @return array - */ - public static function get_shipping_fields( $fields = [] ) { - return array_merge( - [ - 'weight' => [ - 'type' => 'String', - 'description' => __( 'Product\'s weight', 'wp-graphql-woocommerce' ), - ], - 'length' => [ - 'type' => 'String', - 'description' => __( 'Product\'s length', 'wp-graphql-woocommerce' ), - ], - 'width' => [ - 'type' => 'String', - 'description' => __( 'Product\'s width', 'wp-graphql-woocommerce' ), - ], - 'height' => [ - 'type' => 'String', - 'description' => __( 'Product\'s height', 'wp-graphql-woocommerce' ), - ], - 'shippingClassId' => [ - 'type' => 'Int', - 'description' => __( 'shipping class ID', 'wp-graphql-woocommerce' ), - ], - 'shippingRequired' => [ - 'type' => 'Boolean', - 'description' => __( 'Does product need to be shipped?', 'wp-graphql-woocommerce' ), - ], - 'shippingTaxable' => [ - 'type' => 'Boolean', - 'description' => __( 'Is product shipping taxable?', 'wp-graphql-woocommerce' ), - ], - ], - $fields - ); - } - - /** - * Defines fields related to pricing and taxes. - * - * @param array $fields Fields array for overwriting any of the pricing and tax fields. - * - * @return array - */ - public static function get_pricing_and_tax_fields( $fields = [] ) { - return array_merge( - [ - 'price' => [ - 'type' => 'String', - 'description' => __( 'Product\'s active price', 'wp-graphql-woocommerce' ), - 'args' => [ - 'format' => [ - 'type' => 'PricingFieldFormatEnum', - 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), - ], - ], - 'resolve' => static function ( $source, $args ) { - if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { - // @codingStandardsIgnoreLine. - return $source->priceRaw; - } else { - return $source->price; - } - }, - ], - 'regularPrice' => [ - 'type' => 'String', - 'description' => __( 'Product\'s regular price', 'wp-graphql-woocommerce' ), - 'args' => [ - 'format' => [ - 'type' => 'PricingFieldFormatEnum', - 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), - ], - ], - 'resolve' => static function ( $source, $args ) { - if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { - // @codingStandardsIgnoreLine. - return $source->regularPriceRaw; - } else { - // @codingStandardsIgnoreLine. - return $source->regularPrice; - } - }, - ], - 'salePrice' => [ - 'type' => 'String', - 'description' => __( 'Product\'s sale price', 'wp-graphql-woocommerce' ), - 'args' => [ - 'format' => [ - 'type' => 'PricingFieldFormatEnum', - 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), - ], - ], - 'resolve' => static function ( $source, $args ) { - if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { - // @codingStandardsIgnoreLine. - return $source->salePriceRaw; - } else { - // @codingStandardsIgnoreLine. - return $source->salePrice; - } - }, - ], - 'taxStatus' => [ - 'type' => 'TaxStatusEnum', - 'description' => __( 'Tax status', 'wp-graphql-woocommerce' ), - ], - 'taxClass' => [ - 'type' => 'TaxClassEnum', - 'description' => __( 'Tax class', 'wp-graphql-woocommerce' ), - ], - ], - $fields - ); - } - - /** - * Defines fields related to virtual product info. - * - * @param array $fields Fields array for overwriting any of the virtual data fields. - * - * @return array - */ - public static function get_virtual_data_fields( $fields = [] ) { - return array_merge( - [ - 'virtual' => [ - 'type' => 'Boolean', - 'description' => __( 'Is product virtual?', 'wp-graphql-woocommerce' ), - ], - 'downloadExpiry' => [ - 'type' => 'Int', - 'description' => __( 'Download expiry', 'wp-graphql-woocommerce' ), - ], - 'downloadable' => [ - 'type' => 'Boolean', - 'description' => __( 'Is downloadable?', 'wp-graphql-woocommerce' ), - ], - 'downloadLimit' => [ - 'type' => 'Int', - 'description' => __( 'Download limit', 'wp-graphql-woocommerce' ), - ], - 'downloads' => [ - 'type' => [ 'list_of' => 'ProductDownload' ], - 'description' => __( 'Product downloads', 'wp-graphql-woocommerce' ), - ], - ], - $fields - ); - } - /** * Register "SimpleProduct" type. * @@ -258,15 +61,18 @@ private static function register_simple_product_type() { register_graphql_object_type( 'SimpleProduct', [ - 'description' => __( 'A simple product object', 'wp-graphql-woocommerce' ), - 'interfaces' => self::get_product_interfaces(), - 'fields' => array_merge( - Product::get_fields(), - self::get_pricing_and_tax_fields(), - self::get_inventory_fields(), - self::get_shipping_fields(), - self::get_virtual_data_fields() + 'eagerlyLoadType' => true, + 'description' => __( 'A simple product object', 'wp-graphql-woocommerce' ), + 'interfaces' => array_merge( + self::get_product_interfaces(), + [ + 'ProductsWithPricing', + 'InventoriedProducts', + 'ProductsWithDimensions', + 'DownloadableProducts', + ] ), + 'fields' => [], ] ); } @@ -280,14 +86,18 @@ private static function register_variable_product_type() { register_graphql_object_type( 'VariableProduct', [ - 'description' => __( 'A variable product object', 'wp-graphql-woocommerce' ), - 'interfaces' => self::get_product_interfaces(), - 'fields' => array_merge( - Product::get_fields(), - self::get_pricing_and_tax_fields(), - self::get_inventory_fields(), - self::get_shipping_fields() + 'eagerlyLoadType' => true, + 'description' => __( 'A variable product object', 'wp-graphql-woocommerce' ), + 'interfaces' => array_merge( + self::get_product_interfaces(), + [ + 'ProductsWithPricing', + 'InventoriedProducts', + 'ProductsWithDimensions', + 'ProductsWithVariations', + ] ), + 'fields' => [], ] ); } @@ -301,11 +111,13 @@ private static function register_external_product_type() { register_graphql_object_type( 'ExternalProduct', [ - 'description' => __( 'A external product object', 'wp-graphql-woocommerce' ), - 'interfaces' => self::get_product_interfaces(), - 'fields' => array_merge( - Product::get_fields(), - self::get_pricing_and_tax_fields(), + 'eagerlyLoadType' => true, + 'description' => __( 'A external product object', 'wp-graphql-woocommerce' ), + 'interfaces' => array_merge( + self::get_product_interfaces(), + [ 'ProductsWithPricing' ] + ), + 'fields' => array_merge( [ 'externalUrl' => [ 'type' => 'String', @@ -330,59 +142,60 @@ private static function register_group_product_type() { register_graphql_object_type( 'GroupProduct', [ - 'description' => __( 'A group product object', 'wp-graphql-woocommerce' ), - 'interfaces' => self::get_product_interfaces(), - 'fields' => array_merge( - Product::get_fields(), - [ - 'addToCartText' => [ - 'type' => 'String', - 'description' => __( 'Product\'s add to cart button text description', 'wp-graphql-woocommerce' ), - ], - 'addToCartDescription' => [ - 'type' => 'String', - 'description' => __( 'Product\'s add to cart button text description', 'wp-graphql-woocommerce' ), - ], - 'price' => [ - 'type' => 'String', - 'description' => __( 'Products\' price range', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( Model $source ) { - $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); - $child_prices = []; - $children = array_filter( array_map( 'wc_get_product', $source->grouped_ids ) ); - $children = array_filter( $children, 'wc_products_array_filter_visible_grouped' ); - - foreach ( $children as $child ) { - if ( ! $child ) { - continue; - } - - if ( '' !== $child->get_price() ) { - $child_prices[] = 'incl' === $tax_display_mode ? wc_get_price_including_tax( $child ) : wc_get_price_excluding_tax( $child ); - } - } - - if ( ! empty( $child_prices ) ) { - $min_price = min( $child_prices ); - $max_price = max( $child_prices ); - } else { - $min_price = ''; - $max_price = ''; - } - - if ( empty( $min_price ) ) { - return null; + 'eagerlyLoadType' => true, + 'description' => __( 'A group product object', 'wp-graphql-woocommerce' ), + 'interfaces' => array_merge( + self::get_product_interfaces(), + [ 'ProductsWithPricing' ] + ), + 'fields' => [ + 'addToCartText' => [ + 'type' => 'String', + 'description' => __( 'Product\'s add to cart button text description', 'wp-graphql-woocommerce' ), + ], + 'addToCartDescription' => [ + 'type' => 'String', + 'description' => __( 'Product\'s add to cart button text description', 'wp-graphql-woocommerce' ), + ], + 'price' => [ + 'type' => 'String', + 'description' => __( 'Products\' price range', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( Model $source ) { + $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); + $child_prices = []; + $children = array_filter( array_map( 'wc_get_product', $source->grouped_ids ) ); + $children = array_filter( $children, 'wc_products_array_filter_visible_grouped' ); + + foreach ( $children as $child ) { + if ( ! $child ) { + continue; } - if ( $min_price !== $max_price ) { - return wc_graphql_price_range( $min_price, $max_price ); + if ( '' !== $child->get_price() ) { + $child_prices[] = 'incl' === $tax_display_mode ? wc_get_price_including_tax( $child ) : wc_get_price_excluding_tax( $child ); } - - return wc_graphql_price( $min_price ); - }, - ], - ] - ), + } + + if ( ! empty( $child_prices ) ) { + $min_price = min( $child_prices ); + $max_price = max( $child_prices ); + } else { + $min_price = ''; + $max_price = ''; + } + + if ( empty( $min_price ) ) { + return null; + } + + if ( $min_price !== $max_price ) { + return wc_graphql_price_range( $min_price, $max_price ); + } + + return wc_graphql_price( $min_price ); + }, + ], + ], ] ); } @@ -396,24 +209,26 @@ private static function register_unsupported_product_type() { register_graphql_object_type( WooGraphQL::get_supported_product_type(), [ - 'description' => __( 'A product object for a product type that is unsupported by the current API.', 'wp-graphql-woocommerce' ), - 'interfaces' => self::get_product_interfaces(), - 'fields' => array_merge( - Product::get_fields(), + 'eagerlyLoadType' => true, + 'description' => __( 'A product object for a product type that is unsupported by the current API.', 'wp-graphql-woocommerce' ), + 'interfaces' => array_merge( + self::get_product_interfaces(), [ - 'type' => [ - 'type' => 'ProductTypesEnum', - 'description' => __( 'Product type', 'wp-graphql-woocommerce' ), - 'resolve' => static function () { - return 'unsupported'; - }, - ], - ], - self::get_pricing_and_tax_fields(), - self::get_inventory_fields(), - self::get_shipping_fields(), - self::get_virtual_data_fields() + 'ProductsWithPricing', + 'InventoriedProducts', + 'ProductsWithDimensions', + 'DownloadableProducts', + ] ), + 'fields' => [ + 'type' => [ + 'type' => 'ProductTypesEnum', + 'description' => __( 'Product type', 'wp-graphql-woocommerce' ), + 'resolve' => static function () { + return 'unsupported'; + }, + ], + ], ] ); } diff --git a/tests/wpunit/ProductQueriesTest.php b/tests/wpunit/ProductQueriesTest.php index 8776a464..d68dda68 100644 --- a/tests/wpunit/ProductQueriesTest.php +++ b/tests/wpunit/ProductQueriesTest.php @@ -1357,4 +1357,104 @@ public function testProductGalleryImagesConnection() { ] ); } + + // tests + public function testProductQueryWithInterfaces() { + $product_id = $this->factory->product->createSimple(); + $product = wc_get_product( $product_id ); + + $query = ' + query ( $id: ID!, $format: PostObjectFieldFormatEnum ) { + product(id: $id) { + id + databaseId + name + slug + date + modified + status + featured + catalogVisibility + description(format: $format) + shortDescription(format: $format) + sku + dateOnSaleFrom + dateOnSaleTo + totalSales + averageRating + reviewCount + onSale + purchasable + link + reviewsAllowed + purchaseNote + menuOrder + ... on ProductsWithPricing { + price + regularPrice + salePrice + taxStatus + taxClass + } + ... on InventoriedProducts { + manageStock + stockQuantity + backorders + soldIndividually + backordersAllowed + stockStatus + } + ... on ProductsWithDimensions { + weight + length + width + height + shippingClassId + shippingRequired + shippingTaxable + } + ... on DownloadableProducts { + virtual + downloadExpiry + downloadable + downloadLimit + } + } + } + '; + + /** + * Assertion One + * + * Test querying product. + */ + $variables = [ 'id' => $this->toRelayId( 'product', $product_id ) ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = $this->getExpectedProductData( $product_id ); + + $this->assertQuerySuccessful( $response, $expected ); + + // Clear cache + $this->clearLoaderCache( 'wc_post' ); + + /** + * Assertion Two + * + * Test querying product with unformatted content (edit-product cap required). + */ + $this->loginAsShopManager(); + $variables = [ + 'id' => $this->toRelayId( 'product', $product_id ), + 'format' => 'RAW', + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( 'product.description', $product->get_description() ), + $this->expectedField( 'product.shortDescription', $product->get_short_description() ), + $this->expectedField( 'product.totalSales', $product->get_total_sales() ), + $this->expectedField( 'product.catalogVisibility', strtoupper( $product->get_catalog_visibility() ) ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } }