From a30842cdb0f35f84833025d524321f98026f487f Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Wed, 15 Oct 2025 11:50:16 -0500 Subject: [PATCH] Remove ApiVersion::LATEST constant and require explicit version selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent semantic versioning violations where library updates automatically change API versions, potentially breaking production apps without developer awareness or control Breaking change: apiVersion parameter now required in Context::initialize() Migration: Developers must explicitly specify API version (e.g., '2025-10') Aligns with Ruby library PR #1411 and broader Shopify library strategy See BREAKING_CHANGES_FOR_V6.md for complete migration guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- BREAKING_CHANGES_FOR_V6.md | 84 ++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + docs/getting_started.md | 2 +- src/ApiVersion.php | 2 +- src/Context.php | 5 ++- tests/BaseTestCase.php | 7 ++++ tests/ContextTest.php | 12 ++++-- 7 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 BREAKING_CHANGES_FOR_V6.md diff --git a/BREAKING_CHANGES_FOR_V6.md b/BREAKING_CHANGES_FOR_V6.md new file mode 100644 index 00000000..a479dde9 --- /dev/null +++ b/BREAKING_CHANGES_FOR_V6.md @@ -0,0 +1,84 @@ +# Breaking change notice for version 6.0.0 + +## Removal of `ApiVersion::LATEST` constant + +The `ApiVersion::LATEST` constant has been removed to prevent semantic versioning (semver) breaking changes. Previously, this constant would automatically update every quarter when new API versions were released, causing unintended breaking changes for apps. + +### Migration Guide + +**If you were using the constant directly:** + +```php +// Before (v5 and earlier) +$apiVersion = ApiVersion::LATEST; + +// After (v6+) +$apiVersion = '2025-07'; // Explicitly specify the version you want to use +``` + +**In your Context::initialize():** + +The `apiVersion` parameter is now **required** in `Context::initialize()`. You must explicitly specify which API version you want to use: + +```php +// Before (v5 and earlier) +Context::initialize( + apiKey: $_ENV['SHOPIFY_API_KEY'], + apiSecretKey: $_ENV['SHOPIFY_API_SECRET'], + scopes: $_ENV['SHOPIFY_APP_SCOPES'], + hostName: $_ENV['SHOPIFY_APP_HOST_NAME'], + sessionStorage: new FileSessionStorage('/tmp/php_sessions'), + // apiVersion was optional with default ApiVersion::LATEST +); + +// After (v6+) +Context::initialize( + apiKey: $_ENV['SHOPIFY_API_KEY'], + apiSecretKey: $_ENV['SHOPIFY_API_SECRET'], + scopes: $_ENV['SHOPIFY_APP_SCOPES'], + hostName: $_ENV['SHOPIFY_APP_HOST_NAME'], + sessionStorage: new FileSessionStorage('/tmp/php_sessions'), + apiVersion: '2025-07', // Now required - explicitly specify the version +); +``` + +**Finding the right API version:** + +You can reference the available API versions in the `ApiVersion` class: + +```php +use Shopify\ApiVersion; + +// Available constants (as of this release): +ApiVersion::UNSTABLE // "unstable" +ApiVersion::JULY_2025 // "2025-07" +ApiVersion::APRIL_2025 // "2025-04" +ApiVersion::JANUARY_2025 // "2025-01" +ApiVersion::OCTOBER_2024 // "2024-10" +// ... and older versions +``` + +Or you can use string literals directly: + +```php +Context::initialize( + // ... other parameters + apiVersion: '2025-07', +); +``` + +**Why this change?** + +By requiring explicit version specification, apps can: +- Control when they upgrade to new API versions +- Test thoroughly before upgrading +- Avoid unexpected breaking changes from automatic version updates +- Follow semantic versioning principles more accurately + +**Recommended approach:** + +1. Review the [Shopify API Changelog](https://shopify.dev/changelog) to understand changes between versions +2. Choose the API version that best suits your app's needs +3. Explicitly specify that version in your `Context::initialize()` call +4. Test your app thoroughly with the chosen version +5. Upgrade to newer versions on your own schedule after proper testing diff --git a/CHANGELOG.md b/CHANGELOG.md index 05dd4556..705396be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +- ⚠️ [Breaking] Remove `ApiVersion::LATEST` constant to prevent semver violations. The `apiVersion` parameter is now required in `Context::initialize()`. Developers must explicitly specify API versions. See the [migration guide](BREAKING_CHANGES_FOR_V6.md#removal-of-apiversionlatest-constant) for details. - [#425](https://github.com/Shopify/shopify-api-php/pull/425) [Patch] Add compliance webhook topics ## v5.11.0 - 2025-07-10 diff --git a/docs/getting_started.md b/docs/getting_started.md index 1075a076..cc19c1d4 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -19,7 +19,7 @@ The first thing your app will need to do to use this library is to set up your c | `scopes` | `string \| array` | Yes | - | App scopes | | `hostName` | `string` | Yes | - | App host name e.g. `my-app.my-domain.ca`. You may optionally include `https://` or `http://` to determine which scheme to use | | `sessionStorage` | `SessionStorage` | Yes | - | Session storage strategy. Read our [notes on session handling](issues.md#notes-on-session-handling) for more information | -| `apiVersion` | `string` | No | `ApiVersion::LATEST` | App API version, defaults to `ApiVersion::LATEST` | +| `apiVersion` | `string` | Yes | - | App API version. You must explicitly specify which API version to use (e.g., `'2025-07'`, `'2024-10'`, etc.) | | `isEmbeddedApp` | `bool` | No | `true` | Whether the app is an embedded app | | `isPrivateApp` | `bool` | No | `false` | Whether the app is a private app | | `userAgentPrefix` | `string` | No | - | Prefix for user agent header sent with a request | diff --git a/src/ApiVersion.php b/src/ApiVersion.php index 9073a292..71d199ae 100644 --- a/src/ApiVersion.php +++ b/src/ApiVersion.php @@ -69,5 +69,5 @@ class ApiVersion /** * @var string */ - public const LATEST = self::JULY_2025; + public const OCTOBER_2025 = "2025-10"; } diff --git a/src/Context.php b/src/Context.php index b16707d0..1e16394b 100644 --- a/src/Context.php +++ b/src/Context.php @@ -60,7 +60,7 @@ class Context * @param string|array $scopes App scopes * @param string $hostName App host name e.g. www.google.ca. May include scheme * @param SessionStorage $sessionStorage Session storage strategy - * @param string $apiVersion App API key, defaults to unstable + * @param string $apiVersion App API version * @param bool $isEmbeddedApp Whether the app is an embedded app, defaults to true * @param bool $isPrivateApp Whether the app is a private app, defaults to false * @param string|null $privateAppStorefrontAccessToken The Storefront API Access Token for a private app @@ -77,7 +77,7 @@ public static function initialize( $scopes, string $hostName, SessionStorage $sessionStorage, - string $apiVersion = ApiVersion::LATEST, + string $apiVersion, bool $isEmbeddedApp = true, bool $isPrivateApp = false, ?string $privateAppStorefrontAccessToken = null, @@ -93,6 +93,7 @@ public static function initialize( 'apiSecretKey' => $apiSecretKey, 'scopes' => implode((array)$scopes), 'hostName' => $hostName, + 'apiVersion' => $apiVersion, ]; $missing = array(); foreach ($requiredValues as $key => $value) { diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 87adbbd5..e6197249 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Client\ClientInterface; +use Shopify\ApiVersion; use Shopify\Clients\HttpClientFactory; use Shopify\Context; use Shopify\Exception\HttpRequestException; @@ -18,6 +19,11 @@ class BaseTestCase extends TestCase { + /** + * API version to use for tests. Uses the latest available version. + */ + protected const TEST_API_VERSION = ApiVersion::OCTOBER_2025; + /** @var string */ protected $domain = 'test-shop.myshopify.io'; /** @var string */ @@ -32,6 +38,7 @@ public function setUp(): void scopes: ['sleepy', 'kitty'], hostName: 'www.my-friends-cats.com', sessionStorage: new MockSessionStorage(), + apiVersion: self::TEST_API_VERSION, ); Context::$RETRY_TIME_IN_SECONDS = 0; $this->version = require dirname(__FILE__) . '/../src/version.php'; diff --git a/tests/ContextTest.php b/tests/ContextTest.php index 160a336a..2c95f456 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -28,6 +28,7 @@ public function testCanCreateContext() scopes: ['sleepy', 'kitty'], hostName: 'my-friends-cats', sessionStorage: new MockSessionStorage(), + apiVersion: self::TEST_API_VERSION, ); $this->assertEquals('ash', Context::$API_KEY); @@ -52,6 +53,7 @@ public function testCanUpdateContext() scopes: ['silly', 'doggo'], hostName: 'yay-for-doggos', sessionStorage: new MockSessionStorage(), + apiVersion: self::TEST_API_VERSION, ); $this->assertEquals('tuck', Context::$API_KEY); @@ -64,7 +66,8 @@ public function testThrowsIfMissingArguments() { $this->expectException(MissingArgumentException::class); $this->expectExceptionMessage( - 'Cannot initialize Shopify API Library. Missing values for: apiKey, apiSecretKey, scopes, hostName' + 'Cannot initialize Shopify API Library. ' . + 'Missing values for: apiKey, apiSecretKey, scopes, hostName, apiVersion' ); Context::initialize( apiKey: '', @@ -72,6 +75,7 @@ public function testThrowsIfMissingArguments() scopes: [], hostName: '', sessionStorage: new MockSessionStorage(), + apiVersion: '', ); } @@ -96,7 +100,7 @@ public function testThrowsIfPrivateApp() scopes: ['sleepy', 'kitty'], hostName: 'my-friends-cats', sessionStorage: new MockSessionStorage(), - apiVersion: 'unstable', + apiVersion: self::TEST_API_VERSION, isPrivateApp: true, ); $this->expectException(PrivateAppException::class); @@ -223,6 +227,7 @@ public function testCanSetHostScheme($host, $expectedScheme, $expectedHost) scopes: ['sleepy', 'kitty'], hostName: $host, sessionStorage: new MockSessionStorage(), + apiVersion: self::TEST_API_VERSION, ); $this->assertEquals($expectedHost, Context::$HOST_NAME); @@ -249,6 +254,7 @@ public function testFailsOnInvalidHost() scopes: ['sleepy', 'kitty'], hostName: 'not-a-host-!@#$%^&*()', sessionStorage: new MockSessionStorage(), + apiVersion: self::TEST_API_VERSION, ); } @@ -262,7 +268,7 @@ public function testCanSetCustomShopDomains() scopes: ['sleepy', 'kitty'], hostName: 'my-friends-cats', sessionStorage: new MockSessionStorage(), - apiVersion: ApiVersion::LATEST, + apiVersion: self::TEST_API_VERSION, isPrivateApp: false, customShopDomains: $domains, );