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 f137f42e..a867535d 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 - [#433](https://github.com/Shopify/shopify-api-php/pull/433) [Minor] Add support for 2025-10 API version 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 a7452ec4..71d199ae 100644 --- a/src/ApiVersion.php +++ b/src/ApiVersion.php @@ -70,8 +70,4 @@ class ApiVersion * @var string */ public const OCTOBER_2025 = "2025-10"; - /** - * @var string - */ - public const LATEST = self::OCTOBER_2025; } 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, );