mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* fix: address WordPress.org plugin review feedback - Prefix the session transaction queue transient with the plugin's graphql_woocommerce_ namespace instead of the generic "woo_" word, to avoid collisions (Plugin Directory: prefix data storage). - Declare the WooCommerce dependency via the "Requires Plugins: woocommerce" plugin header. - Bump README.txt "Tested up to" to 7.0. - Ship composer.json in the distributed plugin (drop it, and composer.lock, from composer archive excludes) so the build is reproducible/reviewable. * chore: rename plugin to "GraphQL for eCommerce" for trademark compliance The WordPress.org plugin review flagged the display name/slug for beginning with the "WPGraphQL" trademark (and the "WooGraphQL" portmanteau of the WooCommerce mark), which can imply official affiliation. - Display name (plugin header + readme title) -> "GraphQL for eCommerce". - Slug/text domain -> "graphql-for-ecommerce" (header, all i18n string literals, and the PHPCS WordPress.WP.I18n text_domain config). - Update user-facing notices/errors that named the old plugin. "WooGraphQL" remains the project's informal nickname (repo, docs, community), just not in the WordPress.org directory's official name/slug. Internal file names and GitHub URLs are unchanged. * fix: keep test-only dev deps out of the committed composer.json The committed manifest mirrors develop (lint/stan dev deps only); CI adds the test suite deps at runtime via `composer installTestEnv`. A previous commit captured the installTestEnv-modified composer.json, desyncing it from composer.lock and breaking `composer install` in CI. * chore: regenerate composer.lock (refresh dev dependencies) Regenerate the lock from the manifest so it is in sync (fixes the CI `composer install` failure) and refresh dependencies in the process — firebase/php-jwt v7.0.4 -> v7.1.0 plus 11 others, with vendor-prefixed re-strauss'd to match. Full wpunit suite passes against the updated deps (305 tests, 835 assertions). * chore: rename text domain in createdVia/attribution strings from #1018 #1018 (createdVia + order attribution) merged into develop after the rename commit was authored, so its new i18n strings still used the old 'wp-graphql-woocommerce' text domain. Update them to 'graphql-for-ecommerce' to match the rename.
394 lines
11 KiB
PHP
394 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Defines helper functions for executing mutations related to products.
|
|
*
|
|
* @package WPGraphQL\WooCommerce\Data\Mutation
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
namespace WPGraphQL\WooCommerce\Data\Mutation;
|
|
|
|
use GraphQL\Error\UserError;
|
|
|
|
/**
|
|
* Class - Product_Mutation
|
|
*/
|
|
class Product_Mutation {
|
|
/**
|
|
* Save product shipping data
|
|
*
|
|
* @param \WC_Product $product Product object.
|
|
* @param array $input Mutation input.
|
|
*
|
|
* @return \WC_Product
|
|
*/
|
|
public static function save_product_shipping_data( $product, $input ) {
|
|
// Virtual.
|
|
if ( isset( $input['virtual'] ) && true === $input['virtual'] ) {
|
|
$product->set_weight( '' );
|
|
$product->set_height( '' );
|
|
$product->set_length( '' );
|
|
$product->set_width( '' );
|
|
} else {
|
|
if ( isset( $input['weight'] ) ) {
|
|
$product->set_weight( $input['weight'] );
|
|
}
|
|
|
|
if ( isset( $input['dimensions']['height'] ) ) {
|
|
$product->set_height( $input['dimensions']['height'] );
|
|
}
|
|
|
|
if ( isset( $input['dimensions']['width'] ) ) {
|
|
$product->set_width( $input['dimensions']['width'] );
|
|
}
|
|
|
|
if ( isset( $input['dimensions']['length'] ) ) {
|
|
$product->set_length( $input['dimensions']['length'] );
|
|
}
|
|
}
|
|
|
|
if ( isset( $input['shippingClass'] ) ) {
|
|
/**
|
|
* Shipping class.
|
|
*
|
|
* @var string $shippingClass
|
|
*/
|
|
$shippingClass = wc_clean( $input['shippingClass'] );
|
|
/**
|
|
* WC product data store instance.
|
|
*
|
|
* @var \WC_Product_Data_Store_Interface $data_store
|
|
*/
|
|
$data_store = $product->get_data_store();
|
|
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( $shippingClass );
|
|
if ( $shipping_class_id ) {
|
|
$product->set_shipping_class_id( $shipping_class_id );
|
|
} else {
|
|
graphql_debug(
|
|
/* translators: %s: Shipping class */
|
|
sprintf( __( 'Invalid shipping class: %s', 'graphql-for-ecommerce' ), $shippingClass )
|
|
);
|
|
}
|
|
}
|
|
|
|
return $product;
|
|
}
|
|
|
|
/**
|
|
* Prepare product attribute
|
|
*
|
|
* @param array $attribute Product attribute data.
|
|
*
|
|
* @return \WC_Product_Attribute|null
|
|
*/
|
|
public static function prepare_attribute( $attribute ) {
|
|
/**
|
|
* Attribute ID.
|
|
*
|
|
* @var int $attribute_id
|
|
*/
|
|
$attribute_id = 0;
|
|
/**
|
|
* Attribute name.
|
|
*
|
|
* @var string $attribute_name
|
|
*/
|
|
$attribute_name = '';
|
|
|
|
if ( ! empty( $attribute['id'] ) ) {
|
|
$attribute_id = absint( $attribute['id'] );
|
|
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
|
|
} elseif ( ! empty( $attribute['name'] ) ) {
|
|
/**
|
|
* @var string $attribute_name
|
|
*/
|
|
$attribute_name = wc_clean( $attribute['name'] );
|
|
}
|
|
|
|
if ( ! $attribute_id && ! $attribute_name ) {
|
|
return null;
|
|
}
|
|
|
|
if ( $attribute_id ) {
|
|
if ( isset( $attribute['options'] ) ) {
|
|
$options = $attribute['options'];
|
|
|
|
if ( ! is_array( $options ) ) {
|
|
$options = explode( WC_DELIMITER, $options );
|
|
}
|
|
|
|
$values = array_map( 'wc_sanitize_term_text_based', $options );
|
|
/** @var callable(string): bool */
|
|
$callback = 'strlen';
|
|
$values = array_filter( $values, $callback );
|
|
} else {
|
|
$values = [];
|
|
}
|
|
|
|
if ( ! empty( $values ) ) {
|
|
$attribute_object = new \WC_Product_Attribute();
|
|
$attribute_object->set_id( $attribute_id );
|
|
$attribute_object->set_name( $attribute_name );
|
|
$attribute_object->set_options( $values );
|
|
$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
|
|
$attribute_object->set_visible( isset( $attribute['visible'] ) ? $attribute['visible'] : false );
|
|
$attribute_object->set_variation( isset( $attribute['variation'] ) ? $attribute['variation'] : false );
|
|
|
|
return $attribute_object;
|
|
}
|
|
} elseif ( isset( $attribute['options'] ) ) {
|
|
if ( is_array( $attribute['options'] ) ) {
|
|
$values = $attribute['options'];
|
|
} else {
|
|
$values = explode( WC_DELIMITER, $attribute['options'] );
|
|
}
|
|
|
|
$attribute_object = new \WC_Product_Attribute();
|
|
$attribute_object->set_name( $attribute_name );
|
|
$attribute_object->set_options( $values );
|
|
$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
|
|
$attribute_object->set_visible( isset( $attribute['visible'] ) ? $attribute['visible'] : false );
|
|
$attribute_object->set_variation( isset( $attribute['variation'] ) ? $attribute['variation'] : false );
|
|
|
|
return $attribute_object;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Save product attributes
|
|
*
|
|
* @param \WC_Product $product Product instance.
|
|
* @param array $term_ids Terms IDs.
|
|
* @param string $taxonomy Taxonomy name.
|
|
*
|
|
* @return \WC_Product
|
|
*/
|
|
public static function save_taxonomy_terms( $product, $term_ids, $taxonomy = 'cat' ) {
|
|
if ( 'cat' === $taxonomy ) {
|
|
$product->set_category_ids( $term_ids );
|
|
} elseif ( 'tag' === $taxonomy ) {
|
|
$product->set_tag_ids( $term_ids );
|
|
}
|
|
|
|
return $product;
|
|
}
|
|
|
|
/**
|
|
* Save product downloadable files
|
|
*
|
|
* @param \WC_Product|\WC_Product_Variation $product Product instance.
|
|
* @param array $downloads Downloads data.
|
|
*
|
|
* @return \WC_Product|\WC_Product_Variation
|
|
*/
|
|
public static function save_downloadable_files( $product, $downloads ) {
|
|
$files = [];
|
|
foreach ( $downloads as $key => $file ) {
|
|
if ( empty( $file['file'] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$download = new \WC_Product_Download();
|
|
$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
|
|
$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
|
|
$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
|
|
$files[] = $download;
|
|
}
|
|
|
|
$product->set_downloads( $files );
|
|
|
|
return $product;
|
|
}
|
|
|
|
/**
|
|
* Save variable product default attributes
|
|
*
|
|
* @param \WC_Product $product Product object.
|
|
* @param array $input Mutation input.
|
|
*
|
|
* @return \WC_Product
|
|
*/
|
|
public static function save_default_attributes( $product, $input ) {
|
|
if ( ! empty( $input['defaultAttributes'] ) ) {
|
|
$attributes = $product->get_attributes();
|
|
$default_attributes = [];
|
|
|
|
foreach ( $input['defaultAttributes'] as $attribute ) {
|
|
/**
|
|
* Attribute ID.
|
|
*
|
|
* @var int $attribute_id
|
|
*/
|
|
$attribute_id = 0;
|
|
/**
|
|
* Attribute name.
|
|
*
|
|
* @var string $attribute_name
|
|
*/
|
|
$attribute_name = '';
|
|
|
|
if ( ! empty( $attribute['id'] ) ) {
|
|
$attribute_id = absint( $attribute['id'] );
|
|
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
|
|
} elseif ( ! empty( $attribute['attributeName'] ) ) {
|
|
$attribute_name = sanitize_title( $attribute['attributeName'] );
|
|
}
|
|
|
|
if ( ! $attribute_id && ! $attribute_name ) {
|
|
continue;
|
|
}
|
|
|
|
if ( isset( $attributes[ $attribute_name ] ) ) {
|
|
$_attribute = $attributes[ $attribute_name ];
|
|
|
|
if ( $_attribute['is_variation'] ) {
|
|
/**
|
|
* Attribute value.
|
|
*
|
|
* @var string $value
|
|
*/
|
|
$value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : '';
|
|
|
|
if ( ! empty( $_attribute['is_taxonomy'] ) ) {
|
|
$term = get_term_by( 'name', $value, $attribute_name );
|
|
|
|
if ( ! empty( $term ) ) {
|
|
$value = $term->slug;
|
|
} else {
|
|
$value = sanitize_title( $value );
|
|
}
|
|
}
|
|
|
|
if ( $value ) {
|
|
$default_attributes[ $attribute_name ] = $value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$product->set_default_attributes( $default_attributes );
|
|
}
|
|
|
|
return $product;
|
|
}
|
|
|
|
/**
|
|
* Set product images
|
|
*
|
|
* @param \WC_Product $product Product instance.
|
|
* @param array $images Images data.
|
|
*
|
|
* @throws \GraphQL\Error\UserError If image upload fails | Invalid image ID.
|
|
*
|
|
* @return \WC_Product
|
|
*/
|
|
public static function set_product_images( $product, $images ) {
|
|
$images = is_array( $images ) ? array_filter( $images ) : [];
|
|
|
|
if ( ! empty( $images ) ) {
|
|
$gallery_positions = [];
|
|
|
|
foreach ( $images as $index => $image ) {
|
|
$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
|
|
|
|
if ( 0 === $attachment_id && isset( $image['src'] ) ) {
|
|
$upload = \wc_rest_upload_image_from_url( $image['src'] );
|
|
|
|
if ( is_wp_error( $upload ) ) {
|
|
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
|
|
throw new UserError( $upload->get_error_message() );
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$attachment_id = \wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
|
|
}
|
|
|
|
if ( ! wp_attachment_is_image( $attachment_id ) ) {
|
|
throw new UserError(
|
|
/* translators: %s: Attachment ID */
|
|
sprintf( __( '#%s is an invalid image ID.', 'graphql-for-ecommerce' ), $attachment_id )
|
|
);
|
|
}
|
|
|
|
$gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index );
|
|
|
|
// Set the image alt if present.
|
|
if ( ! empty( $image['altText'] ) ) {
|
|
update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
|
|
}
|
|
|
|
// Set the image name if present.
|
|
if ( ! empty( $image['name'] ) ) {
|
|
wp_update_post(
|
|
[
|
|
'ID' => $attachment_id,
|
|
'post_title' => $image['name'],
|
|
]
|
|
);
|
|
}
|
|
|
|
// Set the image source if present, for future reference.
|
|
if ( ! empty( $image['src'] ) ) {
|
|
update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) );
|
|
}
|
|
}
|
|
|
|
// Sort images and get IDs in correct order.
|
|
asort( $gallery_positions );
|
|
|
|
// Get gallery in correct order.
|
|
$gallery = array_keys( $gallery_positions );
|
|
|
|
/**
|
|
* Featured image ID in position 0.
|
|
*
|
|
* @var int $image_id
|
|
*/
|
|
$image_id = array_shift( $gallery );
|
|
|
|
// Set images.
|
|
$product->set_image_id( $image_id );
|
|
$product->set_gallery_image_ids( $gallery );
|
|
} else {
|
|
$product->set_image_id( '' );
|
|
$product->set_gallery_image_ids( [] );
|
|
}
|
|
|
|
return $product;
|
|
}
|
|
|
|
/**
|
|
* Retrieve product attribute
|
|
*
|
|
* @param int $id Attribute ID.
|
|
*
|
|
* @throws \GraphQL\Error\UserError If attribute ID is invalid.
|
|
*
|
|
* @return object{'attribute_id': int}
|
|
*/
|
|
public static function get_attribute( $id ) {
|
|
global $wpdb;
|
|
|
|
$attribute = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
"
|
|
SELECT *
|
|
FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
|
|
WHERE attribute_id = %d
|
|
",
|
|
$id
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
|
|
throw new UserError( __( 'Invalid attribute ID.', 'graphql-for-ecommerce' ) );
|
|
}
|
|
|
|
return $attribute;
|
|
}
|
|
}
|