Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the ability to sync variant meta fields #103

Merged
merged 10 commits into from
Apr 3, 2024
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart inversion here. CLI is going to be more reliable for 99% of users!


> [!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

Expand Down Expand Up @@ -426,7 +426,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;
Expand All @@ -441,6 +442,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

Expand Down Expand Up @@ -723,7 +726,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
Expand Down Expand Up @@ -782,6 +785,24 @@ 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 |
|-------------------------|--------|---------|-------------|
| `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()`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collected syncProductMetafields and syncVariantMetafields into this section, and added keys for the other settings.

### Events

#### `craft\shopify\services\Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT`
Expand Down Expand Up @@ -852,4 +873,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.
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.
2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/jobs/UpdateProductMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace craft\shopify\migrations;

use craft\db\Migration;
use craft\db\Query;

/**
* m240402_105857_add_metafields_property_to_variants migration.
*/
class m240402_105857_add_metafields_property_to_variants extends Migration
{
/**
* @inheritdoc
*/
public function safeUp(): bool
{
$productRows = (new Query())
->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;
}
}
16 changes: 16 additions & 0 deletions src/models/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
31 changes: 30 additions & 1 deletion src/services/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);

Expand Down
34 changes: 25 additions & 9 deletions src/services/Products.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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);
}

/**
Expand All @@ -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);
}
}

Expand Down
Loading