From 996ddd92f1b95fb249c31cce72b38581948868f7 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Fri, 15 Sep 2023 20:33:32 -0400 Subject: [PATCH 1/2] feat: ProductWithAttributes and ProductVariation interfaces added --- includes/class-core-schema-filters.php | 28 +- includes/class-type-registry.php | 14 +- includes/class-wp-graphql-woocommerce.php | 16 +- includes/connection/class-products.php | 25 +- includes/model/class-product.php | 2 +- ...cts.php => class-downloadable-product.php} | 14 +- ...ucts.php => class-inventoried-product.php} | 16 +- .../type/interface/class-product-union.php | 65 +++- .../interface/class-product-variation.php | 289 ++++++++++++++++++ .../class-product-with-attributes.php | 75 +++++ ....php => class-product-with-dimensions.php} | 14 +- ...ing.php => class-product-with-pricing.php} | 12 +- ....php => class-product-with-variations.php} | 12 +- includes/type/object/class-product-types.php | 71 +++-- tests/wpunit/ProductQueriesTest.php | 15 +- 15 files changed, 568 insertions(+), 100 deletions(-) rename includes/type/interface/{class-downloadable-products.php => class-downloadable-product.php} (84%) rename includes/type/interface/{class-inventoried-products.php => class-inventoried-product.php} (83%) create mode 100644 includes/type/interface/class-product-variation.php create mode 100644 includes/type/interface/class-product-with-attributes.php rename includes/type/interface/{class-products-with-dimensions.php => class-product-with-dimensions.php} (86%) rename includes/type/interface/{class-products-with-pricing.php => class-product-with-pricing.php} (93%) rename includes/type/interface/{class-products-with-variations.php => class-product-with-variations.php} (87%) diff --git a/includes/class-core-schema-filters.php b/includes/class-core-schema-filters.php index c8bf861a..4e621ece 100644 --- a/includes/class-core-schema-filters.php +++ b/includes/class-core-schema-filters.php @@ -401,7 +401,7 @@ public static function resolve_product_type( $value ) { 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' ); + return self::resolve_product_variation_type( $value ); } elseif ( 'on' === woographql_setting( 'enable_unsupported_product_type', 'off' ) ) { $unsupported_type = WooGraphQL::get_supported_product_type(); return $type_registry->get_type( $unsupported_type ); @@ -415,4 +415,30 @@ public static function resolve_product_type( $value ) { ) ); } + + /** + * Resolves GraphQL type for provided product variation model. + * + * @param \WPGraphQL\WooCommerce\Model\Product $value Product model. + * + * @throws \GraphQL\Error\UserError Invalid product type requested. + * + * @return mixed + */ + public static function resolve_product_variation_type( $value ) { + $type_registry = \WPGraphQL::get_type_registry(); + $possible_types = WooGraphQL::get_enabled_product_variation_types(); + $product_type = $value->get_type(); + if ( isset( $possible_types[ $product_type ] ) ) { + return $type_registry->get_type( $possible_types[ $product_type ] ); + } + + throw new UserError( + sprintf( + /* translators: %s: Product type */ + __( 'The "%s" product variation 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 8d6e12a9..2517b768 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -65,17 +65,19 @@ public function init() { * Interfaces. */ Type\WPInterface\Product::register_interface(); + Type\WPInterface\Product_Variation::register_interface(); Type\WPInterface\Attribute::register_interface(); Type\WPInterface\Product_Attribute::register_interface(); Type\WPInterface\Cart_Error::register_interface(); 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(); + Type\WPInterface\Downloadable_Product::register_interface(); + Type\WPInterface\Inventoried_Product::register_interface(); + Type\WPInterface\Product_With_Dimensions::register_interface(); + Type\WPInterface\Product_With_Pricing::register_interface(); + Type\WPInterface\Product_With_Variations::register_interface(); + Type\WPInterface\Product_With_Attributes::register_interface(); /** * Objects. @@ -85,7 +87,6 @@ public function init() { Type\WPObject\Coupon_Type::register(); Type\WPObject\Product_Types::register(); Type\WPObject\Product_Attribute_Types::register(); - Type\WPObject\Product_Variation_Type::register(); Type\WPObject\Order_Item_Type::register(); Type\WPObject\Order_Type::register(); Type\WPObject\Refund_Type::register(); @@ -132,7 +133,6 @@ public function init() { Connection\Products::register_connections(); Connection\Orders::register_connections(); Connection\Product_Attributes::register_connections(); - Connection\Variation_Attributes::register_connections(); Connection\Customers::register_connections(); Connection\Tax_Rates::register_connections(); Connection\Shipping_Methods::register_connections(); diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index fe62b041..fc87e1ca 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -88,7 +88,7 @@ public static function get_enabled_product_types() { * @return array */ public static function get_enabled_product_variation_types() { - return apply_filters( 'graphql_woocommerce_product_variation_types', [ 'variation' => 'ProductVariation' ] ); + return apply_filters( 'graphql_woocommerce_product_variation_types', [ 'variation' => 'SimpleProductVariation' ] ); } /** @@ -242,14 +242,16 @@ private function includes() { require $include_directory_path . 'type/interface/class-cart-error.php'; require $include_directory_path . 'type/interface/class-product-attribute.php'; require $include_directory_path . 'type/interface/class-product.php'; + require $include_directory_path . 'type/interface/class-product-variation.php'; 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'; + require $include_directory_path . 'type/interface/class-downloadable-product.php'; + require $include_directory_path . 'type/interface/class-inventoried-product.php'; + require $include_directory_path . 'type/interface/class-product-with-dimensions.php'; + require $include_directory_path . 'type/interface/class-product-with-pricing.php'; + require $include_directory_path . 'type/interface/class-product-with-variations.php'; + require $include_directory_path . 'type/interface/class-product-with-attributes.php'; // Include object type class files. require $include_directory_path . 'type/object/class-cart-error-types.php'; @@ -266,7 +268,6 @@ private function includes() { require $include_directory_path . 'type/object/class-product-category-type.php'; require $include_directory_path . 'type/object/class-product-download-type.php'; require $include_directory_path . 'type/object/class-product-types.php'; - require $include_directory_path . 'type/object/class-product-variation-type.php'; require $include_directory_path . 'type/object/class-refund-type.php'; require $include_directory_path . 'type/object/class-root-query.php'; require $include_directory_path . 'type/object/class-shipping-method-type.php'; @@ -334,7 +335,6 @@ private function includes() { require $include_directory_path . 'connection/class-products.php'; require $include_directory_path . 'connection/class-shipping-methods.php'; require $include_directory_path . 'connection/class-tax-rates.php'; - require $include_directory_path . 'connection/class-variation-attributes.php'; require $include_directory_path . 'connection/class-wc-terms.php'; // Include admin files. diff --git a/includes/connection/class-products.php b/includes/connection/class-products.php index a34dca14..ea53dbc6 100644 --- a/includes/connection/class-products.php +++ b/includes/connection/class-products.php @@ -258,11 +258,12 @@ public static function set_connection_config( $config ) { public static function get_connection_config( $args = [] ): array { return array_merge( [ - 'fromType' => 'RootQuery', - 'toType' => 'ProductUnion', - 'fromFieldName' => 'products', - 'connectionArgs' => self::get_connection_args(), - 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + 'fromType' => 'RootQuery', + 'toType' => 'ProductUnion', + 'fromFieldName' => 'products', + 'connectionArgs' => self::get_connection_args(), + 'connectionFields' => self::get_connection_fields(), + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { $resolver = new Product_Connection_Resolver( $source, $args, $context, $info ); return $resolver->get_connection(); @@ -272,6 +273,20 @@ public static function get_connection_config( $args = [] ): array { ); } + /** + * Returns array of edge fields. + * + * @return array + */ + public static function get_connection_fields(): array { + return [ + 'found' => [ + 'type' => 'Number', + 'description' => __( 'Total products founds', 'wp-graphql-woocommerce' ), + ], + ]; + } + /** * Returns array of where args. * diff --git a/includes/model/class-product.php b/includes/model/class-product.php index 1ce241b0..94d17f2a 100644 --- a/includes/model/class-product.php +++ b/includes/model/class-product.php @@ -382,7 +382,7 @@ protected function init() { ) { $fields += [ 'manageStock' => function () { - return $this->wc_data->get_manage_stock(); + return ! empty( $this->wc_data->get_manage_stock() ) ? $this->wc_data->get_manage_stock() : null; }, 'stockQuantity' => function () { return ! empty( $this->wc_data->get_stock_quantity() ) ? $this->wc_data->get_stock_quantity() : null; diff --git a/includes/type/interface/class-downloadable-products.php b/includes/type/interface/class-downloadable-product.php similarity index 84% rename from includes/type/interface/class-downloadable-products.php rename to includes/type/interface/class-downloadable-product.php index 0ef8ef24..53d50be8 100644 --- a/includes/type/interface/class-downloadable-products.php +++ b/includes/type/interface/class-downloadable-product.php @@ -1,6 +1,6 @@ __( 'Downloadable products.', 'wp-graphql-woocommerce' ), + 'description' => __( 'A downloadable product.', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], 'fields' => self::get_fields(), 'resolveType' => [ Core::class, 'resolve_product_type' ], @@ -33,7 +33,7 @@ public static function register_interface(): void { } /** - * Defines "DownloadableProducts" fields. + * Defines fields of "DownloadableProduct". * * @return array */ diff --git a/includes/type/interface/class-inventoried-products.php b/includes/type/interface/class-inventoried-product.php similarity index 83% rename from includes/type/interface/class-inventoried-products.php rename to includes/type/interface/class-inventoried-product.php index 0c45281c..9e6daddf 100644 --- a/includes/type/interface/class-inventoried-products.php +++ b/includes/type/interface/class-inventoried-product.php @@ -1,6 +1,6 @@ __( 'Products with stock information.', 'wp-graphql-woocommerce' ), + 'description' => __( 'A product with stock information.', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], 'fields' => self::get_fields(), 'resolveType' => [ Core::class, 'resolve_product_type' ], @@ -33,7 +33,7 @@ public static function register_interface(): void { } /** - * Defines "InventoriedProducts" fields. + * Defines fields of "InventoriedProduct". * * @return array */ @@ -48,7 +48,7 @@ public static function get_fields() { 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), ], 'manageStock' => [ - 'type' => 'Boolean', + 'type' => 'ManageStockEnum', 'description' => __( 'If product manage stock', 'wp-graphql-woocommerce' ), ], 'stockQuantity' => [ diff --git a/includes/type/interface/class-product-union.php b/includes/type/interface/class-product-union.php index 1fcdfd11..c8bd1bc3 100644 --- a/includes/type/interface/class-product-union.php +++ b/includes/type/interface/class-product-union.php @@ -8,6 +8,7 @@ namespace WPGraphQL\WooCommerce\Type\WPInterface; +use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Core_Schema_Filters as Core; /** @@ -40,14 +41,74 @@ public static function register_interface(): void { public static function get_fields() { return array_merge( [ - 'id' => [ + 'id' => [ 'type' => [ 'non_null' => 'ID' ], 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), ], - 'databaseId' => [ + 'databaseId' => [ 'type' => [ 'non_null' => 'Int' ], 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Product slug', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ProductTypesEnum', + 'description' => __( 'Product type', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Product name', 'wp-graphql-woocommerce' ), + ], + 'featured' => [ + 'type' => 'Boolean', + 'description' => __( 'If the product is featured', 'wp-graphql-woocommerce' ), + ], + 'catalogVisibility' => [ + 'type' => 'CatalogVisibilityEnum', + 'description' => __( 'Catalog visibility', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Product SKU', 'wp-graphql-woocommerce' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Product description', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PostObjectFieldFormatEnum', + 'description' => __( 'Format of the field output', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, $args ) { + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + // @codingStandardsIgnoreLine. + return $source->descriptionRaw; + } + return $source->description; + }, + ], + 'image' => [ + 'type' => 'MediaItem', + 'description' => __( 'Main image', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, AppContext $context ) { + // @codingStandardsIgnoreLine. + if ( empty( $source->image_id ) || ! absint( $source->image_id ) ) { + return null; + } + return $context->get_loader( 'post' )->load_deferred( $source->image_id ); + }, + ], + 'onSale' => [ + 'type' => 'Boolean', + 'description' => __( 'Is product on sale?', 'wp-graphql-woocommerce' ), + ], + 'purchasable' => [ + 'type' => 'Boolean', + 'description' => __( 'Can product be purchased?', 'wp-graphql-woocommerce' ), + ], ], Product::get_fields() ); diff --git a/includes/type/interface/class-product-variation.php b/includes/type/interface/class-product-variation.php new file mode 100644 index 00000000..5101556d --- /dev/null +++ b/includes/type/interface/class-product-variation.php @@ -0,0 +1,289 @@ + __( 'A product variation.', 'wp-graphql-woocommerce' ), + 'interfaces' => [ + 'Node', + 'NodeWithFeaturedImage', + 'ContentNode', + 'UniformResourceIdentifiable', + 'ProductUnion', + 'ProductWithPricing', + 'ProductWithDimensions', + 'InventoriedProduct', + 'DownloadableProduct', + ], + 'fields' => self::get_fields(), + 'connections' => self::get_connections(), + 'resolveType' => [ Core::class, 'resolve_product_variation_type' ], + ] + ); + + register_graphql_object_type( + 'SimpleProductVariation', + [ + 'eagerlyLoadType' => true, + 'description' => __( 'A product variation', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node', 'ProductVariation' ], + 'fields' => [], + ] + ); + } + + /** + * Defines fields of "ProductVariation". + * + * @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' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Product name', 'wp-graphql-woocommerce' ), + ], + 'date' => [ + 'type' => 'String', + 'description' => __( 'Date variation created', 'wp-graphql-woocommerce' ), + ], + 'modified' => [ + 'type' => 'String', + 'description' => __( 'Date variation last updated', 'wp-graphql-woocommerce' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Product description', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Product variation SKU (Stock-keeping unit)', 'wp-graphql-woocommerce' ), + ], + 'price' => [ + 'type' => 'String', + 'description' => __( 'Product variation\'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 variation\'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 variation\'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; + } + }, + ], + 'dateOnSaleFrom' => [ + 'type' => 'String', + 'description' => __( 'Date on sale from', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleTo' => [ + 'type' => 'String', + 'description' => __( 'Date on sale to', 'wp-graphql-woocommerce' ), + ], + 'onSale' => [ + 'type' => 'Boolean', + 'description' => __( 'Is variation on sale?', 'wp-graphql-woocommerce' ), + ], + 'status' => [ + 'type' => 'String', + 'description' => __( 'Variation status', 'wp-graphql-woocommerce' ), + ], + 'purchasable' => [ + 'type' => 'Boolean', + 'description' => __( 'If product variation can be bought', 'wp-graphql-woocommerce' ), + ], + 'virtual' => [ + 'type' => 'Boolean', + 'description' => __( 'Is product virtual?', 'wp-graphql-woocommerce' ), + ], + 'downloadable' => [ + 'type' => 'Boolean', + 'description' => __( 'Is downloadable?', 'wp-graphql-woocommerce' ), + ], + 'downloads' => [ + 'type' => [ 'list_of' => 'ProductDownload' ], + 'description' => __( 'Product downloads', 'wp-graphql-woocommerce' ), + ], + 'downloadLimit' => [ + 'type' => 'Int', + 'description' => __( 'Download limit', 'wp-graphql-woocommerce' ), + ], + 'downloadExpiry' => [ + 'type' => 'Int', + 'description' => __( 'Download expiry', 'wp-graphql-woocommerce' ), + ], + 'taxStatus' => [ + 'type' => 'TaxStatusEnum', + 'description' => __( 'Tax status', 'wp-graphql-woocommerce' ), + ], + 'taxClass' => [ + 'type' => 'TaxClassEnum', + 'description' => __( 'Product variation tax class', 'wp-graphql-woocommerce' ), + ], + 'manageStock' => [ + 'type' => 'ManageStockEnum', + 'description' => __( 'if/how product variation stock is managed', 'wp-graphql-woocommerce' ), + ], + 'stockQuantity' => [ + 'type' => 'Int', + 'description' => __( 'Product variation stock quantity', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => 'StockStatusEnum', + 'description' => __( 'Product stock status', 'wp-graphql-woocommerce' ), + ], + 'backorders' => [ + 'type' => 'BackordersEnum', + 'description' => __( 'Product variation backorders', 'wp-graphql-woocommerce' ), + ], + 'backordersAllowed' => [ + 'type' => 'Boolean', + 'description' => __( 'Can product be backordered?', 'wp-graphql-woocommerce' ), + ], + 'weight' => [ + 'type' => 'String', + 'description' => __( 'Product variation weight', 'wp-graphql-woocommerce' ), + ], + 'length' => [ + 'type' => 'String', + 'description' => __( 'Product variation length', 'wp-graphql-woocommerce' ), + ], + 'width' => [ + 'type' => 'String', + 'description' => __( 'Product variation width', 'wp-graphql-woocommerce' ), + ], + 'height' => [ + 'type' => 'String', + 'description' => __( 'Product variation height', 'wp-graphql-woocommerce' ), + ], + 'menuOrder' => [ + 'type' => 'Int', + 'description' => __( 'Menu order', 'wp-graphql-woocommerce' ), + ], + 'purchaseNote' => [ + 'type' => 'String', + 'description' => __( 'Product variation purchase_note', 'wp-graphql-woocommerce' ), + ], + 'shippingClass' => [ + 'type' => 'String', + 'description' => __( 'Product variation shipping class', 'wp-graphql-woocommerce' ), + ], + 'catalogVisibility' => [ + 'type' => 'CatalogVisibilityEnum', + 'description' => __( 'Product variation catalog visibility', 'wp-graphql-woocommerce' ), + ], + 'hasAttributes' => [ + 'type' => 'Boolean', + 'description' => __( 'Does product variation have any visible attributes', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ProductTypesEnum', + 'description' => __( 'Product type', 'wp-graphql-woocommerce' ), + ], + 'image' => [ + 'type' => 'MediaItem', + 'description' => __( 'Product variation main image', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, AppContext $context ) { + return ! empty( $source->image_id ) + ? $context->get_loader( 'post' )->load_deferred( $source->image_id ) + : null; + }, + ], + 'metaData' => Meta_Data_Type::get_metadata_field_definition(), + ]; + } + + /** + * Defines connections of "ProductVariation". + * + * @return array + */ + public static function get_connections() { + return [ + 'attributes' => [ + 'toType' => 'VariationAttribute', + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Variation_Attribute_Connection_Resolver(); + + return $resolver->resolve( $source, $args, $context, $info ); + }, + ], + ]; + } +} diff --git a/includes/type/interface/class-product-with-attributes.php b/includes/type/interface/class-product-with-attributes.php new file mode 100644 index 00000000..79e678fe --- /dev/null +++ b/includes/type/interface/class-product-with-attributes.php @@ -0,0 +1,75 @@ + __( 'Products with default attributes.', '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 [ + 'defaultAttributes' => [ + 'toType' => 'VariationAttribute', + 'fromFieldName' => 'defaultAttributes', + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Variation_Attribute_Connection_Resolver(); + + return $resolver->resolve( $source, $args, $context, $info ); + }, + ], + ]; + } +} diff --git a/includes/type/interface/class-products-with-dimensions.php b/includes/type/interface/class-product-with-dimensions.php similarity index 86% rename from includes/type/interface/class-products-with-dimensions.php rename to includes/type/interface/class-product-with-dimensions.php index 42d063ee..15b11a48 100644 --- a/includes/type/interface/class-products-with-dimensions.php +++ b/includes/type/interface/class-product-with-dimensions.php @@ -1,6 +1,6 @@ __( 'Physical products.', 'wp-graphql-woocommerce' ), + 'description' => __( 'A physical product.', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], 'fields' => self::get_fields(), 'resolveType' => [ Core::class, 'resolve_product_type' ], @@ -33,7 +33,7 @@ public static function register_interface(): void { } /** - * Defines "ProductsWithDimensions" fields. + * Defines fields of "ProductWithDimensions". * * @return array */ diff --git a/includes/type/interface/class-products-with-pricing.php b/includes/type/interface/class-product-with-pricing.php similarity index 93% rename from includes/type/interface/class-products-with-pricing.php rename to includes/type/interface/class-product-with-pricing.php index a44511ad..3b089490 100644 --- a/includes/type/interface/class-products-with-pricing.php +++ b/includes/type/interface/class-product-with-pricing.php @@ -1,6 +1,6 @@ __( 'Products with pricing.', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], @@ -33,7 +33,7 @@ public static function register_interface(): void { } /** - * Defines "ProductsWithPricing" fields. + * Defines fields of "ProductWithPricing". * * @return array */ diff --git a/includes/type/interface/class-products-with-variations.php b/includes/type/interface/class-product-with-variations.php similarity index 87% rename from includes/type/interface/class-products-with-variations.php rename to includes/type/interface/class-product-with-variations.php index 760332e8..65f86f34 100644 --- a/includes/type/interface/class-products-with-variations.php +++ b/includes/type/interface/class-product-with-variations.php @@ -1,6 +1,6 @@ __( 'Products with variations.', 'wp-graphql-woocommerce' ), + 'description' => __( 'A product with variations.', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], 'fields' => self::get_fields(), 'connections' => self::get_connections(), diff --git a/includes/type/object/class-product-types.php b/includes/type/object/class-product-types.php index 148e2d12..6acb938d 100644 --- a/includes/type/object/class-product-types.php +++ b/includes/type/object/class-product-types.php @@ -37,19 +37,25 @@ public static function register() { /** * Returns the GraphQL interfaces for product types. * + * @param array $other_interfaces Other interfaces to merge with the product interfaces. + * * @return array */ - public static function get_product_interfaces() { - return [ - 'Node', - 'Product', - 'ProductUnion', - 'NodeWithComments', - 'NodeWithContentEditor', - 'NodeWithFeaturedImage', - 'ContentNode', - 'UniformResourceIdentifiable', - ]; + public static function get_product_interfaces( $other_interfaces = [] ) { + return array_merge( + [ + 'Node', + 'Product', + 'ProductUnion', + 'ProductWithAttributes', + 'NodeWithComments', + 'NodeWithContentEditor', + 'NodeWithFeaturedImage', + 'ContentNode', + 'UniformResourceIdentifiable', + ], + $other_interfaces + ); } /** @@ -63,13 +69,12 @@ private static function register_simple_product_type() { [ 'eagerlyLoadType' => true, 'description' => __( 'A simple product object', 'wp-graphql-woocommerce' ), - 'interfaces' => array_merge( - self::get_product_interfaces(), + 'interfaces' => self::get_product_interfaces( [ - 'ProductsWithPricing', - 'InventoriedProducts', - 'ProductsWithDimensions', - 'DownloadableProducts', + 'DownloadableProduct', + 'InventoriedProduct', + 'ProductWithDimensions', + 'ProductWithPricing', ] ), 'fields' => [], @@ -88,13 +93,12 @@ private static function register_variable_product_type() { [ 'eagerlyLoadType' => true, 'description' => __( 'A variable product object', 'wp-graphql-woocommerce' ), - 'interfaces' => array_merge( - self::get_product_interfaces(), + 'interfaces' => self::get_product_interfaces( [ - 'ProductsWithPricing', - 'InventoriedProducts', - 'ProductsWithDimensions', - 'ProductsWithVariations', + 'InventoriedProduct', + 'ProductWithDimensions', + 'ProductWithPricing', + 'ProductWithVariations', ] ), 'fields' => [], @@ -113,10 +117,7 @@ private static function register_external_product_type() { [ 'eagerlyLoadType' => true, 'description' => __( 'A external product object', 'wp-graphql-woocommerce' ), - 'interfaces' => array_merge( - self::get_product_interfaces(), - [ 'ProductsWithPricing' ] - ), + 'interfaces' => self::get_product_interfaces( [ 'ProductWithPricing' ] ), 'fields' => array_merge( [ 'externalUrl' => [ @@ -144,10 +145,7 @@ private static function register_group_product_type() { [ 'eagerlyLoadType' => true, 'description' => __( 'A group product object', 'wp-graphql-woocommerce' ), - 'interfaces' => array_merge( - self::get_product_interfaces(), - [ 'ProductsWithPricing' ] - ), + 'interfaces' => self::get_product_interfaces( [ 'ProductWithPricing' ] ), 'fields' => [ 'addToCartText' => [ 'type' => 'String', @@ -211,13 +209,12 @@ private static function register_unsupported_product_type() { [ '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(), + 'interfaces' => self::get_product_interfaces( [ - 'ProductsWithPricing', - 'InventoriedProducts', - 'ProductsWithDimensions', - 'DownloadableProducts', + 'DownloadableProduct', + 'InventoriedProduct', + 'ProductWithDimensions', + 'ProductWithPricing', ] ), 'fields' => [ diff --git a/tests/wpunit/ProductQueriesTest.php b/tests/wpunit/ProductQueriesTest.php index d68dda68..4f9b64ae 100644 --- a/tests/wpunit/ProductQueriesTest.php +++ b/tests/wpunit/ProductQueriesTest.php @@ -75,7 +75,12 @@ public function getExpectedProductData( $product_id ) { 'product.taxClass', $this->maybe( $product->get_tax_class(), 'STANDARD' ) ), - $this->expectedField( 'product.manageStock', $product->get_manage_stock() ), + $this->expectedField( + 'product.manageStock', + ! empty( $product->get_manage_stock() ) + ? \WPGraphQL\Type\WPEnumType::get_safe_name( $product->get_manage_stock() ) + : self::IS_NULL + ), $this->expectedField( 'product.stockQuantity', $this->maybe( $product->get_stock_quantity(), self::IS_NULL ) @@ -1389,14 +1394,14 @@ public function testProductQueryWithInterfaces() { reviewsAllowed purchaseNote menuOrder - ... on ProductsWithPricing { + ... on ProductWithPricing { price regularPrice salePrice taxStatus taxClass } - ... on InventoriedProducts { + ... on InventoriedProduct { manageStock stockQuantity backorders @@ -1404,7 +1409,7 @@ public function testProductQueryWithInterfaces() { backordersAllowed stockStatus } - ... on ProductsWithDimensions { + ... on ProductWithDimensions { weight length width @@ -1413,7 +1418,7 @@ public function testProductQueryWithInterfaces() { shippingRequired shippingTaxable } - ... on DownloadableProducts { + ... on DownloadableProduct { virtual downloadExpiry downloadable From 03796eacbf80b5e5ff2251c796db9d6933a64f4f Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Fri, 15 Sep 2023 20:52:26 -0400 Subject: [PATCH 2/2] chore: Unused files removed --- .../connection/class-variation-attributes.php | 68 ----- .../object/class-product-variation-type.php | 244 ------------------ 2 files changed, 312 deletions(-) delete mode 100644 includes/connection/class-variation-attributes.php delete mode 100644 includes/type/object/class-product-variation-type.php diff --git a/includes/connection/class-variation-attributes.php b/includes/connection/class-variation-attributes.php deleted file mode 100644 index f30f69da..00000000 --- a/includes/connection/class-variation-attributes.php +++ /dev/null @@ -1,68 +0,0 @@ - $product_type, - 'fromFieldName' => 'defaultAttributes', - ] - ) - ); - } - } - - /** - * Given an array of $args, this returns the connection config, merging the provided args - * with the defaults. - * - * @param array $args - Connection configuration. - * @return array - */ - public static function get_connection_config( $args = [] ): array { - return array_merge( - [ - 'fromType' => 'ProductVariation', - 'toType' => 'VariationAttribute', - 'fromFieldName' => 'attributes', - 'connectionArgs' => [], - 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { - $resolver = new Variation_Attribute_Connection_Resolver(); - - return $resolver->resolve( $source, $args, $context, $info ); - }, - ], - $args - ); - } -} diff --git a/includes/type/object/class-product-variation-type.php b/includes/type/object/class-product-variation-type.php deleted file mode 100644 index c8fb846a..00000000 --- a/includes/type/object/class-product-variation-type.php +++ /dev/null @@ -1,244 +0,0 @@ - __( 'A product variation object', 'wp-graphql-woocommerce' ), - 'interfaces' => [ - 'Node', - 'NodeWithFeaturedImage', - 'ContentNode', - 'UniformResourceIdentifiable', - 'ProductUnion', - ], - 'fields' => [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'The globally unique identifier for the product variation', 'wp-graphql-woocommerce' ), - ], - 'databaseId' => [ - 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'The ID of the refund in the database', 'wp-graphql-woocommerce' ), - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'Product name', 'wp-graphql-woocommerce' ), - ], - 'date' => [ - 'type' => 'String', - 'description' => __( 'Date variation created', 'wp-graphql-woocommerce' ), - ], - 'modified' => [ - 'type' => 'String', - 'description' => __( 'Date variation last updated', 'wp-graphql-woocommerce' ), - ], - 'description' => [ - 'type' => 'String', - 'description' => __( 'Product description', 'wp-graphql-woocommerce' ), - ], - 'sku' => [ - 'type' => 'String', - 'description' => __( 'Product variation SKU (Stock-keeping unit)', 'wp-graphql-woocommerce' ), - ], - 'price' => [ - 'type' => 'String', - 'description' => __( 'Product variation\'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 variation\'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 variation\'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; - } - }, - ], - 'dateOnSaleFrom' => [ - 'type' => 'String', - 'description' => __( 'Date on sale from', 'wp-graphql-woocommerce' ), - ], - 'dateOnSaleTo' => [ - 'type' => 'String', - 'description' => __( 'Date on sale to', 'wp-graphql-woocommerce' ), - ], - 'onSale' => [ - 'type' => 'Boolean', - 'description' => __( 'Is variation on sale?', 'wp-graphql-woocommerce' ), - ], - 'status' => [ - 'type' => 'String', - 'description' => __( 'Variation status', 'wp-graphql-woocommerce' ), - ], - 'purchasable' => [ - 'type' => 'Boolean', - 'description' => __( 'If product variation can be bought', 'wp-graphql-woocommerce' ), - ], - 'virtual' => [ - 'type' => 'Boolean', - 'description' => __( 'Is product virtual?', 'wp-graphql-woocommerce' ), - ], - 'downloadable' => [ - 'type' => 'Boolean', - 'description' => __( 'Is downloadable?', 'wp-graphql-woocommerce' ), - ], - 'downloads' => [ - 'type' => [ 'list_of' => 'ProductDownload' ], - 'description' => __( 'Product downloads', 'wp-graphql-woocommerce' ), - ], - 'downloadLimit' => [ - 'type' => 'Int', - 'description' => __( 'Download limit', 'wp-graphql-woocommerce' ), - ], - 'downloadExpiry' => [ - 'type' => 'Int', - 'description' => __( 'Download expiry', 'wp-graphql-woocommerce' ), - ], - 'taxStatus' => [ - 'type' => 'TaxStatusEnum', - 'description' => __( 'Tax status', 'wp-graphql-woocommerce' ), - ], - 'taxClass' => [ - 'type' => 'TaxClassEnum', - 'description' => __( 'Product variation tax class', 'wp-graphql-woocommerce' ), - ], - 'manageStock' => [ - 'type' => 'ManageStockEnum', - 'description' => __( 'if/how product variation stock is managed', 'wp-graphql-woocommerce' ), - ], - 'stockQuantity' => [ - 'type' => 'Int', - 'description' => __( 'Product variation stock quantity', 'wp-graphql-woocommerce' ), - ], - 'stockStatus' => [ - 'type' => 'StockStatusEnum', - 'description' => __( 'Product stock status', 'wp-graphql-woocommerce' ), - ], - 'backorders' => [ - 'type' => 'BackordersEnum', - 'description' => __( 'Product variation backorders', 'wp-graphql-woocommerce' ), - ], - 'backordersAllowed' => [ - 'type' => 'Boolean', - 'description' => __( 'Can product be backordered?', 'wp-graphql-woocommerce' ), - ], - 'weight' => [ - 'type' => 'String', - 'description' => __( 'Product variation weight', 'wp-graphql-woocommerce' ), - ], - 'length' => [ - 'type' => 'String', - 'description' => __( 'Product variation length', 'wp-graphql-woocommerce' ), - ], - 'width' => [ - 'type' => 'String', - 'description' => __( 'Product variation width', 'wp-graphql-woocommerce' ), - ], - 'height' => [ - 'type' => 'String', - 'description' => __( 'Product variation height', 'wp-graphql-woocommerce' ), - ], - 'menuOrder' => [ - 'type' => 'Int', - 'description' => __( 'Menu order', 'wp-graphql-woocommerce' ), - ], - 'purchaseNote' => [ - 'type' => 'String', - 'description' => __( 'Product variation purchase_note', 'wp-graphql-woocommerce' ), - ], - 'shippingClass' => [ - 'type' => 'String', - 'description' => __( 'Product variation shipping class', 'wp-graphql-woocommerce' ), - ], - 'catalogVisibility' => [ - 'type' => 'CatalogVisibilityEnum', - 'description' => __( 'Product variation catalog visibility', 'wp-graphql-woocommerce' ), - ], - 'hasAttributes' => [ - 'type' => 'Boolean', - 'description' => __( 'Does product variation have any visible attributes', 'wp-graphql-woocommerce' ), - ], - 'type' => [ - 'type' => 'ProductTypesEnum', - 'description' => __( 'Product type', 'wp-graphql-woocommerce' ), - ], - 'image' => [ - 'type' => 'MediaItem', - 'description' => __( 'Product variation main image', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source, array $args, AppContext $context ) { - return ! empty( $source->image_id ) - ? $context->get_loader( 'post' )->load_deferred( $source->image_id ) - : null; - }, - ], - - 'metaData' => Meta_Data_Type::get_metadata_field_definition(), - ], - ] - ); - } -}