diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3855616 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug Report +about: Report an issue or unexpected behavior +labels: + - bug # Linear + - Plugins # Linear + - Plugins → Shopify # Linear +--- + +### Description + + + +### Steps to reproduce + +1. +2. + +### Additional info + +- Craft version: +- PHP version: +- Database driver & version: +- Plugins & versions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c6ae76..d3e769c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,22 @@ # Release Notes for Shopify +## 5.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. +- Added `craft\shopify\models\Settings::$syncProductMetafields`. +- Added `craft\shopify\models\Settings::$syncVariantMetafields`. + ## 5.0.0 - 2024-03-20 -- Added Craft CMS 5 compatibility. +- Shopify now requires Craft CMS 5.0.0-beta.10 or later. + +## 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. +- Added `craft\shopify\models\Settings::$syncProductMetafields`. +- Added `craft\shopify\models\Settings::$syncVariantMetafields`. ## 4.0.0 - 2023-11-02 diff --git a/README.md b/README.md index 68fed09..771f080 100644 --- a/README.md +++ b/README.md @@ -113,16 +113,16 @@ 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 +``` + +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). -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. ### Native Attributes @@ -422,7 +422,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; @@ -436,7 +437,9 @@ Once you have a reference to a variant, you can output its properties: ``` > **Note** -> The built-in [`currency`](https://craftcms.com/docs/5.x/dev/filters.html#currency) Twig filter is a great way to format money values. +> 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 @@ -707,6 +710,24 @@ Relationships defined with the _Shopify Products_ field use stable element IDs u ## Going Further +### Settings + +The following settings can be controlled by creating a `shopify.php` file in your `config/` directory. + +| Setting | Type | Default | Description | +|-------------------------|--------|---------|-------------| +| `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). You can still reference environment values from the config file with `craft\helpers\App::env()`. + ### Events #### `craft\shopify\services\Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT` @@ -777,4 +798,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. diff --git a/src/Plugin.php b/src/Plugin.php index 150cacd..5a29e2b 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/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/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..32470e3 --- /dev/null +++ b/src/migrations/m240402_105857_add_metafields_property_to_variants.php @@ -0,0 +1,47 @@ +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; + } +} 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..d6c595a 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -94,12 +94,41 @@ 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, ], ]); 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); } }