Skip to content

Commit

Permalink
Add support for multiple versions of the Shopify API
Browse files Browse the repository at this point in the history
  • Loading branch information
nfourtythree committed Dec 19, 2024
1 parent 8771dda commit d44fad1
Show file tree
Hide file tree
Showing 11 changed files with 676 additions and 341 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Release Notes for Shopify 5.3 (WIP)

- It is now possible to set the API version in the plugin settings.
- Added Craft CMS 4 compatibility.
- Added support for selecting products in Link fields.
- Syncing products now returns presentment prices by default. ([#122](https://github.com/craftcms/shopify/issues/122))
- Added `craft\shopify\linktypes\Product`.
- Added `craft\shopify\linktypes\Product`.
- Added `craft\shopify\models\Settings::getApiVersion()`.
- Added `craft\shopify\models\Settings::setApiVersion()`.
- Added `craft\shopify\services\Api::getMetaFieldClass()`.
- Added `craft\shopify\services\Api::getProductClass()`.
- Added `craft\shopify\services\Api::getSupportedApiVersions()`.
- Added `craft\shopify\services\Api::getVariantClass()`.
- Deprecated `craft\shopify\services\Api::SHOPIFY_API_VERSION`.
789 changes: 478 additions & 311 deletions composer.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Plugin extends BasePlugin
/**
* @var string
*/
public string $schemaVersion = '5.1.3.0';
public string $schemaVersion = '5.3.0.0';

/**
* @inheritdoc
Expand Down
5 changes: 3 additions & 2 deletions src/events/ShopifyProductSyncEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use craft\events\CancelableEvent;
use craft\shopify\elements\Product as ProductElement;
use Shopify\Rest\Admin2023_10\Product as ShopifyProduct;
use Shopify\Rest\Admin2024_10\Product as ShopifyProduct2410;

/**
* Event triggered just before a synchronized product element is going to be saved.
Expand All @@ -24,7 +25,7 @@ class ShopifyProductSyncEvent extends CancelableEvent
public ProductElement $element;

/**
* @var ShopifyProduct Source Shopify API resource.
* @var ShopifyProduct|ShopifyProduct2410 Source Shopify API resource.
*/
public ShopifyProduct $source;
public ShopifyProduct|ShopifyProduct2410 $source;
}
5 changes: 3 additions & 2 deletions src/helpers/Metafields.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use craft\helpers\Json;
use Shopify\Rest\Admin2023_10\Metafield as ShopifyMetafield;
use Shopify\Rest\Admin2024_10\Metafield as ShopifyMetafield2410;

class Metafields
{
Expand All @@ -33,7 +34,7 @@ class Metafields
/**
* Unpacks metadata from the Shopify API.
*
* @param ShopifyMetafield[] $fields
* @param ShopifyMetafield[]|ShopifyMetafield2410[] $fields
* @return array
*/
public static function unpack(array $fields): array
Expand All @@ -50,7 +51,7 @@ public static function unpack(array $fields): array
/**
* Turn a metafield API resource into a simple value, based on its type.
*/
public static function decode(ShopifyMetafield $field)
public static function decode(ShopifyMetafield|ShopifyMetafield2410 $field)
{
$value = $field->value;

Expand Down
41 changes: 41 additions & 0 deletions src/migrations/m241218_145031_set_api_version.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace craft\shopify\migrations;

use Craft;
use craft\db\Migration;

/**
* m241218_145031_set_api_version migration.
*/
class m241218_145031_set_api_version extends Migration
{
/**
* @inheritdoc
*/
public function safeUp(): bool
{
// Don't make the same config changes twice
$projectConfig = Craft::$app->getProjectConfig();
$schemaVersion = $projectConfig->get('plugins.shopify.schemaVersion', true);

if (version_compare($schemaVersion, '5.3.0', '<')) {
$muteEvents = $projectConfig->muteEvents;
$projectConfig->muteEvents = true;

$projectConfig->set('plugins.shopify.settings.apiVersion', '2023-10', 'Save the current Shopify API version');
$projectConfig->muteEvents = $muteEvents;
}

return true;
}

/**
* @inheritdoc
*/
public function safeDown(): bool
{
echo "m241218_145031_set_api_version cannot be reverted.\n";
return false;
}
}
42 changes: 41 additions & 1 deletion src/models/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@

use Craft;
use craft\base\Model;
use craft\helpers\App;
use craft\helpers\UrlHelper;
use craft\shopify\elements\Product;
use craft\shopify\Plugin;
use Shopify\ApiVersion;

/**
* Shopify Settings model.
Expand All @@ -28,6 +31,13 @@ class Settings extends Model
public string $template = '';
private mixed $_productFieldLayout;

/**
* @var string The Shopify API version to use.
* @see setApiVersion()
* @see getApiVersion()
*/
private string $_apiVersion = ApiVersion::OCTOBER_2024;

/**
* Whether product metafields should be included when syncing products. This adds an extra API request per product.
*
Expand All @@ -47,10 +57,19 @@ class Settings extends Model
public function rules(): array
{
return [
[['apiSecretKey', 'apiKey', 'accessToken', 'hostName'], 'required'],
[['apiSecretKey', 'apiKey', 'accessToken', 'hostName', 'apiVersion'], 'required'],
[['apiVersion'], 'in', 'range' => Plugin::getInstance()->getApi()->getSupportedApiVersions()],
];
}

public function attributes()
{
$names = parent::attributes();
$names[] = 'apiVersion';

return $names;
}

/**
* @inheritdoc
*/
Expand All @@ -59,13 +78,34 @@ public function attributeLabels(): array
return [
'apiKey' => Craft::t('shopify', 'Shopify API Key'),
'apiSecretKey' => Craft::t('shopify', 'Shopify API Secret Key'),
'apiVersion' => Craft::t('shopify', 'Shopify API Version'),
'accessToken' => Craft::t('shopify', 'Shopify Access Token'),
'hostName' => Craft::t('shopify', 'Shopify Host Name'),
'uriFormat' => Craft::t('shopify', 'Product URI format'),
'template' => Craft::t('shopify', 'Product Template'),
];
}

/**
* @param string $apiVersion
* @return void
* @since 5.3.0
*/
public function setApiVersion(string $apiVersion): void
{
$this->_apiVersion = $apiVersion;
}

/**
* @param bool $parse
* @return string
* @since 5.3.0
*/
public function getApiVersion(bool $parse = true): string
{
return $parse ? App::parseEnv($this->_apiVersion) : $this->_apiVersion;
}

/**
* @return \craft\models\FieldLayout|mixed
*/
Expand Down
84 changes: 70 additions & 14 deletions src/services/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use craft\shopify\Plugin;
use GuzzleHttp\Client;
use Psr\Http\Client\ClientInterface;
use Shopify\ApiVersion;
use Shopify\Auth\FileSessionStorage;
use Shopify\Auth\Session;
use Shopify\Clients\HttpClientFactory;
Expand All @@ -22,6 +23,9 @@
use Shopify\Rest\Admin2023_10\Metafield as ShopifyMetafield;
use Shopify\Rest\Admin2023_10\Product as ShopifyProduct;
use Shopify\Rest\Admin2023_10\Variant as ShopifyVariant;
use Shopify\Rest\Admin2024_10\Metafield as ShopifyMetafield2410;
use Shopify\Rest\Admin2024_10\Product as ShopifyProduct2410;
use Shopify\Rest\Admin2024_10\Variant as ShopifyVariant2410;
use Shopify\Rest\Base as ShopifyBaseResource;

/**
Expand All @@ -37,6 +41,7 @@ class Api extends Component
{
/**
* @var string
* @deprecated in 5.3.0. Use `Settings::getApiVersion()` instead.
*/
public const SHOPIFY_API_VERSION = '2023-10';

Expand All @@ -50,27 +55,39 @@ class Api extends Component
*/
private ?Rest $_client = null;

/**
* @return array
* @since 5.3.0
*/
public function getSupportedApiVersions(): array
{
return [
ApiVersion::OCTOBER_2024,
ApiVersion::OCTOBER_2023,
];
}

/**
* Retrieve all a shop’s products.
*
* @return ShopifyProduct[]
* @return ShopifyProduct[]|ShopifyProduct2410[]
*/
public function getAllProducts(): array
{
/** @var ShopifyProduct[] $all */
$all = $this->getAll(ShopifyProduct::class);
/** @var ShopifyProduct[]|ShopifyProduct2410[] $all */
$all = $this->getAll($this->getProductClass());

return $all;
}

/**
* Retrieve a single product by its Shopify ID.
*
* @return ShopifyProduct
* @return ShopifyProduct|ShopifyProduct2410
*/
public function getProductByShopifyId($id): ShopifyProduct
public function getProductByShopifyId($id): ShopifyProduct|ShopifyProduct2410
{
return ShopifyProduct::find($this->getSession(), $id);
return $this->getProductClass()::find($this->getSession(), $id);
}

/**
Expand All @@ -95,7 +112,7 @@ public function getProductIdByInventoryItemId($id): ?int
* Retrieves "metafields" for the provided Shopify product ID.
*
* @param int $id Shopify Product ID
* @return ShopifyMetafield[]
* @return ShopifyMetafield[]|ShopifyMetafield2410[]
*/
public function getMetafieldsByProductId(int $id): array
{
Expand All @@ -108,7 +125,7 @@ public function getMetafieldsByProductId(int $id): array

/**
* @param int $id
* @return ShopifyMetafield[]
* @return ShopifyMetafield[]|ShopifyMetafield2410[]
* @since 4.1.0
*/
public function getMetafieldsByVariantId(int $id): array
Expand All @@ -123,7 +140,7 @@ public function getMetafieldsByVariantId(int $id): array
/**
* @param int $id
* @param string $ownerResource
* @return ShopifyMetafield[]
* @return ShopifyMetafield[]|ShopifyMetafield2410[]
* @since 4.1.0
*/
public function getMetafieldsByIdAndOwnerResource(int $id, string $ownerResource): array
Expand All @@ -143,7 +160,8 @@ public function getMetafieldsByIdAndOwnerResource(int $id, string $ownerResource
$return = [];

foreach ($metafields['metafields'] as $metafield) {
$return[] = new ShopifyMetafield($this->getSession(), $metafield);
$metafieldClass = $this->getMetaFieldClass();
$return[] = new $metafieldClass($this->getSession(), $metafield);
}

return $return;
Expand All @@ -160,12 +178,12 @@ public function getVariantsByProductId(int $id): array
$params = ['limit' => 250];

do {
$resources = array_merge($resources, ShopifyVariant::all(
$resources = array_merge($resources, $this->getVariantClass()::all(
$this->getSession(),
['product_id' => $id],
ShopifyVariant::$NEXT_PAGE_QUERY ?: $params,
$this->getVariantClass()::$NEXT_PAGE_QUERY ?: $params,
));
} while (ShopifyVariant::$NEXT_PAGE_QUERY);
} while ($this->getVariantClass()::$NEXT_PAGE_QUERY);

$variants = [];
foreach ($resources as $resource) {
Expand Down Expand Up @@ -253,7 +271,7 @@ public function getSession(): ?Session
// Shopify wants a name for the host/environment that is initiating the connection.
hostName: !Craft::$app->request->isConsoleRequest ? Craft::$app->getRequest()->getHostName() : 'localhost',
sessionStorage: new FileSessionStorage(Craft::$app->getPath()->getStoragePath() . DIRECTORY_SEPARATOR . 'shopify_api_sessions'),
apiVersion: self::SHOPIFY_API_VERSION,
apiVersion: $pluginSettings->getApiVersion(),
isEmbeddedApp: false,
logger: $webLogTarget->getLogger(),
);
Expand Down Expand Up @@ -281,4 +299,42 @@ public function client(): ClientInterface

return $this->_session;
}

/**
* @return string
* @since 5.3.0
* @phpstan-return class-string<ShopifyProduct|ShopifyProduct2410>
*/
public function getProductClass(): string
{
return $this->_apiNamespace() . '\Product';
}

/**
* @return string
* @since 5.3.0
* @phpstan-return class-string<ShopifyVariant|ShopifyVariant2410>
*/
public function getVariantClass(): string
{
return $this->_apiNamespace() . '\Variant';
}

/**
* @return string
* @since 5.3.0
* @phpstan-return class-string<ShopifyMetafield|ShopifyMetafield2410>
*/
public function getMetaFieldClass(): string
{
return $this->_apiNamespace() . '\Metafield';
}

/**
* @return string
*/
private function _apiNamespace(): string
{
return 'Shopify\Rest\Admin' . str_replace('-', '_', Plugin::getInstance()->getSettings()->getApiVersion());
}
}
Loading

0 comments on commit d44fad1

Please sign in to comment.