Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
# WIP Release Notes for Shopify 8.0

- Shopify for Craft now requires Craft CMS 5.10.7 or later.
### Extensibility

- Added `craft\shopify\models\BulkOperation::$shopifyGid`.
- Added `craft\shopify\models\Variant::$shopifyGid`.
- Added `craft\shopify\jobs\ProcessBulkOperationData::$bulkOperationShopifyGid`.
- Added `craft\shopify\services\BulkOperations::getBulkOperationByShopifyGid()`.
- Added `craft\shopify\services\Products::deleteProductByShopifyGid()`.
- Added `craft\shopify\services\Products::deleteShopifyDataByShopifyGid()`.
- Added `craft\shopify\services\Products::syncProductByShopifyGid()`.
- `craft\shopify\models\Variant::$shopifyId` now holds the numeric Shopify ID. The full GID is now available via `$shopifyGid`.
- `craft\shopify\records\ShopifyData::$shopifyId` is now a generated (read-only) column containing the numeric Shopify ID. The full GID is now available via `$shopifyGid`.
- Deprecated `craft\shopify\jobs\ProcessBulkOperationData::$bulkOperationShopifyId`. Use `$bulkOperationShopifyGid` instead.
- Deprecated `craft\shopify\models\BulkOperation::$shopifyId`. Use `$shopifyGid` instead.
Comment thread
Copilot marked this conversation as resolved.
Outdated
- Deprecated `craft\shopify\services\BulkOperations::getBulkOperationByShopifyId()`. Use `getBulkOperationByShopifyGid()` instead.
- Deprecated `craft\shopify\services\Products::deleteProductByShopifyId()`. Use `deleteProductByShopifyGid()` instead.
- Deprecated `craft\shopify\services\Products::deleteShopifyDataByShopifyId()`. Use `deleteShopifyDataByShopifyGid()` instead.
- Deprecated `craft\shopify\services\Products::syncProductByShopifyId()`. Use `syncProductByShopifyGid()` instead.

### System

- The `shopify_data` table's `shopifyId` column has been renamed to `shopifyGid`. A new generated `shopifyId` column (the numeric ID at the end of the GID) has been added.
- The `shopify_bulkoperations` table's `shopifyId` column has been renamed to `shopifyGid`.
- Shopify for Craft now requires Craft CMS 5.10.7 or later.
10 changes: 5 additions & 5 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class Plugin extends BasePlugin
/**
* @var string
*/
public string $schemaVersion = '7.1.0.0';
public string $schemaVersion = '8.0.0';

/**
* @inheritdoc
Expand Down Expand Up @@ -450,8 +450,8 @@ private function _registerGarbageCollection(): void
'products.shopifyId',
])
->from(Table::PRODUCTS . ' products')
->leftJoin(Table::DATA . ' data', '[[data.shopifyId]] = [[products.shopifyGid]]')
->where(['data.shopifyId' => null])
->leftJoin(Table::DATA . ' data', '[[data.shopifyGid]] = [[products.shopifyGid]]')
->where(['data.shopifyGid' => null])
->all();

$shopifyIds = ArrayHelper::getColumn($shopifyProductElementsMissingData, 'shopifyId');
Expand Down
1 change: 1 addition & 0 deletions src/collections/VariantCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static function make($items = [])
$item = Craft::createObject([
'class' => Variant::class,
'id' => $item->id,
'shopifyGid' => $item->shopifyGid,
'shopifyId' => $item->shopifyId,
'type' => $item->type,
'parentId' => $item->parentId,
Expand Down
21 changes: 10 additions & 11 deletions src/elements/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use craft\shopify\fieldlayoutelements\MetafieldsField;
use craft\shopify\fieldlayoutelements\OptionsField;
use craft\shopify\fieldlayoutelements\VariantsField;
use craft\shopify\helpers\Metafield as MetafieldHelper;
use craft\shopify\helpers\Product as ProductHelper;
use craft\shopify\models\Variant;
use craft\shopify\Plugin;
Expand Down Expand Up @@ -359,16 +360,21 @@ public function getOptions(): array
}

/**
* @param string|array $value
* @param string|array $value A list-shaped array of `{key, value}` objects, or a JSON-encoded string of the same.
* @return void
* @throws \InvalidArgumentException if the value is not a list-shaped array or JSON string of one.
*/
public function setMetafields(string|array $value): void
{
if (is_string($value)) {
$value = Json::decodeIfJson($value);
}

$this->_metaFields = $value;
if (!is_array($value) || !array_is_list($value)) {
throw new \InvalidArgumentException('setMetafields() expects a list-shaped array of {key, value} objects or a JSON-encoded string of the same.');
}

$this->_metaFields = MetafieldHelper::normalizeToMap($value);
}

