diff --git a/includes/class-actions.php b/includes/class-actions.php index 023c7e8c2..1c77dff16 100644 --- a/includes/class-actions.php +++ b/includes/class-actions.php @@ -35,6 +35,7 @@ use WPGraphQL\Extensions\WooCommerce\Type\WPInputObject\Meta_Data_Input; use WPGraphQL\Extensions\WooCommerce\Type\WPInputObject\Shipping_Line_Input; use WPGraphQL\Extensions\WooCommerce\Type\WPInputObject\Create_Account_Input; +use WPGraphQL\Extensions\WooCommerce\Type\WPInputObject\Cart_Item_Quantity_Input; use WPGraphQL\Extensions\WooCommerce\Type\WPObject\Coupon_Type; use WPGraphQL\Extensions\WooCommerce\Type\WPObject\Order_Type; use WPGraphQL\Extensions\WooCommerce\Type\WPObject\Order_Item_Type; @@ -67,7 +68,7 @@ use WPGraphQL\Extensions\WooCommerce\Mutation\Customer_Register; use WPGraphQL\Extensions\WooCommerce\Mutation\Customer_Update; use WPGraphQL\Extensions\WooCommerce\Mutation\Cart_Add_Item; -use WPGraphQL\Extensions\WooCommerce\Mutation\Cart_Update_Item_Quantity; +use WPGraphQL\Extensions\WooCommerce\Mutation\Cart_Update_Item_Quantities; use WPGraphQL\Extensions\WooCommerce\Mutation\Cart_Remove_Items; use WPGraphQL\Extensions\WooCommerce\Mutation\Cart_Restore_Items; use WPGraphQL\Extensions\WooCommerce\Mutation\Cart_Empty; @@ -128,6 +129,7 @@ public static function graphql_register_types() { Meta_Data_Input::register(); Shipping_Line_Input::register(); Create_Account_Input::register(); + Cart_Item_Quantity_Input::register(); // Objects. Coupon_Type::register(); @@ -167,7 +169,7 @@ public static function graphql_register_types() { Customer_Register::register_mutation(); Customer_Update::register_mutation(); Cart_Add_Item::register_mutation(); - Cart_Update_Item_Quantity::register_mutation(); + Cart_Update_Item_Quantities::register_mutation(); Cart_Remove_Items::register_mutation(); Cart_Restore_Items::register_mutation(); Cart_Empty::register_mutation(); diff --git a/includes/data/mutation/class-cart-mutation.php b/includes/data/mutation/class-cart-mutation.php index ef27dc8e4..986a43ce2 100644 --- a/includes/data/mutation/class-cart-mutation.php +++ b/includes/data/mutation/class-cart-mutation.php @@ -85,4 +85,21 @@ public static function prepare_cart_fee( $input, $context, $info ) { return apply_filters( 'woocommerce_new_cart_fee_data', $cart_item_args, $input, $context, $info ); } + + /** + * Validate CartItemQuantityInput item. + * + * @param array $item CartItemQuantityInput object. + * + * @return boolean + */ + public static function item_is_valid( array $item ) { + if ( empty( $item['key'] ) ) { + return false; + } + if ( ! isset( $item['quantity'] ) || ! is_numeric( $item['quantity'] ) ) { + return false; + } + return true; + } } diff --git a/includes/mutation/class-cart-update-item-quantities.php b/includes/mutation/class-cart-update-item-quantities.php new file mode 100644 index 000000000..c500cf3de --- /dev/null +++ b/includes/mutation/class-cart-update-item-quantities.php @@ -0,0 +1,167 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ) + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return array( + 'items' => array( + 'type' => array( 'list_of' => 'CartItemQuantityInput' ), + 'description' => __( 'Cart item being updated', 'wp-graphql-woocommerce' ), + ), + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return array( + 'updated' => array( + 'type' => array( 'list_of' => 'CartItem' ), + 'resolve' => function ( $payload ) { + $items = array(); + foreach ( $payload['updated'] as $key ) { + $items[] = WC()->cart->get_cart_item( $key ); + } + + return $items; + }, + ), + 'removed' => array( + 'type' => array( 'list_of' => 'CartItem' ), + 'resolve' => function ( $payload ) { + return $payload['removed']; + }, + ), + 'items' => array( + 'type' => array( 'list_of' => 'CartItem' ), + 'resolve' => function ( $payload ) { + $updated = array(); + foreach ( $payload['updated'] as $key ) { + $updated[] = \WC()->cart->get_cart_item( $key ); + } + + return array_merge( $updated, $payload['removed'] ); + }, + ), + ); + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return function( $input, AppContext $context, ResolveInfo $info ) { + // Confirm "items" exists. + if ( empty( $input['items'] ) ) { + throw new UserError( __( 'No item data provided', 'wp-graphql-woocommerce' ) ); + } + + // Confirm "items" is value. + if ( ! is_array( $input['items'] ) ) { + throw new UserError( __( 'Provided "items" invalid', 'wp-graphql-woocommerce' ) ); + } + + do_action( 'woocommerce_graphql_before_set_item_quantities', $input['items'], $input, $context, $info ); + + // Update quantities. If quantity set to 0, the items in removed. + $removed = array(); + $updated = array(); + $removed_items = array(); + foreach ( $input['items'] as $item ) { + if ( Cart_Mutation::item_is_valid( $item ) ) { + $key = $item['key']; + $quantity = $item['quantity']; + if ( 0 === $quantity ) { + $removed_item = \WC()->cart->get_cart_item( $key ); + $removed_items[] = $removed_item; + do_action( 'woocommerce_graphql_before_remove_item', $removed_item, 'update_quantity', $input, $context, $info ); + $removed[ $key ] = \WC()->cart->remove_cart_item( $key ); + do_action( 'woocommerce_graphql_after_remove_item', $removed_item, 'update_quantity', $input, $context, $info ); + continue; + } + do_action( 'woocommerce_graphql_before_set_item_quantity', \WC()->cart->get_cart_item( $key ), $input, $context, $info ); + $updated[ $key ] = \WC()->cart->set_quantity( $key, $quantity, true ); + do_action( 'woocommerce_graphql_after_set_item_quantity', \WC()->cart->get_cart_item( $key ), $input, $context, $info ); + } + } + + // Throw failed. + try { + $errors = array_keys( + array_filter( + array_merge( $removed, $updated ), + function( $value ) { + return ! $value; + } + ) + ); + if ( 0 < count( $errors ) ) { + throw new \Exception( + sprintf( + /* translators: %s: Cart item keys */ + __( 'Cart items identified with keys %s failed to update', 'wp-graphql-woocommerce' ), + implode( ', ', $errors ) + ) + ); + } + } catch ( \Exception $e ) { + throw new UserError( $e->getMessage() ); + } + + do_action( + 'woocommerce_graphql_before_set_item_quantities', + array_keys( $updated ), + array_keys( $removed ), + $input, + $context, + $info + ); + + return array( + 'removed' => $removed_items, + 'updated' => array_keys( $updated ), + ); + }; + } +} diff --git a/includes/mutation/class-cart-update-item-quantity.php b/includes/mutation/class-cart-update-item-quantity.php deleted file mode 100644 index 02dc53c87..000000000 --- a/includes/mutation/class-cart-update-item-quantity.php +++ /dev/null @@ -1,93 +0,0 @@ - self::get_input_fields(), - 'outputFields' => self::get_output_fields(), - 'mutateAndGetPayload' => self::mutate_and_get_payload(), - ) - ); - } - - /** - * Defines the mutation input field configuration - * - * @return array - */ - public static function get_input_fields() { - return array_merge( - array( - 'key' => array( - 'type' => array( 'non_null' => 'ID' ), - 'description' => __( 'Cart item being updated', 'wp-graphql-woocommerce' ), - ), - 'quantity' => array( - 'type' => array( 'non_null' => 'Int' ), - 'description' => __( 'Cart item\'s new quantity', 'wp-graphql-woocommerce' ), - ), - ) - ); - } - - /** - * Defines the mutation output field configuration - * - * @return array - */ - public static function get_output_fields() { - return Cart_Add_Item::get_output_fields(); - } - - /** - * Defines the mutation data modification closure. - * - * @return callable - */ - public static function mutate_and_get_payload() { - return function( $input, AppContext $context, ResolveInfo $info ) { - // Retrieve product database ID if relay ID provided. - if ( empty( $input['key'] ) ) { - throw new UserError( __( 'No cart item key provided', 'wp-graphql-woocommerce' ) ); - } - - // Retrieve product database ID if relay ID provided. - if ( empty( $input['quantity'] ) ) { - throw new UserError( __( 'No new quantity provided', 'wp-graphql-woocommerce' ) ); - } - - // Get WC_Cart instance. - $success = WC()->cart->set_quantity( $input['key'], $input['quantity'] ); - - if ( true !== $success ) { - throw new UserError( __( 'Cart item failed to update', 'wp-graphql-woocommerce' ) ); - } - - // Return payload. - return array( 'key' => $input['key'] ); - }; - } -} diff --git a/includes/type/input/class-cart-item-quantity-input.php b/includes/type/input/class-cart-item-quantity-input.php new file mode 100644 index 000000000..718613603 --- /dev/null +++ b/includes/type/input/class-cart-item-quantity-input.php @@ -0,0 +1,36 @@ + __( 'Cart item quantity', 'wp-graphql-woocommerce' ), + 'fields' => array( + 'key' => array( + 'type' => array( 'non_null' => 'ID' ), + 'description' => __( 'Cart item being updated', 'wp-graphql-woocommerce' ), + ), + 'quantity' => array( + 'type' => array( 'non_null' => 'Int' ), + 'description' => __( 'Cart item\'s new quantity', 'wp-graphql-woocommerce' ), + ), + ), + ) + ); + } +} diff --git a/tests/wpunit/CartMutationsTest.php b/tests/wpunit/CartMutationsTest.php index 4bff1d75a..944ab179a 100644 --- a/tests/wpunit/CartMutationsTest.php +++ b/tests/wpunit/CartMutationsTest.php @@ -229,28 +229,56 @@ public function testAddToCartMutationWithProductVariation() { $this->assertEqualSets( $expected, $actual ); } - public function testUpdateCartItemQuantityMutation() { - $product_id = $this->product->create_simple(); + public function testUpdateCartItemQuantitiesMutation() { + // Create products. + $product_1 = $this->product->create_simple(); + $product_2 = $this->product->create_simple(); + $product_3 = $this->product->create_simple(); + + // Add items to cart and retrieve keys $addToCart = $this->addToCart( array( 'clientMutationId' => 'someId', - 'productId' => $product_id, + 'productId' => $product_1, 'quantity' => 2, ) ); - - // Retrieve cart item key. $this->assertArrayHasKey('data', $addToCart ); - $this->assertArrayHasKey('addToCart', $addToCart['data'] ); - $this->assertArrayHasKey('cartItem', $addToCart['data']['addToCart'] ); - $this->assertArrayHasKey('key', $addToCart['data']['addToCart']['cartItem'] ); - $key = $addToCart['data']['addToCart']['cartItem']['key']; + $key_1 = $addToCart['data']['addToCart']['cartItem']['key']; + $addToCart = $this->addToCart( + array( + 'clientMutationId' => 'someId', + 'productId' => $product_2, + 'quantity' => 5, + ) + ); + $this->assertArrayHasKey('data', $addToCart ); + $key_2 = $addToCart['data']['addToCart']['cartItem']['key']; + $addToCart = $this->addToCart( + array( + 'clientMutationId' => 'someId', + 'productId' => $product_3, + 'quantity' => 1, + ) + ); + $this->assertArrayHasKey('data', $addToCart ); + $key_3 = $addToCart['data']['addToCart']['cartItem']['key']; + // Update items mutation. $mutation = ' - mutation updateItemQuantity( $input: UpdateItemQuantityInput! ) { - updateItemQuantity( input: $input ) { + mutation updateItemQuantities( $input: UpdateItemQuantitiesInput! ) { + updateItemQuantities( input: $input ) { clientMutationId - cartItem { + updated { + key + quantity + } + removed { + key + quantity + } + items { + key quantity } } @@ -260,13 +288,16 @@ public function testUpdateCartItemQuantityMutation() { $actual = graphql( array( 'query' => $mutation, - 'operation_name' => 'updateItemQuantity', + 'operation_name' => 'updateItemQuantities', 'variables' => array( 'input' => array( 'clientMutationId' => 'someId', - 'key' => $key, - 'quantity' => 4, - ) + 'items' => array( + array( 'key' => $key_1, 'quantity' => 4 ), + array( 'key' => $key_2, 'quantity' => 2 ), + array( 'key' => $key_3, 'quantity' => 0 ), + ), + ), ), ) ); @@ -277,11 +308,20 @@ public function testUpdateCartItemQuantityMutation() { // Check cart item data. $expected = array( 'data' => array( - 'updateItemQuantity' => array( + 'updateItemQuantities' => array( 'clientMutationId' => 'someId', - 'cartItem' => array( - 'quantity' => 4, - ), + 'updated' => array( + array( 'key' => $key_1, 'quantity' => 4 ), + array( 'key' => $key_2, 'quantity' => 2 ), + ), + 'removed' => array( + array( 'key' => $key_3, 'quantity' => 1 ), + ), + 'items' => array( + array( 'key' => $key_1, 'quantity' => 4 ), + array( 'key' => $key_2, 'quantity' => 2 ), + array( 'key' => $key_3, 'quantity' => 1 ), + ) ), ), ); diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php index 95f7e0978..fce8549f0 100644 --- a/vendor/composer/ClassLoader.php +++ b/vendor/composer/ClassLoader.php @@ -279,7 +279,7 @@ public function isClassMapAuthoritative() */ public function setApcuPrefix($apcuPrefix) { - $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index ed319afe1..8d1fadee3 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -61,7 +61,7 @@ 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Remove_Coupons' => $baseDir . '/includes/mutation/class-cart-remove-coupons.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Remove_Items' => $baseDir . '/includes/mutation/class-cart-remove-items.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Restore_Items' => $baseDir . '/includes/mutation/class-cart-restore-items.php', - 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Update_Item_Quantity' => $baseDir . '/includes/mutation/class-cart-update-item-quantity.php', + 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Update_Item_Quantities' => $baseDir . '/includes/mutation/class-cart-update-item-quantities.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Checkout' => $baseDir . '/includes/mutation/class-checkout.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Customer_Register' => $baseDir . '/includes/mutation/class-customer-register.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Customer_Update' => $baseDir . '/includes/mutation/class-customer-update.php', @@ -83,6 +83,7 @@ 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPEnum\\Tax_Rate_Connection_Orderby_Enum' => $baseDir . '/includes/type/enum/class-tax-rate-connection-orderby-enum.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPEnum\\Tax_Status' => $baseDir . '/includes/type/enum/class-tax-status.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPEnum\\WC_Connection_Orderby_Enum' => $baseDir . '/includes/type/enum/class-wc-connection-orderby-enum.php', + 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Cart_Item_Quantity_Input' => $baseDir . '/includes/type/input/class-cart-item-quantity-input.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Create_Account_Input' => $baseDir . '/includes/type/input/class-create-account-input.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Customer_Address_Input' => $baseDir . '/includes/type/input/class-customer-address-input.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Fee_Line_Input' => $baseDir . '/includes/type/input/class-fee-line-input.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 8f28cc913..1e1649df5 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -76,7 +76,7 @@ class ComposerStaticInitee0d17af17b841ed3a93c4a0e5cc5e5f 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Remove_Coupons' => __DIR__ . '/../..' . '/includes/mutation/class-cart-remove-coupons.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Remove_Items' => __DIR__ . '/../..' . '/includes/mutation/class-cart-remove-items.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Restore_Items' => __DIR__ . '/../..' . '/includes/mutation/class-cart-restore-items.php', - 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Update_Item_Quantity' => __DIR__ . '/../..' . '/includes/mutation/class-cart-update-item-quantity.php', + 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Cart_Update_Item_Quantities' => __DIR__ . '/../..' . '/includes/mutation/class-cart-update-item-quantities.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Checkout' => __DIR__ . '/../..' . '/includes/mutation/class-checkout.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Customer_Register' => __DIR__ . '/../..' . '/includes/mutation/class-customer-register.php', 'WPGraphQL\\Extensions\\WooCommerce\\Mutation\\Customer_Update' => __DIR__ . '/../..' . '/includes/mutation/class-customer-update.php', @@ -98,6 +98,7 @@ class ComposerStaticInitee0d17af17b841ed3a93c4a0e5cc5e5f 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPEnum\\Tax_Rate_Connection_Orderby_Enum' => __DIR__ . '/../..' . '/includes/type/enum/class-tax-rate-connection-orderby-enum.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPEnum\\Tax_Status' => __DIR__ . '/../..' . '/includes/type/enum/class-tax-status.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPEnum\\WC_Connection_Orderby_Enum' => __DIR__ . '/../..' . '/includes/type/enum/class-wc-connection-orderby-enum.php', + 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Cart_Item_Quantity_Input' => __DIR__ . '/../..' . '/includes/type/input/class-cart-item-quantity-input.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Create_Account_Input' => __DIR__ . '/../..' . '/includes/type/input/class-create-account-input.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Customer_Address_Input' => __DIR__ . '/../..' . '/includes/type/input/class-customer-address-input.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPInputObject\\Fee_Line_Input' => __DIR__ . '/../..' . '/includes/type/input/class-fee-line-input.php',