From e6e284226433044614c36e36541d3eb95cb42977 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 2 Apr 2024 11:48:50 +0100 Subject: [PATCH 01/10] Fixed #99 syncing variant metafields --- CHANGELOG.md | 7 ++++++ README.md | 26 ++++++++++++++++------- src/jobs/UpdateProductMetadata.php | 1 + src/models/Settings.php | 16 ++++++++++++++ src/services/Api.php | 33 +++++++++++++++++++++++++++-- src/services/Products.php | 34 ++++++++++++++++++++++-------- 6 files changed, 98 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba55f0..2d2f8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release Notes for Shopify +## Unreleased + +- Added support for syncing variant meta fields. ([#99](https://github.com/craftcms/shopify/issues/99)) +- Added the `syncProductMetafields` and `syncVariantMetafields` config settings, which can be enabled to sync meta fields. +- Added `craft\shopify\models\Settings::$syncProductMetafields`. +- Added `craft\shopify\models\Settings::$syncVariantMetafields`. + ## 4.0.0 - 2023-11-02 > [!IMPORTANT] diff --git a/README.md b/README.md index 644c25c..3774a2a 100644 --- a/README.md +++ b/README.md @@ -117,17 +117,24 @@ Products from your Shopify store are represented in Craft as product [elements]( ### Synchronization -Once the plugin has been configured, you can perform an initial synchronization of all products via the **Shopify Sync** utility. +Once the plugin has been configured, you can perform an initial synchronization of all products via the command line. -> [!NOTE] -> Larger stores with 100+ products should perform the initial synchronization via the command line instead: -> -> ```sh -> php craft shopify/sync/products -> ``` +```sh +php craft shopify/sync/products +``` Going forward, your products will be automatically kept in sync via [webhooks](#set-up-webhooks). +The following settings available for controlling the product synchronization process: + +| Setting | Type | Default | Description | +|-------------------------|--------|---------|-------------| +| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | +| `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | + +> [!NOTE] +> Smaller stores with only a few products can perform synchronization via the **Shopify Sync** utility. + ### Native Attributes In addition to the standard element attributes like `id`, `title`, and `status`, each Shopify product element contains the following mappings to its canonical [Shopify Product resource](https://shopify.dev/api/admin-rest/2023-10/resources/product#resource-object): @@ -426,7 +433,8 @@ You can get an array of variant objects for a product by calling [`product.getVa Unlike products, variants in Craft… -- …are represented exactly as [the API](https://shopify.dev/api/admin-rest/2023-10/resources/product-variant#resource-object) returns them; +- …are represented as [the API](https://shopify.dev/api/admin-rest/2023-10/resources/product-variant#resource-object) returns them; +- …the `metafields` property is accessible in addition to the API’s returned properties; - …use Shopify’s convention of underscores in property names instead of exposing [camel-cased equivalents](#native-attributes); - …are plain associative arrays; - …have no methods of their own; @@ -441,6 +449,8 @@ Once you have a reference to a variant, you can output its properties: > **Note** > The built-in [`currency`](https://craftcms.com/docs/4.x/dev/filters.html#currency) Twig filter is a great way to format money values. +> +> The `metafields` property will only be populated if the `syncVariantMetafields` setting is enabled. ### Using Options diff --git a/src/jobs/UpdateProductMetadata.php b/src/jobs/UpdateProductMetadata.php index e768e6e..4b04cd7 100644 --- a/src/jobs/UpdateProductMetadata.php +++ b/src/jobs/UpdateProductMetadata.php @@ -11,6 +11,7 @@ /** * Updates the metadata for a Shopify product. * + * @TODO remove in next major version * @deprecated 4.0.0 No longer used internally due to the use of `Retry-After` headers in the Shopify API. */ class UpdateProductMetadata extends BaseJob diff --git a/src/models/Settings.php b/src/models/Settings.php index b032d91..17e3b63 100644 --- a/src/models/Settings.php +++ b/src/models/Settings.php @@ -28,6 +28,22 @@ class Settings extends Model public string $template = ''; private mixed $_productFieldLayout; + /** + * Whether product metafields should be included when syncing products. This adds an extra API request per product. + * + * @var bool + * @since 4.1.0 + */ + public bool $syncProductMetafields = true; + + /** + * Whether variant metafields should be included when syncing products. This adds an extra API request per variant. + * + * @var bool + * @since 4.1.0 + */ + public bool $syncVariantMetafields = false; + public function rules(): array { return [ diff --git a/src/services/Api.php b/src/services/Api.php index 367d6bc..be1e05b 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -94,13 +94,42 @@ public function getProductIdByInventoryItemId($id): ?int * @return ShopifyMetafield[] */ public function getMetafieldsByProductId(int $id): array + { + if (!Plugin::getInstance()->getSettings()->syncProductMetafields) { + return []; + } + + return $this->getMetafieldsByIdAndOwnerResource($id, 'product'); + } + + /** + * @param int $id + * @return ShopifyMetafield[] + * @since 4.1.0 + */ + public function getMetafieldsByVariantId(int $id): array + { + if (!Plugin::getInstance()->getSettings()->syncVariantMetafields) { + return []; + } + + return $this->getMetafieldsByIdAndOwnerResource($id, 'variants'); + } + + /** + * @param int $id + * @param string $ownerResource + * @return ShopifyMetafield[] + * @since 4.1.0 + */ + public function getMetafieldsByIdAndOwnerResource(int $id, string $ownerResource): array { /** @var ShopifyMetafield[] $metafields */ $metafields = $this->getAll(ShopifyMetafield::class, [ 'metafield' => [ 'owner_id' => $id, - 'owner_resource' => 'product', - ], + 'owner_resource' => $ownerResource, + ] ]); return $metafields; diff --git a/src/services/Products.php b/src/services/Products.php index 14a059f..85320d0 100644 --- a/src/services/Products.php +++ b/src/services/Products.php @@ -53,6 +53,27 @@ class Products extends Component */ public const EVENT_BEFORE_SYNCHRONIZE_PRODUCT = 'beforeSynchronizeProduct'; + /** + * @param ShopifyProduct $product + * @return void + * @throws \yii\base\InvalidConfigException + * @since 4.1.0 + */ + private function _updateProduct(ShopifyProduct $product): void + { + $api = Plugin::getInstance()->getApi(); + + $variants = $api->getVariantsByProductId($product->id); + $productMetafields = $api->getMetafieldsByProductId($product->id); + + foreach ($variants as &$variant) { + $variantMetafields = $api->getMetafieldsByVariantId($variant['id']); + $variant['metafields'] = $variantMetafields; + } + + $this->createOrUpdateProduct($product, $productMetafields, $variants); + } + /** * @return void * @throws \Throwable @@ -64,9 +85,7 @@ public function syncAllProducts(): void $products = $api->getAllProducts(); foreach ($products as $product) { - $variants = $api->getVariantsByProductId($product->id); - $metafields = $api->getMetafieldsByProductId($product->id); - $this->createOrUpdateProduct($product, $metafields, $variants); + $this->_updateProduct($product); } // Remove any products that are no longer in Shopify just in case. @@ -88,10 +107,8 @@ public function syncProductByShopifyId($id): void $api = Plugin::getInstance()->getApi(); $product = $api->getProductByShopifyId($id); - $metaFields = $api->getMetafieldsByProductId($id); - $variants = $api->getVariantsByProductId($id); - $this->createOrUpdateProduct($product, $metaFields, $variants); + $this->_updateProduct($product); } /** @@ -105,9 +122,8 @@ public function syncProductByInventoryItemId($id): void if ($productId = $api->getProductIdByInventoryItemId($id)) { $product = $api->getProductByShopifyId($productId); - $metaFields = $api->getMetafieldsByProductId($product->id); - $variants = $api->getVariantsByProductId($product->id); - $this->createOrUpdateProduct($product, $metaFields, $variants); + + $this->_updateProduct($product); } } From 125c25e884e014f43b2335e7d9d8c11819e0a723 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 2 Apr 2024 12:03:24 +0100 Subject: [PATCH 02/10] Tidy readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3774a2a..814232c 100644 --- a/README.md +++ b/README.md @@ -733,7 +733,7 @@ It’s safe to remove the old plugin package (`nmaier95/shopify-product-fetcher` For each legacy Shopify Product field in your project, do the following: -1. Create a _new_ [Shopify Products](#product-field) field, giving it a a new handle and name; +1. Create a _new_ [Shopify Products](#product-field) field, giving it a new handle and name; 2. Add the field to any layouts where the legacy field appeared; ### Re-saving Data From a56e98f0dfd11e9474cf4cb1f81b1ca810b8176e Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 2 Apr 2024 12:06:46 +0100 Subject: [PATCH 03/10] Migration to add `metafields` property to variants --- src/Plugin.php | 2 +- ...57_add_metafields_property_to_variants.php | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/migrations/m240402_105857_add_metafields_property_to_variants.php diff --git a/src/Plugin.php b/src/Plugin.php index 179669d..3d414b2 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -56,7 +56,7 @@ class Plugin extends BasePlugin /** * @var string */ - public string $schemaVersion = '4.0.6'; // For some reason the 2.2+ version of the plugin was at 4.0 schema version + public string $schemaVersion = '4.0.7'; /** * @inheritdoc diff --git a/src/migrations/m240402_105857_add_metafields_property_to_variants.php b/src/migrations/m240402_105857_add_metafields_property_to_variants.php new file mode 100644 index 0000000..be75be2 --- /dev/null +++ b/src/migrations/m240402_105857_add_metafields_property_to_variants.php @@ -0,0 +1,49 @@ +select(['shopifyId', 'variants']) + ->from('{{%shopify_productdata}}') + ->all(); + + foreach ($productRows as $product) { + $variants = json_decode($product['variants'], true); + foreach ($variants as &$variant) { + if (isset($variant['metafields'])) { + continue; + } + + $variant['metafields'] = []; + } + + $this->update('{{%shopify_productdata}}', ['variants' => json_encode($variants)], ['shopifyId' => $product['shopifyId']]); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m240402_105857_add_metafields_property_to_variants cannot be reverted.\n"; + return false; + } +} From e4772aa5ca005b06f656e984906b926ad6f65554 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 2 Apr 2024 12:07:08 +0100 Subject: [PATCH 04/10] fix cs --- .../m240402_105857_add_metafields_property_to_variants.php | 2 -- src/services/Api.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/migrations/m240402_105857_add_metafields_property_to_variants.php b/src/migrations/m240402_105857_add_metafields_property_to_variants.php index be75be2..32470e3 100644 --- a/src/migrations/m240402_105857_add_metafields_property_to_variants.php +++ b/src/migrations/m240402_105857_add_metafields_property_to_variants.php @@ -2,10 +2,8 @@ namespace craft\shopify\migrations; -use Craft; use craft\db\Migration; use craft\db\Query; -use craft\shopify\records\ProductData; /** * m240402_105857_add_metafields_property_to_variants migration. diff --git a/src/services/Api.php b/src/services/Api.php index be1e05b..d6c595a 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -129,7 +129,7 @@ public function getMetafieldsByIdAndOwnerResource(int $id, string $ownerResource 'metafield' => [ 'owner_id' => $id, 'owner_resource' => $ownerResource, - ] + ], ]); return $metafields; From bf86cdecbcf62e42eb89bd1cc5c68697be945d4c Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 2 Apr 2024 17:10:14 +0100 Subject: [PATCH 05/10] Fix readme table alignment --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 814232c..90af835 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,10 @@ Going forward, your products will be automatically kept in sync via [webhooks](# The following settings available for controlling the product synchronization process: -| Setting | Type | Default | Description | -|-------------------------|--------|---------|-------------| -| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | -| `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | +| Setting | Type | Default | Description | +|-------------------------|--------|---------|-------------------------------------------------------------------------------------------------------------------| +| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | +| `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | > [!NOTE] > Smaller stores with only a few products can perform synchronization via the **Shopify Sync** utility. From 5eb77d14720cb2b2e6885512f31b0250e8308276 Mon Sep 17 00:00:00 2001 From: August Miller Date: Tue, 2 Apr 2024 09:12:51 -0700 Subject: [PATCH 06/10] Dedicated settings section --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 814232c..0393114 100644 --- a/README.md +++ b/README.md @@ -125,12 +125,7 @@ php craft shopify/sync/products Going forward, your products will be automatically kept in sync via [webhooks](#set-up-webhooks). -The following settings available for controlling the product synchronization process: - -| Setting | Type | Default | Description | -|-------------------------|--------|---------|-------------| -| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | -| `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | +The [`syncProductMetafields` and `syncVariantMetafields` settings](#settings) govern what data is synchronized via this process. > [!NOTE] > Smaller stores with only a few products can perform synchronization via the **Shopify Sync** utility. @@ -792,6 +787,16 @@ There is no need to query the Shopify API to render product details in your temp ## Going Further +### Settings + +The following settings can be controlled by creating a `shopify.php` file in your `config/` directory. + +| Setting | Type | Default | Description | +|-------------------------|--------|---------|-------------| +| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | +| `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | + + ### Events #### `craft\shopify\services\Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT` @@ -862,4 +867,4 @@ return [ ## Rate Limiting -The Shopify API implements [rate limiting rules](https://shopify.dev/docs/api/usage/rate-limits) the plugin makes its best effort to avoid hitting these limits. \ No newline at end of file +The Shopify API implements [rate limiting rules](https://shopify.dev/docs/api/usage/rate-limits) the plugin makes its best effort to avoid hitting these limits. From a0bba481f8d39e38e594b5dff8fb727cf653f9fa Mon Sep 17 00:00:00 2001 From: August Miller Date: Tue, 2 Apr 2024 09:20:57 -0700 Subject: [PATCH 07/10] No linebreak --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 2ccd6ad..eacca60 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,7 @@ Once the plugin has been configured, you can perform an initial synchronization php craft shopify/sync/products ``` -The [`syncProductMetafields` and `syncVariantMetafields` settings](#settings) govern what data is synchronized via this process. - -Going forward, your products will be automatically kept in sync via [webhooks](#set-up-webhooks). +The [`syncProductMetafields` and `syncVariantMetafields` settings](#settings) govern what data is synchronized via this process. Going forward, your products will be automatically kept in sync via [webhooks](#set-up-webhooks). > [!NOTE] > Smaller stores with only a few products can perform synchronization via the **Shopify Sync** utility. From 8a224156f755fe424bb476827bb32fa47cb65164 Mon Sep 17 00:00:00 2001 From: August Miller Date: Tue, 2 Apr 2024 09:30:29 -0700 Subject: [PATCH 08/10] Document other valid settings keys --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eacca60..6b47a31 100644 --- a/README.md +++ b/README.md @@ -791,9 +791,17 @@ The following settings can be controlled by creating a `shopify.php` file in you | Setting | Type | Default | Description | |-------------------------|--------|---------|-------------| -| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | +| `apiKey` | `string` | — | Shopify API key. | +| `apiSecretKey` | `string` | — | Shopify API secret key. | +| `accessToken` | `string` | — | Shopify API access token. | +| `hostName` | `string` | — | Shopify [host name](#store-hostname) | +| `uriFormat` | `string` | — | Product element URI format. | +| `template` | `string` | — | Product element template path. | +| `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | | `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | +> [!NOTE] +> Setting `apiKey`, `apiSecretKey`, `accessToken`, and `hostName` via `shopify.php` will override Project Config values set via the control panel during [app setup](#create-a-shopify-app). ### Events From 51f51551062071ff2dc0cebbd3a81d901d55f890 Mon Sep 17 00:00:00 2001 From: August Miller Date: Tue, 2 Apr 2024 09:39:12 -0700 Subject: [PATCH 09/10] Using environment variables --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b47a31..32f4a9b 100644 --- a/README.md +++ b/README.md @@ -794,14 +794,14 @@ The following settings can be controlled by creating a `shopify.php` file in you | `apiKey` | `string` | — | Shopify API key. | | `apiSecretKey` | `string` | — | Shopify API secret key. | | `accessToken` | `string` | — | Shopify API access token. | -| `hostName` | `string` | — | Shopify [host name](#store-hostname) | +| `hostName` | `string` | — | Shopify [host name](#store-hostname). | | `uriFormat` | `string` | — | Product element URI format. | | `template` | `string` | — | Product element template path. | | `syncProductMetafields` | `bool` | `true` | Whether product metafields should be included when syncing products. This adds an extra API request per product. | | `syncVariantMetafields` | `bool` | `false` | Whether variant metafields should be included when syncing products. This adds an extra API request per variant. | > [!NOTE] -> Setting `apiKey`, `apiSecretKey`, `accessToken`, and `hostName` via `shopify.php` will override Project Config values set via the control panel during [app setup](#create-a-shopify-app). +> Setting `apiKey`, `apiSecretKey`, `accessToken`, and `hostName` via `shopify.php` will override Project Config values set via the control panel during [app setup](#create-a-shopify-app). You can still reference environment values from the config file with `craft\helpers\App::env()`. ### Events From 60637b7e84661834f0d931a4608e3edc782f7857 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 3 Apr 2024 08:05:58 +0100 Subject: [PATCH 10/10] Finish 4.1.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2f8ad..5e2a01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Notes for Shopify -## Unreleased +## 4.1.0 - 2024-04-03 - Added support for syncing variant meta fields. ([#99](https://github.com/craftcms/shopify/issues/99)) - Added the `syncProductMetafields` and `syncVariantMetafields` config settings, which can be enabled to sync meta fields.