/**
Expand All @@ -387,14 +393,7 @@ public function getMetafields(): array

$data = Plugin::getInstance()->getApi()->getShopifyDataByType('Metafield', $this->shopifyGid);

$metafields = $data
->mapWithKeys(function($d) {
return [
$d['key'] => Json::decodeIfJson($d['value']),
];
});

$this->setMetafields($metafields);
$this->setMetafields($data->all());

return $this->_metaFields ?? [];
}
Expand Down Expand Up @@ -814,7 +813,7 @@ public function afterDelete(): void
{
// Remove all the product shopify data
if ($this->shopifyGid && $this->getIsCanonical()) {
Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId($this->shopifyGid);
Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid($this->shopifyGid);
}

parent::afterDelete();
Expand Down
4 changes: 2 additions & 2 deletions src/elements/db/ProductQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ protected function beforePrepare(): bool
// join standard product element table that only contains the shopifyId
$this->joinElementTable('shopify_products');

$this->query->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyId]] = [[shopify_products.shopifyGid]]'));
$this->subQuery->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyId]] = [[shopify_products.shopifyGid]]'));
$this->query->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyGid]] = [[shopify_products.shopifyGid]]'));
$this->subQuery->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyGid]] = [[shopify_products.shopifyGid]]'));

$this->query->select([
'shopify_products.shopifyId',
Expand Down
2 changes: 1 addition & 1 deletion src/fieldlayoutelements/VariantsField.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ protected function inputHtml(ElementInterface $element = null, bool $static = fa
];

foreach ($variants as $variant) {
$link = sprintf('%s/variants/%s', $element->getShopifyEditUrl(), str_replace('gid://shopify/ProductVariant/', '', $variant->shopifyId));
$link = sprintf('%s/variants/%s', $element->getShopifyEditUrl(), $variant->shopifyId);

$title = $variant->title;
$sku = $variant->sku;
Expand Down
2 changes: 0 additions & 2 deletions src/gql/types/Variant.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,11 @@ public static function getFieldDefinitions(): array
'name' => 'shopifyId',
'type' => Type::string(),
'description' => 'Shopify ID of the variant.',
'resolve' => fn(VariantElement $source) => str_replace('gid://shopify/ProductVariant/', '', $source->shopifyId),
],
'shopifyGid' => [
'name' => 'shopifyGid',
'type' => Type::string(),
'description' => 'Shopify GID of the variant.',
'resolve' => fn(VariantElement $source) => $source->shopifyId,
],
'title' => [
'name' => 'title',
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/Webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ public function handle(string $topic, string $shop, array $body): void
switch ($topic) {
case Topics::PRODUCTS_UPDATE:
case Topics::PRODUCTS_CREATE:
Plugin::getInstance()->getProducts()->syncProductByShopifyId($body['id']);
Plugin::getInstance()->getProducts()->syncProductByShopifyGid($body['id']);
break;
case Topics::PRODUCTS_DELETE:
Plugin::getInstance()->getProducts()->deleteProductByShopifyId($body['id']);
Plugin::getInstance()->getProducts()->deleteProductByShopifyGid($body['id']);
break;
case Topics::INVENTORY_ITEMS_UPDATE:
Plugin::getInstance()->getProducts()->syncProductByInventoryItemId($body['admin_graphql_api_id']);
Expand Down
53 changes: 53 additions & 0 deletions src/helpers/Metafield.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\shopify\helpers;

use craft\helpers\Json;
use craft\shopify\records\ShopifyData;

/**
* Shopify Metafield Helper.
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 8.0.0
*/
class Metafield
{
/**
* Normalizes an iterable of metafield data into a flat key => value map.
*
* Accepts either:
* - [[ShopifyData]] ActiveRecord objects (from [[Api::getShopifyDataByType()]] with `$returnRecords = true`),
* where each record's `data` column holds a JSON-encoded `{key, value}` object.
* - Pre-decoded associative arrays (from [[Api::getShopifyDataByType()]] without `$returnRecords`),
* where each item already has `key` and `value` keys.
*
* Rows that do not carry both `key` and `value` are silently skipped.
*
* @param iterable $rows
* @return array<string, mixed>
*/
public static function normalizeToMap(iterable $rows): array
{
return collect($rows)
->mapWithKeys(function($d) {
$data = match (true) {
$d instanceof ShopifyData => Json::decodeIfJson($d->data),
is_string($d) => Json::decodeIfJson($d),
default => $d,
};

if (!is_array($data) || !isset($data['key']) || !isset($data['value'])) {
return [];
}

return [$data['key'] => Json::decodeIfJson($data['value'])];
})
->all();
}
}
2 changes: 1 addition & 1 deletion src/helpers/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public static function renderCardHtml(ProductElement $product, array $excludeMet

// This is the date updated in the database which represents the last time it was updated from a Shopify webhook or sync.
/** @var ShopifyData $productData */
$productData = ShopifyData::find()->where(['shopifyId' => $product->shopifyGid])->one();
$productData = ShopifyData::find()->where(['shopifyGid' => $product->shopifyGid])->one();
$dateUpdated = DateTimeHelper::toDateTime($productData->dateUpdated);
$now = new \DateTime();
$diff = $now->diff($dateUpdated);
Expand Down
12 changes: 6 additions & 6 deletions src/jobs/ProcessBulkOperationData.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ProcessBulkOperationData extends BaseBatchedJob
/**
* @var string
*/
public string $bulkOperationShopifyId;
public string $bulkOperationShopifyGid;

Comment thread
nfourtythree marked this conversation as resolved.
/**
* @var string
Expand Down Expand Up @@ -110,7 +110,7 @@ protected function processItem(mixed $item): void

// Find data records based on their Shopify ID and parent ID. This is to avoid overwriting
// records with the same ID but different parents (e.g. Metafields, Images etc).
$record = ShopifyData::findOne(['shopifyId' => $item['id'], 'parentId' => $parentId]);
$record = ShopifyData::findOne(['shopifyGid' => $item['id'], 'parentId' => $parentId]);
if (!$record) {
$record = new ShopifyData();
}
Expand All @@ -119,7 +119,7 @@ protected function processItem(mixed $item): void
$parts = explode('/', str_replace('gid://shopify/', '', $item['id']));
$type = $parts[0];

$record->shopifyId = $item['id'];
$record->shopifyGid = $item['id'];
$record->type = $type;
$record->data = $item;
$record->parentId = $item['__parentId'] ?? null;
Expand All @@ -139,7 +139,7 @@ protected function before(): void
parent::before();

// Make sure bulk op is marked as processing
$bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId($this->bulkOperationShopifyId);
$bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid($this->bulkOperationShopifyGid);

Comment thread
nfourtythree marked this conversation as resolved.
if (!$bulkOperation) {
return;
Expand All @@ -153,7 +153,7 @@ protected function before(): void
if ($this->clearData === BulkOperationRecord::CLEAR_DATA_ALL) {
ShopifyData::deleteAll();
} elseif ($this->clearData !== BulkOperationRecord::CLEAR_DATA_NONE) {
Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId($this->clearData);
Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid($this->clearData);
}
}

Expand All @@ -165,7 +165,7 @@ protected function after(): void
parent::after();

// Mark bulk op as completed
$bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId($this->bulkOperationShopifyId);
$bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid($this->bulkOperationShopifyGid);

Comment thread
nfourtythree marked this conversation as resolved.
if (!$bulkOperation) {
return;
Expand Down
17 changes: 14 additions & 3 deletions src/migrations/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function createTables(): void
$this->archiveTableIfExists(Table::DATA);
$this->createTable(Table::DATA, [
'id' => $this->primaryKey(),
'shopifyId' => $this->string(),
'shopifyGid' => $this->string(),
'type' => $this->string(),
'data' => $this->json(),
'parentId' => $this->string(),
Expand All @@ -69,7 +69,7 @@ public function createTables(): void
$this->archiveTableIfExists(Table::BULK_OPERATIONS);
$this->createTable(Table::BULK_OPERATIONS, [
'id' => $this->primaryKey(),
'shopifyId' => $this->string(),
'shopifyGid' => $this->string(),
'url' => $this->text(),
'objectCount' => $this->integer(),
'query' => $this->text(),
Expand Down Expand Up @@ -139,6 +139,17 @@ public function createGeneratedColumns(): void
$db->quoteColumnName($alias) . ' ' . $qb->getColumnType($this->integer()) . " GENERATED ALWAYS AS (" .
$expression . ") STORED;");
}

// Derived shopifyId — the numeric ID at the end of the GID (e.g. "7136060145715" from "gid://shopify/Product/7136060145715")
if ($db->getIsPgsql()) {
$shopifyIdExpression = "regexp_replace(\"shopifyGid\", '^.*/', '')";
} else {
$shopifyIdExpression = "SUBSTRING_INDEX(`shopifyGid`, '/', -1)";
}

$this->execute("ALTER TABLE " . Table::DATA . " ADD COLUMN " .
$db->quoteColumnName('shopifyId') . ' ' . $qb->getColumnType($this->string()) . " GENERATED ALWAYS AS (" .
$shopifyIdExpression . ") STORED;");
}

/**
Expand All @@ -148,7 +159,7 @@ public function createIndexes(): void
{
$this->createIndex(null, Table::PRODUCTS, ['shopifyId'], false);
$this->createIndex(null, Table::PRODUCTS, ['shopifyGid'], false);
$this->createIndex(null, Table::DATA, ['shopifyId'], false);
$this->createIndex(null, Table::DATA, ['shopifyGid'], false);
$this->createIndex(null, Table::DATA, ['parentId'], false);
}

Expand Down
Loading
Loading