diff --git a/includes/wc-attribute-functions.php b/includes/wc-attribute-functions.php index 9d1abcf84f2..d44e6c98619 100644 --- a/includes/wc-attribute-functions.php +++ b/includes/wc-attribute-functions.php @@ -347,3 +347,177 @@ function wc_is_attribute_in_product_name( $attribute, $name ) { function wc_array_filter_default_attributes( $attribute ) { return ( ! empty( $attribute ) || '0' === $attribute ); } + +/** + * Get attribute data by ID. + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @return stdClass|null + */ +function wc_get_attribute( $id ) { + global $wpdb; + + $data = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $data ) || is_null( $data ) ) { + return null; + } + + $attribute = new stdClass(); + $attribute->id = (int) $data->attribute_id; + $attribute->name = $data->attribute_label; + $attribute->slug = wc_attribute_taxonomy_name( $data->attribute_name ); + $attribute->type = $data->attribute_type; + $attribute->order_by = $data->attribute_orderby; + $attribute->has_archives = (bool) $data->attribute_public; + + return $attribute; +} + +/** + * Create attribute. + * + * Options: + * + * id - Unique identifier, used to update an attribute. + * name - Attribute name. + * slug - Attribute alphanumeric identifier. + * type - Type of attribute, core options: 'select' and 'text', default to 'select'. + * order_by - Sort order, options: 'menu_order', 'name', 'name_num' and 'id'. + * has_archives - Enable or disable attribute archives. + * + * @since 3.2.0 + * @param array $args Attribute arguments. + * @return int|WP_Error + */ +function wc_create_attribute( $args ) { + global $wpdb; + + $args = wp_unslash( $args ); + $id = ! empty( $args['id'] ) ? intval( $args['id'] ) : 0; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + + // Name is required. + if ( empty( $args['name'] ) ) { + return new WP_Error( 'missing_attribute_name', __( 'Missing attribute name', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Set the attribute slug. + if ( empty( $args['slug'] ) ) { + $slug = wc_sanitize_taxonomy_name( $args['name'] ); + } else { + $slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) ); + } + + // Validate slug. + if ( strlen( $slug ) >= 28 ) { + return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( 0 === $id && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + return new WP_Error( 'invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + // Validate type. + if ( empty( $args['type'] ) || ! array_key_exists( $args['type'], wc_get_attribute_types() ) ) { + $args['type'] = 'select'; + } + + // Validate order by. + if ( empty( $args['order_by'] ) || ! in_array( $args['order_by'], array( 'menu_order', 'name', 'name_num', 'id' ), true ) ) { + $args['order_by'] = 'menu_order'; + } + + $data = array( + 'attribute_label' => $args['name'], + 'attribute_name' => $slug, + 'attribute_type' => $args['type'], + 'attribute_orderby' => $args['order_by'], + 'attribute_public' => isset( $args['has_archives'] ) ? (bool) $args['has_archives'] : false, + ); + + // Create or update. + if ( 0 === $id ) { + $results = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $data, + $format + ); + + if ( is_wp_error( $results ) ) { + return new WP_Error( 'cannot_create_attribute', $results->get_error_message(), array( 'status' => 400 ) ); + } + + $id = $wpdb->insert_id; + } else { + $results = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $data, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + if ( false === $results ) { + return new WP_Error( 'cannot_update_attribute', __( 'Could not update the attribute.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Clear transients. + delete_transient( 'wc_attribute_taxonomies' ); + + return $id; +} + +/** + * Update an attribute. + * + * For available args see wc_create_attribute(). + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @param array $args Attribute arguments. + * @return int|WP_Error] + */ +function wc_update_attribute( $id, $args ) { + $attribute = wc_get_attribute( $id ); + + $args['id'] = $attribute ? $attribute->id : 0; + + if ( $args['id'] && empty( $args['name'] ) ) { + $args['name'] = $attribute->name; + } + + return wc_create_attribute( $args ); +} + +/** + * Delete attribute by ID. + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @return bool + */ +function wc_delete_attribute( $id ) { + global $wpdb; + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( in_array( $deleted, array( 0, false ), true ) ) { + return false; + } + + // Clear transients. + delete_transient( 'wc_attribute_taxonomies' ); + + return true; +} diff --git a/tests/unit-tests/attributes/functions.php b/tests/unit-tests/attributes/functions.php new file mode 100644 index 00000000000..02042a19497 --- /dev/null +++ b/tests/unit-tests/attributes/functions.php @@ -0,0 +1,116 @@ +< 'Brand', + 'type' => 'select', + 'order_by' => 'name', + 'has_archives' => true, + ); + + $id = wc_create_attribute( $args ); + + $attribute = (array) wc_get_attribute( $id ); + $expected = array( + 'id' => $id, + 'name' => 'Brand', + 'slug' => 'pa_brand', + 'type' => 'select', + 'order_by' => 'name', + 'has_archives' => true, + ); + + wc_delete_attribute( $id ); + + $this->assertEquals( $expected, $attribute ); + } + + /** + * Tests wc_create_attribute(). + * + * @since 3.2.0 + */ + public function test_wc_create_attribute() { + // Test success. + $id = wc_create_attribute( array( 'name' => 'Brand' ) ); + $this->assertInternalType( 'int', $id ); + + // Test failures. + $err = wc_create_attribute( array() ); + $this->assertEquals( 'missing_attribute_name', $err->get_error_code() ); + + $err = wc_create_attribute( array( 'name' => 'This is a big name for a product attribute!' ) ); + $this->assertEquals( 'invalid_product_attribute_slug_too_long', $err->get_error_code() ); + + $err = wc_create_attribute( array( 'name' => 'Cat' ) ); + $this->assertEquals( 'invalid_product_attribute_slug_reserved_name', $err->get_error_code() ); + + register_taxonomy( 'pa_brand', array( 'product' ), array( 'labels' => array( 'name' => 'Brand' ) ) ); + $err = wc_create_attribute( array( 'name' => 'Brand' ) ); + $this->assertEquals( 'invalid_product_attribute_slug_already_exists', $err->get_error_code() ); + unregister_taxonomy( 'pa_brand' ); + + wc_delete_attribute( $id ); + } + + /** + * Tests wc_update_attribute(). + * + * @since 3.2.0 + */ + public function test_wc_update_attribute() { + $args = array( + 'name' => 'Brand', + 'type' => 'select', + 'order_by' => 'name', + 'has_archives' => true, + ); + + $id = wc_create_attribute( $args ); + + $updated = array( + 'id' => $id, + 'name' => 'Brand', + 'slug' => 'pa_brand', + 'type' => 'text', + 'order_by' => 'menu_order', + 'has_archives' => true, + ); + + wc_update_attribute( $id, $updated ); + + $attribute = (array) wc_get_attribute( $id ); + + wc_delete_attribute( $id ); + + $this->assertEquals( $updated, $attribute ); + } + + /** + * Tests wc_delete_attribute(). + * + * @since 3.2.0 + */ + public function test_wc_delete_attribute() { + // Success. + $id = wc_create_attribute( array( 'name' => 'Brand' ) ); + $result = wc_delete_attribute( $id ); + $this->assertTrue( $result ); + + // Failure. + $result = wc_delete_attribute( 9999999 ); + $this->assertFalse( $result ); + } +}